40064-vm/backend/src/routes/publicLlm.js
Flatlogic Bot 67d9fa33e5 test
2026-05-24 17:47:22 +00:00

344 lines
9.4 KiB
JavaScript

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;