From 66e2fcc6f83eb6aef394401a19342d7af19b1478 Mon Sep 17 00:00:00 2001 From: NIO Date: Fri, 26 Jun 2026 02:32:53 +0800 Subject: [PATCH] fix(speech): bound TTS/STT voice-list and transcription JSON response reads (#96496) Route success JSON reads through readProviderJsonResponse (16 MiB cap) in azure-speech, elevenlabs, microsoft, minimax/tts, xai/stt, and openrouter/media-understanding to prevent OOM from oversized or hostile endpoint responses. Mirrors the response-limit campaign already applied to other provider paths. AI-assisted. Co-authored-by: Cursor --- extensions/azure-speech/tts.ts | 10 ++++++++-- extensions/elevenlabs/speech-provider.ts | 9 ++++++--- extensions/microsoft/speech-provider.ts | 10 ++++++++-- extensions/minimax/tts.ts | 13 +++++++------ .../openrouter/media-understanding-provider.test.ts | 2 ++ .../openrouter/media-understanding-provider.ts | 6 +++++- extensions/xai/stt.ts | 5 +++-- 7 files changed, 39 insertions(+), 16 deletions(-) diff --git a/extensions/azure-speech/tts.ts b/extensions/azure-speech/tts.ts index 1f5eeb1f9460..fb90ad11c930 100644 --- a/extensions/azure-speech/tts.ts +++ b/extensions/azure-speech/tts.ts @@ -2,7 +2,10 @@ * Azure Speech REST helpers. They normalize endpoints, build SSML, list voices, * and synthesize speech with response-size and SSRF guards. */ -import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http"; +import { + assertOkOrThrowProviderError, + readProviderJsonResponse, +} from "openclaw/plugin-sdk/provider-http"; import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; import type { SpeechVoiceOption } from "openclaw/plugin-sdk/speech-core"; import { trimToUndefined } from "openclaw/plugin-sdk/speech-core"; @@ -160,7 +163,10 @@ export async function listAzureSpeechVoices(params: { try { await assertOkOrThrowProviderError(response, "Azure Speech voices API error"); - const voices = (await response.json()) as AzureSpeechVoiceEntry[]; + const voices = await readProviderJsonResponse( + response, + "azure-speech.voices", + ); return Array.isArray(voices) ? voices .filter((voice) => !isDeprecatedVoice(voice)) diff --git a/extensions/elevenlabs/speech-provider.ts b/extensions/elevenlabs/speech-provider.ts index 063ade2bb96d..b3e0f4f9640b 100644 --- a/extensions/elevenlabs/speech-provider.ts +++ b/extensions/elevenlabs/speech-provider.ts @@ -1,7 +1,10 @@ // Elevenlabs provider module implements model/runtime integration. import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { parseStrictFiniteNumber, parseStrictInteger } from "openclaw/plugin-sdk/number-runtime"; -import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http"; +import { + assertOkOrThrowProviderError, + readProviderJsonResponse, +} from "openclaw/plugin-sdk/provider-http"; import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input"; import type { SpeechDirectiveTokenParseContext, @@ -367,14 +370,14 @@ async function listElevenLabsVoices(params: { }); try { await assertOkOrThrowProviderError(response, "ElevenLabs voices API error"); - const json = (await response.json()) as { + const json = await readProviderJsonResponse<{ voices?: Array<{ voice_id?: string; name?: string; category?: string; description?: string; }>; - }; + }>(response, "elevenlabs.voices"); return Array.isArray(json.voices) ? json.voices .map((voice) => ({ diff --git a/extensions/microsoft/speech-provider.ts b/extensions/microsoft/speech-provider.ts index 2e4cdabebced..a8ec98163e92 100644 --- a/extensions/microsoft/speech-provider.ts +++ b/extensions/microsoft/speech-provider.ts @@ -7,7 +7,10 @@ import { generateSecMsGecToken, } from "node-edge-tts/dist/drm.js"; import { isVoiceCompatibleAudio } from "openclaw/plugin-sdk/media-runtime"; -import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http"; +import { + assertOkOrThrowProviderError, + readProviderJsonResponse, +} from "openclaw/plugin-sdk/provider-http"; import { captureHttpExchange, isDebugProxyGlobalFetchPatchInstalled, @@ -166,7 +169,10 @@ export async function listMicrosoftVoices(): Promise { }); } await assertOkOrThrowProviderError(response, "Microsoft voices API error"); - const voices = (await response.json()) as MicrosoftVoiceListEntry[]; + const voices = await readProviderJsonResponse( + response, + "microsoft.speech-voices", + ); return Array.isArray(voices) ? voices .map((voice) => ({ diff --git a/extensions/minimax/tts.ts b/extensions/minimax/tts.ts index aaac67d5da87..8b9507ef08aa 100644 --- a/extensions/minimax/tts.ts +++ b/extensions/minimax/tts.ts @@ -1,6 +1,9 @@ // Minimax plugin module implements tts behavior. import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; -import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http"; +import { + assertOkOrThrowProviderError, + readProviderJsonResponse, +} from "openclaw/plugin-sdk/provider-http"; import { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedHostname, @@ -105,10 +108,10 @@ export async function minimaxTTS(params: { try { await assertOkOrThrowProviderError(response, "MiniMax TTS API error"); - const body = (await response.json()) as { + const body = await readProviderJsonResponse<{ data?: { audio?: string }; base_resp?: { status_code?: number; status_msg?: string }; - }; + }>(response, "minimax.tts"); // Check base_resp for envelope errors (HTTP 200 with non-zero status_code). // Other MiniMax providers (image, video, music, web-search) already check this. @@ -119,9 +122,7 @@ export async function minimaxTTS(params: { body.base_resp.status_code !== 0 ) { const msg = body.base_resp.status_msg ?? "unknown error"; - throw new Error( - `MiniMax TTS API error (${body.base_resp.status_code}): ${msg}`, - ); + throw new Error(`MiniMax TTS API error (${body.base_resp.status_code}): ${msg}`); } const hexAudio = body?.data?.audio; diff --git a/extensions/openrouter/media-understanding-provider.test.ts b/extensions/openrouter/media-understanding-provider.test.ts index 1b49047b6ce3..25a926b60408 100644 --- a/extensions/openrouter/media-understanding-provider.test.ts +++ b/extensions/openrouter/media-understanding-provider.test.ts @@ -24,6 +24,8 @@ const { assertOkOrThrowHttpErrorMock, postJsonRequestMock, resolveProviderHttpRe vi.mock("openclaw/plugin-sdk/provider-http", () => ({ assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock, postJsonRequest: postJsonRequestMock, + // Pass-through: bounded-reader enforcement is tested via bounded-reader unit tests. + readProviderJsonResponse: async (response: { json(): Promise }) => response.json(), requireTranscriptionText: (value: string | undefined, message: string) => { const text = value?.trim(); if (!text) { diff --git a/extensions/openrouter/media-understanding-provider.ts b/extensions/openrouter/media-understanding-provider.ts index ffdabd9c96a0..c57cab6f8e48 100644 --- a/extensions/openrouter/media-understanding-provider.ts +++ b/extensions/openrouter/media-understanding-provider.ts @@ -10,6 +10,7 @@ import { import { assertOkOrThrowHttpError, postJsonRequest, + readProviderJsonResponse, requireTranscriptionText, resolveProviderHttpRequestConfig, } from "openclaw/plugin-sdk/provider-http"; @@ -148,7 +149,10 @@ export async function transcribeOpenRouterAudio( try { await assertOkOrThrowHttpError(response, "OpenRouter audio transcription failed"); - const payload = (await response.json()) as OpenRouterSttResponse; + const payload = await readProviderJsonResponse( + response, + "openrouter.stt", + ); return { text: requireTranscriptionText( payload.text, diff --git a/extensions/xai/stt.ts b/extensions/xai/stt.ts index eeeb4a5f2611..6dd8043ff821 100644 --- a/extensions/xai/stt.ts +++ b/extensions/xai/stt.ts @@ -8,8 +8,9 @@ import { assertOkOrThrowHttpError, buildAudioTranscriptionFormData, postTranscriptionRequest, - resolveProviderHttpRequestConfig, + readProviderJsonResponse, requireTranscriptionText, + resolveProviderHttpRequestConfig, } from "openclaw/plugin-sdk/provider-http"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { XAI_BASE_URL } from "./model-definitions.js"; @@ -68,7 +69,7 @@ export async function transcribeXaiAudio( try { await assertOkOrThrowHttpError(response, "xAI audio transcription failed"); - const payload = (await response.json()) as XaiSttResponse; + const payload = await readProviderJsonResponse(response, "xai.stt"); return { text: requireTranscriptionText(payload.text, "xAI transcription response missing text"), ...(model ? { model } : {}),