From bdc30ca4ddc31f67c3f691ffb44c13c18738d599 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Mon, 17 Aug 2026 05:50:09 -0300 Subject: [PATCH] fix(providers): strip uniqueItems from Gemini tool schemas to avoid upstream 400 (#9617) (#10511) Co-authored-by: adevwithpurpose --- .../fixes/9617-gemini-uniqueitems-strip.md | 1 + open-sse/translator/helpers/geminiHelper.ts | 5 ++ tests/unit/9617-gemini-uniqueitems.test.ts | 77 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 changelog.d/fixes/9617-gemini-uniqueitems-strip.md create mode 100644 tests/unit/9617-gemini-uniqueitems.test.ts diff --git a/changelog.d/fixes/9617-gemini-uniqueitems-strip.md b/changelog.d/fixes/9617-gemini-uniqueitems-strip.md new file mode 100644 index 0000000000..8e01e17a28 --- /dev/null +++ b/changelog.d/fixes/9617-gemini-uniqueitems-strip.md @@ -0,0 +1 @@ +- fix(providers): strip uniqueItems from Gemini tool schemas (Gemini rejects it with 400 'Unknown name uniqueItems') (#9617) diff --git a/open-sse/translator/helpers/geminiHelper.ts b/open-sse/translator/helpers/geminiHelper.ts index 5d7bfc676e..623e39cba3 100644 --- a/open-sse/translator/helpers/geminiHelper.ts +++ b/open-sse/translator/helpers/geminiHelper.ts @@ -58,6 +58,11 @@ export const GEMINI_UNSUPPORTED_SCHEMA_KEYS = new Set([ "contains", "minContains", "maxContains", + // #9617: array uniqueness keyword — agentic-CLI tool schemas (JSON-Schema + // generators) set this routinely and Gemini's schema parser has no field for + // it, rejecting the whole request with "Unknown name \"uniqueItems\"". + // Upstream 9router already strips it alongside `contains` for the same error. + "uniqueItems", // Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf) "anyOf", "oneOf", diff --git a/tests/unit/9617-gemini-uniqueitems.test.ts b/tests/unit/9617-gemini-uniqueitems.test.ts new file mode 100644 index 0000000000..287bbc564d --- /dev/null +++ b/tests/unit/9617-gemini-uniqueitems.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { buildGeminiTools } from "../../open-sse/translator/helpers/geminiToolsSanitizer.ts"; + +// Issue #9617: Gemini rejects `uniqueItems` in function_declarations parameter schemas +// with HTTP 400 "Unknown name \"uniqueItems\" ... Cannot find field" (Gemini's protobuf-JSON +// schema parser only accepts a subset of JSON Schema/OpenAPI 3.0 — the same class of error +// already fixed for `multipleOf`, `minItems`, `maxItems`, `strict`, `encrypted` in +// GEMINI_UNSUPPORTED_SCHEMA_KEYS, open-sse/translator/helpers/geminiHelper.ts). +test("buildGeminiTools strips uniqueItems from array schemas (issue #9617)", () => { + const tools = [ + { + type: "function", + function: { + name: "exit_worktree", + description: "test tool with an array-of-objects parameter", + parameters: { + type: "object", + properties: { + items: { + type: "array", + uniqueItems: true, + items: { + type: "object", + properties: { + name: { type: "string" }, + action: { type: "string" }, + }, + required: ["name", "action"], + }, + }, + }, + required: ["items"], + }, + }, + }, + ]; + + const geminiTools = buildGeminiTools(tools); + const serialized = JSON.stringify(geminiTools); + + assert.ok(geminiTools, "expected buildGeminiTools to return a tools array"); + assert.equal( + serialized.includes("uniqueItems"), + false, + `uniqueItems leaked into the Gemini payload (would trigger upstream 400 "Unknown name \\"uniqueItems\\""): ${serialized}` + ); +}); + +// Companion: a top-level (non-nested) array property with uniqueItems is also stripped — +// matches the reporter's deeply-nested case with extra path coverage. +test("buildGeminiTools strips uniqueItems from a top-level array parameter schema (issue #9617)", () => { + const tools = [ + { + type: "function", + function: { + name: "list_worktrees", + description: "test tool with a top-level array parameter", + parameters: { + type: "object", + properties: { + paths: { + type: "array", + uniqueItems: true, + items: { type: "string" }, + }, + }, + required: ["paths"], + }, + }, + }, + ]; + + const serialized = JSON.stringify(buildGeminiTools(tools)); + assert.equal(serialized.includes("uniqueItems"), false); +}); \ No newline at end of file