fix litellm metadata model discovery

This commit is contained in:
Daniel 2026-05-09 04:46:06 +02:00
parent 091cf5bc4b
commit 0e9ae8e1af
5 changed files with 92 additions and 36 deletions

View file

@ -9,9 +9,10 @@ var { authMiddleware, adminMiddleware } = require('../middleware/auth');
var PROMPTS = require('../utils/prompts'); var PROMPTS = require('../utils/prompts');
var logger = require('../utils/logger'); var logger = require('../utils/logger');
var { gatewayUrl } = require('../utils/errors'); var { gatewayUrl } = require('../utils/errors');
var { GOOGLE_TTS_VOICES, OPENAI_TTS_VOICES, getTTSEnvProvider, getLiteLLMTTSModels, getTTSProvider, getTTSVoiceLists } = require('../utils/ttsProvider'); var { GOOGLE_TTS_VOICES, getTTSEnvProvider, getLiteLLMTTSModels, getTTSProvider, getTTSVoiceLists } = require('../utils/ttsProvider');
var { getLiteLLMHeaders } = require('../utils/litellm'); var { getLiteLLMHeaders } = require('../utils/litellm');
var { getSTTDependencies, getLiteLLMSTTModels, getSTTModelLists, getSTTProvider } = require('../utils/sttProvider'); var { getSTTDependencies, getLiteLLMSTTModels, getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
var { getLiteLLMEmbeddingModels } = require('../utils/embeddings');
router.use(authMiddleware); router.use(authMiddleware);
@ -570,7 +571,16 @@ router.post('/config/tts/test', async function(req, res) {
var db = require('../db/database'); var db = require('../db/database');
var adminModel = await db.getSetting('tts.model') || ''; var adminModel = await db.getSetting('tts.model') || '';
var adminVoice = await db.getSetting('tts.voice') || ''; var adminVoice = await db.getSetting('tts.voice') || '';
if (!adminModel && adminVoice && (adminVoice.indexOf('/') !== -1 || /^(openai-tts|tts-1|openai-gpt.*tts)/i.test(adminVoice))) { adminModel = adminVoice; adminVoice = ''; } if (!adminModel && adminVoice) {
try {
var liteLLMBase = (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, '');
var modelInfoResp = await axios.get(liteLLMBase + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 });
if (getLiteLLMTTSModels(modelInfoResp.data && modelInfoResp.data.data).indexOf(adminVoice) !== -1) {
adminModel = adminVoice;
adminVoice = '';
}
} catch (e) { logger.warn('LiteLLM TTS mode lookup failed: ' + e.message); }
}
var resolvedVoice = voice || adminVoice || ''; var resolvedVoice = voice || adminVoice || '';
var rawModel = adminModel || process.env.LITELLM_TTS_MODEL || 'tts-1'; var rawModel = adminModel || process.env.LITELLM_TTS_MODEL || 'tts-1';
var ttsModel = rawModel; var ttsModel = rawModel;
@ -659,7 +669,6 @@ router.get('/config/stt/discover', async function(req, res) {
} }
if (provider === 'litellm' && process.env.LITELLM_API_BASE) { if (provider === 'litellm' && process.env.LITELLM_API_BASE) {
// base URL handled by gatewayUrl() helper
try { try {
var liteLLMBase = (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, ''); 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(liteLLMBase + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 });
@ -723,9 +732,13 @@ router.post('/config/stt/test', async function(req, res) {
var db = require('../db/database'); var db = require('../db/database');
var adminSttModel = await db.getSetting('stt.model') || ''; var adminSttModel = await db.getSetting('stt.model') || '';
var sttModel = adminSttModel || process.env.LITELLM_STT_MODEL || 'gemini-2.0-flash'; var sttModel = adminSttModel || process.env.LITELLM_STT_MODEL || 'gemini-2.0-flash';
// base URL handled by gatewayUrl() helper
var isTranscriptionModel = /whisper|parakeet|deepgram|nova|groq|scribe|elevenlabs|transcri/i.test(sttModel); var isTranscriptionModel = false;
try {
var liteLLMBase = (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, '');
var modelInfoResp = await axios.get(liteLLMBase + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 });
isTranscriptionModel = getLiteLLMSTTModels(modelInfoResp.data && modelInfoResp.data.data).indexOf(sttModel) !== -1;
} catch (e) { logger.warn('LiteLLM STT mode lookup failed: ' + e.message); }
if (isTranscriptionModel) { if (isTranscriptionModel) {
var ext = mime.split('/')[1] || 'webm'; var ext = mime.split('/')[1] || 'webm';
@ -820,23 +833,20 @@ router.get('/config/embeddings/discover', async function(req, res) {
]; ];
if (provider === 'litellm') { if (provider === 'litellm') {
// base URL handled by gatewayUrl() helper
try { try {
var eResp = await axios.get(gatewayUrl('/models'), { headers: getLiteLLMHeaders(), timeout: 10000 }); var liteLLMBase = (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, '');
if (eResp.data && eResp.data.data) { var eResp = await axios.get(liteLLMBase + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 });
eResp.data.data.forEach(function(m) { getLiteLLMEmbeddingModels(eResp.data && eResp.data.data).forEach(function(m) {
if ((m.id || '').toLowerCase().indexOf('embed') !== -1) { discovered.push({ id: m.id, name: m.name, dims: m.dims, source: 'gateway-api' });
var dims = m.id.indexOf('3-large') !== -1 ? 3072 : m.id.indexOf('3-small') !== -1 ? 1536 : m.id.indexOf('005') !== -1 ? 768 : '?'; });
discovered.push({ id: m.id, name: m.id, dims: dims, source: 'gateway-api' }); } catch(e) { logger.warn('LiteLLM embedding metadata discovery failed: ' + e.message); }
} if (discovered.length === 0) {
}); knownVertex.forEach(function(m) {
} if (!discovered.find(function(d) { return d.id === m.id; })) {
} catch(e) { logger.warn('LiteLLM embedding discovery failed: ' + e.message); } discovered.push(Object.assign({ source: 'builtin' }, m));
knownVertex.forEach(function(m) { }
if (!discovered.find(function(d) { return d.id === m.id; })) { });
discovered.push(Object.assign({ source: 'builtin' }, m)); }
}
});
} }
if (provider === 'vertex') { if (provider === 'vertex') {

View file

@ -239,6 +239,34 @@ function stripHtml(html) {
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
} }
function getLiteLLMModelId(model) {
if (!model) return '';
if (typeof model === 'string') return model;
return model.id || model.model_name || '';
}
function isLiteLLMEmbeddingModel(model) {
var mode = model && model.model_info && model.model_info.mode ? String(model.model_info.mode) : '';
return mode === 'embedding';
}
function getLiteLLMEmbeddingDimensions(model) {
var info = model && model.model_info ? model.model_info : {};
var dims = info.output_vector_size || info.dimensions || info.embedding_dimensions || model.output_vector_size || model.dimensions || model.embedding_dimensions;
var parsed = parseInt(dims, 10);
return Number.isFinite(parsed) ? parsed : '?';
}
function getLiteLLMEmbeddingModels(models) {
return (models || [])
.filter(isLiteLLMEmbeddingModel)
.map(function(model) {
var id = getLiteLLMModelId(model);
return { id: id, name: id, dims: getLiteLLMEmbeddingDimensions(model) };
})
.filter(function(model) { return !!model.id; });
}
/** /**
* Check if embeddings are available (provider configured) * Check if embeddings are available (provider configured)
*/ */
@ -254,8 +282,10 @@ function isEmbeddingsAvailable() {
module.exports = { module.exports = {
generateEmbedding, generateEmbedding,
generateContentEmbedding, generateContentEmbedding,
getLiteLLMEmbeddingModels,
searchSimilar, searchSimilar,
isEmbeddingsAvailable, isEmbeddingsAvailable,
isLiteLLMEmbeddingModel,
DEFAULT_MODEL, DEFAULT_MODEL,
DEFAULT_DIMS DEFAULT_DIMS
}; };

View file

@ -28,10 +28,6 @@ function getTTSVoiceLists() {
}; };
} }
function isOpenAITTSModel(modelId) {
return /^openai[-/](tts-1|tts-1-hd|gpt-4o-mini-tts|gpt-audio)/.test(String(modelId || '').toLowerCase());
}
function isLiteLLMTTSModel(model) { function isLiteLLMTTSModel(model) {
var mode = model && model.model_info && model.model_info.mode ? String(model.model_info.mode) : ''; var mode = model && model.model_info && model.model_info.mode ? String(model.model_info.mode) : '';
return mode === 'audio_speech'; return mode === 'audio_speech';
@ -52,6 +48,5 @@ module.exports = {
getLiteLLMTTSModels, getLiteLLMTTSModels,
getTTSProvider, getTTSProvider,
getTTSVoiceLists, getTTSVoiceLists,
isLiteLLMTTSModel, isLiteLLMTTSModel
isOpenAITTSModel
}; };

View file

@ -0,0 +1,25 @@
const { test } = require('node:test');
const assert = require('node:assert/strict');
test('LiteLLM embedding discovery uses embedding metadata only', () => {
const embeddings = require('../src/utils/embeddings');
assert.equal(embeddings.isLiteLLMEmbeddingModel({ id: 'openai-text-embedding-3-large' }), false);
assert.equal(embeddings.isLiteLLMEmbeddingModel({ id: 'mistral-codestral-embed' }), false);
assert.equal(embeddings.isLiteLLMEmbeddingModel({ model_name: 'openai-text-embedding-3-large', model_info: { mode: 'embedding' } }), true);
assert.equal(embeddings.isLiteLLMEmbeddingModel({ model_name: 'local-parakeet-v3', model_info: { mode: 'audio_transcription' } }), false);
});
test('LiteLLM embedding extraction includes metadata dimensions', () => {
const embeddings = require('../src/utils/embeddings');
assert.deepEqual(embeddings.getLiteLLMEmbeddingModels([
{ model_name: 'openai-text-embedding-3-large', model_info: { mode: 'embedding', output_vector_size: 3072 } },
{ model_name: 'openai-text-embedding-3-small', model_info: { mode: 'embedding', dimensions: '1536' } },
{ model_name: 'mistral-embed', model_info: { mode: 'embedding' } },
{ model_name: 'looks-like-embed' },
{ model_name: 'local-kokoro-tts', model_info: { mode: 'audio_speech' } }
]), [
{ id: 'openai-text-embedding-3-large', name: 'openai-text-embedding-3-large', dims: 3072 },
{ id: 'openai-text-embedding-3-small', name: 'openai-text-embedding-3-small', dims: 1536 },
{ id: 'mistral-embed', name: 'mistral-embed', dims: '?' }
]);
});

View file

@ -48,19 +48,15 @@ test('LiteLLM TTS voice list stays OpenAI-compatible', () => {
]); ]);
}); });
test('TTS model discovery keeps OpenAI speech models only', () => {
const ttsProvider = require('../src/utils/ttsProvider');
assert.equal(ttsProvider.isOpenAITTSModel('openai/tts-1'), true);
assert.equal(ttsProvider.isOpenAITTSModel('openai-tts-1-hd'), true);
assert.equal(ttsProvider.isOpenAITTSModel('openai-gpt-4o-mini-tts'), true);
assert.equal(ttsProvider.isOpenAITTSModel('mistral-voxtral-mini-tts'), false);
assert.equal(ttsProvider.isOpenAITTSModel('vertex-gemini-2.5-flash-tts'), false);
});
test('TTS model discovery uses audio_speech metadata only', () => { test('TTS model discovery uses audio_speech metadata only', () => {
const ttsProvider = require('../src/utils/ttsProvider'); const ttsProvider = require('../src/utils/ttsProvider');
assert.equal(ttsProvider.isOpenAITTSModel, undefined);
assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'local-chatterbox-turbo' }), false); assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'local-chatterbox-turbo' }), false);
assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'local-kokoro-tts' }), false); assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'local-kokoro-tts' }), false);
assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'openai-tts-1' }), false);
assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'openai-gpt-4o-mini-tts' }), false);
assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'mistral-voxtral-mini-tts' }), false);
assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'vertex-gemini-2.5-flash-tts' }), false);
assert.equal(ttsProvider.isLiteLLMTTSModel({ model_name: 'local-chatterbox-turbo', model_info: { mode: 'audio_speech' } }), true); assert.equal(ttsProvider.isLiteLLMTTSModel({ model_name: 'local-chatterbox-turbo', model_info: { mode: 'audio_speech' } }), true);
assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'custom-provider-model', model_info: { mode: 'audio_speech' } }), true); assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'custom-provider-model', model_info: { mode: 'audio_speech' } }), true);
assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'local-parakeet-v3', model_info: { mode: 'audio_transcription' } }), false); assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'local-parakeet-v3', model_info: { mode: 'audio_transcription' } }), false);