From 7314819f0fe30fa616f32fa3a1fc8d1a856a1446 Mon Sep 17 00:00:00 2001 From: triagent Date: Fri, 23 Oct 2026 05:30:25 +0000 Subject: [PATCH] fix(ai): send const as enum in Gemini tool schemas --- packages/ai/src/api/google-shared.ts | 30 ++++++++++++- .../test/google-shared-convert-tools.test.ts | 45 +++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/api/google-shared.ts b/packages/ai/src/api/google-shared.ts index 129812344..3f889170a 100644 --- a/packages/ai/src/api/google-shared.ts +++ b/packages/ai/src/api/google-shared.ts @@ -371,11 +371,37 @@ function sanitizeForOpenApi(schema: unknown): unknown { return result; } +/** Schema keywords whose values map names to subschemas, so their keys are not schema keywords. */ +const JSON_SCHEMA_NAME_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"]); + +/** + * Rewrite string and number `const` values to single-value `enum`s. + * Gemini ignores `const` in `parametersJsonSchema` but honors `enum`. + */ +function rewriteConstAsEnum(schema: unknown, isNameMap = false): unknown { + if (Array.isArray(schema)) return schema.map((item) => rewriteConstAsEnum(item)); + if (typeof schema !== "object" || schema === null) return schema; + + const result: Record = {}; + for (const [key, value] of Object.entries(schema)) { + result[key] = isNameMap ? rewriteConstAsEnum(value) : rewriteConstAsEnum(value, JSON_SCHEMA_NAME_MAPS.has(key)); + } + if (!isNameMap && "const" in result && result.enum === undefined) { + const value = result.const; + if (typeof value === "string" || typeof value === "number") { + delete result.const; + result.enum = [value]; + } + } + return result; +} + /** * Convert tools to Gemini function declarations format. * * By default uses `parametersJsonSchema` which supports full JSON Schema (including - * anyOf, oneOf, const, etc.). Set `useParameters` to true to use the legacy `parameters` + * anyOf, oneOf, etc.). Gemini ignores `const` there, so string and number `const` + * values are sent as single-value `enum`s. Set `useParameters` to true to use the legacy `parameters` * field instead (OpenAPI 3.03 Schema). This is needed for Cloud Code Assist with Claude * models, where the API translates `parameters` into Anthropic's `input_schema`. */ @@ -395,7 +421,7 @@ export function convertTools( description: tool.description, ...(useParameters ? { parameters: sanitizeForOpenApi(parameters as unknown) } - : { parametersJsonSchema: parameters }), + : { parametersJsonSchema: rewriteConstAsEnum(parameters) }), }; }), }, diff --git a/packages/ai/test/google-shared-convert-tools.test.ts b/packages/ai/test/google-shared-convert-tools.test.ts index 82876e5fe..d9d4d8b36 100644 --- a/packages/ai/test/google-shared-convert-tools.test.ts +++ b/packages/ai/test/google-shared-convert-tools.test.ts @@ -201,3 +201,48 @@ describe("google-shared convertTools", () => { expect(convertTools([], true)).toBeUndefined(); }); }); + +describe("google-shared convertTools const handling", () => { + it("rewrites string and number const values to single-value enums in parametersJsonSchema", () => { + const parameters = { + type: "object", + properties: { + origin: { type: "string", const: "zq-src-7" }, + mode: { + anyOf: [ + { type: "string", const: "a" }, + { type: "string", const: "b" }, + ], + }, + level: { oneOf: [{ const: 1 }, { const: 2 }] }, + flag: { type: "boolean", const: true }, + list: { type: "array", items: { const: "x" } }, + const: { type: "string" }, + }, + required: ["origin"], + }; + const tools = [makeTool(parameters)]; + + const result = convertTools(tools); + const decl = result?.[0]?.functionDeclarations?.[0]; + + expect(decl?.parametersJsonSchema).toEqual({ + type: "object", + properties: { + origin: { type: "string", enum: ["zq-src-7"] }, + mode: { + anyOf: [ + { type: "string", enum: ["a"] }, + { type: "string", enum: ["b"] }, + ], + }, + level: { oneOf: [{ enum: [1] }, { enum: [2] }] }, + flag: { type: "boolean", const: true }, + list: { type: "array", items: { enum: ["x"] } }, + const: { type: "string" }, + }, + required: ["origin"], + }); + expect(parameters.properties.origin).toEqual({ type: "string", const: "zq-src-7" }); + }); +});