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 <cursoragent@cursor.com>
This commit is contained in:
NIO
2026-06-26 02:32:53 +08:00
committed by GitHub
parent b3ac552c82
commit 66e2fcc6f8
7 changed files with 39 additions and 16 deletions
+8 -2
View File
@@ -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<AzureSpeechVoiceEntry[]>(
response,
"azure-speech.voices",
);
return Array.isArray(voices)
? voices
.filter((voice) => !isDeprecatedVoice(voice))
+6 -3
View File
@@ -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) => ({
+8 -2
View File
@@ -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<SpeechVoiceOption[]> {
});
}
await assertOkOrThrowProviderError(response, "Microsoft voices API error");
const voices = (await response.json()) as MicrosoftVoiceListEntry[];
const voices = await readProviderJsonResponse<MicrosoftVoiceListEntry[]>(
response,
"microsoft.speech-voices",
);
return Array.isArray(voices)
? voices
.map((voice) => ({
+7 -6
View File
@@ -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;
@@ -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<unknown> }) => response.json(),
requireTranscriptionText: (value: string | undefined, message: string) => {
const text = value?.trim();
if (!text) {
@@ -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<OpenRouterSttResponse>(
response,
"openrouter.stt",
);
return {
text: requireTranscriptionText(
payload.text,
+3 -2
View File
@@ -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<XaiSttResponse>(response, "xai.stt");
return {
text: requireTranscriptionText(payload.text, "xAI transcription response missing text"),
...(model ? { model } : {}),