fix(gemini): derive the items schema Gemini requires from a tuple-style array - #454
Conversation
…le array A tool whose parameter is a fixed-shape array — `where: [field, operator, value]` written as `prefixItems` with no `items` — reached the Gemini backend as an array without `items`, which it rejects for the whole request: `GenerateContentRequest.tools[0].function_declarations[1] .parameters.properties[query].properties[where].items.items: missing field.` (400, seen through the antigravity provider on 2026-09-03). The schema sanitizer now gives every array schema an `items`: the tuple's positions become one schema — the single schema when they agree, `anyOf` over the distinct typed ones otherwise — and `prefixItems`, which has no Gemini counterpart, is dropped. A position with no `type` contributes nothing, since a typeless branch is the same missing-field failure one level down; an array that declares nothing about its elements is given string elements as a last resort so the request reaches the model at all. An array that already has `items` keeps it as written.
There was a problem hiding this comment.
Code Review
This pull request introduces logic to ensure array schemas carry the items schema required by the Gemini backend, deriving it from prefixItems (JSON Schema tuple) when necessary to prevent 400 errors. It also adds comprehensive unit tests covering various array schema sanitization scenarios. There are no review comments, so I have no feedback to provide.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR updates Gemini tool-schema sanitization so tuple-style arrays are translated into the single
Confidence Score: 5/5The PR appears safe to merge with no concrete actionable defects identified. The changed sanitizer supplies Gemini-compatible
|
| Filename | Overview |
|---|---|
| src/model/gemini_request.rs | Adds tuple-array normalization that guarantees recognized Gemini array schemas have an items definition while preserving existing items. |
| src/model/gemini_request/tests.rs | Adds regression coverage for the observed nested tuple failure and six derivation edge cases. |
| .please/docs/research/md/003-json-schema-sanitization-for-gemini-functiondeclar.md | Documents reference implementations and the rationale for folding or dropping prefixItems. |
| .please/docs/research/README.md | Adds the new Gemini schema-sanitization research entry to the index. |
| .claude/agent-memory/review-review-cubic-reviewer/cubic-empty-review-clean-tree.md | Extends reviewer guidance to cover the inverse base-diff scope trap. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Tool input schema] --> B[sanitize_gemini_schema]
B --> C{Array schema?}
C -- No --> D[Remove unsupported keywords and recurse]
C -- Yes, existing items --> E[Keep items and drop prefixItems]
C -- Yes, no items --> F[Collect distinct typed prefixItems]
F --> G{Typed branches found?}
G -- No --> H[Use string items fallback]
G -- One --> I[Use that schema as items]
G -- Multiple --> J[Use anyOf as items]
E --> K[Gemini function declaration]
H --> K
I --> K
J --> K
Reviews (1): Last reviewed commit: "chore(agent-memory): record that cubic -..." | Re-trigger Greptile
There was a problem hiding this comment.
All reported issues were addressed across 5 files
Architecture diagram
sequenceDiagram
participant Client as Client (Claude Code)
participant Translator as Gemini Request Translator
participant Sanitizer as Schema Sanitizer
participant GeminiAPI as Gemini/Antigravity API
Note over Client,GeminiAPI: Tool Schema Translation Flow
Client->>Translator: Send request with tool input_schema
Translator->>Sanitizer: sanitize_gemini_schema()
Note over Sanitizer: Array schema handling
Sanitizer->>Sanitizer: Check if type includes "array"
Sanitizer->>Sanitizer: Extract prefixItems, check for existing items
alt Array schema with prefixItems, no items
Sanitizer->>Sanitizer: Collect typed branches from prefixItems
alt Single distinct schema
Sanitizer->>Sanitizer: Use that schema as items
else Multiple distinct schemas
Sanitizer->>Sanitizer: Build anyOf from branches
else No typed branches
Sanitizer->>Sanitizer: Default items to {"type":"string"}
end
else Array schema with existing items
Sanitizer->>Sanitizer: Drop prefixItems, keep items as-is
else Non-array schema
Sanitizer->>Sanitizer: Leave untouched
end
Sanitizer->>Sanitizer: Recursively sanitize child schemas
Sanitizer-->>Translator: Sanitized schema
Translator->>Translator: Build Gemini functionDeclarations
alt Valid schema with items
Translator->>GeminiAPI: POST GenerateContent with tools
GeminiAPI-->>Translator: 200 OK + model response
else Missing items field (pre-fix behavior)
Translator->>GeminiAPI: POST GenerateContent
GeminiAPI-->>Translator: 400 error: items.items: missing field
end
Translator-->>Client: Return translated response
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
… for the Schema proto
The `parameters` field is Gemini's `Schema` proto, where `type` is
REQUIRED on every node, nullability is its own `nullable` field, and
neither `prefixItems` nor a JSON `type` list exists. The first cut folded
a heterogeneous tuple into `items: {anyOf: [...]}`, a node with no `type`,
which is the shape Google's own bridges collapse rather than forward
(google/adk-python#3424, googleapis/python-genai#1807), so the captured
`[field, operator, value]` case would still have failed.
- Tuple positions now collapse to one typed schema: the single schema when
they agree, their shared `type` when they agree on that much, otherwise
the first position's schema. Array positions keep a complete branch so
the derived `items` never lacks its own `items`.
- A position contributes its type directly, through `anyOf`/`oneOf` arms,
or through an `enum` of uniformly typed values; a typeless position
contributes nothing.
- A `prefixItems`-only tuple (no `type`) is recognized as an array and
gains `type: "array"` (cubic P2 on #454); draft-07 array-valued `items`
is folded the same way and a boolean `items` is replaced.
- A JSON `type` list becomes the scalar type plus `nullable: true`,
mirroring CLIProxyAPI's flattenTypeArrays.
- `properties`/`$defs`/`definitions` values are walked as schemas while
their keys are left alone; `default`, `example(s)` and `enum` hold
instances and are not walked.
- The array fix runs after the child walk so folded positions are already
sanitized.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements JSON Schema sanitization for Gemini tool definitions, specifically folding tuple arrays (using prefixItems or array-valued items) into a single items schema and flattening type lists to a scalar type with a nullable flag. The reviewer correctly identified that setting the schema type to "null" for null-only type lists is invalid for Gemini and will cause a 400 error, suggesting a fallback to a nullable "string" instead, along with updating the corresponding test.
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
A JSON Schema `type` list naming only `"null"` was flattened to
`type: "null"`. The generativelanguage v1beta proto lists a `NULL`
member, but the Code Assist surface this provider talks to is unverified
there and three reviewers flagged it as a likely 400, so the list now
becomes `{"type":"string","nullable":true}` like every other nullable
list. Test expectation updated to match.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces JSON Schema sanitization for Gemini function declarations, specifically addressing the issue where array schemas without an items definition cause 400 errors by folding tuple definitions (prefixItems or draft-07 array-valued items) into a single items schema. It also adds a research document detailing how reference implementations handle this sanitization and includes extensive unit tests. The review feedback suggests optimizing collect_typed_branches to avoid unnecessary cloning of JSON values and updating is_array_schema to correctly recognize draft-07 tuples that omit an explicit type: array declaration.
…ning tuple arms Apply two Gemini Code Assist review suggestions on #454: - is_array_schema also accepts an array-valued items when type is absent, so a draft-07 tuple that omits "type": "array" is folded into the single items schema Gemini requires instead of reaching the backend as an array-valued items it rejects. - collect_typed_branches consumes the owned position and moves its anyOf/oneOf arms out instead of cloning them.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements robust JSON Schema sanitization for Gemini tool definitions in Rust (src/model/gemini_request.rs), specifically addressing how array schemas and tuple-based arrays (using prefixItems or legacy array-valued items) are normalized to meet Gemini's strict schema requirements. It includes detailed research documentation on how reference implementations handle these cases, along with comprehensive unit tests validating the new sanitization and folding logic. No review comments were provided, and the implementation is clean, well-tested, and conforms to the guidelines; therefore, I have no additional feedback to provide.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 79528d79bb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Apply the Codex review finding on #454: an object-valued items schema that declared no type returned early and reached the backend typeless, which the Schema proto rejects the same way it rejects a missing items. - A typeless element gets the type its enum or properties imply, and otherwise string, keeping the rest of the schema. - An element whose type lives only in anyOf/oneOf arms folds like a tuple of one position, reusing the branch derivation. - is_array_schema also recognizes an object-valued items, so a nested array that omits type is typed on its own pass instead of being declared a string by the parent's fallback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 411c97ade7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…d document the adaptation Apply the Codex and cubic review findings on #454: - An untyped items beside prefixItems, or one whose type lives in anyOf/oneOf/allOf arms, is folded as one more tuple position instead of discarding the positions or gaining a contradicting fallback. - A position speaks through its type, its composition arms, or what its enum or properties imply, in that order; arms that name no type hand back to the position itself. - The Antigravity provider page and its ko/ja/zh-cn copies describe the tool-schema adaptation and its lossy fallbacks.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad41fc2fc4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
All reported issues were addressed across 6 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
… compositions in order Apply the Codex and cubic review findings on #454: - dependentSchemas is walked by value like properties, dependentRequired is left alone, and draft-07 dependencies walks only its schema-valued entries, so a property named items under those maps is no longer read as the array keyword and folded into a string element. - A position carrying several composition keywords is read past a typeless one, and no further than the first that names a type, so arms that differ only in constraints are not merged down to a bare type.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9bcc4ed55c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…aptation The schema sanitizer runs in translate_request_for_model before the Gemini adapter picks a transport, so kind = "gemini" upstreams get the same tuple folding and typeless-element fallbacks as antigravity. Point the Gemini section of the providers guide at the Antigravity page's description and say there that it covers both providers, in all four locales.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a6f0cf659
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…refixItems A 2020-12 tuple that already carries a typed items schema keeps that schema and drops the positions, which the fold description left implicit. Say so in the Antigravity page's adaptation paragraph and its three locale copies.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 05d98f2399
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A type list such as ["string", "array"] flattens to string, and the prefixItems, array-valued items, or boolean items that described the array member rode along into a shape the Schema proto has no field for. Remove them from any schema whose resolved type is not array, keeping an object-valued items as written, and document the rule in the Antigravity page and its three locale copies.
There was a problem hiding this comment.
All reported issues were addressed across 6 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Guarding the tuple-keyword cleanup on is_array_schema rather than on a non-array string type also catches a schema that declares no type and carries only a boolean items, which otherwise reached Gemini as a value its Schema field cannot hold.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f6c356a55a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
All reported issues were addressed across 6 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…ng a list Every typed position was compared against every distinct earlier one, which a request with many distinct positions turned quadratic. Keep only what the fold decides on — the first branch, whether the rest match it, and the type they share — so each branch is compared once. Also tighten the Antigravity page's note on typeless schemas: one carrying prefixItems or an array or object items is an array and is folded; only a lone boolean items is dropped.
|



Summary
A tool parameter declared as a fixed-shape array (
prefixItemswith noitems, JSON Schema 2020-12's tuple form) reached the Gemini backend as an array withoutitems, which it rejects for the whole request:Seen 2026-09-03 through the
antigravityprovider ongemini-3.8-flash, from a Claude Code tool whosequery.whereis[field, operator, value].The declaration is emitted under
parameters, i.e. Gemini'sSchemaproto, wheretypeis REQUIRED on every node, nullability is the separatenullablefield, and neitherprefixItemsnor a JSONtypelist exists.sanitize_gemini_schemanow translates the JSON Schema spellings into that shape:items.prefixItems(2020-12) and an array-valueditems(draft-07) are folded into a single element schema; a booleanitemsis discarded. Positions contribute the type they declare, directly, throughanyOf/oneOfarms, or through anenumof uniformly typed values; a position that declares nothing contributes nothing. The contributions collapse to the single schema when they agree, to their sharedtypewhen they agree on that much, and otherwise to the first position's schema. NoanyOfnode without a siblingtypeis ever emitted (MCP tools withanyOfschemas fail Gemini validation - missing top-leveltypefield google/adk-python#3424, VertexAI schema validation issue with complex Union Types googleapis/python-genai#1807; CLIProxyAPI'sflattenAnyOfOneOfdoes the same).typeis still an array (prefixItemsconstrains nothing else) and gainstype: "array"before the derivation runs.{"type":"string"}as a last resort, mirroring CLIProxyAPI and opencode. The cost is silent string coercion of the element, not a rejected request.typelist (["array","null"]) becomes the scalar type plusnullable: true, mirroring CLIProxyAPI'sflattenTypeArrays.default,example,examples,enum) are no longer walked as schemas, so a documented default is handed to the model verbatim instead of having anitemsinvented inside it.items, and two positions that differ only in a stripped key dedup correctly.itemskeeps it as written; non-arrays are untouched.Tests:
src/model/gemini_request/tests.rscovers the captured field shape and each rule above (seven tests). Mutation-checked: removing theprefixItems-implies-array arm turns the tuple-without-typecase red; removing the helper call turns the headline case red.Also in this PR: a research note on how the reference bridges (CLIProxyAPI, gemini-cli, opencode) sanitize tool schemas for Gemini, and an agent-memory note that
cubic review -breads the committed diff only.Milestone / spec
n/a — Gemini request translation (
src/model/gemini_request.rs). No doc surface describes the tool-schema sanitizer (README,docs/,site/checked), so no doc update.Checklist
cargo buildpassescargo testpasses (2361 passed, 0 failed, full workspace)cargo clippy --all-targets -- -D warningscleancargo fmt --all --checkcleangemini_request.rsis 666 lines; see notes)docs/updated if this change deviates from it (n/a)Notes for reviewers
src/model/gemini_request.rsis 666 lines after this change. The sanitizer (sanitize_gemini_schemaplus its helpers) is a self-contained unit and moves cleanly tosrc/model/gemini_request/schema.rs; left for a separate pure-move commit so the diff here stays reviewable and the Sonar new-coverage gate is not tripped by a move.$ref-only tuple positions still contribute nothing and fall to the string fallback; inlining$defsis separate work (CLIProxyAPI does it in its own pass).