Skip to content

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

Description

@tombeckenham

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

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

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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions