fix(ai): keep optional object tool parameters typed for Gemini - #355
Merged
Conversation
A JSON Schema nullable union such as `"type": ["object", "null"]` projected to
`anyOf: [{ type: "object" }]` and dropped the node's own `type`. Gemini reads
`properties` and `required" only on an OBJECT-typed node, so it rejected the whole
request with INVALID_ARGUMENT and every tool call to the model failed. MCP servers
emit that shape for optional object parameters.
Project a single non-null type back onto `type` and keep the combiner only for a
real multi-type union, matching what the equivalent `anyOf` input already produced.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Every Gemini tool call failed with
HTTP 400 INVALID_ARGUMENTwhen any registered tool declared an optional object parameter:Cause
projectNodeingemini-tool-schema.tsdropped a node's owntypewhenever the input used a JSON Schema nullable union, and re-expressed it as a combiner instead:So
type: ["object", "null"]projected toanyOf: [{ type: "object" }]with notypeat all. Gemini readspropertiesandrequiredonly on an OBJECT-typed node, so it rejected the entire request — one such parameter anywhere in the tool set broke every call in the session.The file already flattened the equivalent
anyOf: [{…}, { type: "null" }]input correctly totype: "string", nullable: true. The array form simply never got the same treatment, so two spellings of the same schema projected differently.MCP servers emit the array form routinely for optional object parameters, which is how this surfaced.
Fix
Project a single non-null type back onto
type, and keep the combiner only for a genuine multi-type union. This makes both input spellings agree.Verification
tool-schema-projectioncase covering the MCP shape — red before the fix, green after.gemini-3.7-flash-medium,gemini-3.7-flash-low,gemini-3.6-flash-high, andgemini-3.1-pro-high."type": "object", "nullable": truewith no remaining type unions.bun typecheckclean;oxlintreports 0 warnings and 0 errors on the changed files.One existing assertion changed
gemini.test.tsassertedtype: ["string","null"]→{ nullable: true, anyOf: [{ type: "string" }] }. That encoded the bug: the same test already expected the equivalentanyOfinput to produce{ type: "string", nullable: true }. Multi-type unions are untouched and still assertanyOf.Pre-existing failures
packages/aihas 7 failures onintegration-v2today (Azure api-key header, OpenRouter reasoning details, Bedrock Mantle bearer auth). I reproduced the identical 7 at the parent commit in a separate workspace: 515 tests / 7 fail before, 516 tests / 7 fail after. No regressions from this change.