diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.test.ts index 1dbf63d9630c..d8c7e1a5d183 100644 --- a/src/agents/tools/image-tool.test.ts +++ b/src/agents/tools/image-tool.test.ts @@ -1696,8 +1696,8 @@ describe("image tool implicit imageModel config", () => { items: { type: "string" }, }, model: { type: "string" }, - maxBytesMb: { type: "number" }, - maxImages: { type: "number" }, + maxBytesMb: { type: "number", exclusiveMinimum: 0 }, + maxImages: { type: "integer", minimum: 1 }, }, }); }); @@ -2343,6 +2343,40 @@ describe("image tool MiniMax VLM routing", () => { expect(tooManyDetails?.max).toBe(1); }); + it("rejects invalid image cap values before loading images", async () => { + const { fetch, tool } = await createMinimaxVlmFixture({ status_code: 0, status_msg: "" }); + + await expect( + tool.execute("t1", { + prompt: "Compare these images.", + image: `data:image/png;base64,${ONE_PIXEL_PNG_B64}`, + maxImages: 1.5, + }), + ).rejects.toThrow("maxImages must be a positive integer"); + + await expect( + tool.execute("t2", { + prompt: "Compare these images.", + image: `data:image/png;base64,${ONE_PIXEL_PNG_B64}`, + maxBytesMb: 0, + }), + ).rejects.toThrow("maxBytesMb must be greater than 0"); + expect(fetch).not.toHaveBeenCalled(); + }); + + it("accepts string image caps through shared numeric readers", async () => { + const { fetch, tool } = await createMinimaxVlmFixture({ status_code: 0, status_msg: "" }); + + await tool.execute("t1", { + prompt: "Describe this image.", + image: `data:image/png;base64,${ONE_PIXEL_PNG_B64}`, + maxImages: "1", + maxBytesMb: "1", + }); + + expect(fetch).toHaveBeenCalledTimes(1); + }); + it("surfaces MiniMax API errors from /v1/coding_plan/vlm", async () => { const { tool } = await createMinimaxVlmFixture({ status_code: 1004, status_msg: "bad key" }); diff --git a/src/agents/tools/image-tool.ts b/src/agents/tools/image-tool.ts index f4fafb3f2634..e001263d022f 100644 --- a/src/agents/tools/image-tool.ts +++ b/src/agents/tools/image-tool.ts @@ -45,6 +45,8 @@ import { resolveImageFallbackCandidates, resolveImageFallbackDefaultProvider, } from "../model-fallback.js"; +import { optionalFiniteNumberSchema, optionalPositiveIntegerSchema } from "../schema/typebox.js"; +import { readFiniteNumberParam, readPositiveIntegerParam } from "./common.js"; import { coerceImageAssistantText, coerceImageModelConfig, @@ -761,8 +763,8 @@ export function createImageTool(options?: { }), ), model: Type.Optional(Type.String()), - maxBytesMb: Type.Optional(Type.Number()), - maxImages: Type.Optional(Type.Number()), + maxBytesMb: optionalFiniteNumberSchema({ exclusiveMinimum: 0 }), + maxImages: optionalPositiveIntegerSchema(), }), execute: async (_toolCallId, args) => { const record = args && typeof args === "object" ? (args as Record) : {}; @@ -794,11 +796,7 @@ export function createImageTool(options?: { } // MARK: - Enforce max images cap - const maxImagesRaw = typeof record.maxImages === "number" ? record.maxImages : undefined; - const maxImages = - typeof maxImagesRaw === "number" && Number.isFinite(maxImagesRaw) && maxImagesRaw > 0 - ? Math.floor(maxImagesRaw) - : DEFAULT_MAX_IMAGES; + const maxImages = readPositiveIntegerParam(record, "maxImages") ?? DEFAULT_MAX_IMAGES; if (imageInputs.length > maxImages) { return { content: [ @@ -815,7 +813,11 @@ export function createImageTool(options?: { record, DEFAULT_PROMPT, ); - const maxBytesMb = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined; + const maxBytesMb = readFiniteNumberParam(record, "maxBytesMb", { + min: 0, + minExclusive: true, + message: "maxBytesMb must be greater than 0", + }); const maxBytes = pickMaxBytes(options?.config, maxBytesMb); const imageModelConfig = resolvedImageModelConfig ??