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"
|
- "127.0.0.1:3552:3000"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
- /root/litellm/.env
|
||||||
environment:
|
environment:
|
||||||
CLINICAL_ASSISTANT_MCP_URL: http://mcp:8000/mcp
|
CLINICAL_ASSISTANT_MCP_URL: http://mcp:8000/mcp
|
||||||
REDIS_URL: redis://ped-ai-redis:6379
|
REDIS_URL: redis://ped-ai-redis:6379
|
||||||
LOKI_URL: http://monitoring-loki:3100
|
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:
|
volumes:
|
||||||
- scribe-logs:/app/data/logs
|
- scribe-logs:/app/data/logs
|
||||||
- clinical-assistant-mcp-data:/app/mcp-data:ro
|
- clinical-assistant-mcp-data:/app/mcp-data:ro
|
||||||
|
|
@ -24,6 +29,7 @@ services:
|
||||||
- default
|
- default
|
||||||
- mcp-server_default
|
- mcp-server_default
|
||||||
- monitoring_default
|
- monitoring_default
|
||||||
|
- speech_net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
|
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
@ -80,3 +86,5 @@ networks:
|
||||||
external: true
|
external: true
|
||||||
monitoring_default:
|
monitoring_default:
|
||||||
external: true
|
external: true
|
||||||
|
speech_net:
|
||||||
|
external: true
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ 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, OPENAI_TTS_VOICES, getTTSEnvProvider, getLiteLLMTTSModels, getTTSProvider, getTTSVoiceLists } = require('../utils/ttsProvider');
|
||||||
var { getLiteLLMHeaders } = require('../utils/litellm');
|
var { getLiteLLMHeaders } = require('../utils/litellm');
|
||||||
var { getSTTDependencies, getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
|
var { getSTTDependencies, getLiteLLMSTTModels, getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
|
||||||
|
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
|
|
@ -478,9 +478,9 @@ router.get('/config/tts', async function(req, res) {
|
||||||
envVoice: envVoice,
|
envVoice: envVoice,
|
||||||
envModel: envModel,
|
envModel: envModel,
|
||||||
configured: {
|
configured: {
|
||||||
google: isGoogleTTSConfigured(),
|
google: false,
|
||||||
litellm: !!process.env.LITELLM_API_BASE,
|
litellm: !!process.env.LITELLM_API_BASE,
|
||||||
elevenlabs: !!process.env.ELEVENLABS_API_KEY
|
elevenlabs: false
|
||||||
},
|
},
|
||||||
voices: getTTSVoiceLists()
|
voices: getTTSVoiceLists()
|
||||||
});
|
});
|
||||||
|
|
@ -519,32 +519,13 @@ router.get('/config/tts/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 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) {
|
getLiteLLMTTSModels(lResp.data && lResp.data.data).forEach(function(id) {
|
||||||
discovered.push({ id: id, name: id, source: 'gateway-api' });
|
discovered.push({ id: id, name: id, source: 'gateway-api' });
|
||||||
});
|
});
|
||||||
} catch (e) { logger.warn('LiteLLM TTS model list failed: ' + e.message); }
|
} 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) {
|
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) {
|
if (provider === 'litellm' && process.env.LITELLM_API_BASE) {
|
||||||
// base URL handled by gatewayUrl() helper
|
// base URL handled by gatewayUrl() helper
|
||||||
try {
|
try {
|
||||||
var sResp = await axios.get(gatewayUrl('/models'), { headers: getLiteLLMHeaders(), timeout: 10000 });
|
var liteLLMBase = (process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, '');
|
||||||
if (sResp.data && sResp.data.data) {
|
var sResp = await axios.get(liteLLMBase + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 });
|
||||||
sResp.data.data.forEach(function(m) {
|
getLiteLLMSTTModels(sResp.data && sResp.data.data).forEach(function(id) {
|
||||||
var mid = (m.id || '').toLowerCase();
|
discovered.push({ id: id, name: id, source: 'gateway-api' });
|
||||||
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' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch(e) { logger.warn('STT discovery failed: ' + e.message); }
|
} 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') {
|
if (provider === 'local') {
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ const { synthesizeWithGoogleTTS } = require('../utils/ttsGoogle');
|
||||||
const { authMiddleware } = require('../middleware/auth');
|
const { authMiddleware } = require('../middleware/auth');
|
||||||
var logger = require('../utils/logger');
|
var logger = require('../utils/logger');
|
||||||
var { gatewayUrl } = require('../utils/errors');
|
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):
|
// 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=google → Google Cloud TTS direct (HIPAA eligible, opt-in)
|
||||||
// TTS_PROVIDER=elevenlabs → ElevenLabs direct (not HIPAA, opt-in)
|
// TTS_PROVIDER=elevenlabs → ElevenLabs direct (not HIPAA, opt-in)
|
||||||
// Auto: litellm > google > elevenlabs
|
// Auto: litellm > google > elevenlabs
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
function getLiteLLMHeaders(contentType) {
|
function getLiteLLMHeaders(contentType) {
|
||||||
var headers = {};
|
var headers = {};
|
||||||
if (contentType) headers['Content-Type'] = contentType;
|
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;
|
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 = {
|
module.exports = {
|
||||||
AWS_STT_MODES,
|
AWS_STT_MODES,
|
||||||
GOOGLE_STT_MODELS,
|
GOOGLE_STT_MODELS,
|
||||||
|
|
@ -48,6 +59,8 @@ module.exports = {
|
||||||
LOCAL_WHISPER_MODELS,
|
LOCAL_WHISPER_MODELS,
|
||||||
OPENAI_STT_MODELS,
|
OPENAI_STT_MODELS,
|
||||||
getSTTDependencies,
|
getSTTDependencies,
|
||||||
|
getLiteLLMSTTModels,
|
||||||
getSTTModelLists,
|
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());
|
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) {
|
function getLiteLLMTTSModels(models) {
|
||||||
return (models || [])
|
return (models || [])
|
||||||
.map(function(model) { return model && model.id ? model.id : ''; })
|
.filter(isLiteLLMTTSModel)
|
||||||
.filter(isOpenAITTSModel);
|
.map(function(model) { return model && (model.id || model.model_name) ? (model.id || model.model_name) : String(model || ''); });
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
@ -47,5 +52,6 @@ module.exports = {
|
||||||
getLiteLLMTTSModels,
|
getLiteLLMTTSModels,
|
||||||
getTTSProvider,
|
getTTSProvider,
|
||||||
getTTSVoiceLists,
|
getTTSVoiceLists,
|
||||||
|
isLiteLLMTTSModel,
|
||||||
isOpenAITTSModel
|
isOpenAITTSModel
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,31 @@ test('STT model lists preserve admin defaults', () => {
|
||||||
local: ['tiny', 'base', 'small', 'medium', 'large']
|
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);
|
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', () => {
|
test('LiteLLM TTS model extraction filters gateway model objects', () => {
|
||||||
const ttsProvider = require('../src/utils/ttsProvider');
|
const ttsProvider = require('../src/utils/ttsProvider');
|
||||||
assert.deepEqual(ttsProvider.getLiteLLMTTSModels([
|
assert.deepEqual(ttsProvider.getLiteLLMTTSModels([
|
||||||
{ id: 'openai-tts-1' },
|
{ id: 'openai-tts-1' },
|
||||||
{ id: 'mistral-voxtral-mini-tts' },
|
{ id: 'mistral-voxtral-mini-tts' },
|
||||||
{ id: 'openai-gpt-4o-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: 'vertex-gemini-2.5-flash-tts' },
|
||||||
{ id: 'gpt-4.1' }
|
{ 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', () => {
|
test('LiteLLM headers include optional content type without exposing key value', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue