From 413b9bc353747d899780cf7d3e682d616f6fa8a4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 9 May 2026 04:50:55 +0200 Subject: [PATCH] fix litellm tts search fallbacks --- public/js/admin.js | 6 +++--- src/routes/adminConfig.js | 14 ++++++++++---- src/utils/ttsProvider.js | 25 +++++++++++++++++++++++++ test/tts-provider.test.js | 29 +++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index 144a2db..95c6843 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1275,12 +1275,12 @@ function adminFlashButtonBackground(btn, color) { } var items = data.voices || []; if (items.length === 0) { - container.innerHTML = '

No voices found' + (search ? ' matching "' + esc(search) + '"' : '') + '

'; + container.innerHTML = '

No voices/models found' + (search ? ' matching "' + esc(search) + '"' : '') + '

'; return; } - container.innerHTML = '

Found ' + data.count + ' voices (provider: ' + esc(data.provider) + ')

' + + container.innerHTML = '

Found ' + data.count + ' voices/models (provider: ' + esc(data.provider) + ')

' + 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 ? 'MODEL' : 'VOICE'; return '
' + diff --git a/src/routes/adminConfig.js b/src/routes/adminConfig.js index 2811920..4390508 100644 --- a/src/routes/adminConfig.js +++ b/src/routes/adminConfig.js @@ -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) { diff --git a/src/utils/ttsProvider.js b/src/utils/ttsProvider.js index e278de9..ab6b35e 100644 --- a/src/utils/ttsProvider.js +++ b/src/utils/ttsProvider.js @@ -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, diff --git a/test/tts-provider.test.js b/test/tts-provider.test.js index b6791ab..219ec4d 100644 --- a/test/tts-provider.test.js +++ b/test/tts-provider.test.js @@ -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' }, () => {