fix(vydra): bound control response reads

Reviewed and accepted after live preflight: mergeable clean, checks passing, no unresolved review threads.

(cherry picked from commit 74a9beb83f)
This commit is contained in:
Alix-007
2026-06-29 12:11:23 +08:00
committed by Dallin Romney
parent 526e0a204e
commit e40f3ee89a
6 changed files with 88 additions and 3 deletions
@@ -17,6 +17,13 @@ function fetchCall(fetchMock: ReturnType<typeof vi.fn>, 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");
});
});
+2 -1
View File
@@ -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<unknown>(response, "Vydra job status");
const status = resolveVydraResponseStatus(payload);
if (status === "completed" || extractVydraResultUrls(payload, params.kind).length > 0) {
return payload;
+20
View File
@@ -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");
});
});
+2 -1
View File
@@ -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<unknown>(response, "Vydra speech synthesis");
const audioUrl = extractVydraResultUrls(payload, "audio")[0];
if (!audioUrl) {
throw new Error("Vydra speech synthesis response missing audio URL");
@@ -18,6 +18,13 @@ function fetchCall(fetchMock: ReturnType<typeof vi.fn>, 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());
@@ -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<unknown>(
response,
"Vydra video generation",
);
const completedPayload = await resolveCompletedVydraPayload({
submitted,
baseUrl,