Skip to content

Commit 66e2fcc

Browse files
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 <[email protected]>
1 parent b3ac552 commit 66e2fcc

7 files changed

Lines changed: 39 additions & 16 deletions

File tree

extensions/azure-speech/tts.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
* Azure Speech REST helpers. They normalize endpoints, build SSML, list voices,
33
* and synthesize speech with response-size and SSRF guards.
44
*/
5-
import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http";
5+
import {
6+
assertOkOrThrowProviderError,
7+
readProviderJsonResponse,
8+
} from "openclaw/plugin-sdk/provider-http";
69
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
710
import type { SpeechVoiceOption } from "openclaw/plugin-sdk/speech-core";
811
import { trimToUndefined } from "openclaw/plugin-sdk/speech-core";
@@ -160,7 +163,10 @@ export async function listAzureSpeechVoices(params: {
160163

161164
try {
162165
await assertOkOrThrowProviderError(response, "Azure Speech voices API error");
163-
const voices = (await response.json()) as AzureSpeechVoiceEntry[];
166+
const voices = await readProviderJsonResponse<AzureSpeechVoiceEntry[]>(
167+
response,
168+
"azure-speech.voices",
169+
);
164170
return Array.isArray(voices)
165171
? voices
166172
.filter((voice) => !isDeprecatedVoice(voice))

extensions/elevenlabs/speech-provider.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Elevenlabs provider module implements model/runtime integration.
22
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
33
import { parseStrictFiniteNumber, parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
4-
import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http";
4+
import {
5+
assertOkOrThrowProviderError,
6+
readProviderJsonResponse,
7+
} from "openclaw/plugin-sdk/provider-http";
58
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
69
import type {
710
SpeechDirectiveTokenParseContext,
@@ -367,14 +370,14 @@ async function listElevenLabsVoices(params: {
367370
});
368371
try {
369372
await assertOkOrThrowProviderError(response, "ElevenLabs voices API error");
370-
const json = (await response.json()) as {
373+
const json = await readProviderJsonResponse<{
371374
voices?: Array<{
372375
voice_id?: string;
373376
name?: string;
374377
category?: string;
375378
description?: string;
376379
}>;
377-
};
380+
}>(response, "elevenlabs.voices");
378381
return Array.isArray(json.voices)
379382
? json.voices
380383
.map((voice) => ({

extensions/microsoft/speech-provider.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
generateSecMsGecToken,
88
} from "node-edge-tts/dist/drm.js";
99
import { isVoiceCompatibleAudio } from "openclaw/plugin-sdk/media-runtime";
10-
import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http";
10+
import {
11+
assertOkOrThrowProviderError,
12+
readProviderJsonResponse,
13+
} from "openclaw/plugin-sdk/provider-http";
1114
import {
1215
captureHttpExchange,
1316
isDebugProxyGlobalFetchPatchInstalled,
@@ -166,7 +169,10 @@ export async function listMicrosoftVoices(): Promise<SpeechVoiceOption[]> {
166169
});
167170
}
168171
await assertOkOrThrowProviderError(response, "Microsoft voices API error");
169-
const voices = (await response.json()) as MicrosoftVoiceListEntry[];
172+
const voices = await readProviderJsonResponse<MicrosoftVoiceListEntry[]>(
173+
response,
174+
"microsoft.speech-voices",
175+
);
170176
return Array.isArray(voices)
171177
? voices
172178
.map((voice) => ({

extensions/minimax/tts.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Minimax plugin module implements tts behavior.
22
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
3-
import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http";
3+
import {
4+
assertOkOrThrowProviderError,
5+
readProviderJsonResponse,
6+
} from "openclaw/plugin-sdk/provider-http";
47
import {
58
fetchWithSsrFGuard,
69
ssrfPolicyFromHttpBaseUrlAllowedHostname,
@@ -105,10 +108,10 @@ export async function minimaxTTS(params: {
105108
try {
106109
await assertOkOrThrowProviderError(response, "MiniMax TTS API error");
107110

108-
const body = (await response.json()) as {
111+
const body = await readProviderJsonResponse<{
109112
data?: { audio?: string };
110113
base_resp?: { status_code?: number; status_msg?: string };
111-
};
114+
}>(response, "minimax.tts");
112115

113116
// Check base_resp for envelope errors (HTTP 200 with non-zero status_code).
114117
// Other MiniMax providers (image, video, music, web-search) already check this.
@@ -119,9 +122,7 @@ export async function minimaxTTS(params: {
119122
body.base_resp.status_code !== 0
120123
) {
121124
const msg = body.base_resp.status_msg ?? "unknown error";
122-
throw new Error(
123-
`MiniMax TTS API error (${body.base_resp.status_code}): ${msg}`,
124-
);
125+
throw new Error(`MiniMax TTS API error (${body.base_resp.status_code}): ${msg}`);
125126
}
126127

127128
const hexAudio = body?.data?.audio;

extensions/openrouter/media-understanding-provider.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const { assertOkOrThrowHttpErrorMock, postJsonRequestMock, resolveProviderHttpRe
2424
vi.mock("openclaw/plugin-sdk/provider-http", () => ({
2525
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
2626
postJsonRequest: postJsonRequestMock,
27+
// Pass-through: bounded-reader enforcement is tested via bounded-reader unit tests.
28+
readProviderJsonResponse: async (response: { json(): Promise<unknown> }) => response.json(),
2729
requireTranscriptionText: (value: string | undefined, message: string) => {
2830
const text = value?.trim();
2931
if (!text) {

extensions/openrouter/media-understanding-provider.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import {
1111
assertOkOrThrowHttpError,
1212
postJsonRequest,
13+
readProviderJsonResponse,
1314
requireTranscriptionText,
1415
resolveProviderHttpRequestConfig,
1516
} from "openclaw/plugin-sdk/provider-http";
@@ -148,7 +149,10 @@ export async function transcribeOpenRouterAudio(
148149

149150
try {
150151
await assertOkOrThrowHttpError(response, "OpenRouter audio transcription failed");
151-
const payload = (await response.json()) as OpenRouterSttResponse;
152+
const payload = await readProviderJsonResponse<OpenRouterSttResponse>(
153+
response,
154+
"openrouter.stt",
155+
);
152156
return {
153157
text: requireTranscriptionText(
154158
payload.text,

extensions/xai/stt.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import {
88
assertOkOrThrowHttpError,
99
buildAudioTranscriptionFormData,
1010
postTranscriptionRequest,
11-
resolveProviderHttpRequestConfig,
11+
readProviderJsonResponse,
1212
requireTranscriptionText,
13+
resolveProviderHttpRequestConfig,
1314
} from "openclaw/plugin-sdk/provider-http";
1415
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
1516
import { XAI_BASE_URL } from "./model-definitions.js";
@@ -68,7 +69,7 @@ export async function transcribeXaiAudio(
6869

6970
try {
7071
await assertOkOrThrowHttpError(response, "xAI audio transcription failed");
71-
const payload = (await response.json()) as XaiSttResponse;
72+
const payload = await readProviderJsonResponse<XaiSttResponse>(response, "xai.stt");
7273
return {
7374
text: requireTranscriptionText(payload.text, "xAI transcription response missing text"),
7475
...(model ? { model } : {}),

0 commit comments

Comments
 (0)