fix litellm speech model discovery
This commit is contained in:
parent
18061b45bd
commit
c1b6a5176b
8 changed files with 92 additions and 40 deletions
|
|
@ -6,10 +6,15 @@ services:
|
|||
- "127.0.0.1:3552:3000"
|
||||
env_file:
|
||||
- .env
|
||||
- /root/litellm/.env
|
||||
environment:
|
||||
CLINICAL_ASSISTANT_MCP_URL: http://mcp:8000/mcp
|
||||
REDIS_URL: redis://ped-ai-redis:6379
|
||||
LOKI_URL: http://monitoring-loki:3100
|
||||
LITELLM_API_BASE: http://litellm:4000
|
||||
TTS_PROVIDER: litellm
|
||||
LITELLM_TTS_MODEL: local-kokoro-tts
|
||||
LITELLM_TTS_VOICE: sherpa/kokoro:am_adam
|
||||
volumes:
|
||||
- scribe-logs:/app/data/logs
|
||||
- clinical-assistant-mcp-data:/app/mcp-data:ro
|
||||
|
|
@ -24,6 +29,7 @@ services:
|
|||
- default
|
||||
- mcp-server_default
|
||||
- monitoring_default
|
||||
- speech_net
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
|
|
@ -80,3 +86,5 @@ networks:
|
|||
external: true
|
||||
monitoring_default:
|
||||
external: true
|
||||
speech_net:
|
||||
external: true
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ 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 { getLiteLLMHeaders } = require('../utils/litellm');
|
||||
var { getSTTDependencies, getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
|
||||
var { getSTTDependencies, getLiteLLMSTTModels, getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
|
|
@ -478,9 +478,9 @@ router.get('/config/tts', async function(req, res) {
|
|||
envVoice: envVoice,
|
||||
envModel: envModel,
|
||||
configured: {
|
||||
google: isGoogleTTSConfigured(),
|
||||
google: false,
|
||||
litellm: !!process.env.LITELLM_API_BASE,
|
||||
elevenlabs: !!process.env.ELEVENLABS_API_KEY
|
||||
elevenlabs: false
|
||||
},
|
||||
voices: getTTSVoiceLists()
|
||||
});
|
||||
|
|
@ -519,32 +519,13 @@ router.get('/config/tts/discover', async function(req, res) {
|
|||
}
|
||||
|
||||
if (provider === 'litellm' && process.env.LITELLM_API_BASE) {
|
||||
// base URL handled by gatewayUrl() helper
|
||||
try {
|
||||
var lResp = await axios.get(gatewayUrl('/models'), { headers: getLiteLLMHeaders(), timeout: 10000 });
|
||||
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' });
|
||||
});
|
||||
} catch (e) { logger.warn('LiteLLM TTS model list failed: ' + e.message); }
|
||||
|
||||
// OpenAI voices
|
||||
OPENAI_TTS_VOICES.forEach(function(v) {
|
||||
discovered.push({ id: v, name: v + ' (OpenAI voice)', source: 'openai-voices' });
|
||||
});
|
||||
|
||||
// ElevenLabs voices — fetch from API if key available
|
||||
if (process.env.ELEVENLABS_API_KEY) {
|
||||
try {
|
||||
var elResp = await axios.get('https://api.elevenlabs.io/v1/voices', {
|
||||
headers: { 'xi-api-key': process.env.ELEVENLABS_API_KEY }, timeout: 10000
|
||||
});
|
||||
if (elResp.data && elResp.data.voices) {
|
||||
elResp.data.voices.forEach(function(v) {
|
||||
discovered.push({ id: v.voice_id, name: v.name + ' (ElevenLabs)', source: 'elevenlabs-api' });
|
||||
});
|
||||
}
|
||||
} catch (e) { logger.warn('ElevenLabs voice fetch failed: ' + e.message); }
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'elevenlabs' && process.env.ELEVENLABS_API_KEY) {
|
||||
|
|
@ -680,16 +661,17 @@ 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 sResp = await axios.get(gatewayUrl('/models'), { headers: getLiteLLMHeaders(), timeout: 10000 });
|
||||
if (sResp.data && sResp.data.data) {
|
||||
sResp.data.data.forEach(function(m) {
|
||||
var mid = (m.id || '').toLowerCase();
|
||||
if (mid.indexOf('whisper') !== -1 || mid.indexOf('parakeet') !== -1 || mid.indexOf('gemini') !== -1 || mid.indexOf('audio') !== -1 || mid.indexOf('transcri') !== -1 || mid.indexOf('scribe') !== -1 || mid.indexOf('nova') !== -1 || mid.indexOf('deepgram') !== -1 || mid.indexOf('eleven') !== -1 || mid.indexOf('chirp') !== -1) {
|
||||
discovered.push({ id: m.id, name: m.id, source: 'gateway-api' });
|
||||
}
|
||||
});
|
||||
}
|
||||
var liteLLMBase = (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, '');
|
||||
var sResp = await axios.get(liteLLMBase + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 });
|
||||
getLiteLLMSTTModels(sResp.data && sResp.data.data).forEach(function(id) {
|
||||
discovered.push({ id: id, name: id, source: 'gateway-api' });
|
||||
});
|
||||
} catch(e) { logger.warn('STT discovery failed: ' + e.message); }
|
||||
if (discovered.length === 0) {
|
||||
getSTTModelLists().litellm.forEach(function(id) {
|
||||
discovered.push({ id: id, name: id, source: 'configured-default' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === 'local') {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ const { synthesizeWithGoogleTTS } = require('../utils/ttsGoogle');
|
|||
const { authMiddleware } = require('../middleware/auth');
|
||||
var logger = require('../utils/logger');
|
||||
var { gatewayUrl } = require('../utils/errors');
|
||||
var { getLiteLLMHeaders, getTTSProvider } = require('../utils/ttsProvider');
|
||||
var { getTTSProvider } = require('../utils/ttsProvider');
|
||||
var { getLiteLLMHeaders } = require('../utils/litellm');
|
||||
|
||||
// Provider priority (auto-detect):
|
||||
// TTS_PROVIDER=litellm → LiteLLM proxy at /v1/audio/speech (default when LITELLM_API_BASE is set)
|
||||
// TTS_PROVIDER=litellm → LiteLLM proxy at /v1/audio/speech
|
||||
// TTS_PROVIDER=google → Google Cloud TTS direct (HIPAA eligible, opt-in)
|
||||
// TTS_PROVIDER=elevenlabs → ElevenLabs direct (not HIPAA, opt-in)
|
||||
// Auto: litellm > google > elevenlabs
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
function getLiteLLMHeaders(contentType) {
|
||||
var headers = {};
|
||||
if (contentType) headers['Content-Type'] = contentType;
|
||||
if (process.env.LITELLM_API_KEY) headers.Authorization = 'Bearer ' + process.env.LITELLM_API_KEY;
|
||||
var key = process.env.LITELLM_API_KEY || process.env.LITELLM_MASTER_KEY;
|
||||
if (key) headers.Authorization = 'Bearer ' + key;
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,17 @@ function getSTTModelLists() {
|
|||
};
|
||||
}
|
||||
|
||||
function isLiteLLMSTTModel(model) {
|
||||
var mode = model && model.model_info && model.model_info.mode ? String(model.model_info.mode) : '';
|
||||
return mode === 'audio_transcription';
|
||||
}
|
||||
|
||||
function getLiteLLMSTTModels(models) {
|
||||
return (models || [])
|
||||
.filter(isLiteLLMSTTModel)
|
||||
.map(function(model) { return model && (model.id || model.model_name) ? (model.id || model.model_name) : String(model || ''); });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AWS_STT_MODES,
|
||||
GOOGLE_STT_MODELS,
|
||||
|
|
@ -48,6 +59,8 @@ module.exports = {
|
|||
LOCAL_WHISPER_MODELS,
|
||||
OPENAI_STT_MODELS,
|
||||
getSTTDependencies,
|
||||
getLiteLLMSTTModels,
|
||||
getSTTModelLists,
|
||||
getSTTProvider
|
||||
getSTTProvider,
|
||||
isLiteLLMSTTModel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,10 +32,15 @@ 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';
|
||||
}
|
||||
|
||||
function getLiteLLMTTSModels(models) {
|
||||
return (models || [])
|
||||
.map(function(model) { return model && model.id ? model.id : ''; })
|
||||
.filter(isOpenAITTSModel);
|
||||
.filter(isLiteLLMTTSModel)
|
||||
.map(function(model) { return model && (model.id || model.model_name) ? (model.id || model.model_name) : String(model || ''); });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
@ -47,5 +52,6 @@ module.exports = {
|
|||
getLiteLLMTTSModels,
|
||||
getTTSProvider,
|
||||
getTTSVoiceLists,
|
||||
isLiteLLMTTSModel,
|
||||
isOpenAITTSModel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,3 +46,31 @@ test('STT model lists preserve admin defaults', () => {
|
|||
local: ['tiny', 'base', 'small', 'medium', 'large']
|
||||
});
|
||||
});
|
||||
|
||||
test('LiteLLM STT discovery prefers audio_transcription metadata', () => {
|
||||
const sttProvider = require('../src/utils/sttProvider');
|
||||
assert.equal(sttProvider.isLiteLLMSTTModel({ id: 'custom-asr', model_info: { mode: 'audio_transcription' } }), true);
|
||||
assert.equal(sttProvider.isLiteLLMSTTModel({ id: 'local-parakeet-v3', model_info: { mode: 'audio_transcription' } }), true);
|
||||
assert.equal(sttProvider.isLiteLLMSTTModel({ id: 'local-chatterbox-turbo', model_info: { mode: 'audio_speech' } }), false);
|
||||
});
|
||||
|
||||
test('LiteLLM STT discovery ignores names when metadata is absent', () => {
|
||||
const sttProvider = require('../src/utils/sttProvider');
|
||||
assert.equal(sttProvider.isLiteLLMSTTModel({ id: 'groq-whisper-large-v3' }), false);
|
||||
assert.equal(sttProvider.isLiteLLMSTTModel({ id: 'mistral-voxtral-mini-transcribe' }), false);
|
||||
assert.equal(sttProvider.isLiteLLMSTTModel({ id: 'openrouter-nova-pro-v1' }), false);
|
||||
assert.equal(sttProvider.isLiteLLMSTTModel({ id: 'openrouter-gemini-2.5-flash' }), false);
|
||||
assert.equal(sttProvider.isLiteLLMSTTModel({ id: 'openrouter-gpt-audio' }), false);
|
||||
assert.equal(sttProvider.isLiteLLMSTTModel({ id: 'local-chatterbox-turbo' }), false);
|
||||
});
|
||||
|
||||
test('LiteLLM STT model extraction filters by metadata only', () => {
|
||||
const sttProvider = require('../src/utils/sttProvider');
|
||||
assert.deepEqual(sttProvider.getLiteLLMSTTModels([
|
||||
{ model_name: 'custom-asr', model_info: { mode: 'audio_transcription' } },
|
||||
{ model_name: 'local-parakeet-v3', model_info: { mode: 'audio_transcription' } },
|
||||
{ model_name: 'local-chatterbox-turbo', model_info: { mode: 'audio_speech' } },
|
||||
{ id: 'groq-whisper-large-v3' },
|
||||
{ id: 'gpt-4.1' }
|
||||
]), ['custom-asr', 'local-parakeet-v3']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -57,15 +57,28 @@ test('TTS model discovery keeps OpenAI speech models only', () => {
|
|||
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.isLiteLLMTTSModel({ id: 'local-chatterbox-turbo' }), false);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSModel({ id: 'local-kokoro-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);
|
||||
});
|
||||
|
||||
test('LiteLLM TTS model extraction filters gateway model objects', () => {
|
||||
const ttsProvider = require('../src/utils/ttsProvider');
|
||||
assert.deepEqual(ttsProvider.getLiteLLMTTSModels([
|
||||
{ id: 'openai-tts-1' },
|
||||
{ id: 'mistral-voxtral-mini-tts' },
|
||||
{ id: 'openai-gpt-4o-mini-tts' },
|
||||
{ id: 'local-chatterbox-turbo' },
|
||||
{ model_name: 'local-chatterbox-turbo', model_info: { mode: 'audio_speech' } },
|
||||
{ id: 'custom-provider-model', model_info: { mode: 'audio_speech' } },
|
||||
{ id: 'local-parakeet-v3', model_info: { mode: 'audio_transcription' } },
|
||||
{ id: 'vertex-gemini-2.5-flash-tts' },
|
||||
{ id: 'gpt-4.1' }
|
||||
]), ['openai-tts-1', 'openai-gpt-4o-mini-tts']);
|
||||
]), ['local-chatterbox-turbo', 'custom-provider-model']);
|
||||
});
|
||||
|
||||
test('LiteLLM headers include optional content type without exposing key value', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue