惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
T
The Blog of Author Tim Ferriss
B
Blog
L
LangChain Blog
Y
Y Combinator Blog
美团技术团队
博客园 - 三生石上(FineUI控件)
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
G
Google Developers Blog
量子位
博客园_首页
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
C
Check Point Blog
D
Docker
小众软件
小众软件
The Cloudflare Blog
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 聂微东
Blog — PlanetScale
Blog — PlanetScale
GbyAI
GbyAI
Google DeepMind News
Google DeepMind News
IT之家
IT之家

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(tts): bound generated speech downloads · openclaw/ope...
vincentkoc · 2026-05-29 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -98,6 +98,29 @@ describe("gradium speech provider", () => {

9898

expect(result.audioBuffer).toEqual(audioData);

9999

});

100100
101+

it("applies the configured media byte cap to synthesized audio", async () => {

102+

vi.stubGlobal(

103+

"fetch",

104+

vi.fn().mockResolvedValue(new Response(new Uint8Array(2048), { status: 200 })),

105+

);

106+
107+

await expect(

108+

provider.synthesize({

109+

text: "OpenClaw test",

110+

cfg: {

111+

agents: {

112+

defaults: {

113+

mediaMaxMb: 0.001,

114+

},

115+

},

116+

} as never,

117+

providerConfig: { apiKey: "gsk_test123" },

118+

target: "audio-file",

119+

timeoutMs: 30_000,

120+

}),

121+

).rejects.toThrow("Gradium TTS audio response exceeds");

122+

});

123+
101124

it("uses ulaw_8000 for telephony synthesis", async () => {

102125

const audioData = Buffer.from("ulaw-audio-data");

103126

const fetchMock = vi.fn().mockResolvedValue(new Response(audioData, { status: 200 }));

Original file line numberDiff line numberDiff line change

@@ -8,6 +8,8 @@ import { asObject, trimToUndefined } from "openclaw/plugin-sdk/speech";

88

import { DEFAULT_GRADIUM_VOICE_ID, GRADIUM_VOICES, normalizeGradiumBaseUrl } from "./shared.js";

99

import { gradiumTTS } from "./tts.js";

1010
11+

const DEFAULT_GENERATED_AUDIO_MAX_BYTES = 16 * 1024 * 1024;

12+
1113

type GradiumProviderConfig = {

1214

apiKey?: string;

1315

baseUrl: string;

@@ -36,6 +38,16 @@ function readGradiumProviderConfig(config: SpeechProviderConfig): GradiumProvide

3638

};

3739

}

3840
41+

function resolveGeneratedAudioMaxBytes(req: {

42+

cfg: { agents?: { defaults?: { mediaMaxMb?: number } } };

43+

}): number {

44+

const configured = req.cfg.agents?.defaults?.mediaMaxMb;

45+

if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {

46+

return Math.floor(configured * 1024 * 1024);

47+

}

48+

return DEFAULT_GENERATED_AUDIO_MAX_BYTES;

49+

}

50+
3951

function parseDirectiveToken(ctx: SpeechDirectiveTokenParseContext): {

4052

handled: boolean;

4153

overrides?: Record<string, unknown>;

@@ -86,6 +98,7 @@ export function buildGradiumSpeechProvider(): SpeechProviderPlugin {

8698

voiceId: trimToUndefined(overrides.voiceId) ?? config.voiceId,

8799

outputFormat,

88100

timeoutMs: req.timeoutMs,

101+

maxBytes: resolveGeneratedAudioMaxBytes(req),

89102

});

90103

return {

91104

audioBuffer,

@@ -110,6 +123,7 @@ export function buildGradiumSpeechProvider(): SpeechProviderPlugin {

110123

voiceId: trimToUndefined(overrides.voiceId) ?? config.voiceId,

111124

outputFormat,

112125

timeoutMs: req.timeoutMs,

126+

maxBytes: resolveGeneratedAudioMaxBytes(req),

113127

});

114128

return { audioBuffer, outputFormat, sampleRate };

115129

},

Original file line numberDiff line numberDiff line change

@@ -28,6 +28,14 @@ describe("gradium tts diagnostics", () => {

2828

};

2929

}

3030
31+

function createStreamingAudioResponse(params: {

32+

chunkCount: number;

33+

chunkSize: number;

34+

byte: number;

35+

}): { response: Response; getReadCount: () => number } {

36+

return createStreamingErrorResponse({ ...params, status: 200 });

37+

}

38+
3139

afterEach(() => {

3240

vi.unstubAllGlobals();

3341

vi.restoreAllMocks();

@@ -134,4 +142,27 @@ describe("gradium tts diagnostics", () => {

134142

});

135143

expect(result).toEqual(audioData);

136144

});

145+
146+

it("caps streamed audio responses instead of buffering oversized TTS output", async () => {

147+

const streamed = createStreamingAudioResponse({

148+

chunkCount: 20,

149+

chunkSize: 1024,

150+

byte: 121,

151+

});

152+

vi.stubGlobal("fetch", vi.fn().mockResolvedValue(streamed.response));

153+
154+

await expect(

155+

gradiumTTS({

156+

text: "hello",

157+

apiKey: "test-key",

158+

baseUrl: "https://api.gradium.ai",

159+

voiceId: "YTpq7expH9539ERJ",

160+

outputFormat: "wav",

161+

timeoutMs: 5_000,

162+

maxBytes: 2048,

163+

}),

164+

).rejects.toThrow("Gradium TTS audio response exceeds 2048 bytes");

165+
166+

expect(streamed.getReadCount()).toBeLessThan(20);

167+

});

137168

});

Original file line numberDiff line numberDiff line change

@@ -1,16 +1,28 @@

11

import { assertOkOrThrowProviderError } from "openclaw/plugin-sdk/provider-http";

2+

import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";

23

import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";

34

import { normalizeGradiumBaseUrl } from "./shared.js";

45
6+

const DEFAULT_TTS_MAX_BYTES = 16 * 1024 * 1024;

7+
58

export async function gradiumTTS(params: {

69

text: string;

710

apiKey: string;

811

baseUrl: string;

912

voiceId: string;

1013

outputFormat: "wav" | "opus" | "ulaw_8000" | "pcm" | "pcm_24000" | "alaw_8000";

1114

timeoutMs: number;

15+

maxBytes?: number;

1216

}): Promise<Buffer> {

13-

const { text, apiKey, baseUrl, voiceId, outputFormat, timeoutMs } = params;

17+

const {

18+

text,

19+

apiKey,

20+

baseUrl,

21+

voiceId,

22+

outputFormat,

23+

timeoutMs,

24+

maxBytes = DEFAULT_TTS_MAX_BYTES,

25+

} = params;

1426

const normalizedBaseUrl = normalizeGradiumBaseUrl(baseUrl);

1527

const url = `${normalizedBaseUrl}/api/post/speech/tts`;

1628

const hostname = new URL(normalizedBaseUrl).hostname;

@@ -39,7 +51,10 @@ export async function gradiumTTS(params: {

3951

try {

4052

await assertOkOrThrowProviderError(response, "Gradium API error");

4153
42-

return Buffer.from(await response.arrayBuffer());

54+

return await readResponseWithLimit(response, maxBytes, {

55+

onOverflow: ({ maxBytes }) =>

56+

new Error(`Gradium TTS audio response exceeds ${maxBytes} bytes`),

57+

});

4358

} finally {

4459

await release();

4560

}

Original file line numberDiff line numberDiff line change

@@ -291,6 +291,33 @@ describe("buildOpenAISpeechProvider", () => {

291291

expect(result.voiceCompatible).toBe(false);

292292

});

293293
294+

it("applies the configured media byte cap to synthesized audio", async () => {

295+

const provider = buildOpenAISpeechProvider();

296+

globalThis.fetch = vi.fn(

297+

async () => new Response(new Uint8Array(2048), { status: 200 }),

298+

) as unknown as typeof fetch;

299+
300+

await expect(

301+

provider.synthesize({

302+

text: "hello",

303+

cfg: {

304+

agents: {

305+

defaults: {

306+

mediaMaxMb: 0.001,

307+

},

308+

},

309+

} as never,

310+

providerConfig: {

311+

apiKey: "sk-test",

312+

model: "gpt-4o-mini-tts",

313+

voice: "alloy",

314+

},

315+

target: "audio-file",

316+

timeoutMs: 1_000,

317+

}),

318+

).rejects.toThrow("OpenAI TTS audio response exceeds");

319+

});

320+
294321

it("applies provider overrides to telephony synthesis", async () => {

295322

const provider = buildOpenAISpeechProvider();

296323

const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {

Original file line numberDiff line numberDiff line change

@@ -26,6 +26,7 @@ import {

2626

} from "./tts.js";

2727
2828

const OPENAI_SPEECH_RESPONSE_FORMATS = ["mp3", "opus", "wav"] as const;

29+

const DEFAULT_GENERATED_AUDIO_MAX_BYTES = 16 * 1024 * 1024;

2930
3031

type OpenAiSpeechResponseFormat = (typeof OPENAI_SPEECH_RESPONSE_FORMATS)[number];

3132

@@ -174,6 +175,16 @@ function readOpenAIOverrides(

174175

};

175176

}

176177
178+

function resolveGeneratedAudioMaxBytes(req: {

179+

cfg: { agents?: { defaults?: { mediaMaxMb?: number } } };

180+

}): number {

181+

const configured = req.cfg.agents?.defaults?.mediaMaxMb;

182+

if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {

183+

return Math.floor(configured * 1024 * 1024);

184+

}

185+

return DEFAULT_GENERATED_AUDIO_MAX_BYTES;

186+

}

187+
177188

function renderOpenAITtsPersonaInstructions(req: {

178189

label?: string;

179190

prompt?: {

@@ -328,6 +339,7 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {

328339

responseFormat,

329340

extraBody: config.extraBody,

330341

timeoutMs: req.timeoutMs,

342+

maxBytes: resolveGeneratedAudioMaxBytes(req),

331343

});

332344

return {

333345

audioBuffer,

@@ -356,6 +368,7 @@ export function buildOpenAISpeechProvider(): SpeechProviderPlugin {

356368

responseFormat: outputFormat,

357369

extraBody: config.extraBody,

358370

timeoutMs: req.timeoutMs,

371+

maxBytes: resolveGeneratedAudioMaxBytes(req),

359372

});

360373

return { audioBuffer, outputFormat, sampleRate };

361374

},

Original file line numberDiff line numberDiff line change

@@ -322,6 +322,32 @@ describe("openai tts", () => {

322322

).rejects.toThrow("OpenAI TTS API error (503): temporary upstream outage");

323323

});

324324
325+

it("caps streamed audio responses instead of buffering oversized TTS output", async () => {

326+

const streamed = createStreamingErrorResponse({

327+

status: 200,

328+

chunkCount: 20,

329+

chunkSize: 1024,

330+

byte: 121,

331+

});

332+

const fetchMock = vi.fn(async () => streamed.response);

333+

globalThis.fetch = fetchMock as unknown as typeof fetch;

334+
335+

await expect(

336+

openaiTTS({

337+

text: "hello",

338+

apiKey: "test-key",

339+

baseUrl: "https://api.openai.com/v1",

340+

model: "gpt-4o-mini-tts",

341+

voice: "alloy",

342+

responseFormat: "mp3",

343+

timeoutMs: 5_000,

344+

maxBytes: 2048,

345+

}),

346+

).rejects.toThrow("OpenAI TTS audio response exceeds 2048 bytes");

347+
348+

expect(streamed.getReadCount()).toBeLessThan(20);

349+

});

350+
325351

it("caps streamed non-JSON error reads instead of consuming full response bodies", async () => {

326352

const streamed = createStreamingErrorResponse({

327353

status: 503,

Original file line numberDiff line numberDiff line change

@@ -6,12 +6,14 @@ import {

66

captureHttpExchange,

77

isDebugProxyGlobalFetchPatchInstalled,

88

} from "openclaw/plugin-sdk/proxy-capture";

9+

import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";

910

import {

1011

fetchWithSsrFGuard,

1112

ssrfPolicyFromHttpBaseUrlAllowedHostname,

1213

} from "openclaw/plugin-sdk/ssrf-runtime";

1314
1415

export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";

16+

const DEFAULT_TTS_MAX_BYTES = 16 * 1024 * 1024;

1517
1618

export const OPENAI_TTS_MODELS = ["gpt-4o-mini-tts", "tts-1", "tts-1-hd"] as const;

1719

@@ -100,6 +102,7 @@ export async function openaiTTS(params: {

100102

responseFormat: "mp3" | "opus" | "pcm" | "wav";

101103

extraBody?: Record<string, unknown>;

102104

timeoutMs: number;

105+

maxBytes?: number;

103106

}): Promise<Buffer> {

104107

const {

105108

text,

@@ -112,6 +115,7 @@ export async function openaiTTS(params: {

112115

responseFormat,

113116

extraBody,

114117

timeoutMs,

118+

maxBytes = DEFAULT_TTS_MAX_BYTES,

115119

} = params;

116120

const effectiveInstructions = resolveOpenAITtsInstructions(model, instructions, baseUrl);

117121

@@ -177,7 +181,10 @@ export async function openaiTTS(params: {

177181
178182

await assertOkOrThrowProviderError(response, "OpenAI TTS API error");

179183
180-

return Buffer.from(await response.arrayBuffer());

184+

return await readResponseWithLimit(response, maxBytes, {

185+

onOverflow: ({ maxBytes }) =>

186+

new Error(`OpenAI TTS audio response exceeds ${maxBytes} bytes`),

187+

});

181188

} finally {

182189

await release();

183190

}

Original file line numberDiff line numberDiff line change

@@ -37,6 +37,7 @@ function requireLastTtsCall(): {

3737

language?: string;

3838

speed?: number;

3939

responseFormat?: string;

40+

maxBytes?: number;

4041

} {

4142

const params = (xaiTTSMock.mock.calls as unknown as Array<[unknown]>).at(-1)?.[0] as

4243

| {

@@ -47,6 +48,7 @@ function requireLastTtsCall(): {

4748

language?: string;

4849

speed?: number;

4950

responseFormat?: string;

51+

maxBytes?: number;

5052

}

5153

| undefined;

5254

if (!params) {

@@ -68,7 +70,13 @@ describe("xai speech provider", () => {

6870

const provider = buildXaiSpeechProvider();

6971

const result = await provider.synthesize({

7072

text: "hello",

71-

cfg: {},

73+

cfg: {

74+

agents: {

75+

defaults: {

76+

mediaMaxMb: 2,

77+

},

78+

},

79+

},

7280

providerConfig: {

7381

apiKey: "xai-key",

7482

voiceId: "eve",

@@ -87,6 +95,7 @@ describe("xai speech provider", () => {

8795

expect(tts.baseUrl).toBe("https://api.x.ai/v1");

8896

expect(tts.voiceId).toBe("eve");

8997

expect(tts.responseFormat).toBe("mp3");

98+

expect(tts.maxBytes).toBe(2 * 1024 * 1024);

9099

});

91100
92101

it("honors configured response formats", async () => {

Original file line numberDiff line numberDiff line change

@@ -26,6 +26,7 @@ import {

2626

} from "./tts.js";

2727
2828

const XAI_SPEECH_RESPONSE_FORMATS = ["mp3", "wav", "pcm", "mulaw", "alaw"] as const;

29+

const DEFAULT_GENERATED_AUDIO_MAX_BYTES = 16 * 1024 * 1024;

2930
3031

type XaiSpeechResponseFormat = (typeof XAI_SPEECH_RESPONSE_FORMATS)[number];

3132

@@ -130,6 +131,16 @@ function readXaiOverrides(overrides: SpeechProviderOverrides | undefined): XaiTt

130131

};

131132

}

132133
134+

function resolveGeneratedAudioMaxBytes(req: {

135+

cfg: { agents?: { defaults?: { mediaMaxMb?: number } } };

136+

}): number {

137+

const configured = req.cfg.agents?.defaults?.mediaMaxMb;

138+

if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {

139+

return Math.floor(configured * 1024 * 1024);

140+

}

141+

return DEFAULT_GENERATED_AUDIO_MAX_BYTES;

142+

}

143+
133144

function parseDirectiveToken(ctx: SpeechDirectiveTokenParseContext): {

134145

handled: boolean;

135146

overrides?: SpeechProviderOverrides;

@@ -231,6 +242,7 @@ export function buildXaiSpeechProvider(): SpeechProviderPlugin {

231242

speed: overrides.speed ?? config.speed,

232243

responseFormat,

233244

timeoutMs: req.timeoutMs,

245+

maxBytes: resolveGeneratedAudioMaxBytes(req),

234246

});

235247

return {

236248

audioBuffer,

@@ -254,6 +266,7 @@ export function buildXaiSpeechProvider(): SpeechProviderPlugin {

254266

speed: overrides.speed ?? config.speed,

255267

responseFormat: outputFormat,

256268

timeoutMs: req.timeoutMs,

269+

maxBytes: resolveGeneratedAudioMaxBytes(req),

257270

});

258271

return { audioBuffer, outputFormat, sampleRate };

259272

},