Follow-up to #605, which landed native combined mode (`supportsCombinedToolsAndSchema()`) for OpenAI (Chat Completions + Responses), Anthropic Claude 4.5+, Gemini 3.x, and the Grok 4 family. OpenRouter was deliberately deferred because it has a fundamentally different design problem from the others.
What native combined mode buys
`chat({ outputSchema, tools, stream: true })` against a capable adapter wires the schema into the regular `chatStream` request alongside `tools` — the agent loop's final-turn text is the schema-constrained JSON, the engine harvests it from accumulated content, and the separate finalization adapter call is skipped. See #605 for the engine plumbing.
Why OpenRouter is harder than the others
The other native-combined adapters declare capability statically (or via a model-meta set check on `this.model`). OpenRouter is a routing layer — a single OpenRouter adapter instance can route to many upstream models (`anthropic/claude-opus-4.6`, `openai/gpt-5.2`, `x-ai/grok-4.1-fast`, etc.), each with its own combined-mode capability. The capability for an OpenRouter call depends on which upstream model is resolved at request time, not on the adapter.
Design choices to consider
-
Static model-string prefix check. The `supportsCombinedToolsAndSchema(modelOptions)` method receives `modelOptions` from the call site. The OpenRouter adapter inspects `this.model` (e.g. `anthropic/claude-opus-4.6`) plus any `modelOptions.model` / `modelOptions.variant` overrides and matches against a known-good prefix list:
- `anthropic/claude-sonnet-4-5` and later
- `anthropic/claude-opus-4-5` and later
- `openai/gpt-4o-2024-08-06` and later
- `openai/gpt-5*`, `openai/o3*`, `openai/o4*`
- `google/gemini-3*`
- `x-ai/grok-4*`
Pro: stays adapter-local, no cross-package coupling. Con: duplicates the upstream gating that's already in each native adapter's own model-meta set.
-
Delegate to upstream adapter capability sets. OpenRouter imports the model-meta sets from `@tanstack/ai-anthropic`, `@tanstack/ai-gemini`, etc., strips the `upstream/` prefix from the model id, and checks each set in turn. Stays in sync automatically but creates dependency edges OpenRouter probably wants to avoid.
-
Per-model registry on `OpenRouterTextAdapter`. A static map keyed by OpenRouter's catalog model ids. Maintained inside `ai-openrouter/src/model-meta.ts` alongside the existing catalog. Pro: explicit and inspectable. Con: yet another list to keep current as new upstream models land.
Suggested: (3) is the most idiomatic match for how OpenRouter already organizes its catalog. The `supportsCombinedToolsAndSchema(modelOptions)` method inspects `this.model` (and `modelOptions.model` override if present) against a `OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS: Set` exported from model-meta.
Wiring
Both OpenRouter adapter variants need updating:
- `OpenRouterTextAdapter` (chat-completions endpoint): when `options.outputSchema` is set AND the resolved model is in the set, attach `response_format: { type: 'json_schema', json_schema: { strict: true, schema } }` to the upstream request alongside `tools`. Skip when not in the set so the engine takes the legacy finalization path.
- `OpenRouterResponsesTextAdapter` (Responses-beta endpoint): same pattern but with `text.format` instead.
Acceptance
Follow-up to #605, which landed native combined mode (`supportsCombinedToolsAndSchema()`) for OpenAI (Chat Completions + Responses), Anthropic Claude 4.5+, Gemini 3.x, and the Grok 4 family. OpenRouter was deliberately deferred because it has a fundamentally different design problem from the others.
What native combined mode buys
`chat({ outputSchema, tools, stream: true })` against a capable adapter wires the schema into the regular `chatStream` request alongside `tools` — the agent loop's final-turn text is the schema-constrained JSON, the engine harvests it from accumulated content, and the separate finalization adapter call is skipped. See #605 for the engine plumbing.
Why OpenRouter is harder than the others
The other native-combined adapters declare capability statically (or via a model-meta set check on `this.model`). OpenRouter is a routing layer — a single OpenRouter adapter instance can route to many upstream models (`anthropic/claude-opus-4.6`, `openai/gpt-5.2`, `x-ai/grok-4.1-fast`, etc.), each with its own combined-mode capability. The capability for an OpenRouter call depends on which upstream model is resolved at request time, not on the adapter.
Design choices to consider
Static model-string prefix check. The `supportsCombinedToolsAndSchema(modelOptions)` method receives `modelOptions` from the call site. The OpenRouter adapter inspects `this.model` (e.g. `anthropic/claude-opus-4.6`) plus any `modelOptions.model` / `modelOptions.variant` overrides and matches against a known-good prefix list:
Pro: stays adapter-local, no cross-package coupling. Con: duplicates the upstream gating that's already in each native adapter's own model-meta set.
Delegate to upstream adapter capability sets. OpenRouter imports the model-meta sets from `@tanstack/ai-anthropic`, `@tanstack/ai-gemini`, etc., strips the `upstream/` prefix from the model id, and checks each set in turn. Stays in sync automatically but creates dependency edges OpenRouter probably wants to avoid.
Per-model registry on `OpenRouterTextAdapter`. A static map keyed by OpenRouter's catalog model ids. Maintained inside `ai-openrouter/src/model-meta.ts` alongside the existing catalog. Pro: explicit and inspectable. Con: yet another list to keep current as new upstream models land.
Suggested: (3) is the most idiomatic match for how OpenRouter already organizes its catalog. The `supportsCombinedToolsAndSchema(modelOptions)` method inspects `this.model` (and `modelOptions.model` override if present) against a `OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS: Set` exported from model-meta.
Wiring
Both OpenRouter adapter variants need updating:
Acceptance