Skip to content

feat(ai): type OpenRouter options on the Responses route - #60044

Merged
rekram1-node merged 1 commit into
devfrom
openrouter-responses-options
Oct 30, 2026
Merged

rekram1-node merged 1 commit into
devfrom
openrouter-responses-options

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

OpenRouter.responses(id) used the generic Responses options type and protocol. OpenRouter's own request options (provider routing, models fallbacks, plugins, ...) were not typed there, and the generic Responses body schema dropped them, so they only reached OpenRouter through a raw body overlay.

Change

I checked the POST /responses request schema in OpenRouter's API reference (https://openrouter.ai/docs/api/api-reference/responses/create-a-response, accessed 2026-10-28). Of the options the Chat route already types, these are also accepted on /responses, and are now typed and sent on the Responses route:

Option Chat Responses
provider yes yes (new)
models yes yes (new)
plugins yes yes (new)
debug yes yes (new)
user yes yes (new)
usage, web_search_options yes not in the /responses schema

The Responses route keeps the standard Responses options (reasoningEffort, include, store, ...), and reasoning keeps going through them.

Both routes share one schema and one lowering, so nothing is duplicated:

- const Options = Struct({ usage, models, provider, plugins, web_search_options, debug, user, reasoning })
+ const SharedOptions = Struct({ models, provider, plugins, debug, user })
+ const Options = Struct({ ...SharedOptions.fields, usage, web_search_options, reasoning })

  chat body      = { ...openAIChatBody, usage, ...sharedOptions(known), web_search_options, reasoning }
+ responses body = { ...openResponsesBody, ...sharedOptions(known) }

The type reuses the existing option types: OpenResponsesProviderOptionsInput & Pick<OpenRouterOptions, "debug" | "models" | "plugins" | "provider" | "user">. Enum-like strings such as provider.sort and data_collection still accept any string, and the shared lenient decoding drops a malformed known option without failing the request.

The Responses route now uses its own openrouter-responses protocol (the Chat route already has openrouter-chat). Stream parsing is still OpenResponses.protocol.stream.

Live evidence

I sent requests through OpenRouter.responses("openai/gpt-5.4-mini") with the X-OpenRouter-Metadata: enabled header so OpenRouter reports its routing decision in openrouter_metadata on response.completed.

provider.only (5 endpoints in total; without a preference, 2 were available):

providerOptions summary selected endpoint
none available=2, selected=OpenAI OpenAI
provider: { only: ["azure"], allow_fallbacks: false } available=1, selected=Azure Azure
provider: { only: ["openai"], allow_fallbacks: false } available=1, selected=OpenAI OpenAI

models fallbacks:

providerOptions requested strategy
none openai/gpt-5.4-mini direct
models: ["openai/gpt-5.4-nano"] openai/gpt-5.4-mini,openai/gpt-5.4-nano fallback

Tests

  • A request-body test that captures the HTTP body on the Responses route and checks provider, models, plugins, debug and user next to standard Responses fields. It fails without this change.
  • Type tests for the Responses route, covering open enum strings and rejecting a malformed models value.

Out of scope

/responses also accepts OpenRouter fields that neither route types yet: session_id, trace, cache_control, prompt_cache_options, modalities, image_config, stop_server_tools_when, deferred_tools, and the OpenRouter reasoning.enabled / reasoning.max_tokens extensions. They can still be sent through body.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" stroke="currentColor" stroke-width="2" font-family="ui-monospace,monospace" font-size="9">
  <style>
    .p{animation:m 3s ease-in-out infinite}
    .b{animation:f 3s ease-in-out infinite}
    @keyframes m{0%,15%{transform:translateX(0)}55%,100%{transform:translateX(118px)}}
    @keyframes f{0%,50%{opacity:.25}65%,100%{opacity:1}}
    @media (prefers-reduced-motion:reduce){.p,.b{animation:none}.b{opacity:1}}
  </style>
  <rect x="6" y="20" width="62" height="32" rx="6" opacity=".6"/>
  <text x="14" y="39" fill="currentColor" stroke="none">options</text>
  <path d="M72 36h96" opacity=".3" stroke-dasharray="4 4"/>
  <g class="p"><rect x="44" y="30" width="34" height="12" rx="6" fill="currentColor" stroke="none" opacity=".8"/></g>
  <rect class="b" x="172" y="12" width="62" height="48" rx="6"/>
  <text x="180" y="28" fill="currentColor" stroke="none">/responses</text>
  <text class="b" x="180" y="42" fill="currentColor" stroke="none">provider</text>
  <text class="b" x="180" y="54" fill="currentColor" stroke="none">models</text>
</svg>

Added by the opencode bot (AI-generated).

Public API: adds OpenRouter routing options (provider, models, plugins, debug, user) to the OpenRouter Responses options type; Responses route protocol id open-responses → openrouter-responses (backwards compatible)

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. The new Responses-route body test fails on the base, because provider/models/plugins/debug/user are missing from the /responses body, and passes with this change. The two OpenRouter test files pass (46 tests), and tsc passes for both tsconfigs, including the new type tests. The design matches the existing openrouter-chat protocol: the shared fields are added through SharedOptions, bad values are still dropped by the lenient decoding, and the stream parsing is reused unchanged. Nothing in packages/ai or packages/core checks for the open-responses protocol id, so the rename has no effect on behaviour elsewhere.

Public API (@opencode/ai):

  • Adds the OpenRouterResponsesProviderOptionsInput type.
  • Widens OpenRouter.responses(...) / responsesModel / providers/openrouter/responses Settings from OpenResponsesProviderOptionsInput to OpenRouterResponsesProviderOptionsInput, and Config.providerOptions with it.
  • Changes the OpenRouter Responses route's protocol id from open-responses to openrouter-responses.

All of this is backwards compatible for type users. Only code that compared the route's protocol id to "open-responses" would notice.

No other concerns.

@rekram1-node
rekram1-node force-pushed the openrouter-responses-options branch from dbc9246 to 299ca48 Compare October 30, 2026 01:37

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new push is a rebase onto dev with no change to the PR's own diff. Re-checked on the new head: the OpenRouter provider and package tests pass (46), and tsc passes for both tsconfigs. The public API impact is the same as in my earlier review: it adds OpenRouterResponsesProviderOptionsInput, widens the OpenRouter Responses settings and options types, and changes the Responses route's protocol id from open-responses to openrouter-responses. All of it is backwards compatible. No new concerns.

@rekram1-node
rekram1-node merged commit e705b56 into dev Oct 30, 2026
20 checks passed
@rekram1-node
rekram1-node deleted the openrouter-responses-options branch October 30, 2026 02:29
@amrkmn amrkmn mentioned this pull request Oct 30, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant