Uh oh!
There was an error while loading. Please reload this page.
feat(ai-openrouter): per-request native combined tools + outputSchema mode - #836
Conversation
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughOpenRouter now sends tools and schema-constrained output in one request for supported upstream models. Both text adapters use per-model capability checks. Package exports, tests, and E2E support cover the new behavior. ChangesOpenRouter combined tools + schema mode
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant OpenRouterTextAdapter
participant CapabilityResolver
participant ModelAllowlist
participant ChatCompletionsEndpoint
OpenRouterTextAdapter->>CapabilityResolver: resolve combined-mode support
CapabilityResolver->>ModelAllowlist: check resolved upstream model ids
OpenRouterTextAdapter->>ChatCompletionsEndpoint: send tools and responseFormat JSON schema
sequenceDiagram
participant OpenRouterResponsesTextAdapter
participant CapabilityResolver
participant ModelAllowlist
participant ResponsesEndpoint
OpenRouterResponsesTextAdapter->>CapabilityResolver: resolve combined-mode support
CapabilityResolver->>ModelAllowlist: check resolved upstream model ids
OpenRouterResponsesTextAdapter->>ResponsesEndpoint: send tools and text.format JSON schema
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
f8caa2f to
608f0c7CompareThere was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/ai-openrouter/tests/openrouter-combined-structured-output.test.ts (1)
1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove this unit test alongside the source adapter files.
This test is in
packages/ai-openrouter/tests/, but the guideline requires*.test.tsunit tests to live alongside source code.As per coding guidelines, "
**/*.test.ts: Place unit tests alongside source code in*.test.tsfiles".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-openrouter/tests/openrouter-combined-structured-output.test.ts` at line 1, The unit test is in the wrong location for the project’s test layout guidelines. Move the openrouter structured output test from the separate tests folder to sit alongside the relevant source adapter files, keeping the same `*.test.ts` naming and preserving the existing Vitest setup/imports.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ai-openrouter/src/adapters/responses-text.ts`:
- Line 1578: The `combinedOutputSchema` assignment in `responses-text.ts` uses a
redundant type assertion on `options.outputSchema`, triggering
`no-unnecessary-type-assertion`. Replace the cast with an explicit type
annotation on `combinedOutputSchema` while keeping the `JSONSchema` import
referenced, matching the approach used in the chat-completions adapter and
preserving the existing `options.outputSchema` type.
In `@packages/ai-openrouter/src/adapters/text.ts`:
- Line 1197: The `combinedOutputSchema` assignment in `text.ts` uses a redundant
`as JSONSchema` assertion on `options.outputSchema`, triggering the
`no-unnecessary-type-assertion` lint rule. Replace the cast with an explicit
type annotation on `combinedOutputSchema` (keeping the `JSONSchema` import in
use) and preserve the existing `JSONSchema | undefined` shape from
`options.outputSchema`.
---
Nitpick comments:
In `@packages/ai-openrouter/tests/openrouter-combined-structured-output.test.ts`:
- Line 1: The unit test is in the wrong location for the project’s test layout
guidelines. Move the openrouter structured output test from the separate tests
folder to sit alongside the relevant source adapter files, keeping the same
`*.test.ts` naming and preserving the existing Vitest setup/imports.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 10aa8a2c-1c20-49e3-a970-477975318133
📒 Files selected for processing (10)
.changeset/openrouter-combined-tools-and-schema.mdpackages/ai-openrouter/package.jsonpackages/ai-openrouter/src/adapters/responses-text.tspackages/ai-openrouter/src/adapters/text.tspackages/ai-openrouter/src/index.tspackages/ai-openrouter/src/internal/combined-tools-and-schema.tspackages/ai-openrouter/src/model-meta.tspackages/ai-openrouter/tests/openrouter-combined-structured-output.test.tspackages/ai-openrouter/vite.config.tstesting/e2e/src/lib/feature-support.ts
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.ts`:
- Around line 21-29: Replace the raw outputSchema fixture with an equivalent Zod
schema, and replace the handwritten tools entry with a toolDefinition()-based
fixture. Configure both the .server() and .client() sides of that tool so it
supports isomorphic execution while preserving the existing lookup_weather name,
description, and behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 149cdcb3-2925-4348-aeb9-6475d91f0daf
📒 Files selected for processing (10)
.changeset/openrouter-combined-tools-and-schema.mdpackages/ai-openrouter/package.jsonpackages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.tspackages/ai-openrouter/src/adapters/responses-text.tspackages/ai-openrouter/src/adapters/text.tspackages/ai-openrouter/src/index.tspackages/ai-openrouter/src/internal/combined-tools-and-schema.tspackages/ai-openrouter/src/model-meta.tspackages/ai-openrouter/vite.config.tstesting/e2e/src/lib/feature-support.ts
🚧 Files skipped from review as they are similar to previous changes (8)
- packages/ai-openrouter/src/index.ts
- testing/e2e/src/lib/feature-support.ts
- .changeset/openrouter-combined-tools-and-schema.md
- packages/ai-openrouter/src/internal/combined-tools-and-schema.ts
- packages/ai-openrouter/package.json
- packages/ai-openrouter/src/model-meta.ts
- packages/ai-openrouter/src/adapters/responses-text.ts
- packages/ai-openrouter/src/adapters/text.ts
| const outputSchema = { | ||
| type: 'object', | ||
| properties: { answer: { type: 'string' } }, | ||
| required: ['answer'], | ||
| } | ||
| const tools: Array<Tool> = [ | ||
| { name: 'lookup_weather', description: 'Return the forecast for a location' }, | ||
| ] |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bashset -euo pipefail
echo"Tracked file:"
git ls-files | rg 'packages/ai-openrouter/src/adapters/openrouter-combined-structured-output\.(test\.)?ts$'||trueechoecho"Relevant test file excerpt:"if [ -f packages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.ts ];then
wc -l packages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.ts
sed -n '1,180p' packages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.ts | cat -n
fiechoecho"Search for toolDefinition imports/usages in ai-openrouter fixtures/tests:"
rg -n "toolDefinition|Tool\\<|outputSchema|structuredOutput" packages/ai-openrouter/src -g '*.ts' -g '*.tsx'||trueechoecho"Find toolDefinition implementation:"
rg -n "function toolDefinition|const toolDefinition|toolDefinition\\(" packages -g "*.ts" -g "*.tsx"||trueRepository: TanStack/ai
Length of output: 27493
🏁 Script executed:
#!/bin/bashset -euo pipefail
echo"toolDefinition implementation excerpt:"
sed -n '240,390p' packages/ai/src/activities/chat/tools/tool-definition.ts | cat -n
echoecho"Tool type excerpt:"
sed -n '520,620p' packages/ai/src/types.ts | cat -n
echoecho"Search for openrouter tool fixtures using toolDefinition():180p packages/ai-openrouter/tests openrouter tests and fixtures:"
rg -n "toolDefinition\\(|\\.server\\(|\\.client\\(" packages/ai-openrouter -g '*.ts' -g '*.tsx'||trueechoecho"Package config / dependency availability hints:"forfin package.json packages/ai/package.json packages/ai-openrouter/package.json;doif [ -f"$f" ];thenecho"--- $f"; sed -n '1,220p'"$f"| rg -n '"name"|"dependencies"|"devDependencies"|"peerDependencies"|zod|typescript|packageManager|scripts|exports|types|main'||true;fidoneRepository: TanStack/ai
Length of output: 13248
Use Zod and toolDefinition() for test fixtures.
Lines 21-29 use a raw schema and a handwritten Tool; define the schema with Zod and create the tool with toolDefinition(). If the tool uses toolDefinition(), implement both .server() and .client() sides for isomorphic execution.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.ts`
around lines 21 - 29, Replace the raw outputSchema fixture with an equivalent
Zod schema, and replace the handwritten tools entry with a
toolDefinition()-based fixture. Configure both the .server() and .client() sides
of that tool so it supports isomorphic execution while preserving the existing
lookup_weather name, description, and behavior.
Source: Coding guidelines
… mode
Give both OpenRouter text adapters (chat-completions and Responses) native
combined mode. When chat({ outputSchema, tools, stream: true }) targets a
combined-capable upstream model, the schema is wired into the same streaming
request as the tools and the final-turn JSON is harvested directly, skipping
the separate finalization round-trip.
Capability is per resolved upstream model via the new exported
OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS set, consulted by both adapters'
supportsCombinedToolsAndSchema(). Membership tracks the upstream per-provider
combined-mode gates (Anthropic 4.5+ mirrors ANTHROPIC_COMBINED_TOOLS_AND_SCHEMA_MODELS,
Gemini 3.x, OpenAI strict json_schema era, Grok 4.x) rather than the broader
catalog responseFormat flag.
ClosesTanStack#612.18c7599 to
f651a71Comparetombeckenham
commented
Aug 10, 2026
Rebased onto latest |
View your CI Pipeline Execution ↗ for commit 47d422c
☁️ Nx Cloud last updated this comment at |
Thanks for the PR, @season179! 🙌 @tombeckenham will take a look. Automated pre-review checks
Automated triage — a human review follows. |
main removed anthropic/claude-opus-4.6-fast from the OpenRouter catalog and extended ANTHROPIC_COMBINED_TOOLS_AND_SCHEMA_MODELS through opus 4.8 and the Claude 5 ids, so the mirror list follows: drop the removed id, add opus-4.8(-fast), fable-5, and sonnet-5.
@tanstack/ai@tanstack/ai-acp@tanstack/ai-angular@tanstack/ai-anthropic@tanstack/ai-bedrock@tanstack/ai-byteplus@tanstack/ai-claude-code@tanstack/ai-client@tanstack/ai-code-mode@tanstack/ai-code-mode-snippets@tanstack/ai-codex@tanstack/ai-cohere@tanstack/ai-devtools-core@tanstack/ai-durable-stream@tanstack/ai-elevenlabs@tanstack/ai-event-client@tanstack/ai-fal@tanstack/ai-gemini@tanstack/ai-grok@tanstack/ai-grok-build@tanstack/ai-groq@tanstack/ai-isolate-cloudflare@tanstack/ai-isolate-daytona@tanstack/ai-isolate-node@tanstack/ai-isolate-quickjs@tanstack/ai-isolate-quickjs-bun@tanstack/ai-mcp@tanstack/ai-memory@tanstack/ai-mistral@tanstack/ai-ollama@tanstack/ai-openai@tanstack/ai-opencode@tanstack/ai-openrouter@tanstack/ai-perplexity@tanstack/ai-persistence@tanstack/ai-preact@tanstack/ai-react@tanstack/ai-react-ui@tanstack/ai-sandbox@tanstack/ai-sandbox-cloudflare@tanstack/ai-sandbox-daytona@tanstack/ai-sandbox-docker@tanstack/ai-sandbox-local-process@tanstack/ai-sandbox-sprites@tanstack/ai-sandbox-vercel@tanstack/ai-solid@tanstack/ai-solid-ui@tanstack/ai-svelte@tanstack/ai-utils@tanstack/ai-vercel-gateway@tanstack/ai-vue@tanstack/ai-vue-ui@tanstack/openai-base@tanstack/preact-ai-devtools@tanstack/react-ai-devtools@tanstack/solid-ai-devtoolscommit: |
Sync the combined-model allowlist with current main. Document the OpenRouter tools+outputSchema path. Cover Responses harvest and :variant stripping in tests.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Closes#612 (extends #605).
What changed
This adds native OpenRouter combined mode for
tools + outputSchemain both text adapters:responseFormat: { type: 'json_schema', jsonSchema: { strict: true, schema } }on the combined path.text.formaton the combined path.Because OpenRouter is a routing layer, the capability check is intentionally conservative. The adapter now checks the primary model plus any
modelOptions.modelsfallbacks and only enables combined mode when every possible routed model is in the combined-capable set.:variantsuffixes such as:nitroare still ignored for capability purposes because they are routing directives, not different upstream model capabilities.The new
OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELSset is exported from@tanstack/ai-openrouter/model-meta, as requested in #612. The list is derived from the upstream provider gates rather than the broader OpenRouterresponseFormatflag, so older structured-output-capable models that do not support native tools + schema stay on the legacy path.Tests
Added OpenRouter unit coverage for:
responseFormatpayloadtext.formatpayload:variantsuffixes not changing capabilitytext.*fields on ResponsesOPENROUTER_CHAT_MODELSI also added the
model-metapackage subpath to the build entries so the requested export is actually published.Local verification
Passed:
CI=true pnpm --filter @tanstack/ai-openrouter test:lib -- openrouter-combined-structured-output.test.tsCI=true pnpm --filter @tanstack/ai-openrouter test:typesCI=true pnpm --filter @tanstack/ai-openrouter test:eslintCI=true pnpm --filter @tanstack/ai-openrouter buildCI=true pnpm --filter @tanstack/ai-openrouter test:buildCI=true pnpm --filter @tanstack/ai-e2e exec playwright test tests/agentic-structured-stream.spec.ts --grep "openrouter"I also ran
pnpm test:pr. The OpenRouter affected targets passed, but the full gate currently stops inroot:test:kiiraon unrelated docs snippets indocs/adapters/grok.mdanddocs/media/video-generation.md.Summary by CodeRabbit
New Features
Bug Fixes