feat(ai-openrouter): per-request native combined tools + outputSchema mode - #836
feat(ai-openrouter): per-request native combined tools + outputSchema mode#836season179 wants to merge 9 commits into
Conversation
📝 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)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. 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 |
… 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.
Closes TanStack#612.
f8caa2f to
608f0c7
Compare
There 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
…bined-tools-schema # Conflicts: # testing/e2e/src/lib/feature-support.ts
|
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/bash
set -euo pipefail
echo "Tracked file:"
git ls-files | rg 'packages/ai-openrouter/src/adapters/openrouter-combined-structured-output\.(test\.)?ts$' || true
echo
echo "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
fi
echo
echo "Search for toolDefinition imports/usages in ai-openrouter fixtures/tests:"
rg -n "toolDefinition|Tool\\<|outputSchema|structuredOutput" packages/ai-openrouter/src -g '*.ts' -g '*.tsx' || true
echo
echo "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/bash
set -euo pipefail
echo "toolDefinition implementation excerpt:"
sed -n '240,390p' packages/ai/src/activities/chat/tools/tool-definition.ts | cat -n
echo
echo "Tool type excerpt:"
sed -n '520,620p' packages/ai/src/types.ts | cat -n
echo
echo "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' || true
echo
echo "Package config / dependency availability hints:"
for f in package.json packages/ai/package.json packages/ai-openrouter/package.json; do
if [ -f "$f" ]; then echo "--- $f"; sed -n '1,220p' "$f" | rg -n '"name"|"dependencies"|"devDependencies"|"peerDependencies"|zod|typescript|packageManager|scripts|exports|types|main' || true; fi
doneRepository: 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
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