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
This commit is contained in:
parent
31abddb6e6
commit
f0f48a3578
5 changed files with 61 additions and 7 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -32,7 +32,7 @@
|
|||
"marked": "^18.0.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-pg-migrate": "^7.7.0",
|
||||
"nodemailer": "9.0.1",
|
||||
"nodemailer": "^9.1.1",
|
||||
"openai": "^4.73.0",
|
||||
"openid-client": "^6.8.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
|
|
@ -5958,9 +5958,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz",
|
||||
"integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==",
|
||||
"version": "9.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.1.1.tgz",
|
||||
"integrity": "sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"marked": "^18.0.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-pg-migrate": "^7.7.0",
|
||||
"nodemailer": "9.0.1",
|
||||
"nodemailer": "^9.1.1",
|
||||
"openai": "^4.73.0",
|
||||
"openid-client": "^6.8.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ var express = require('express');
|
|||
var router = express.Router();
|
||||
var db = require('../db/database');
|
||||
var { authMiddleware } = require('../middleware/auth');
|
||||
var { getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
|
||||
var { getSTTModelLists, getSTTProvider, discoverSTTModels } = require('../utils/sttProvider');
|
||||
var { getLiteLLMTTSVoicesForModel, getTTSProvider } = require('../utils/ttsProvider');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
|
@ -57,7 +57,16 @@ router.get('/preferences/options', async function(req, res) {
|
|||
var dbModel = await db.getSetting('tts.model') || '';
|
||||
var dbVoice = await db.getSetting('tts.voice') || '';
|
||||
var ttsModel = dbModel || process.env.LITELLM_TTS_MODEL || '';
|
||||
var sttModels = getSTTModelLists().litellm.map(function(model) { return { value: model, label: model }; });
|
||||
// Offer what the gateway really has. The built-in list is a last resort:
|
||||
// its ids do not resolve on every deployment, and a user who picked one got
|
||||
// "Invalid model name" on every recording, because the user's choice wins
|
||||
// over the admin default in /api/transcribe.
|
||||
var sttIds = await discoverSTTModels();
|
||||
if (!sttIds.length) sttIds = getSTTModelLists().litellm.slice();
|
||||
var adminSttModel = await db.getSetting('stt.model') || process.env.LITELLM_STT_MODEL || '';
|
||||
var sttModels = sttIds.map(function(model) {
|
||||
return { value: model, label: model + (model === adminSttModel ? ' (default)' : '') };
|
||||
});
|
||||
var ttsVoices = getLiteLLMTTSVoicesForModel(ttsModel, { currentVoice: dbVoice }).map(function(voice) { return { value: voice, label: voice }; });
|
||||
|
||||
res.json({
|
||||
|
|
|
|||
|
|
@ -33,8 +33,35 @@ function getLiteLLMSTTModels(models) {
|
|||
.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,
|
||||
|
|
|
|||
|
|
@ -75,3 +75,21 @@ test('audio backup settings render without dynamic HTML templates', () => {
|
|||
assert.match(renderer, /document\.createElement\('button'\)/);
|
||||
assert.match(renderer, /textContent =/);
|
||||
});
|
||||
|
||||
// The Settings picker offered six hardcoded ids. On this gateway none of them
|
||||
// resolve, and /api/transcribe prefers the user's choice over the admin
|
||||
// default — so choosing one broke every recording with "Invalid model name".
|
||||
test('the STT picker offers what the gateway has, not a hardcoded list', () => {
|
||||
const stt = read('src/utils/sttProvider.js');
|
||||
const prefs = read('src/routes/userPreferences.js');
|
||||
|
||||
assert.match(stt, /async function discoverSTTModels\(options\)/);
|
||||
assert.match(stt, /getLiteLLMSTTModels\(resp\.data && resp\.data\.data\)/, 'filtered by audio_transcription mode');
|
||||
assert.match(stt, /STT_DISCOVERY_TTL_MS = 5 \* 60 \* 1000;/, 'cached, so a user-facing page does not hit the gateway every load');
|
||||
assert.match(stt, /module\.exports = \{[\s\S]{0,80}discoverSTTModels,/);
|
||||
|
||||
assert.match(prefs, /var sttIds = await discoverSTTModels\(\);/);
|
||||
assert.match(prefs, /if \(!sttIds\.length\) sttIds = getSTTModelLists\(\)\.litellm\.slice\(\);/,
|
||||
'the built-in list survives only as a fallback');
|
||||
assert.match(prefs, /model === adminSttModel \? ' \(default\)' : ''/, 'the admin default is marked');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue