Skip to content

Commit 765d05c

Browse files
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 <cursoragent@cursor.com> * test(moonshot): add bounds and malformed-JSON coverage for video description --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 66e676d commit 765d05c

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

extensions/moonshot/media-understanding-provider.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,39 @@ import { describeMoonshotVideo } from "./media-understanding-provider.js";
88

99
installPinnedHostnameTestHooks();
1010

11+
function oversizedJsonResponse(params: { chunkCount: number; chunkSize: number }): {
12+
response: Response;
13+
getReadCount: () => number;
14+
wasCanceled: () => boolean;
15+
} {
16+
const chunk = new Uint8Array(params.chunkSize);
17+
let readCount = 0;
18+
let canceled = false;
19+
return {
20+
response: new Response(
21+
new ReadableStream<Uint8Array>({
22+
pull(controller) {
23+
if (readCount >= params.chunkCount) {
24+
controller.close();
25+
return;
26+
}
27+
readCount += 1;
28+
controller.enqueue(chunk);
29+
},
30+
cancel() {
31+
canceled = true;
32+
},
33+
}),
34+
{
35+
status: 200,
36+
headers: { "Content-Type": "application/json" },
37+
},
38+
),
39+
getReadCount: () => readCount,
40+
wasCanceled: () => canceled,
41+
};
42+
}
43+
1144
describe("describeMoonshotVideo", () => {
1245
it("builds an OpenAI-compatible video request", async () => {
1346
const { fetchFn, getRequest } = createRequestCaptureJsonFetch({
@@ -90,4 +123,42 @@ describe("describeMoonshotVideo", () => {
90123
expect(result.text).toBe("reasoned answer");
91124
expect(result.model).toBe("kimi-k2.6");
92125
});
126+
127+
it("bounds successful Moonshot video JSON bodies instead of buffering the whole response", async () => {
128+
const streamed = oversizedJsonResponse({ chunkCount: 64, chunkSize: 1024 * 1024 });
129+
130+
await expect(
131+
describeMoonshotVideo({
132+
buffer: Buffer.from("video-bytes"),
133+
fileName: "clip.mp4",
134+
mime: "video/mp4",
135+
apiKey: "test-key",
136+
timeoutMs: 1500,
137+
baseUrl: "https://example.com/v1",
138+
fetchFn: async () => streamed.response,
139+
}),
140+
).rejects.toThrow("Moonshot video description failed: JSON response exceeds 16777216 bytes");
141+
142+
expect(streamed.getReadCount()).toBeLessThan(64);
143+
expect(streamed.wasCanceled()).toBe(true);
144+
});
145+
146+
it("reports malformed Moonshot video JSON with a provider-owned error", async () => {
147+
const response = new Response("not-json{", {
148+
status: 200,
149+
headers: { "Content-Type": "application/json" },
150+
});
151+
152+
await expect(
153+
describeMoonshotVideo({
154+
buffer: Buffer.from("video-bytes"),
155+
fileName: "clip.mp4",
156+
mime: "video/mp4",
157+
apiKey: "test-key",
158+
timeoutMs: 1500,
159+
baseUrl: "https://example.com/v1",
160+
fetchFn: async () => response,
161+
}),
162+
).rejects.toThrow("Moonshot video description failed: malformed JSON response");
163+
});
93164
});

extensions/moonshot/media-understanding-provider.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import {
1414
assertOkOrThrowHttpError,
1515
postJsonRequest,
16+
readProviderJsonResponse,
1617
resolveProviderHttpRequestConfig,
1718
} from "openclaw/plugin-sdk/provider-http";
1819
import { MOONSHOT_DEFAULT_MODEL_ID } from "./provider-catalog.js";
@@ -64,7 +65,10 @@ export async function describeMoonshotVideo(
6465

6566
try {
6667
await assertOkOrThrowHttpError(res, "Moonshot video description failed");
67-
const payload = (await res.json()) as OpenAiCompatibleVideoPayload;
68+
const payload = await readProviderJsonResponse<OpenAiCompatibleVideoPayload>(
69+
res,
70+
"Moonshot video description failed",
71+
);
6872
const text = coerceOpenAiCompatibleVideoText(payload);
6973
if (!text) {
7074
throw new Error("Moonshot video description response missing content");

0 commit comments

Comments
 (0)