diff --git a/packages/ai/src/providers/google-shared.ts b/packages/ai/src/providers/google-shared.ts index 57b592382..2f0696cea 100644 --- a/packages/ai/src/providers/google-shared.ts +++ b/packages/ai/src/providers/google-shared.ts @@ -239,6 +239,33 @@ export function convertMessages(model: Model, contex return contents; } +const JSON_SCHEMA_META_DECLARATIONS = new Set([ + "$schema", + "$id", + "$anchor", + "$dynamicAnchor", + "$vocabulary", + "$comment", + "$defs", + "definitions", // pre-draft-2019-09 equivalent of $defs +]); + +/** + * Strip meta-declarations from a schema obj + */ +function sanitizeForOpenApi(schema: unknown): unknown { + if (typeof schema !== "object" || schema === null || Array.isArray(schema)) { + return schema; + } + + const result: Record = {}; + for (const [key, value] of Object.entries(schema)) { + if (JSON_SCHEMA_META_DECLARATIONS.has(key)) continue; + result[key] = sanitizeForOpenApi(value); + } + return result; +} + /** * Convert tools to Gemini function declarations format. * @@ -257,7 +284,9 @@ export function convertTools( functionDeclarations: tools.map((tool) => ({ name: tool.name, description: tool.description, - ...(useParameters ? { parameters: tool.parameters } : { parametersJsonSchema: tool.parameters }), + ...(useParameters + ? { parameters: sanitizeForOpenApi(tool.parameters as unknown) } + : { parametersJsonSchema: tool.parameters }), })), }, ]; diff --git a/packages/ai/test/google-shared-convert-tools.test.ts b/packages/ai/test/google-shared-convert-tools.test.ts new file mode 100644 index 000000000..6cce3175e --- /dev/null +++ b/packages/ai/test/google-shared-convert-tools.test.ts @@ -0,0 +1,187 @@ +import { describe, expect, it } from "vitest"; +import { convertTools } from "../src/providers/google-shared.js"; +import type { Tool } from "../src/types.js"; + +function makeTool(parameters: Record): Tool { + return { + name: "test_tool", + description: "A test tool", + parameters: parameters as Tool["parameters"], + }; +} + +describe("google-shared convertTools", () => { + it("strips JSON Schema meta keys from parameters when useParameters=true", () => { + const tools = [ + makeTool({ + $schema: "http://json-schema.org/draft-07/schema#", + $id: "urn:bash-tool", + $comment: "A bash tool for demonstration", + $defs: { + commandDef: { type: "string" }, + }, + definitions: { + legacyDef: { type: "number" }, + }, + type: "object", + properties: { + command: { type: "string" }, + }, + required: ["command"], + }), + ]; + + const result = convertTools(tools, true); + const decl = result?.[0]?.functionDeclarations?.[0]; + + expect(decl).toBeDefined(); + expect(decl?.parameters).toEqual({ + type: "object", + properties: { + command: { type: "string" }, + }, + required: ["command"], + }); + expect(decl?.parameters).not.toHaveProperty("$schema"); + expect(decl?.parameters).not.toHaveProperty("$id"); + expect(decl?.parameters).not.toHaveProperty("$comment"); + expect(decl?.parameters).not.toHaveProperty("$defs"); + expect(decl?.parameters).not.toHaveProperty("definitions"); + }); + + it("recursively strips nested JSON Schema meta keys", () => { + const tools = [ + makeTool({ + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + deep: { + $schema: "http://json-schema.org/draft-07/schema#", + $id: "urn:nested", + type: "string", + }, + }, + }), + ]; + + const result = convertTools(tools, true); + const decl = result?.[0]?.functionDeclarations?.[0]; + + expect(decl).toBeDefined(); + expect(decl?.parameters).toEqual({ + type: "object", + properties: { + deep: { + type: "string", + }, + }, + }); + }); + + it("preserves $ref while stripping meta keys", () => { + const tools = [ + makeTool({ + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + refProp: { + $ref: "#/$defs/someDef", + type: "string", + }, + }, + }), + ]; + + const result = convertTools(tools, true); + const decl = result?.[0]?.functionDeclarations?.[0]; + + expect(decl).toBeDefined(); + expect(decl?.parameters).toEqual({ + type: "object", + properties: { + refProp: { + $ref: "#/$defs/someDef", + type: "string", + }, + }, + }); + }); + + it("does not mutate the original Tool.parameters object", () => { + const originalParameters = { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + command: { type: "string" }, + }, + required: ["command"], + }; + const tools = [makeTool(originalParameters)]; + + convertTools(tools, true); + + expect(originalParameters).toEqual({ + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + command: { type: "string" }, + }, + required: ["command"], + }); + }); + + it("preserves $schema in parametersJsonSchema when useParameters=false", () => { + const tools = [ + makeTool({ + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + command: { type: "string" }, + }, + required: ["command"], + }), + ]; + + const result = convertTools(tools, false); + const decl = result?.[0]?.functionDeclarations?.[0]; + + expect(decl).toBeDefined(); + expect(decl?.parametersJsonSchema).toEqual({ + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + command: { type: "string" }, + }, + required: ["command"], + }); + }); + + it("handles tools without $schema gracefully", () => { + const tools = [ + makeTool({ + type: "object", + properties: { + path: { type: "string" }, + }, + required: ["path"], + }), + ]; + + const result = convertTools(tools, true); + const decl = result?.[0]?.functionDeclarations?.[0]; + + expect(decl).toBeDefined(); + expect(decl?.parameters).toEqual({ + type: "object", + properties: { + path: { type: "string" }, + }, + required: ["path"], + }); + }); + + it("returns undefined for empty tool list", () => { + expect(convertTools([])).toBeUndefined(); + expect(convertTools([], true)).toBeUndefined(); + }); +});