Skip to content

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

Open
season179 wants to merge 3 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 3 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 schema together in a single request for compatible models.
    • The same combined mode is available across both chat-completions and Responses-style adapters.
    • Support checks now account for fallback models and model variants to keep behavior consistent.
  • 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4d6099c-84bd-4997-9619-a95b87ec153a

📥 Commits

Reviewing files that changed from the base of the PR and between 608f0c7 and 0ae4ab6.

📒 Files selected for processing (4)
  • 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/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/ai-openrouter/src/adapters/text.ts
  • packages/ai-openrouter/src/adapters/responses-text.ts

📝 Walkthrough

Walkthrough

OpenRouter now supports sending tools and schema-constrained output in a single request for eligible upstream models. The package exports the combined-mode model set, both OpenRouter text adapters use it to gate request shaping, and tests plus E2E support cover the new paths.

Changes

OpenRouter combined tools + schema mode

Layer / File(s) Summary
Capability catalog and exports
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 allowlist is defined in model-meta, exposed through the package entry points, added to the build entry list, and documented in the changeset.
Chat-completions combined request
packages/ai-openrouter/src/adapters/text.ts, packages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.ts
The chat-completions adapter now conditionally adds strict responseFormat from outputSchema when combined mode is supported, and the tests cover the gate and request shape.
Responses combined request
packages/ai-openrouter/src/adapters/responses-text.ts, packages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.ts
The Responses adapter now conditionally adds text.format from outputSchema when combined mode is supported, preserving existing text fields, and the tests cover the gate and request shape.
Tests and E2E support
testing/e2e/src/lib/feature-support.ts, packages/ai-openrouter/src/adapters/openrouter-combined-structured-output.test.ts
OpenRouter is added to the structured-stream E2E matrix, and the shared tests verify the combined-mode model set matches the chat model catalog.

Sequence Diagram(s)

sequenceDiagram
  participant OpenRouterTextAdapter
  participant openRouterSupportsCombinedToolsAndSchema
  participant OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS
  participant makeStructuredOutputCompatible
  participant OpenRouter Chat Completions endpoint

  OpenRouterTextAdapter->>openRouterSupportsCombinedToolsAndSchema: supportsCombinedToolsAndSchema(modelOptions)
  openRouterSupportsCombinedToolsAndSchema->>OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS: check resolved upstream model ids
  OpenRouterTextAdapter->>makeStructuredOutputCompatible: convert outputSchema
  OpenRouterTextAdapter->>OpenRouter Chat Completions endpoint: send tools + responseFormat
Loading
sequenceDiagram
  participant OpenRouterResponsesTextAdapter
  participant openRouterSupportsCombinedToolsAndSchema
  participant OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS
  participant OpenRouter Responses endpoint

  OpenRouterResponsesTextAdapter->>openRouterSupportsCombinedToolsAndSchema: supportsCombinedToolsAndSchema(modelOptions)
  openRouterSupportsCombinedToolsAndSchema->>OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS: check resolved upstream model ids
  OpenRouterResponsesTextAdapter->>OpenRouter Responses endpoint: send tools + text.format
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • TanStack/ai#609: Adds the same native combined tools + JSON schema flow for OpenRouter adapters, which is the closest code-level match to this change.
  • TanStack/ai#611: Touches the same OpenRouter request-mapping path in responses-text.ts, making it a nearby adapter-flow change.

Suggested reviewers

  • AlemTuzlak

Poem

A bunny hops, the schema glows,
Tools and JSON in one stream flows.
OpenRouter nibbles, swift and bright,
One little leap makes streaming right. 🐰

🚥 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 concisely describes the main OpenRouter combined tools + outputSchema change.
Description check ✅ Passed The description covers the change, testing, and release impact details, though it does not follow the repository template exactly.
Linked Issues check ✅ Passed The code matches #612 by adding combined-mode support to both adapters, exporting the model-meta set, and covering fallback and E2E behavior.
Out of Scope Changes check ✅ Passed The changes are focused on the OpenRouter combined-mode feature and supporting tests/build exports, with no clear unrelated additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

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

packages/ai-openrouter/vite.config.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): packages/ai-openrouter/vite.config.ts


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