fix litellm metadata model discovery
This commit is contained in:
parent
091cf5bc4b
commit
0e9ae8e1af
5 changed files with 92 additions and 36 deletions
|
|
@ -9,9 +9,10 @@ var { authMiddleware, adminMiddleware } = require('../middleware/auth');
|
|||
var PROMPTS = require('../utils/prompts');
|
||||
var logger = require('../utils/logger');
|
||||
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 { getSTTDependencies, getLiteLLMSTTModels, getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
|
||||
var { getLiteLLMEmbeddingModels } = require('../utils/embeddings');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
|
|
@ -570,7 +571,16 @@ router.post('/config/tts/test', async function(req, res) {
|
|||
var db = require('../db/database');
|
||||
var adminModel = await db.getSetting('tts.model') || '';
|
||||
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 rawModel = adminModel || process.env.LITELLM_TTS_MODEL || 'tts-1';
|
||||
var ttsModel = rawModel;
|
||||
|
|
@ -659,7 +669,6 @@ router.get('/config/stt/discover', async function(req, res) {
|
|||
}
|
||||
|
||||
if (provider === 'litellm' && process.env.LITELLM_API_BASE) {
|
||||
// base URL handled by gatewayUrl() helper
|
||||
try {
|
||||
var liteLLMBase = (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, '');
|
||||
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 adminSttModel = await db.getSetting('stt.model') || '';
|
||||
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) {
|
||||
var ext = mime.split('/')[1] || 'webm';
|
||||
|
|
@ -820,23 +833,20 @@ router.get('/config/embeddings/discover', async function(req, res) {
|
|||
];
|
||||
|
||||
if (provider === 'litellm') {
|
||||
// base URL handled by gatewayUrl() helper
|
||||
try {
|
||||
var eResp = await axios.get(gatewayUrl('/models'), { headers: getLiteLLMHeaders(), timeout: 10000 });
|
||||
if (eResp.data && eResp.data.data) {
|
||||
eResp.data.data.forEach(function(m) {
|
||||
if ((m.id || '').toLowerCase().indexOf('embed') !== -1) {
|
||||
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 discovery failed: ' + e.message); }
|
||||
knownVertex.forEach(function(m) {
|
||||
if (!discovered.find(function(d) { return d.id === m.id; })) {
|
||||
discovered.push(Object.assign({ source: 'builtin' }, m));
|
||||
}
|
||||
});
|
||||
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' });
|
||||
});
|
||||
} 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; })) {
|
||||
discovered.push(Object.assign({ source: 'builtin' }, m));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'vertex') {
|
||||
|
|
|
|||
|
|
@ -239,6 +239,34 @@ function stripHtml(html) {
|
|||
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)
|
||||
*/
|
||||
|
|
@ -254,8 +282,10 @@ function isEmbeddingsAvailable() {
|
|||
module.exports = {
|
||||
generateEmbedding,
|
||||
generateContentEmbedding,
|
||||
getLiteLLMEmbeddingModels,
|
||||
searchSimilar,
|
||||
isEmbeddingsAvailable,
|
||||
isLiteLLMEmbeddingModel,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_DIMS
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
var mode = model && model.model_info && model.model_info.mode ? String(model.model_info.mode) : '';
|
||||
return mode === 'audio_speech';
|
||||
|
|
@ -52,6 +48,5 @@ module.exports = {
|
|||
getLiteLLMTTSModels,
|
||||
getTTSProvider,
|
||||
getTTSVoiceLists,
|
||||
isLiteLLMTTSModel,
|
||||
isOpenAITTSModel
|
||||
isLiteLLMTTSModel
|
||||
};
|
||||
|
|
|
|||
25
test/embeddings-provider.test.js
Normal file
25
test/embeddings-provider.test.js
Normal 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: '?' }
|
||||
]);
|
||||
});
|
||||
|
|
@ -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', () => {
|
||||
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-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({ id: 'custom-provider-model', model_info: { mode: 'audio_speech' } }), true);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'local-parakeet-v3', model_info: { mode: 'audio_transcription' } }), false);
|
||||
|
|
|
|||
Loading…
Reference in a new issue