mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
fix(gemini): strip prefixItems from Gemini tool schemas (#12540)
High-impact and precisely diagnosed: `prefixItems` missing from the strip-list rejected every tool-bearing Claude Code request routed to Gemini before generation, because Claude Code's built-in tools describe line ranges as tuples. All three tool shapes going through the same cleaner is what makes the one-key fix sufficient. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- **fix(gemini):** strip the JSON-Schema-2020-12 `prefixItems` keyword from Gemini tool schemas at every nesting level, so Claude Code tool definitions no longer fail with `400 Unknown name "prefixItems"` on Gemini models (#12540 — thanks @pacocartones)
|
||||
@@ -63,6 +63,12 @@ export const GEMINI_UNSUPPORTED_SCHEMA_KEYS = new Set([
|
||||
// it, rejecting the whole request with "Unknown name \"uniqueItems\"".
|
||||
// Upstream 9router already strips it alongside `contains` for the same error.
|
||||
"uniqueItems",
|
||||
// #12509: JSON-Schema-2020-12 tuple keyword. Claude Code's built-in tools
|
||||
// describe `[start_line, end_line]` ranges with it (nested under `items`),
|
||||
// and Gemini's schema parser rejects the whole tool list with
|
||||
// "Unknown name \"prefixItems\" ... Cannot find field". ensureArrayItems
|
||||
// below still guarantees an `items` schema for the tuple-typed array.
|
||||
"prefixItems",
|
||||
// Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf)
|
||||
"anyOf",
|
||||
"oneOf",
|
||||
|
||||
141
tests/unit/12509-gemini-prefixitems.test.ts
Normal file
141
tests/unit/12509-gemini-prefixitems.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { buildGeminiTools } from "../../open-sse/translator/helpers/geminiToolsSanitizer.ts";
|
||||
import { GEMINI_UNSUPPORTED_SCHEMA_KEYS } from "../../open-sse/translator/helpers/geminiHelper.ts";
|
||||
|
||||
// Issue #12509: Gemini rejects the JSON-Schema-2020-12 tuple keyword `prefixItems` in
|
||||
// function_declarations parameter schemas with HTTP 400
|
||||
// `Unknown name "prefixItems" at 'tools[0].function_declarations[1].parameters.properties[5]
|
||||
// .value.properties[0].value.items': Cannot find field.` — the same class of error already
|
||||
// fixed for `uniqueItems` (#9617), `multipleOf`, `strict` and `encrypted` in
|
||||
// GEMINI_UNSUPPORTED_SCHEMA_KEYS (open-sse/translator/helpers/geminiHelper.ts).
|
||||
|
||||
type GeminiFunctionDeclaration = { name: string; parameters: Record<string, unknown> };
|
||||
|
||||
function declarationsOf(tools: unknown[]): GeminiFunctionDeclaration[] {
|
||||
const geminiTools = buildGeminiTools(tools) as Array<{
|
||||
functionDeclarations?: GeminiFunctionDeclaration[];
|
||||
}> | null;
|
||||
assert.ok(geminiTools, "expected buildGeminiTools to return a tools array");
|
||||
return geminiTools.flatMap((tool) => tool.functionDeclarations ?? []);
|
||||
}
|
||||
|
||||
function assertNoPrefixItems(tools: unknown[]): GeminiFunctionDeclaration[] {
|
||||
const declarations = declarationsOf(tools);
|
||||
const serialized = JSON.stringify(declarations);
|
||||
assert.equal(
|
||||
serialized.includes("prefixItems"),
|
||||
false,
|
||||
`prefixItems leaked into the Gemini payload (would trigger upstream 400 "Unknown name \\"prefixItems\\""): ${serialized}`
|
||||
);
|
||||
return declarations;
|
||||
}
|
||||
|
||||
// The reporter's shape: a tuple nested under `items` — an array of `[start_line, end_line]`
|
||||
// ranges, i.e. `properties.ranges.items.prefixItems`.
|
||||
const nestedTupleParameters = {
|
||||
type: "object",
|
||||
properties: {
|
||||
file_path: { type: "string" },
|
||||
ranges: {
|
||||
type: "array",
|
||||
description: "Line ranges to read",
|
||||
items: {
|
||||
type: "array",
|
||||
prefixItems: [{ type: "integer" }, { type: "integer" }],
|
||||
items: false,
|
||||
minItems: 2,
|
||||
maxItems: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["file_path", "ranges"],
|
||||
};
|
||||
|
||||
test("buildGeminiTools strips prefixItems nested under items (OpenAI tool shape, issue #12509)", () => {
|
||||
const [declaration] = assertNoPrefixItems([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "read_ranges",
|
||||
description: "tuple-typed array parameter nested under items",
|
||||
parameters: nestedTupleParameters,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const ranges = (declaration.parameters.properties as Record<string, Record<string, unknown>>)
|
||||
.ranges;
|
||||
assert.equal(ranges.type, "array");
|
||||
const inner = ranges.items as Record<string, unknown>;
|
||||
assert.equal(inner.type, "array");
|
||||
assert.ok(inner.items && typeof inner.items === "object", "inner array keeps an items schema");
|
||||
});
|
||||
|
||||
test("buildGeminiTools strips prefixItems from a Claude input_schema (issue #12509)", () => {
|
||||
const [declaration] = assertNoPrefixItems([
|
||||
{
|
||||
name: "read_ranges",
|
||||
description: "Claude Messages tool shape",
|
||||
input_schema: nestedTupleParameters,
|
||||
},
|
||||
]);
|
||||
assert.equal(declaration.name, "read_ranges");
|
||||
});
|
||||
|
||||
test("buildGeminiTools strips a top-level prefixItems tuple and keeps a usable items schema (issue #12509)", () => {
|
||||
const [declaration] = assertNoPrefixItems([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "read_range",
|
||||
description: "single [start_line, end_line] tuple",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
range: {
|
||||
type: "array",
|
||||
prefixItems: [{ type: "integer" }, { type: "integer" }],
|
||||
},
|
||||
},
|
||||
required: ["range"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const range = (declaration.parameters.properties as Record<string, Record<string, unknown>>)
|
||||
.range;
|
||||
assert.equal(range.type, "array");
|
||||
assert.ok(range.items && typeof range.items === "object", "Gemini requires items on arrays");
|
||||
});
|
||||
|
||||
test("buildGeminiTools strips prefixItems that sits next to a regular items schema (issue #12509)", () => {
|
||||
const [declaration] = assertNoPrefixItems([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "pair",
|
||||
description: "tuple keyword as a sibling of a regular items schema",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
pair: {
|
||||
type: "array",
|
||||
prefixItems: [{ type: "string" }],
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const pair = (declaration.parameters.properties as Record<string, Record<string, unknown>>).pair;
|
||||
assert.deepEqual(pair.items, { type: "string" });
|
||||
});
|
||||
|
||||
test("prefixItems is registered in GEMINI_UNSUPPORTED_SCHEMA_KEYS (issue #12509)", () => {
|
||||
assert.ok(GEMINI_UNSUPPORTED_SCHEMA_KEYS.has("prefixItems"));
|
||||
});
|
||||
Reference in New Issue
Block a user