From 67d9fa33e50419128aaa8c617c18c08e4cc81793 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 24 May 2026 17:47:22 +0000 Subject: [PATCH] test --- backend/src/index.js | 4 +- backend/src/routes/publicLlm.js | 343 ++++++++++ frontend/src/components/NavBarItem.tsx | 3 +- frontend/src/layouts/Authenticated.tsx | 3 +- frontend/src/pages/index.tsx | 845 +++++++++++++++++++++---- 5 files changed, 1052 insertions(+), 146 deletions(-) create mode 100644 backend/src/routes/publicLlm.js diff --git a/backend/src/index.js b/backend/src/index.js index bcd788a..c1984d2 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -6,7 +6,6 @@ const passport = require('passport'); const path = require('path'); const fs = require('fs'); const bodyParser = require('body-parser'); -const db = require('./db/models'); const config = require('./config'); const swaggerUI = require('swagger-ui-express'); const swaggerJsDoc = require('swagger-jsdoc'); @@ -16,6 +15,7 @@ const fileRoutes = require('./routes/file'); const searchRoutes = require('./routes/search'); const sqlRoutes = require('./routes/sql'); const pexelsRoutes = require('./routes/pexels'); +const publicLlmRoutes = require('./routes/publicLlm'); const openaiRoutes = require('./routes/openai'); @@ -101,7 +101,7 @@ app.use('/api/auth', authRoutes); app.use('/api/file', fileRoutes); app.use('/api/pexels', pexelsRoutes); app.enable('trust proxy'); - +app.use('/api/public-llm', publicLlmRoutes); app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes); diff --git a/backend/src/routes/publicLlm.js b/backend/src/routes/publicLlm.js new file mode 100644 index 0000000..e091df1 --- /dev/null +++ b/backend/src/routes/publicLlm.js @@ -0,0 +1,343 @@ +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; diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..fcbd9b9 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, { useEffect, useRef, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..73d8391 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 254a0a9..55185a9 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,731 @@ - -import React, { useEffect, useState } from 'react'; -import type { ReactElement } from 'react'; +import React, { ReactElement, useEffect, useMemo, useState } from 'react'; import Head from 'next/head'; import Link from 'next/link'; +import axios from 'axios'; import BaseButton from '../components/BaseButton'; import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; -import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; +import NotificationBar from '../components/NotificationBar'; import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; +import LayoutGuest from '../layouts/Guest'; +type EnvironmentEntry = { + key: string; + value: string; +}; + +type ProxyConfig = { + baseUrl: string; + responsesPath: string | null; + resolvedUrl: string; + defaultModel: string; + projectHeader: string; + projectUuid: string | null; + projectId: string | null; + timeoutSeconds: number; + verifyTls: boolean; +}; + +type ProbeResult = { + model: string; + success: boolean; + latencyMs: number; + previewText: string | null; + error: string | null; +}; + +type CurlCommands = { + appProxy: string; + flatlogicProxy: string; + openaiDirect: string; + geminiDirect: string; +}; + +type DiagnosticsPayload = { + generatedAt: string; + requestOrigin: string; + appProxyUrl: string; + defaultPrompt: string; + envVars: EnvironmentEntry[]; + llmEnvVars: EnvironmentEntry[]; + proxyConfig: ProxyConfig; + probeCandidates: string[]; + probeResults: ProbeResult[]; + availableModels: string[]; + unavailableModels: string[]; + curlCommands: CurlCommands; +}; + +type ProxyResponsePayload = { + success: boolean; + model: string; + text: string; + response: Record; +}; + +type FeedbackState = { + type: 'success' | 'error'; + text: string; +} | null; + +const shellQuote = (value: string) => `'${String(value ?? '').replace(/'/g, `'"'"'`)}'`; + +const joinCurlLines = (lines: string[]) => lines + .filter(Boolean) + .map((line, index, allLines) => (index < allLines.length - 1 ? `${line} \\` : line)) + .join('\n'); + +const buildResponsePayload = (model: string, prompt: string) => ({ + model, + input: [{ role: 'user', content: prompt }], +}); + +const buildGeminiPayload = (prompt: string) => ({ + contents: [ + { + role: 'user', + parts: [{ text: prompt }], + }, + ], +}); + +const formatDateTime = (value?: string) => { + if (!value) { + return '—'; + } + + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return value; + } + + return date.toLocaleString(); +}; export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('video'); - const [contentPosition, setContentPosition] = useState('left'); - const textColor = useAppSelector((state) => state.style.linkColor); + const [diagnostics, setDiagnostics] = useState(null); + const [isLoadingDiagnostics, setIsLoadingDiagnostics] = useState(true); + const [diagnosticsError, setDiagnosticsError] = useState(''); + const [prompt, setPrompt] = useState('Reply with a concise health check for this LLM proxy.'); + const [selectedModel, setSelectedModel] = useState(''); + const [envSearch, setEnvSearch] = useState(''); + const [proxyResponse, setProxyResponse] = useState(null); + const [proxyError, setProxyError] = useState(''); + const [isProxying, setIsProxying] = useState(false); + const [feedback, setFeedback] = useState(null); - const title = 'App Preview' + const loadDiagnostics = async (forceRefresh = false) => { + setIsLoadingDiagnostics(true); + setDiagnosticsError(''); - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); + try { + const response = await axios.get( + forceRefresh ? '/public-llm/diagnostics?refresh=true' : '/public-llm/diagnostics', + ); + const payload = response.data; + setDiagnostics(payload); + setPrompt((currentPrompt) => currentPrompt || payload.defaultPrompt); + setSelectedModel((currentModel) => currentModel || payload.availableModels[0] || payload.proxyConfig.defaultModel); + } catch (error) { + console.error('Failed to load diagnostics:', error); + setDiagnosticsError(error instanceof Error ? error.message : 'Failed to load diagnostics.'); + } finally { + setIsLoadingDiagnostics(false); + } + }; - const imageBlock = (image) => ( - - ); + useEffect(() => { + loadDiagnostics(); + }, []); - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; + const environmentMap = useMemo(() => { + const entries = diagnostics?.envVars || []; + return new Map(entries.map((entry) => [entry.key, entry.value])); + }, [diagnostics]); + + const activeModel = useMemo(() => { + if (!diagnostics) { + return selectedModel || 'gpt-5-mini'; + } + + return selectedModel.trim() || diagnostics.availableModels[0] || diagnostics.proxyConfig.defaultModel; + }, [diagnostics, selectedModel]); + + const activePrompt = useMemo(() => { + return prompt.trim() || diagnostics?.defaultPrompt || 'Reply with OK only.'; + }, [diagnostics?.defaultPrompt, prompt]); + + const filteredEnvVars = useMemo(() => { + const query = envSearch.trim().toLowerCase(); + const entries = diagnostics?.envVars || []; + + if (!query) { + return entries; + } + + return entries.filter((entry) => { + const haystack = `${entry.key} ${entry.value}`.toLowerCase(); + return haystack.includes(query); + }); + }, [diagnostics?.envVars, envSearch]); + + const curlCards = useMemo(() => { + if (!diagnostics) { + return []; + } + + const openAiKey = environmentMap.get('OPENAI_API_KEY') || ''; + const geminiKey = environmentMap.get('GEMINI_API_KEY') || environmentMap.get('GOOGLE_API_KEY') || ''; + const geminiModelFromEnv = (environmentMap.get('GEMINI_MODEL') || '').trim(); + const geminiModel = geminiModelFromEnv.startsWith('gemini-') ? geminiModelFromEnv : 'gemini-2.5-flash'; + + const appProxyCommand = joinCurlLines([ + `curl -sS ${shellQuote(diagnostics.appProxyUrl)}`, + '-X POST', + "-H 'Content-Type: application/json'", + `--data ${shellQuote(JSON.stringify({ model: activeModel, prompt: activePrompt }))}`, + ]); + + const flatlogicProxyCommand = joinCurlLines([ + `curl -sS ${shellQuote(diagnostics.proxyConfig.resolvedUrl)}`, + '-X POST', + "-H 'Content-Type: application/json'", + `-H ${shellQuote(`${diagnostics.proxyConfig.projectHeader}: ${diagnostics.proxyConfig.projectUuid || ''}`)}`, + `--data ${shellQuote(JSON.stringify({ + ...buildResponsePayload(activeModel, activePrompt), + project_uuid: diagnostics.proxyConfig.projectUuid, + }))}`, + ]); + + const openAiCommand = joinCurlLines([ + `curl -sS ${shellQuote('https://api.openai.com/v1/responses')}`, + '-X POST', + "-H 'Content-Type: application/json'", + `-H ${shellQuote(`Authorization: Bearer ${openAiKey}`)}`, + `--data ${shellQuote(JSON.stringify(buildResponsePayload(activeModel, activePrompt)))}`, + ]); + + const geminiCommand = joinCurlLines([ + `curl -sS ${shellQuote(`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(geminiModel)}:generateContent?key=${encodeURIComponent(geminiKey)}`)}`, + '-X POST', + "-H 'Content-Type: application/json'", + `--data ${shellQuote(JSON.stringify(buildGeminiPayload(activePrompt)))}`, + ]); + + return [ + { + id: 'appProxy', + title: 'App proxy curl', + description: 'Calls the new public reverse-proxy endpoint exposed by this app.', + command: appProxyCommand, + }, + { + id: 'flatlogicProxy', + title: 'Flatlogic AI proxy curl', + description: 'Uses the project UUID header and the configured Flatlogic AI proxy URL.', + command: flatlogicProxyCommand, + }, + { + id: 'openaiDirect', + title: 'OpenAI direct curl', + description: 'Prints the full OpenAI URL and API key from the current environment.', + command: openAiCommand, + }, + { + id: 'geminiDirect', + title: 'Gemini direct curl', + description: 'Prints a Gemini generateContent command with the current API key.', + command: geminiCommand, + }, + ]; + }, [activeModel, activePrompt, diagnostics, environmentMap]); + + const handleCopy = async (value: string, label: string) => { + try { + await navigator.clipboard.writeText(value); + setFeedback({ type: 'success', text: `${label} copied.` }); + window.setTimeout(() => { + setFeedback((current) => (current?.text === `${label} copied.` ? null : current)); + }, 2200); + } catch (error) { + console.error(`Failed to copy ${label}:`, error); + setFeedback({ type: 'error', text: `Failed to copy ${label}.` }); + } + }; + + const handleProxySubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setProxyError(''); + setProxyResponse(null); + setIsProxying(true); + + try { + const response = await axios.post('/public-llm/proxy', { + model: activeModel, + prompt: activePrompt, + }); + + setProxyResponse(response.data); + } catch (error) { + console.error('Proxy request failed:', error); + setProxyError(error instanceof Error ? error.message : 'Proxy request failed.'); + } finally { + setIsProxying(false); + } + }; + + const envExportText = useMemo(() => { + return (diagnostics?.envVars || []) + .map((entry) => `${entry.key}=${entry.value}`) + .join('\n'); + }, [diagnostics?.envVars]); + + const stats = [ + { + label: 'Visible env vars', + value: diagnostics?.envVars.length ?? '—', + }, + { + label: 'LLM env keys', + value: diagnostics?.llmEnvVars.length ?? '—', + }, + { + label: 'Available models', + value: diagnostics?.availableModels.length ?? '—', + }, + { + label: 'Failed probes', + value: diagnostics?.unavailableModels.length ?? '—', + }, + ]; return ( -
+ <> - {getPageTitle('Starter Page')} + {getPageTitle('LLM Proxy Console')} - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - +
+
+
+
- - +
+
+
+

+ Public LLM reverse proxy diagnostics +

+

+ LLM Proxy Console with live probes, full env dump, and copy-ready curl commands. +

+

+ 首页现在直接展示系统环境变量、检测到的 LLM 参数、可用模型探测结果,以及一个可立即发送 prompt 的反向代理测试区。 +

+
+ +
+ + +
+
+ + {feedback && ( + + {feedback.text} + + )} + + {diagnosticsError && ( + + Diagnostics failed to load: {diagnosticsError} + + )} + +
+ {stats.map((stat) => ( +
+
{stat.label}
+
{stat.value}
+
+ ))} +
+ +
+ +
+
+

Probe summary

+

Model availability and proxy wiring

+

+ These probes are executed against the configured Flatlogic AI proxy using live project credentials. Refresh to rerun the checks. +

+
+ +
+ + +
+
+ +
+
+
+
Available via proxy
+
+ {(diagnostics?.availableModels || []).length > 0 ? ( + diagnostics?.availableModels.map((model) => ( + + )) + ) : ( +

No successful probes yet.

+ )} +
+
+ +
+
Failed candidates
+
+ {(diagnostics?.probeResults || []).filter((result) => !result.success).length > 0 ? ( + diagnostics?.probeResults + .filter((result) => !result.success) + .map((result) => ( +
+
+ {result.model} + {result.latencyMs} ms +
+

{result.error}

+
+ )) + ) : ( +

No failed probes in the latest run.

+ )} +
+
+
+ +
+
+
Proxy config
+
+
+
Base URL
+
{diagnostics?.proxyConfig.baseUrl || '—'}
+
+
+
Response path
+
{diagnostics?.proxyConfig.responsesPath || '—'}
+
+
+
Resolved URL
+
{diagnostics?.proxyConfig.resolvedUrl || '—'}
+
+
+
Project header
+
{diagnostics?.proxyConfig.projectHeader || '—'}
+
+
+
Project UUID
+
{diagnostics?.proxyConfig.projectUuid || '—'}
+
+
+
Default model
+
{diagnostics?.proxyConfig.defaultModel || '—'}
+
+
+
Refreshed
+
{formatDateTime(diagnostics?.generatedAt)}
+
+
+
+ +
+
Quick admin links
+
+ {[ + { href: '/dashboard', label: 'Dashboard' }, + { href: '/llm_requests/llm_requests-list', label: 'LLM requests' }, + { href: '/model_catalog/model_catalog-list', label: 'Model catalog' }, + { href: '/environment_variables/environment_variables-list', label: 'Environment variables' }, + ].map((item) => ( + + {item.label} + + ))} +
+
+
+
+
+ + +
+

Reverse proxy test

+

Send a live prompt through the app proxy

+

+ This uses /api/public-llm/proxy and returns the extracted model text plus the raw upstream payload. +

+
+ +
+
+ + setSelectedModel(event.target.value)} + className="w-full rounded-2xl border border-white/10 bg-slate-950/40 px-4 py-3 font-mono text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-cyan-300/60 focus:ring-4 focus:ring-cyan-400/20" + placeholder="gpt-5-mini" + /> +
+ +
+ +