fix(agents): MiniMax VLM can consume oversized success responses (#100694)

* Cap MiniMax VLM success response reads

* fix: share bounded MiniMax response reader

* test: return fresh MiniMax response bodies

* test: allow stream prefetch at MiniMax bound

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
xingzhou
2026-07-06 19:03:53 +08:00
committed by GitHub
parent a53565f6df
commit d86cb67a4a
4 changed files with 54 additions and 24 deletions

View File

@@ -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<Uint8Array>({
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", () => {

View File

@@ -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<unknown>(res, responseLabel);
if (!isRecord(json)) {
const trace = traceId ? ` Trace-Id: ${traceId}` : "";
throw new Error(`MiniMax VLM response was not JSON.${trace}`);

View File

@@ -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;
}

View File

@@ -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",