Compare commits

..

1 Commits

Author SHA1 Message Date
adevwithpurpose
c126108a15 fix(providers): strip uniqueItems from Gemini tool schemas to avoid upstream 400 (#9617) 2026-08-15 19:43:20 -03:00
6 changed files with 86 additions and 20 deletions

View File

@@ -1 +0,0 @@
- **fix(cliproxy):** read platform/arch at runtime via `os.platform()`/`os.arch()` in `binaryManager` so the embedded installer selects the Windows/ARM assets even when the release bundle is built on a Linux runner (fixes #10244)

View File

@@ -0,0 +1 @@
- fix(providers): strip uniqueItems from Gemini tool schemas (Gemini rejects it with 400 'Unknown name uniqueItems') (#9617)

View File

@@ -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",

View File

@@ -16,7 +16,7 @@ type Platform = "linux" | "darwin" | "windows" | "freebsd";
type Arch = "amd64" | "arm64";
function detectPlatform(): Platform {
const p = os.platform();
const p = process.platform;
if (p === "linux") return "linux";
if (p === "darwin") return "darwin";
if (p === "win32") return "windows";
@@ -24,7 +24,7 @@ function detectPlatform(): Platform {
}
function detectArch(): Arch {
const a = os.arch();
const a = process.arch;
if (a === "x64") return "amd64";
if (a === "arm64") return "arm64";
return "amd64";

View File

@@ -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);
});

View File

@@ -1,4 +1,4 @@
import { describe, it, afterEach, after, mock } from "node:test";
import { describe, it, afterEach, after } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import fs from "node:fs";
@@ -63,22 +63,6 @@ describe("binaryManager", () => {
assert.ok(["linux", "darwin", "windows"].includes(platform));
assert.ok(["amd64", "arm64"].includes(arch));
});
it("should read platform/arch at runtime from os (anti build-folding guard) (#10244)", () => {
// Regression guard for #10244/#10293: detectPlatform/detectArch must read
// os.platform()/os.arch() at call time, NOT the build-machine foldable
// process.platform/process.arch constants. Turbopack `next build` running
// on Linux constant-folds `process.platform` and prunes every Windows/arm64
// branch from the published npm artifact. Simulate a Windows arm64 host via
// the runtime os.* functions; the Windows/arm64 branch must be reachable.
mock.method(os, "platform", () => "win32");
mock.method(os, "arch", () => "arm64");
assert.deepEqual(mod.getTargetPlatform(), { platform: "windows", arch: "arm64" });
assert.equal(
mod.getAssetName(),
"CLIProxyAPI_{version}_windows_arm64.zip"
);
});
});
describe("getCurrentBinaryPath", () => {