diff --git a/extensions/vydra/image-generation-provider.test.ts b/extensions/vydra/image-generation-provider.test.ts index e98a21cdf99a..514af26ab337 100644 --- a/extensions/vydra/image-generation-provider.test.ts +++ b/extensions/vydra/image-generation-provider.test.ts @@ -17,6 +17,13 @@ function fetchCall(fetchMock: ReturnType, index = 0): [string, Req return call as [string, RequestInit]; } +function oversizedJsonResponse(): Response { + return new Response(Buffer.alloc(16 * 1024 * 1024 + 1, 0x20), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + describe("vydra image-generation provider", () => { installPinnedHostnameTestHooks(); @@ -94,6 +101,21 @@ describe("vydra image-generation provider", () => { ).rejects.toThrow("Vydra image download exceeds 1 bytes"); }); + it("rejects image creation JSON responses that exceed the provider cap", async () => { + stubVydraApiKey(); + stubFetch(oversizedJsonResponse()); + + const provider = buildVydraImageGenerationProvider(); + await expect( + provider.generateImage({ + provider: "vydra", + model: "grok-imagine", + prompt: "draw a cat", + cfg: {}, + }), + ).rejects.toThrow("vydra.image-generation: JSON response exceeds 16777216 bytes"); + }); + it("passes request SSRF policy to the image creation request", async () => { stubVydraApiKey(); const fetchMock = stubFetch( @@ -151,4 +173,19 @@ describe("vydra image-generation provider", () => { expect(pollCall[0]).toBe("https://www.vydra.ai/api/v1/jobs/job-456"); expect(pollCall[1].method).toBe("GET"); }); + + it("rejects job poll JSON responses that exceed the provider cap", async () => { + stubVydraApiKey(); + stubFetch(jsonResponse({ jobId: "job-456", status: "queued" }), oversizedJsonResponse()); + + const provider = buildVydraImageGenerationProvider(); + await expect( + provider.generateImage({ + provider: "vydra", + model: "grok-imagine", + prompt: "draw a cat", + cfg: {}, + }), + ).rejects.toThrow("Vydra job status: JSON response exceeds 16777216 bytes"); + }); }); diff --git a/extensions/vydra/shared.ts b/extensions/vydra/shared.ts index 6532c9c073a4..1bec44e714b7 100644 --- a/extensions/vydra/shared.ts +++ b/extensions/vydra/shared.ts @@ -6,6 +6,7 @@ import { assertOkOrThrowHttpError, createProviderOperationDeadline, fetchWithTimeout, + readProviderJsonResponse, resolveProviderOperationTimeoutMs, resolveProviderHttpRequestConfig, waitProviderOperationPollInterval, @@ -287,7 +288,7 @@ async function waitForVydraJob(params: { params.fetchFn, ); await assertOkOrThrowHttpError(response, "Vydra job status request failed"); - const payload = await response.json(); + const payload = await readProviderJsonResponse(response, "Vydra job status"); const status = resolveVydraResponseStatus(payload); if (status === "completed" || extractVydraResultUrls(payload, params.kind).length > 0) { return payload; diff --git a/extensions/vydra/speech-provider.test.ts b/extensions/vydra/speech-provider.test.ts index f669afc03890..c6a19bf0beed 100644 --- a/extensions/vydra/speech-provider.test.ts +++ b/extensions/vydra/speech-provider.test.ts @@ -8,6 +8,12 @@ describe("vydra speech provider", () => { const provider = buildVydraSpeechProvider(); + const oversizedJsonResponse = () => + new Response(Buffer.alloc(16 * 1024 * 1024 + 1, 0x20), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks(); @@ -103,4 +109,18 @@ describe("vydra speech provider", () => { }), ).rejects.toThrow("Vydra audio download exceeds 1 bytes"); }); + + it("rejects speech synthesis JSON responses that exceed the provider cap", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValueOnce(oversizedJsonResponse())); + + await expect( + provider.synthesize({ + text: "OpenClaw test", + cfg: {} as never, + providerConfig: { apiKey: "vydra-test-key" }, + target: "audio-file", + timeoutMs: 30_000, + }), + ).rejects.toThrow("Vydra speech synthesis: JSON response exceeds 16777216 bytes"); + }); }); diff --git a/extensions/vydra/speech-provider.ts b/extensions/vydra/speech-provider.ts index 74e02e0c91a0..3a2b6daefece 100644 --- a/extensions/vydra/speech-provider.ts +++ b/extensions/vydra/speech-provider.ts @@ -2,6 +2,7 @@ import { assertOkOrThrowHttpError, postJsonRequest, + readProviderJsonResponse, resolveProviderHttpRequestConfig, } from "openclaw/plugin-sdk/provider-http"; import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input"; @@ -129,7 +130,7 @@ export function buildVydraSpeechProvider(): SpeechProviderPlugin { try { await assertOkOrThrowHttpError(response, "Vydra speech synthesis failed"); - const payload = await response.json(); + const payload = await readProviderJsonResponse(response, "Vydra speech synthesis"); const audioUrl = extractVydraResultUrls(payload, "audio")[0]; if (!audioUrl) { throw new Error("Vydra speech synthesis response missing audio URL"); diff --git a/extensions/vydra/video-generation-provider.test.ts b/extensions/vydra/video-generation-provider.test.ts index c748bee5ccda..c66f1156a6a2 100644 --- a/extensions/vydra/video-generation-provider.test.ts +++ b/extensions/vydra/video-generation-provider.test.ts @@ -18,6 +18,13 @@ function fetchCall(fetchMock: ReturnType, index: number) { return call; } +function oversizedJsonResponse(): Response { + return new Response(Buffer.alloc(16 * 1024 * 1024 + 1, 0x20), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + describe("vydra video-generation provider", () => { installPinnedHostnameTestHooks(); @@ -96,6 +103,21 @@ describe("vydra video-generation provider", () => { ).rejects.toThrow("Vydra video download exceeds 1 bytes"); }); + it("rejects video creation JSON responses that exceed the provider cap", async () => { + stubVydraApiKey(); + stubFetch(oversizedJsonResponse()); + + const provider = buildVydraVideoGenerationProvider(); + await expect( + provider.generateVideo({ + provider: "vydra", + model: "veo3", + prompt: "tiny city at sunrise", + cfg: {}, + }), + ).rejects.toThrow("Vydra video generation: JSON response exceeds 16777216 bytes"); + }); + it("requires a remote image url for kling", async () => { stubVydraApiKey(); vi.stubGlobal("fetch", vi.fn()); diff --git a/extensions/vydra/video-generation-provider.ts b/extensions/vydra/video-generation-provider.ts index 7e8e95816d54..02310b558b0a 100644 --- a/extensions/vydra/video-generation-provider.ts +++ b/extensions/vydra/video-generation-provider.ts @@ -5,6 +5,7 @@ import { createProviderOperationDeadline, createProviderOperationTimeoutResolver, postJsonRequest, + readProviderJsonResponse, resolveProviderOperationTimeoutMs, } from "openclaw/plugin-sdk/provider-http"; import type { VideoGenerationProvider } from "openclaw/plugin-sdk/video-generation"; @@ -111,7 +112,10 @@ export function buildVydraVideoGenerationProvider(): VideoGenerationProvider { try { await assertOkOrThrowHttpError(response, "Vydra video generation failed"); - const submitted = await response.json(); + const submitted = await readProviderJsonResponse( + response, + "Vydra video generation", + ); const completedPayload = await resolveCompletedVydraPayload({ submitted, baseUrl,