fix: harden custom TTS URL handling and availability checks

Validate and normalize custom TTS server URLs before proxying requests.
Gate server TTS availability by provider mode to avoid OpenAI/custom misrouting.
Keep browser and message TTS hooks aligned with the new provider-specific checks.
This commit is contained in:
Bohdan Triapitsyn
2026-04-12 10:38:16 +03:00
parent 055b6f6af0
commit c7f1501ad2
5 changed files with 36 additions and 8 deletions
+9 -2
View File
@@ -1,4 +1,5 @@
import express from 'express';
import { normalizeCustomOpenAIBaseURL } from './base-url.js';
export function registerTtsRoutes(app, { resolveZenModel, sayTTSCapability }) {
let ttsModulePromise = null;
@@ -44,6 +45,12 @@ export function registerTtsRoutes(app, { resolveZenModel, sayTTSCapability }) {
try {
const { text, voice = 'nova', model = 'gpt-4o-mini-tts', speed = 0.9, instructions, summarize = false, providerId, modelId, threshold = 200, maxLength = 500, apiKey, baseURL } = req.body || {};
const normalizedBaseURLResult = normalizeCustomOpenAIBaseURL(baseURL);
if (normalizedBaseURLResult.error) {
return res.status(400).json({ error: normalizedBaseURLResult.error });
}
const normalizedBaseURL = normalizedBaseURLResult.value;
console.log('[TTS] Request received:', { voice, model, speed, textLength: text?.length, hasApiKey: !!apiKey, hasBaseURL: !!baseURL });
if (!text || typeof text !== 'string' || !text.trim()) {
@@ -56,7 +63,7 @@ export function registerTtsRoutes(app, { resolveZenModel, sayTTSCapability }) {
// Check availability - server-configured key, client-provided key, or custom server URL
const hasServerKey = ttsService.isAvailable();
const hasClientKey = apiKey && typeof apiKey === 'string' && apiKey.trim().length > 0;
const hasCustomBaseURL = baseURL && typeof baseURL === 'string' && baseURL.trim().length > 0;
const hasCustomBaseURL = typeof normalizedBaseURL === 'string' && normalizedBaseURL.length > 0;
if (!hasServerKey && !hasClientKey && !hasCustomBaseURL) {
return res.status(503).json({
@@ -89,7 +96,7 @@ export function registerTtsRoutes(app, { resolveZenModel, sayTTSCapability }) {
speed,
instructions,
apiKey: hasClientKey ? apiKey.trim() : undefined,
baseURL: hasCustomBaseURL ? baseURL.trim() : undefined,
baseURL: hasCustomBaseURL ? normalizedBaseURL : undefined,
});
res.setHeader('Content-Type', result.contentType);
+11 -4
View File
@@ -7,6 +7,7 @@
import OpenAI from 'openai';
import { readAuthFile } from '../opencode/auth.js';
import { normalizeCustomOpenAIBaseURL } from './base-url.js';
// Voice options from OpenAI
export const TTS_VOICES = [
@@ -82,13 +83,19 @@ class TTSService {
baseURL,
} = options;
const normalizedBaseURLResult = normalizeCustomOpenAIBaseURL(baseURL);
if (normalizedBaseURLResult.error) {
throw new Error(normalizedBaseURLResult.error);
}
const normalizedBaseURL = normalizedBaseURLResult.value;
// Use provided API key / baseURL or fall back to configured key
let client;
if (baseURL || apiKey) {
if (normalizedBaseURL || apiKey) {
const clientOpts = {};
if (apiKey) clientOpts.apiKey = apiKey;
if (!apiKey) clientOpts.apiKey = 'not-required';
if (baseURL) clientOpts.baseURL = baseURL;
if (normalizedBaseURL) clientOpts.baseURL = normalizedBaseURL;
client = new OpenAI(clientOpts);
} else {
client = this._getClient();
@@ -105,7 +112,7 @@ class TTSService {
try {
// OpenAI-compatible servers (custom baseURL) may not support `instructions`
// or `response_format`, but do support `speed`. Send the safe subset.
const speechParams = baseURL
const speechParams = normalizedBaseURL
? { model, voice, input: text, speed }
: {
model,
@@ -116,7 +123,7 @@ class TTSService {
response_format: 'mp3',
};
console.log('[TTSService] Generating speech — model:', model, 'voice:', voice, 'baseURL:', baseURL ?? '(openai)');
console.log('[TTSService] Generating speech — model:', model, 'voice:', voice, 'baseURL:', normalizedBaseURL ?? '(openai)');
const response = await client.audio.speech.create(speechParams);
const arrayBuffer = await response.arrayBuffer();