fix litellm tts search fallbacks

This commit is contained in:
Daniel 2026-05-09 04:50:55 +02:00
parent 0e9ae8e1af
commit 413b9bc353
4 changed files with 67 additions and 7 deletions

View file

@ -1275,12 +1275,12 @@ function adminFlashButtonBackground(btn, color) {
}
var items = data.voices || [];
if (items.length === 0) {
container.innerHTML = '<p style="font-size:13px;color:var(--g400);">No voices found' + (search ? ' matching "' + esc(search) + '"' : '') + '</p>';
container.innerHTML = '<p style="font-size:13px;color:var(--g400);">No voices/models found' + (search ? ' matching "' + esc(search) + '"' : '') + '</p>';
return;
}
container.innerHTML = '<p style="font-size:12px;color:var(--g500);margin:0 0 6px;">Found ' + data.count + ' voices (provider: ' + esc(data.provider) + ')</p>' +
container.innerHTML = '<p style="font-size:12px;color:var(--g500);margin:0 0 6px;">Found ' + data.count + ' voices/models (provider: ' + esc(data.provider) + ')</p>' +
items.slice(0, 100).map(function(v) {
var isModel = (v.source || '').indexOf('gateway') !== -1 || (v.source || '').indexOf('builtin-model') !== -1;
var isModel = v.kind === 'model' || (v.source || '').indexOf('gateway') !== -1 || (v.source || '').indexOf('builtin-model') !== -1 || (v.source || '').indexOf('configured-model') !== -1;
var setType = isModel ? 'model' : 'voice';
var badge = isModel ? '<span style="font-size:9px;padding:1px 5px;border-radius:4px;background:var(--blue);color:white;margin-left:4px;">MODEL</span>' : '<span style="font-size:9px;padding:1px 5px;border-radius:4px;background:var(--green);color:white;margin-left:4px;">VOICE</span>';
return '<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;background:var(--g50);font-size:13px;">' +

View file

@ -9,7 +9,7 @@ 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, getTTSEnvProvider, getLiteLLMTTSModels, getTTSProvider, getTTSVoiceLists } = require('../utils/ttsProvider');
var { GOOGLE_TTS_VOICES, getTTSEnvProvider, getLiteLLMTTSDiscoveryItems, getLiteLLMTTSModels, getTTSProvider, getTTSVoiceLists } = require('../utils/ttsProvider');
var { getLiteLLMHeaders } = require('../utils/litellm');
var { getSTTDependencies, getLiteLLMSTTModels, getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
var { getLiteLLMEmbeddingModels } = require('../utils/embeddings');
@ -520,13 +520,19 @@ router.get('/config/tts/discover', async function(req, res) {
}
if (provider === 'litellm' && process.env.LITELLM_API_BASE) {
var dbVoice = await db.getSetting('tts.voice') || '';
var dbModel = await db.getSetting('tts.model') || '';
var currentVoice = dbVoice || process.env.LITELLM_TTS_VOICE || '';
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 });
getLiteLLMTTSModels(lResp.data && lResp.data.data).forEach(function(id) {
discovered.push({ id: id, name: id, source: 'gateway-api' });
});
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) {
discovered.push(item);
});
}
if (provider === 'elevenlabs' && process.env.ELEVENLABS_API_KEY) {

View file

@ -39,11 +39,36 @@ function getLiteLLMTTSModels(models) {
.map(function(model) { return model && (model.id || model.model_name) ? (model.id || model.model_name) : String(model || ''); });
}
function pushUniqueTTSItem(items, item) {
if (!item || !item.id) return;
if (items.some(function(existing) { return existing.id === item.id && existing.kind === item.kind; })) return;
items.push(item);
}
function getLiteLLMTTSDiscoveryItems(models, opts) {
opts = opts || {};
var items = [];
getLiteLLMTTSModels(models).forEach(function(id) {
pushUniqueTTSItem(items, { id: id, name: id, source: 'gateway-api', kind: 'model' });
});
if (opts.currentModel) {
pushUniqueTTSItem(items, { id: opts.currentModel, name: opts.currentModel, source: 'configured-model', kind: 'model' });
}
if (opts.currentVoice) {
pushUniqueTTSItem(items, { id: opts.currentVoice, name: opts.currentVoice, source: 'configured-voice', kind: 'voice' });
}
OPENAI_TTS_VOICES.forEach(function(voice) {
pushUniqueTTSItem(items, { id: voice, name: voice, source: 'openai-compatible', kind: 'voice' });
});
return items;
}
module.exports = {
ELEVENLABS_DEFAULT_VOICES,
GOOGLE_TTS_VOICES,
OPENAI_TTS_VOICES,
getTTSEnvProvider,
getLiteLLMTTSDiscoveryItems,
getLiteLLMHeaders,
getLiteLLMTTSModels,
getTTSProvider,

View file

@ -77,6 +77,35 @@ test('LiteLLM TTS model extraction filters gateway model objects', () => {
]), ['local-chatterbox-turbo', 'custom-provider-model']);
});
test('LiteLLM TTS discovery includes metadata models and configured fallbacks', () => {
const ttsProvider = require('../src/utils/ttsProvider');
const items = ttsProvider.getLiteLLMTTSDiscoveryItems([
{ model_name: 'local-kokoro-tts', model_info: { mode: 'audio_speech' } },
{ model_name: 'not-tts-by-name-only' },
{ model_name: 'local-parakeet-v3', model_info: { mode: 'audio_transcription' } }
], {
currentModel: 'local-kokoro-tts',
currentVoice: 'sherpa/kokoro:am_adam'
});
assert.deepEqual(items.slice(0, 3), [
{ id: 'local-kokoro-tts', name: 'local-kokoro-tts', source: 'gateway-api', kind: 'model' },
{ id: 'sherpa/kokoro:am_adam', name: 'sherpa/kokoro:am_adam', source: 'configured-voice', kind: 'voice' },
{ id: 'alloy', name: 'alloy', source: 'openai-compatible', kind: 'voice' }
]);
assert.equal(items.some(function(item) { return item.id === 'not-tts-by-name-only'; }), false);
});
test('LiteLLM TTS discovery still shows configured model if metadata lookup fails', () => {
const ttsProvider = require('../src/utils/ttsProvider');
assert.deepEqual(ttsProvider.getLiteLLMTTSDiscoveryItems([], {
currentModel: 'local-kokoro-tts',
currentVoice: 'sherpa/kokoro:am_adam'
}).slice(0, 2), [
{ id: 'local-kokoro-tts', name: 'local-kokoro-tts', source: 'configured-model', kind: 'model' },
{ id: 'sherpa/kokoro:am_adam', name: 'sherpa/kokoro:am_adam', source: 'configured-voice', kind: 'voice' }
]);
});
test('LiteLLM headers include optional content type without exposing key value', () => {
const ttsProvider = require('../src/utils/ttsProvider');
withEnv({ LITELLM_API_KEY: 'secret-token' }, () => {