fix litellm capability discovery

This commit is contained in:
Daniel 2026-05-09 15:06:28 +02:00
parent da2d74f51e
commit 37720cec38
2 changed files with 69 additions and 20 deletions

View file

@ -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; }
}
</style>

View file

@ -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 });