From dca51df1868a998f61be2fe50c30823c2698ccdb Mon Sep 17 00:00:00 2001 From: NIO Date: Wed, 1 Jul 2026 03:12:46 +0800 Subject: [PATCH] fix(moonshot): bound video description JSON response reads (#96502) * fix(moonshot): bound video description JSON response reads The Moonshot video description endpoint used an unbounded await res.json() to parse the media understanding response. Route through readProviderJsonResponse (16 MiB cap) to match the bound already in place for other media understanding providers (xai, openrouter). AI-assisted. Co-authored-by: Cursor * test(moonshot): add bounds and malformed-JSON coverage for video description --------- Co-authored-by: Cursor (cherry picked from commit 765d05c2e4a891873b782c969957c6eddc163405) --- .../media-understanding-provider.test.ts | 71 +++++++++++++++++++ .../moonshot/media-understanding-provider.ts | 6 +- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/extensions/moonshot/media-understanding-provider.test.ts b/extensions/moonshot/media-understanding-provider.test.ts index a8c9c6fb3c12..27070ed04d44 100644 --- a/extensions/moonshot/media-understanding-provider.test.ts +++ b/extensions/moonshot/media-understanding-provider.test.ts @@ -8,6 +8,39 @@ import { describeMoonshotVideo } from "./media-understanding-provider.js"; installPinnedHostnameTestHooks(); +function oversizedJsonResponse(params: { chunkCount: number; chunkSize: number }): { + response: Response; + getReadCount: () => number; + wasCanceled: () => boolean; +} { + const chunk = new Uint8Array(params.chunkSize); + let readCount = 0; + let canceled = false; + return { + response: new Response( + new ReadableStream({ + pull(controller) { + if (readCount >= params.chunkCount) { + controller.close(); + return; + } + readCount += 1; + controller.enqueue(chunk); + }, + cancel() { + canceled = true; + }, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + getReadCount: () => readCount, + wasCanceled: () => canceled, + }; +} + describe("describeMoonshotVideo", () => { it("builds an OpenAI-compatible video request", async () => { const { fetchFn, getRequest } = createRequestCaptureJsonFetch({ @@ -90,4 +123,42 @@ describe("describeMoonshotVideo", () => { expect(result.text).toBe("reasoned answer"); expect(result.model).toBe("kimi-k2.6"); }); + + it("bounds successful Moonshot video JSON bodies instead of buffering the whole response", async () => { + const streamed = oversizedJsonResponse({ chunkCount: 64, chunkSize: 1024 * 1024 }); + + await expect( + describeMoonshotVideo({ + buffer: Buffer.from("video-bytes"), + fileName: "clip.mp4", + mime: "video/mp4", + apiKey: "test-key", + timeoutMs: 1500, + baseUrl: "https://example.com/v1", + fetchFn: async () => streamed.response, + }), + ).rejects.toThrow("Moonshot video description failed: JSON response exceeds 16777216 bytes"); + + expect(streamed.getReadCount()).toBeLessThan(64); + expect(streamed.wasCanceled()).toBe(true); + }); + + it("reports malformed Moonshot video JSON with a provider-owned error", async () => { + const response = new Response("not-json{", { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + + await expect( + describeMoonshotVideo({ + buffer: Buffer.from("video-bytes"), + fileName: "clip.mp4", + mime: "video/mp4", + apiKey: "test-key", + timeoutMs: 1500, + baseUrl: "https://example.com/v1", + fetchFn: async () => response, + }), + ).rejects.toThrow("Moonshot video description failed: malformed JSON response"); + }); }); diff --git a/extensions/moonshot/media-understanding-provider.ts b/extensions/moonshot/media-understanding-provider.ts index 6c0a4ffb6d97..7c0c287482cc 100644 --- a/extensions/moonshot/media-understanding-provider.ts +++ b/extensions/moonshot/media-understanding-provider.ts @@ -13,6 +13,7 @@ import { import { assertOkOrThrowHttpError, postJsonRequest, + readProviderJsonResponse, resolveProviderHttpRequestConfig, } from "openclaw/plugin-sdk/provider-http"; import { MOONSHOT_DEFAULT_MODEL_ID } from "./provider-catalog.js"; @@ -64,7 +65,10 @@ export async function describeMoonshotVideo( try { await assertOkOrThrowHttpError(res, "Moonshot video description failed"); - const payload = (await res.json()) as OpenAiCompatibleVideoPayload; + const payload = await readProviderJsonResponse( + res, + "Moonshot video description failed", + ); const text = coerceOpenAiCompatibleVideoText(payload); if (!text) { throw new Error("Moonshot video description response missing content");