Skip to content

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

@tombeckenham

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions