pediatric-ai-scribe-v3/src/utils/sttProvider.js
Daniel f0f48a3578 fix: patch nodemailer; Settings offers STT models the gateway really has
Security
- nodemailer 9.0.1 -> 9.1.1, clearing four high advisories, two of which
  are delivery bugs that matter for an app that sends mail: recipient-domain
  validation bypass via RFC 5322 comments, and an IDN/punycode allow-list
  bypass, both of which can route mail to an attacker-controlled domain.

Live transcription
- The Settings picker was a hardcoded list of six ids
  (local-whisper-*, local-parakeet-v3, gemini-*). None of them resolve on
  this gateway, and /api/transcribe prefers the user's choice over the admin
  default, so picking one broke every recording with "Invalid model name".
  Verified against the live gateway: local-whisper-large-v3-turbo -> 400.
- The picker now lists what /model/info advertises as audio_transcription,
  cached for five minutes, with the built-in list kept only as a fallback
  and the admin default marked.
- The pipeline itself is healthy: local-kokoro-tts produced 92KB of speech
  and mistral-voxtral-mini-transcribe returned the sentence back verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-10 15:54:57 +02:00

70 lines
2.4 KiB
JavaScript

const LITELLM_STT_MODELS = ['local-whisper-large-v3-turbo', 'local-whisper-large-v3', 'local-parakeet-v3', 'local-whisper-1', 'gemini-2.0-flash', 'gemini-2.5-flash'];
function getSTTDependencies() {
var { litellmClient } = require('./ai');
return {
litellm: !!litellmClient
};
}
function getSTTProvider(deps) {
var env = process.env.TRANSCRIBE_PROVIDER || 'auto';
if (env === 'litellm') return 'litellm';
var configured = deps || getSTTDependencies();
if (configured.litellm) return 'litellm';
return 'none';
}
function getSTTModelLists() {
return {
litellm: LITELLM_STT_MODELS
};
}
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 || ''); });
}
// What the gateway actually offers, so Settings cannot present a user with a
// model that does not exist. The hardcoded list above is only a last resort:
// on this deployment none of its six ids resolve, and picking one returns
// "Invalid model name" from /audio/transcriptions.
var sttDiscoveryCache = { at: 0, models: [] };
var STT_DISCOVERY_TTL_MS = 5 * 60 * 1000;
async function discoverSTTModels(options) {
var now = Date.now();
var fresh = !(options && options.force) && (now - sttDiscoveryCache.at) < STT_DISCOVERY_TTL_MS;
if (fresh && sttDiscoveryCache.models.length) return sttDiscoveryCache.models.slice();
if (getSTTProvider() !== 'litellm' || !process.env.LITELLM_API_BASE) return [];
try {
var axios = require('axios');
var { getLiteLLMAdminHeaders } = require('./litellm');
var base = String(process.env.LITELLM_API_BASE || '').replace(/\/+$/, '').replace(/\/v1\/?$/, '');
var resp = await axios.get(base + '/model/info', { headers: getLiteLLMAdminHeaders(), timeout: 10000 });
var ids = getLiteLLMSTTModels(resp.data && resp.data.data);
if (ids.length) sttDiscoveryCache = { at: now, models: ids };
return ids;
} catch (e) {
// A gateway hiccup must not empty the picker; the caller falls back.
return sttDiscoveryCache.models.slice();
}
}
module.exports = {
LITELLM_STT_MODELS,
discoverSTTModels,
getSTTDependencies,
getLiteLLMSTTModels,
getSTTModelLists,
getSTTProvider,
isLiteLLMSTTModel
};