From 37720cec382297cfa6187d25db7e6148e9be5ad9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 9 May 2026 15:06:28 +0200 Subject: [PATCH] fix litellm capability discovery --- public/components/assistant.html | 17 +++++++- src/routes/adminConfig.js | 72 +++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/public/components/assistant.html b/public/components/assistant.html index 7d7580e..37e7e39 100644 --- a/public/components/assistant.html +++ b/public/components/assistant.html @@ -105,7 +105,7 @@ .assistant-msg { max-width:900px; margin:0 0 14px; display:grid; gap:6px; } .assistant-msg.user { margin-left:auto; max-width:760px; } .assistant-msg-label { font-size:11px; font-weight:700; color:var(--g400); text-transform:uppercase; letter-spacing:.04em; } -.assistant-bubble { border:1px solid var(--g200); border-radius:14px; padding:12px 14px; background:white; box-shadow:var(--shadow); font-size:13px; line-height:1.75; } +.assistant-bubble { border:1px solid var(--g200); border-radius:14px; padding:12px 14px; background:white; box-shadow:var(--shadow); font-size:13px; line-height:1.75; overflow-wrap:anywhere; } .assistant-msg.user .assistant-bubble { background:var(--blue); color:white; border-color:var(--blue); } .assistant-bubble h1, .assistant-bubble h2, .assistant-bubble h3 { margin:16px 0 8px; line-height:1.25; color:var(--g900); } .assistant-bubble h1:first-child, .assistant-bubble h2:first-child, .assistant-bubble h3:first-child { margin-top:0; } @@ -179,4 +179,19 @@ .assistant-muted { color:var(--g500); font-size:12px; line-height:1.6; } .assistant-mermaid { background:white; border:1px solid var(--g200); border-radius:10px; padding:10px; margin:10px 0; overflow:auto; } @media (max-width: 960px) { .assistant-layout { grid-template-columns:1fr; } .assistant-main { min-height:auto; grid-template-rows:auto minmax(320px,1fr) auto; } } +@media (max-width: 640px) { + .assistant-header { flex-direction:column; align-items:stretch; } + .assistant-status { align-self:flex-start; } + .assistant-toolbar { flex-direction:column; align-items:stretch; } + .assistant-toolbar-actions { display:grid; grid-template-columns:1fr 1fr; } + .assistant-toolbar-actions .btn-sm { width:100%; justify-content:center; } + .assistant-messages { padding:10px; } + .assistant-msg, .assistant-msg.user { max-width:100%; } + .assistant-bubble { font-size:13px; padding:11px 12px; } + .assistant-bubble table { display:block; overflow-x:auto; white-space:nowrap; max-width:100%; } + .assistant-composer { position:sticky; bottom:0; z-index:3; } + .assistant-composer-footer { flex-direction:column; align-items:stretch; } + .assistant-composer-footer .btn-generate { width:100%; } + .assistant-side { gap:10px; } +} diff --git a/src/routes/adminConfig.js b/src/routes/adminConfig.js index 822970c..fadd260 100644 --- a/src/routes/adminConfig.js +++ b/src/routes/adminConfig.js @@ -16,6 +16,44 @@ var { getLiteLLMEmbeddingModels } = require('../utils/embeddings'); router.use(authMiddleware); +function liteLLMBaseUrl() { + return (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, ''); +} + +function searchTerms(search) { + return String(search || '').toLowerCase().split(/[\s,]+/).map(function(term) { return term.trim(); }).filter(Boolean); +} + +function matchesDiscoverySearch(item, search, extra) { + var terms = searchTerms(search); + if (terms.length === 0) return true; + var haystack = [item.id, item.name, item.source, item.kind, item.mode, item.capability, item.dims, extra].filter(Boolean).join(' ').toLowerCase(); + return terms.some(function(term) { return haystack.indexOf(term) !== -1; }); +} + +function liteLLMModelId(model) { + return model && (model.model_name || model.id) ? (model.model_name || model.id) : ''; +} + +function liteLLMModelMode(model) { + return model && model.model_info && model.model_info.mode ? String(model.model_info.mode) : ''; +} + +async function probeLiteLLMEmbeddingDimensions(modelId) { + try { + var axios = require('axios'); + var resp = await axios.post(gatewayUrl('/embeddings'), { + model: modelId, + input: 'dimension probe' + }, { headers: getLiteLLMHeaders('application/json'), timeout: 30000 }); + var embedding = resp.data && resp.data.data && resp.data.data[0] && resp.data.data[0].embedding; + return Array.isArray(embedding) ? embedding.length : '?'; + } catch (e) { + logger.warn('LiteLLM embedding dimension probe failed for ' + modelId + ': ' + e.message); + return '?'; + } +} + // ── GET announcement (any authenticated user) ────────────────────────────── router.get('/config/announcement', async function(req, res) { try { @@ -410,21 +448,17 @@ router.get('/config/image-models/discover', async function(req, res) { var axios = require('axios'); var discovered = []; if (process.env.LITELLM_API_BASE) { - var resp = await axios.get(gatewayUrl('/models'), { headers: getLiteLLMHeaders(), timeout: 15000 }); + var resp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMHeaders(), timeout: 15000 }); var models = (resp.data && resp.data.data) || []; models.forEach(function(m) { - var id = m.id || ''; - if (/image|dall|gpt-image|imagen|flux|stable|sdxl|recraft|ideogram/i.test(id)) { - discovered.push({ id: id, name: id, source: 'litellm-api' }); + if (liteLLMModelMode(m) === 'image_generation') { + var id = liteLLMModelId(m); + if (id) discovered.push({ id: id, name: id, source: 'gateway-api', mode: 'image_generation', capability: 'image' }); } }); } - var builtins = ['openai-gpt-image-1', 'openai-gpt-image-1-mini', 'openai-gpt-image-1.5', 'openai-dall-e-3']; - builtins.forEach(function(id) { - if (!discovered.find(function(m) { return m.id === id; })) discovered.push({ id: id, name: id, source: 'builtin' }); - }); var search = (req.query.q || '').toLowerCase().trim(); - if (search) discovered = discovered.filter(function(m) { return m.id.toLowerCase().indexOf(search) !== -1; }); + if (search) discovered = discovered.filter(function(m) { return matchesDiscoverySearch(m, search, 'image generation'); }); res.json({ success: true, models: discovered, count: discovered.length }); } catch (e) { res.status(500).json({ error: e.message || 'Request failed' }); @@ -501,8 +535,7 @@ router.get('/config/tts/discover', async function(req, res) { var currentModel = dbModel || process.env.LITELLM_TTS_MODEL || ''; var modelInfo = []; try { - var liteLLMBase = (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, ''); - var lResp = await axios.get(liteLLMBase + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 }); + var lResp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 }); modelInfo = lResp.data && lResp.data.data ? lResp.data.data : []; } catch (e) { logger.warn('LiteLLM TTS model list failed: ' + e.message); } getLiteLLMTTSDiscoveryItems(modelInfo, { currentModel: currentModel, currentVoice: currentVoice }).forEach(function(item) { @@ -583,8 +616,7 @@ router.get('/config/stt/discover', async function(req, res) { if (provider === 'litellm' && process.env.LITELLM_API_BASE) { try { - var liteLLMBase = (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, ''); - var sResp = await axios.get(liteLLMBase + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 }); + var sResp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 }); getLiteLLMSTTModels(sResp.data && sResp.data.data).forEach(function(id) { discovered.push({ id: id, name: id, source: 'gateway-api' }); }); @@ -680,17 +712,19 @@ router.get('/config/embeddings/discover', async function(req, res) { if (provider === 'litellm') { try { - var liteLLMBase = (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, ''); - var eResp = await axios.get(liteLLMBase + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 }); - getLiteLLMEmbeddingModels(eResp.data && eResp.data.data).forEach(function(m) { - discovered.push({ id: m.id, name: m.name, dims: m.dims, source: 'gateway-api' }); - }); + var eResp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 }); + var embeddingModels = getLiteLLMEmbeddingModels(eResp.data && eResp.data.data); + for (var i = 0; i < embeddingModels.length; i++) { + var m = embeddingModels[i]; + var dims = m.dims === '?' ? await probeLiteLLMEmbeddingDimensions(m.id) : m.dims; + discovered.push({ id: m.id, name: m.name, dims: dims, source: 'gateway-api', mode: 'embedding', capability: 'embedding' }); + } } catch(e) { logger.warn('LiteLLM embedding metadata discovery failed: ' + e.message); } } if (search) { discovered = discovered.filter(function(d) { - return d.id.toLowerCase().indexOf(search) !== -1 || String(d.name).toLowerCase().indexOf(search) !== -1; + return matchesDiscoverySearch(d, search, 'embedding vector'); }); } res.json({ success: true, provider: provider, models: discovered, count: discovered.length });