diff --git a/src/agents/minimax-vlm.normalizes-api-key.test.ts b/src/agents/minimax-vlm.normalizes-api-key.test.ts index d2c32573e872..1df125ba0292 100644 --- a/src/agents/minimax-vlm.normalizes-api-key.test.ts +++ b/src/agents/minimax-vlm.normalizes-api-key.test.ts @@ -243,6 +243,45 @@ describe("minimaxUnderstandImage apiKey normalization", () => { expect(error.message.length).toBeLessThan(520); expect(canceled).toBe(true); }); + + it("bounds large successful response bodies before parsing JSON", async () => { + let canceled = false; + let pullCount = 0; + const body = new ReadableStream({ + pull(controller) { + pullCount += 1; + controller.enqueue(new Uint8Array(1024 * 1024)); + }, + cancel() { + canceled = true; + }, + }); + const fetchSpy = vi.fn(async () => { + return new Response(body, { + status: 200, + headers: { "Content-Type": "application/json", "Trace-Id": "trace-success" }, + }); + }); + global.fetch = withFetchPreconnect(fetchSpy); + + const error = await minimaxUnderstandImage({ + apiKey: "minimax-test-key", + prompt: "hi", + imageDataUrl: "data:image/png;base64,AAAA", + apiHost: "https://api.minimax.io", + }).catch((caught: unknown) => caught); + + if (!(error instanceof Error)) { + throw new Error("expected MiniMax VLM request to reject oversized successful JSON"); + } + expect(error.message).toBe( + "MiniMax VLM response [Trace-Id=trace-success]: JSON response exceeds 16777216 bytes", + ); + // WHATWG streams may pre-pull one chunk beyond the bytes consumed by the reader. + expect(pullCount).toBeGreaterThanOrEqual(17); + expect(pullCount).toBeLessThanOrEqual(18); + expect(canceled).toBe(true); + }); }); describe("isMinimaxVlmModel", () => { diff --git a/src/agents/minimax-vlm.ts b/src/agents/minimax-vlm.ts index afdbfbd3763d..6d4ee26039ed 100644 --- a/src/agents/minimax-vlm.ts +++ b/src/agents/minimax-vlm.ts @@ -6,6 +6,7 @@ import { ensureGlobalUndiciEnvProxyDispatcher } from "../infra/net/undici-global import { resolvePositiveTimerTimeoutMs } from "../shared/number-coercion.js"; import { isRecord } from "../utils.js"; import { normalizeSecretInput } from "../utils/normalize-secret-input.js"; +import { readProviderJsonResponse } from "./provider-http-errors.js"; type MinimaxBaseResp = { status_code?: number; @@ -142,7 +143,10 @@ export async function minimaxUnderstandImage(params: { ); } - const json = (await res.json().catch(() => null)) as unknown; + const responseLabel = traceId + ? `MiniMax VLM response [Trace-Id=${traceId}]` + : "MiniMax VLM response"; + const json = await readProviderJsonResponse(res, responseLabel); if (!isRecord(json)) { const trace = traceId ? ` Trace-Id: ${traceId}` : ""; throw new Error(`MiniMax VLM response was not JSON.${trace}`); diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.test.ts index c54b12ae64d5..1dff7bb7e443 100644 --- a/src/agents/tools/image-tool.test.ts +++ b/src/agents/tools/image-tool.test.ts @@ -447,32 +447,24 @@ function registerImageToolEnvReset(priorFetch: typeof global.fetch, keys: string } function stubMinimaxOkFetch() { - const fetch = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - statusText: "OK", - headers: new Headers(), - json: async () => ({ + const fetch = vi.fn().mockImplementation(async () => + Response.json({ content: "ok", base_resp: { status_code: 0, status_msg: "" }, }), - }); + ); global.fetch = withFetchPreconnect(fetch); vi.stubEnv("MINIMAX_API_KEY", "minimax-test"); return fetch; } function stubMinimaxFetch(baseResp: { status_code: number; status_msg: string }, content = "ok") { - const fetch = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - statusText: "OK", - headers: new Headers(), - json: async () => ({ + const fetch = vi.fn().mockImplementation(async () => + Response.json({ content, base_resp: baseResp, }), - }); + ); global.fetch = withFetchPreconnect(fetch); return fetch; } diff --git a/src/media-understanding/image.test.ts b/src/media-understanding/image.test.ts index a598722923e2..d728c1eb3912 100644 --- a/src/media-understanding/image.test.ts +++ b/src/media-understanding/image.test.ts @@ -150,17 +150,12 @@ describe("describeImageWithModel", () => { vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", path.join(process.cwd(), "extensions")); vi.stubGlobal("fetch", fetchMock); vi.clearAllMocks(); - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - statusText: "OK", - headers: { get: vi.fn(() => null) }, - json: vi.fn(async () => ({ + fetchMock.mockImplementation(async () => + Response.json({ base_resp: { status_code: 0 }, content: "portal ok", - })), - text: vi.fn(async () => ""), - }); + }), + ); discoverModelsMock.mockReturnValue({ find: vi.fn(() => ({ provider: "minimax-portal",