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");