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>
(cherry picked from commit 765d05c2e4)
This commit is contained in:
NIO
2026-07-01 03:12:46 +08:00
committed by Dallin Romney
parent b8791fe9e8
commit dca51df186
2 changed files with 76 additions and 1 deletions
@@ -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<Uint8Array>({
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");
});
});
@@ -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<OpenAiCompatibleVideoPayload>(
res,
"Moonshot video description failed",
);
const text = coerceOpenAiCompatibleVideoText(payload);
if (!text) {
throw new Error("Moonshot video description response missing content");