Skip to content

feat(ai-openrouter): per-request native combined tools + outputSchema mode - #836

Open
season179 wants to merge 9 commits into
TanStack:mainfrom
season179:feat/openrouter-combined-tools-schema
Open

feat(ai-openrouter): per-request native combined tools + outputSchema mode#836
season179 wants to merge 9 commits into
TanStack:mainfrom
season179:feat/openrouter-combined-tools-schema

Conversation

@season179

@season179 season179 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Closes #612 (extends #605).

What changed

This adds native OpenRouter combined mode for tools + outputSchema in both text adapters:

  • Chat Completions now sends responseFormat: { type: 'json_schema', jsonSchema: { strict: true, schema } } on the combined path.
  • Responses Beta now sends the schema through text.format on the combined path.
  • Models that are not known to support native combined mode continue to use the existing finalization path.

Because OpenRouter is a routing layer, the capability check is intentionally conservative. The adapter now checks the primary model plus any modelOptions.models fallbacks and only enables combined mode when every possible routed model is in the combined-capable set. :variant suffixes such as :nitro are still ignored for capability purposes because they are routing directives, not different upstream model capabilities.

The new OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS set 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 OpenRouter responseFormat flag, so older structured-output-capable models that do not support native tools + schema stay on the legacy path.

Tests

Added OpenRouter unit coverage for:

  • combined-capable and unsupported models on both adapters
  • chat responseFormat payload
  • Responses text.format payload
  • fallback routing disabling combined mode when any fallback is unsupported
  • :variant suffixes not changing capability
  • preserving caller-supplied text.* fields on Responses
  • set integrity against OPENROUTER_CHAT_MODELS

I also added the model-meta package 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.ts
  • CI=true pnpm --filter @tanstack/ai-openrouter test:types
  • CI=true pnpm --filter @tanstack/ai-openrouter test:eslint
  • CI=true pnpm --filter @tanstack/ai-openrouter build
  • CI=true pnpm --filter @tanstack/ai-openrouter test:build
  • CI=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 in root:test:kiira on unrelated docs snippets in docs/adapters/grok.md and docs/media/video-generation.md.

Summary by CodeRabbit

  • New Features

    • OpenRouter now supports sending tools and structured output schemas together in a single request for compatible models.
    • The combined mode is available across both chat-completions and Responses-style adapters.
    • Supported requests can return the final structured response from the same stream, reducing extra processing.
    • Support checks account for fallback models and model variants for consistent behavior.
  • Bug Fixes

    • Older structured-output models continue using the existing flow when combined mode isn’t supported, avoiding unexpected behavior.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

OpenRouter 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.

Changes

OpenRouter combined tools + schema mode

Layer / File(s) Summary
Capability catalog and package wiring
packages/ai-openrouter/src/model-meta.ts, packages/ai-openrouter/src/internal/combined-tools-and-schema.ts, packages/ai-openrouter/src/index.ts, packages/ai-openrouter/package.json, packages/ai-openrouter/vite.config.ts, .changeset/openrouter-combined-tools-and-schema.md
The combined-mode model set and resolver are added. The package exports and build entries expose the model metadata. The changeset documents the feature.
Chat-completions combined request
packages/ai-openrouter/src/adapters/text.ts
The adapter adds a strict responseFormat JSON schema when outputSchema is present and the resolved model supports combined mode.
Responses combined request and validation
packages/ai-openrouter/src/adapters/responses-text.ts, packages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.ts, testing/e2e/src/lib/feature-support.ts
The Responses adapter adds text.format while preserving caller-supplied text fields. Tests cover model gating, payload construction, variant handling, and catalog integrity. OpenRouter is added to structured-stream E2E support.

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
Loading
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
Loading

Possibly related PRs

  • TanStack/ai#1014: Modifies the OpenRouter chat-completions structured-output responseFormat handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. 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 and concisely identifies the main change: per-request native combined tools and outputSchema support for OpenRouter.
Description check ✅ Passed The description explains the changes, testing, verification results, release impact, and known unrelated full-gate failure in sufficient detail.
Linked Issues check ✅ Passed The implementation satisfies issue #612 by covering both adapters, capability gating, model fallbacks, variant handling, exports, tests, and E2E coverage.
Out of Scope Changes check ✅ Passed The changeset, package export, adapter updates, tests, build configuration, and E2E mapping directly support the linked issue objectives.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

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

… 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.
@season179
season179 force-pushed the feat/openrouter-combined-tools-schema branch from f8caa2f to 608f0c7 Compare June 25, 2026 00:20
@season179
season179 marked this pull request as ready for review June 25, 2026 00:20

@coderabbitai coderabbitai Bot 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.

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 win

Move this unit test alongside the source adapter files.

This test is in packages/ai-openrouter/tests/, but the guideline requires *.test.ts unit tests to live alongside source code.

As per coding guidelines, "**/*.test.ts: Place unit tests alongside source code in *.test.ts files".

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 037e15a and 608f0c7.

📒 Files selected for processing (10)
  • .changeset/openrouter-combined-tools-and-schema.md
  • packages/ai-openrouter/package.json
  • packages/ai-openrouter/src/adapters/responses-text.ts
  • packages/ai-openrouter/src/adapters/text.ts
  • packages/ai-openrouter/src/index.ts
  • packages/ai-openrouter/src/internal/combined-tools-and-schema.ts
  • packages/ai-openrouter/src/model-meta.ts
  • packages/ai-openrouter/tests/openrouter-combined-structured-output.test.ts
  • packages/ai-openrouter/vite.config.ts
  • testing/e2e/src/lib/feature-support.ts

Comment thread packages/ai-openrouter/src/adapters/responses-text.ts Outdated
Comment thread packages/ai-openrouter/src/adapters/text.ts Outdated
@coderabbitai

coderabbitai Bot commented Aug 8, 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.

@coderabbitai coderabbitai Bot 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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7d92296 and 18c7599.

📒 Files selected for processing (10)
  • .changeset/openrouter-combined-tools-and-schema.md
  • packages/ai-openrouter/package.json
  • packages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.ts
  • packages/ai-openrouter/src/adapters/responses-text.ts
  • packages/ai-openrouter/src/adapters/text.ts
  • packages/ai-openrouter/src/index.ts
  • packages/ai-openrouter/src/internal/combined-tools-and-schema.ts
  • packages/ai-openrouter/src/model-meta.ts
  • packages/ai-openrouter/vite.config.ts
  • testing/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

Comment on lines +21 to +29
const outputSchema = {
type: 'object',
properties: { answer: { type: 'string' } },
required: ['answer'],
}

const tools: Array<Tool> = [
{ name: 'lookup_weather', description: 'Return the forecast for a location' },
]

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.

📐 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" || true

Repository: 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
done

Repository: 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ai-openrouter): per-request native combined tools + outputSchema mode (extends #605)

1 participant