Skip to content

fix(gemini): preserve JSON Schema tool parameters - #1259

Merged
AlemTuzlak merged 5 commits into
TanStack:mainfrom
kolaworld:fix-1258-gemini-toolcall
Aug 31, 2026
Merged

fix(gemini): preserve JSON Schema tool parameters#1259
AlemTuzlak merged 5 commits into
TanStack:mainfrom
kolaworld:fix-1258-gemini-toolcall

Conversation

@kolaworld

@kolaworld kolaworld commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #1258

🎯 Changes

  • Sends Gemini tool schemas through FunctionDeclaration.parametersJsonSchema instead of the narrower parameters field.
  • Preserves complete JSON Schema keywords, including nested const values produced by ArkType and other Standard Schema libraries.
  • Uses the @google/genai FunctionDeclaration type, matching the existing Gemini Realtime implementation.
  • Extends the existing provider-dispatch unit coverage and ArkType wire E2E coverage. A model-specific raw mount validates the request without intercepting other Gemini tests.
  • Includes a patch changeset for @tanstack/ai-gemini. Docs are unchanged because public usage and the documented contract remain the same.

Alternatives considered:

  • Patch @copilotkit/aimock. Rejected because the project would need to carry a local dependency patch. The upstream aimock Gemini converter reads only parameters, not parametersJsonSchema. No matching upstream tracker issue is currently filed.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with pnpm run test:pr, or these tests do not apply to this pull request.
  • I fully understand the code in this pull request, including any code generated with AI assistance.
  • Docs: I updated docs/ for this change, or this change is not user-facing.
  • Changeset: I added a changeset (pnpm changeset), or this PR does not change a published package.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes

    • Gemini function tools now preserve complete JSON Schema details, including constants and required fields.
    • Updated tool requests to use the current Gemini schema format for improved provider compatibility.
    • Improved support for complex tool inputs, helping prevent schema validation errors.
  • Tests

    • Added coverage verifying rich tool schemas are transmitted and accepted by Gemini.
    • Expanded end-to-end validation for Gemini and OpenRouter tool integrations.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b61d3c92-894d-4336-be32-af74cb104e55

📥 Commits

Reviewing files that changed from the base of the PR and between 9c25b3a and cfeeb82.

📒 Files selected for processing (3)
  • testing/e2e/src/routes/api.arktype-tool-wire.ts
  • testing/e2e/tests/arktype-tool-wire.spec.ts
  • testing/e2e/tests/provider-tool-dispatch-wire.spec.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

The Gemini tool converter now sends input schemas through parametersJsonSchema. Unit and end-to-end tests verify preservation of JSON Schema keywords such as const.

Changes

Gemini JSON Schema tool support

Layer / File(s) Summary
Update Gemini function declaration schema wiring
packages/ai-gemini/src/tools/tool-converter.ts, packages/ai-gemini/tests/provider-tool-dispatch.test.ts, .changeset/gemini-json-schema-tools.md
The converter uses FunctionDeclaration and assigns schemas to parametersJsonSchema. Unit coverage checks the complete declaration, including unit.const. The changeset records a patch release.
Validate Gemini wire format end to end
testing/e2e/src/routes/api.arktype-tool-wire.ts, testing/e2e/global-setup.ts, testing/e2e/tests/arktype-tool-wire.spec.ts, testing/e2e/tests/provider-tool-dispatch-wire.spec.ts
The e2e route selects Gemini when requested and collects streamed text. The mock mount validates the raw schema and returns a streamed success response. The e2e tests verify the Gemini response and tool dispatch.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to cfeeb

This change preserves complete JSON Schema tool parameters in Gemini requests without changing the public usage contract. No actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant ArkTypeWireTest
  participant ArkTypeToolWireRoute
  participant GeminiTextAdapter
  participant GeminiJsonSchemaToolMount
  ArkTypeWireTest->>ArkTypeToolWireRoute: request provider=gemini
  ArkTypeToolWireRoute->>GeminiTextAdapter: configure Gemini model and test ID
  GeminiTextAdapter->>GeminiJsonSchemaToolMount: send function declaration
  GeminiJsonSchemaToolMount->>GeminiJsonSchemaToolMount: validate parametersJsonSchema
  GeminiJsonSchemaToolMount-->>ArkTypeWireTest: return Schema accepted
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 6 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: preserving JSON Schema tool parameters for Gemini.
Description check ✅ Passed The description includes the required Changes, Checklist, and Release Impact sections. It explains the implementation, testing, changeset, and documentation decision.
Linked Issues check ✅ Passed The implementation directly addresses issue [#1258] by sending tool schemas through FunctionDeclaration.parametersJsonSchema, preserving keywords such as const. Unit and E2E tests cover the required b…
Out of Scope Changes check ✅ Passed The changeset, converter update, unit tests, E2E route, mock mount, and E2E assertions all support the linked Gemini schema fix. No unrelated code changes are evident.
Full details: Linked Issues check

Explanation

The implementation directly addresses issue [#1258] by sending tool schemas through FunctionDeclaration.parametersJsonSchema, preserving keywords such as const. Unit and E2E tests cover the required behavior.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Aug 27, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit cfeeb82

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 9s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-31 03:36:19 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@1259

@tanstack/ai-acp

npm i https://pkg.pr.new/@tanstack/ai-acp@1259

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@1259

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@1259

@tanstack/ai-bedrock

npm i https://pkg.pr.new/@tanstack/ai-bedrock@1259

@tanstack/ai-byteplus

npm i https://pkg.pr.new/@tanstack/ai-byteplus@1259

@tanstack/ai-claude-code

npm i https://pkg.pr.new/@tanstack/ai-claude-code@1259

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@1259

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@1259

@tanstack/ai-code-mode-snippets

npm i https://pkg.pr.new/@tanstack/ai-code-mode-snippets@1259

@tanstack/ai-codex

npm i https://pkg.pr.new/@tanstack/ai-codex@1259

@tanstack/ai-cohere

npm i https://pkg.pr.new/@tanstack/ai-cohere@1259

@tanstack/ai-compaction

npm i https://pkg.pr.new/@tanstack/ai-compaction@1259

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@1259

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/@tanstack/ai-durable-stream@1259

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@1259

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@1259

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@1259

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@1259

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@1259

@tanstack/ai-grok-build

npm i https://pkg.pr.new/@tanstack/ai-grok-build@1259

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@1259

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@1259

@tanstack/ai-isolate-daytona

npm i https://pkg.pr.new/@tanstack/ai-isolate-daytona@1259

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@1259

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@1259

@tanstack/ai-isolate-quickjs-bun

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs-bun@1259

@tanstack/ai-llmgateway

npm i https://pkg.pr.new/@tanstack/ai-llmgateway@1259

@tanstack/ai-lovable

npm i https://pkg.pr.new/@tanstack/ai-lovable@1259

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@1259

@tanstack/ai-memory

npm i https://pkg.pr.new/@tanstack/ai-memory@1259

@tanstack/ai-mistral

npm i https://pkg.pr.new/@tanstack/ai-mistral@1259

@tanstack/ai-octane

npm i https://pkg.pr.new/@tanstack/ai-octane@1259

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@1259

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@1259

@tanstack/ai-opencode

npm i https://pkg.pr.new/@tanstack/ai-opencode@1259

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@1259

@tanstack/ai-perplexity

npm i https://pkg.pr.new/@tanstack/ai-perplexity@1259

@tanstack/ai-persistence

npm i https://pkg.pr.new/@tanstack/ai-persistence@1259

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@1259

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@1259

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@1259

@tanstack/ai-sandbox

npm i https://pkg.pr.new/@tanstack/ai-sandbox@1259

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-sandbox-cloudflare@1259

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/@tanstack/ai-sandbox-daytona@1259

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/@tanstack/ai-sandbox-docker@1259

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/@tanstack/ai-sandbox-local-process@1259

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/@tanstack/ai-sandbox-sprites@1259

@tanstack/ai-sandbox-upstash-box

npm i https://pkg.pr.new/@tanstack/ai-sandbox-upstash-box@1259

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/@tanstack/ai-sandbox-vercel@1259

@tanstack/ai-skills

npm i https://pkg.pr.new/@tanstack/ai-skills@1259

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@1259

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@1259

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@1259

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@1259

@tanstack/ai-vercel-gateway

npm i https://pkg.pr.new/@tanstack/ai-vercel-gateway@1259

@tanstack/ai-vertex

npm i https://pkg.pr.new/@tanstack/ai-vertex@1259

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@1259

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@1259

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@1259

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@1259

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@1259

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@1259

@tanstack/svelte-ai-devtools

npm i https://pkg.pr.new/@tanstack/svelte-ai-devtools@1259

commit: cfeeb82

@github-actions github-actions Bot added the waiting-on: author Waiting for the author to respond or update label Aug 28, 2026

@harshlocham harshlocham left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The E2E failure appears to be in an existing spec rather than the new ArkType coverage.

tests/provider-tool-dispatch-wire.spec.ts still asserts that aimock's /v1/_requests journal contains function.parameters for the google_search tool. After this change, the Gemini converter intentionally sends the schema via parametersJsonSchema, while aimock's Gemini handler only copies parameters into the journal. As a result, the recorded function becomes:

{
  type: 'function',
  function: {
    name: 'google_search',
    description: 'Run an application function'
  }
}

I can reproduce this with:

pnpm run build:all
CI=1 pnpm --filter @tanstack/ai-e2e test:e2e -- tests/provider-tool-dispatch-wire.spec.ts

The existing name assertion still provides useful coverage that the tool isn't being swallowed into { googleSearch: ... }, while the new ArkType mount already covers the richer schema keywords.

I think the existing matcher should be updated to stop asserting function.parameters here (or, alternatively, inspect parametersJsonSchema through the raw mount). I wouldn't recommend sending both fields just to satisfy aimock, since FunctionDeclaration does not allow both.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the PR, @kolaworld! 🙌 @tombeckenham will take a look.

Automated pre-review checks

  • ⚠️ CI failing — worth a look before review
  • ✅ No merge conflicts
  • ✅ Changeset present
  • ✅ E2E test changes included

Automated triage — a human review follows.

@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@github-actions github-actions Bot added waiting-on: maintainer The ball is in the maintainers’ court waiting-on: author Waiting for the author to respond or update and removed waiting-on: author Waiting for the author to respond or update waiting-on: maintainer The ball is in the maintainers’ court labels Aug 30, 2026
The converter now sends tool schemas via `parametersJsonSchema`, which
aimock's Gemini handler never records (it copies only `parameters` into
its `/v1/_requests` journal). Two consequences, both fixed here.

`provider-tool-dispatch-wire` asserted the journal's `function.parameters`,
so it failed deterministically after the switch. Drop that matcher and the
comment describing the old SDK-side upper-casing; the surviving
`name: 'google_search'` check is what the spec relies on to prove the tool
was not swallowed into `{googleSearch: {...}}`.

The new Gemini case in `arktype-tool-wire` asserted only `ok: true`, which
the route returns unless the SDK throws. Had the raw mount stopped matching,
the request would fall through to aimock's native Gemini handler, return
200, and the test would pass while proving nothing. Assert on the mount's
`Schema accepted` reply instead, which it emits only after verifying the
request carried `parametersJsonSchema` with `unit.const` and no `parameters`.

Verified: pointing the route at a model the mount does not intercept fails
on the new assertion while `ok: true` still comes back.

Claude-Session: https://claude.ai/code/session_01PG6uRwgdsxF9vrj973KsKX

@tombeckenham tombeckenham left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The fix is correct and I verified it against the live Gemini API rather than only against mocks.

Root cause

convertToolsToProviderFormat sent tool schemas on FunctionDeclaration.parameters. @google/genai's tTool routes that field through processJsonSchema, which upper-cases type and copies unrecognised keywords through verbatim — so const arrived on parameters, whose proto has no such member, and the API rejected the request before generation. The SDK only bypasses that path when the schema carries $schema, and the core layer strips $schema (packages/ai/src/activities/chat/tools/schema-converter.ts:22), so every tool took the lossy path. Sending parametersJsonSchema is the right fix — the SDK never reads or rewrites it, and it matches what the Gemini realtime client already does.

Verification

Agent-written repro against the real API on gemini-3.5-flash-lite, run on clean main and on this branch:

  • main400 Invalid JSON payload received. Unknown name "const" at 'tools[0].function_declarations[0].parameters.properties[1].value'
  • this branch — passes

I also confirmed live that a zod-shaped schema (additionalProperties: false, nullable anyOf, enum, arrays) and the no-inputSchema default (required: []) both still work, since those paths had no coverage and were plausible regressions.

convertToolsToProviderFormat has a single call site (adapters/text.ts:877) shared by the streaming and non-streaming paths, so both are covered. ai-vertex re-exports from ai-gemini and inherits the fix; changesets bumps it as a dependent, so the single @tanstack/ai-gemini patch entry is correct.

Test changes I pushed

Two test-hygiene issues, fixed in cfeeb826f:

  1. provider-tool-dispatch-wire.spec.ts asserted aimock's journalled function.parameters. aimock copies only parameters into its journal, so after this change that assertion could never hold — it was the one deterministic E2E failure. Dropped the matcher and the now-false comment about SDK-side upper-casing; the surviving name: 'google_search' check is what proves the tool wasn't swallowed into {googleSearch: {...}}.

  2. The new Gemini case asserted only ok: true, which the route returns unless the SDK throws. Had the raw mount ever stopped matching, the request would fall through to aimock's native handler, return 200, and the test would pass while proving nothing. It now asserts on the mount's Schema accepted reply. I verified this has teeth by repointing the route at a model the mount doesn't intercept: ok: true still came back, and only the new assertion caught it.

CI is green — 634 passed, 0 failed. The 15 flaky are pre-existing 30s timeouts in embedding / tts / multi-turn-structured that hit a different provider set on each run and are unrelated to this change.

Follow-up (not blocking)

Structured output has the same bug class: adapters/text.ts:188 and :861 send output schemas via config.responseSchema, the same narrow Schema dialect through the same conversion, so a const in an output schema still fails. responseJsonSchema exists as a provider option but is typed Schema, so it would need widening too. Worth a separate issue.

Optional polish left alone: the new unit field sits next to a pre-existing units? and reads as a typo (scale would be clearer), and the OpenRouter HTTPClient / adapter are still constructed on the Gemini path.

@AlemTuzlak
AlemTuzlak merged commit 845696b into TanStack:main Aug 31, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on: author Waiting for the author to respond or update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gemini tool calls fail with Unknown name "const" because JSON Schema is incorrectly sent as parameters

4 participants