const express = require('express'); const wrapAsync = require('../helpers').wrapAsync; const { LocalAIApi } = require('../ai/LocalAIApi'); const router = express.Router(); const DIAGNOSTICS_CACHE_TTL_MS = 2 * 60 * 1000; const DEFAULT_PROBE_PROMPT = 'Reply with OK only.'; let diagnosticsCache = { expiresAt: 0, payload: null, }; const RELEVANT_ENV_KEYS = [ 'AI_PROXY_BASE_URL', 'AI_RESPONSES_PATH', 'AI_DEFAULT_MODEL', 'AI_PROJECT_HEADER', 'AI_TIMEOUT', 'AI_VERIFY_TLS', 'PROJECT_ID', 'PROJECT_UUID', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'GEMINI_MODEL', 'GOOGLE_API_KEY', 'GOOGLE_CLOUD_PROJECT', 'GOOGLE_GENAI_USE_VERTEXAI', 'OPENCODE_MODEL', ]; function shellQuote(value) { return `'${String(value ?? '').replace(/'/g, `'"'"'`)}'`; } function uniqueStrings(values) { return [...new Set((values || []) .map((value) => String(value || '').trim()) .filter(Boolean))]; } function getEnvironmentEntries() { return Object.keys(process.env) .sort((left, right) => left.localeCompare(right)) .map((key) => ({ key, value: String(process.env[key] ?? ''), })); } function getRelevantEnvironmentEntries() { const availableKeys = uniqueStrings([ ...RELEVANT_ENV_KEYS, ...Object.keys(process.env).filter((key) => /(AI|LLM|MODEL|OPENAI|GEMINI|GOOGLE|PROJECT|API)/i.test(key)), ]); return availableKeys .filter((key) => Object.prototype.hasOwnProperty.call(process.env, key)) .sort((left, right) => left.localeCompare(right)) .map((key) => ({ key, value: String(process.env[key] ?? ''), })); } function buildAbsoluteUrl(baseUrl, pathValue) { const trimmedBase = String(baseUrl || '').replace(/\/+$/, ''); const trimmedPath = String(pathValue || '').trim(); if (!trimmedPath) { return trimmedBase; } if (/^https?:\/\//i.test(trimmedPath)) { return trimmedPath; } if (trimmedPath.startsWith('/')) { return `${trimmedBase}${trimmedPath}`; } return `${trimmedBase}/${trimmedPath}`; } function resolveProxyConfig() { const baseUrl = process.env.AI_PROXY_BASE_URL || 'https://flatlogic.com'; const projectId = process.env.PROJECT_ID || null; let responsesPath = process.env.AI_RESPONSES_PATH || null; if (!responsesPath && projectId) { responsesPath = `/projects/${projectId}/ai-request`; } const defaultModel = process.env.AI_DEFAULT_MODEL || 'gpt-5-mini'; const projectHeader = process.env.AI_PROJECT_HEADER || 'project-uuid'; const projectUuid = process.env.PROJECT_UUID || null; return { baseUrl, responsesPath, resolvedUrl: buildAbsoluteUrl(baseUrl, responsesPath), defaultModel, projectHeader, projectUuid, projectId, timeoutSeconds: Number.parseInt(String(process.env.AI_TIMEOUT || 30), 10) || 30, verifyTls: !['false', '0'].includes(String(process.env.AI_VERIFY_TLS ?? 'true').toLowerCase()), }; } function buildRequestOrigin(req) { return `${req.protocol}://${req.get('host')}`; } function buildProbeCandidates(proxyConfig) { return uniqueStrings([ proxyConfig.defaultModel, 'gpt-5-mini', 'gpt-4.1-mini', 'gpt-4o-mini', process.env.GEMINI_MODEL, process.env.OPENCODE_MODEL, ]); } async function probeModel(model) { const startedAt = Date.now(); try { const response = await LocalAIApi.createResponse( { model, input: [{ role: 'user', content: DEFAULT_PROBE_PROMPT }], }, { poll_interval: 2, poll_timeout: 25, }, ); if (response.success) { return { model, success: true, latencyMs: Date.now() - startedAt, previewText: LocalAIApi.extractText(response) || null, error: null, }; } return { model, success: false, latencyMs: Date.now() - startedAt, previewText: null, error: response.error || response.message || 'unknown_error', }; } catch (error) { console.error(`Model probe failed for ${model}:`, error); return { model, success: false, latencyMs: Date.now() - startedAt, previewText: null, error: error.message || String(error), }; } } function buildCurlPayload(model, prompt) { return { model, input: [{ role: 'user', content: prompt }], }; } function joinCurlLines(lines) { return lines .filter(Boolean) .map((line, index, allLines) => (index < allLines.length - 1 ? `${line} \\` : line)) .join('\n'); } function buildAppProxyCurl(appProxyUrl, model, prompt) { const payload = { model, prompt, }; return joinCurlLines([ `curl -sS ${shellQuote(appProxyUrl)}`, '-X POST', "-H 'Content-Type: application/json'", `--data ${shellQuote(JSON.stringify(payload))}`, ]); } function buildFlatlogicProxyCurl(proxyConfig, model, prompt) { const payload = buildCurlPayload(model, prompt); payload.project_uuid = proxyConfig.projectUuid; const projectHeaderLine = shellQuote(`${proxyConfig.projectHeader}: ${proxyConfig.projectUuid || ''}`); return joinCurlLines([ `curl -sS ${shellQuote(proxyConfig.resolvedUrl)}`, '-X POST', "-H 'Content-Type: application/json'", `-H ${projectHeaderLine}`, `--data ${shellQuote(JSON.stringify(payload))}`, ]); } function buildOpenAICurl(model, prompt) { const apiKey = process.env.OPENAI_API_KEY || ''; const payload = buildCurlPayload(model, prompt); const url = 'https://api.openai.com/v1/responses'; const authorizationHeader = shellQuote(`Authorization: Bearer ${apiKey}`); return joinCurlLines([ `curl -sS ${shellQuote(url)}`, '-X POST', "-H 'Content-Type: application/json'", `-H ${authorizationHeader}`, `--data ${shellQuote(JSON.stringify(payload))}`, ]); } function buildGeminiCurl(prompt) { const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || ''; const configuredModel = String(process.env.GEMINI_MODEL || '').trim(); const model = configuredModel.startsWith('gemini-') ? configuredModel : 'gemini-2.5-flash'; const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`; const payload = { contents: [ { role: 'user', parts: [{ text: prompt }], }, ], }; return joinCurlLines([ `curl -sS ${shellQuote(url)}`, '-X POST', "-H 'Content-Type: application/json'", `--data ${shellQuote(JSON.stringify(payload))}`, ]); } async function buildDiagnostics(req) { const proxyConfig = resolveProxyConfig(); const requestOrigin = buildRequestOrigin(req); const appProxyUrl = `${requestOrigin}/api/public-llm/proxy`; const defaultPrompt = 'Reply with a concise health check for this LLM proxy.'; const probeCandidates = buildProbeCandidates(proxyConfig); const probeResults = await Promise.all(probeCandidates.map((model) => probeModel(model))); const availableModels = probeResults.filter((result) => result.success).map((result) => result.model); const unavailableModels = probeResults.filter((result) => !result.success).map((result) => result.model); const preferredModel = availableModels[0] || probeCandidates[0] || proxyConfig.defaultModel; return { generatedAt: new Date().toISOString(), requestOrigin, appProxyUrl, defaultPrompt, envVars: getEnvironmentEntries(), llmEnvVars: getRelevantEnvironmentEntries(), proxyConfig, probeCandidates, probeResults, availableModels, unavailableModels, curlCommands: { appProxy: buildAppProxyCurl(appProxyUrl, preferredModel, defaultPrompt), flatlogicProxy: buildFlatlogicProxyCurl(proxyConfig, preferredModel, defaultPrompt), openaiDirect: buildOpenAICurl(preferredModel, defaultPrompt), geminiDirect: buildGeminiCurl(defaultPrompt), }, }; } router.get( '/diagnostics', wrapAsync(async (req, res) => { const forceRefresh = String(req.query.refresh || '').toLowerCase() === 'true'; if (!forceRefresh && diagnosticsCache.payload && diagnosticsCache.expiresAt > Date.now()) { return res.status(200).send(diagnosticsCache.payload); } const payload = await buildDiagnostics(req); diagnosticsCache = { expiresAt: Date.now() + DIAGNOSTICS_CACHE_TTL_MS, payload, }; return res.status(200).send(payload); }), ); router.post( '/proxy', wrapAsync(async (req, res) => { const prompt = typeof req.body.prompt === 'string' ? req.body.prompt.trim() : ''; const input = Array.isArray(req.body.input) ? req.body.input : null; const model = typeof req.body.model === 'string' && req.body.model.trim() ? req.body.model.trim() : resolveProxyConfig().defaultModel; if (!prompt && !input) { return res.status(400).send({ success: false, error: 'input_missing', message: 'Either "prompt" or "input" must be provided.', }); } const payload = { model, input: input || [{ role: 'user', content: prompt }], }; const response = await LocalAIApi.createResponse(payload, { poll_interval: 2, poll_timeout: 60, }); if (!response.success) { console.error('Public LLM proxy error:', response); const status = response.error === 'input_missing' ? 400 : 502; return res.status(status).send(response); } return res.status(200).send({ success: true, model, text: LocalAIApi.extractText(response) || '', response, }); }), ); module.exports = router;