diff --git a/packages/ai/src/utils/validation.ts b/packages/ai/src/utils/validation.ts index f6a65714b..6057c0896 100644 --- a/packages/ai/src/utils/validation.ts +++ b/packages/ai/src/utils/validation.ts @@ -299,6 +299,14 @@ function normalizeOptionalNullsForSchema(schema: unknown, value: unknown): { val return { value: changed ? nextValue : value, changed }; } + // Coerce string → number/integer when the schema branch declares those types. + // This fixes anyOf:[{type:"number"},{type:"null"}] (i.e. Optional) where + // AJV reports an "anyOf" error rather than a "type" error, bypassing + // coerceArgsFromErrors which only handles keyword:"type" errors. + if ((schemaObject.type === "number" || schemaObject.type === "integer") && typeof value === "string") { + return tryParseNumberString(value, [schemaObject.type as string]); + } + if (schemaObject.type !== "object") return { value, changed: false }; if (typeof value !== "object" || value === null) return { value, changed: false }; if (Array.isArray(value)) return { value, changed: false }; diff --git a/packages/ai/test/tool-argument-coercion.test.ts b/packages/ai/test/tool-argument-coercion.test.ts index e375fd18a..c87f1d856 100644 --- a/packages/ai/test/tool-argument-coercion.test.ts +++ b/packages/ai/test/tool-argument-coercion.test.ts @@ -325,4 +325,37 @@ describe("Tool argument coercion", () => { expect(() => validateToolArguments(tool, toolCall)).toThrow('Validation failed for tool "t6"'); }); + + it("coerces numeric string for Optional (anyOf:[number,null])", () => { + const tool: Tool = { + name: "t14", + description: "", + parameters: Type.Object({ tick_size: Type.Optional(Type.Number()) }), + }; + const toolCall: ToolCall = { + type: "toolCall", + id: "call-14", + name: "t14", + arguments: { tick_size: "1.0" }, + }; + const result = validateToolArguments(tool, toolCall); + expect(result.tick_size).toBe(1); + expect(typeof result.tick_size).toBe("number"); + }); + + it("leaves Optional as undefined when absent", () => { + const tool: Tool = { + name: "t15", + description: "", + parameters: Type.Object({ tick_size: Type.Optional(Type.Number()) }), + }; + const toolCall: ToolCall = { + type: "toolCall", + id: "call-15", + name: "t15", + arguments: {}, + }; + const result = validateToolArguments(tool, toolCall); + expect(result.tick_size).toBeUndefined(); + }); });