mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
@@ -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))
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
Reference in New Issue
Block a user