Skip to content

feat(ai): Responses routes for OpenRouter, DigitalOcean and Venice - #59991

Merged
rekram1-node merged 2 commits into
devfrom
provider-responses-routes
Oct 28, 2026
Merged

rekram1-node merged 2 commits into
devfrom
provider-responses-routes

Conversation

@rekram1-node

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

Copy link
Copy Markdown
Contributor

Problem

OpenRouter, DigitalOcean and Venice all serve OpenAI's Responses API at /responses, but their providers in @opencode/ai only had Chat Completions routes. To use Responses with them you had to go through the generic openai-compatible/responses entry point and set the base URL, auth and provider ID yourself.

Change

I added a responses route to each provider. Each one reuses the shared Open Responses protocol and has the same defaults as the generic Responses route (store: false, encrypted reasoning included). It also uses the provider's own base URL, API key env vars and provider metadata key.

 OpenRouter.configure()
   model(id)       -> openrouter              (Chat, unchanged)
+  responses(id)   -> openrouter-responses    POST https://openrouter.ai/api/v1/responses
 DigitalOcean.configure()
   model(id)       -> digitalocean            (Chat, unchanged)
+  responses(id)   -> digitalocean-responses  POST https://inference.do-ai.run/v1/responses
 Venice.configure()
   model(id) / chat(id) -> venice-chat        (unchanged)
+  responses(id)   -> venice-responses        POST https://api.venice.ai/api/v1/responses

Each provider also gets a package entry point, following the existing subpath convention, and core lists them in its built-in loader map and its reasoning-effort table:

  • @opencode/ai/providers/openrouter/responses
  • @opencode/ai/providers/digitalocean/responses
  • @opencode/ai/providers/venice/responses

Each provider's default model() stays on Chat, so existing callers see no change. Freeform (custom) tools, reasoning replay and the other Responses features now work through these routes.

I recorded cassettes against all three live APIs on a GPT-5.4 mini model. They cover a streamed text reply, a full tool loop, and a freeform { type: "text" } tool call.

Out of scope

  • OpenRouter's Responses endpoint rejects a named custom tool_choice ({ type: "custom", name }). It accepts required. I left the request as is rather than rewriting it on the client.
  • OpenRouter-specific body options (provider, plugins, models) are only typed on the Chat route. On the Responses route they can still be sent through body.
  • models.dev still points these providers' [api.responses] entries at @opencode/ai/providers/openai-compatible/responses. They can move to the new entry points once this version is published.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" stroke="currentColor" font-family="ui-monospace,monospace" font-size="9">
  <style>
    .p{stroke-dasharray:4 4;animation:f 1.6s linear infinite}
    .b{animation:g 3.2s ease-in-out infinite}
    .b2{animation-delay:.4s}.b3{animation-delay:.8s}
    @keyframes f{to{stroke-dashoffset:-16}}
    @keyframes g{0%,100%{opacity:.35}50%{opacity:1}}
    @media (prefers-reduced-motion:reduce){.p,.b{animation:none}}
  </style>
  <rect x="6" y="26" width="52" height="20" rx="4" opacity=".8"/>
  <text x="32" y="39" text-anchor="middle" fill="currentColor" stroke="none">model</text>
  <path class="p" d="M58 36 C90 36 100 14 130 14" opacity=".6"/>
  <path class="p" d="M58 36 H130" opacity=".6"/>
  <path class="p" d="M58 36 C90 36 100 58 130 58" opacity=".6"/>
  <g class="b"><rect x="130" y="5" width="104" height="18" rx="4"/><text x="182" y="17" text-anchor="middle" fill="currentColor" stroke="none">openrouter /responses</text></g>
  <g class="b b2"><rect x="130" y="27" width="104" height="18" rx="4"/><text x="182" y="39" text-anchor="middle" fill="currentColor" stroke="none">digitalocean /responses</text></g>
  <g class="b b3"><rect x="130" y="49" width="104" height="18" rx="4"/><text x="182" y="61" text-anchor="middle" fill="currentColor" stroke="none">venice /responses</text></g>
</svg>

Added by the opencode bot (AI-generated).

Public API: adds Responses routes and @opencode/ai/providers/{openrouter,digitalocean,venice}/responses entry points (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.

The new Responses routes do what the description says, and each provider's default model() still uses Chat. The recorded tests and the full packages/ai suite pass, and tsc --noEmit is clean for packages/ai and packages/core.

Public API: additions only, backwards compatible:

  • OpenRouter, DigitalOcean and Venice each gain responses(id) on the configured provider, plus top-level responses and responsesModel exports.
  • New routes entries: openrouter-responses, digitalocean-responses and venice-responses.
  • New subpath exports: @opencode/ai/providers/{openrouter,digitalocean,venice}/responses.
  • Settings becomes generic, with the old type as its default, and Config.providerOptions also accepts Responses options.

Existing callers don't need to change.

One gap: core's reasoning-effort table in packages/core/src/variant.ts (PROTOCOLS) has no entries for the three new entry points, so once models.dev moves to them those models lose their reasoning-effort options (see inline comment).

["@opencode/ai/providers/openrouter", () => import("@opencode/ai/providers/openrouter")],
["@opencode/ai/providers/openrouter/decisions", () => import("@opencode/ai/providers/openrouter/decisions")],
["@opencode/ai/providers/openrouter/images", () => import("@opencode/ai/providers/openrouter/images")],
["@opencode/ai/providers/openrouter/responses", () => import("@opencode/ai/providers/openrouter/responses")],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These entry points are added to the loader, but core's reasoning-effort table in packages/core/src/variant.ts (PROTOCOLS) has no entries for them, so Variant.resolve returns []. I checked with an effort-capable model: @opencode/ai/providers/openai-compatible/responses gives 2 variants and each of the three new .../responses packages gives 0. Once models.dev points [api.responses] at these, those models would lose their reasoning-effort options. Could you map them to openResponses, as openai-compatible/responses is, and add a case to test/variant.test.ts?

@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 commit fixes the reasoning-effort gap: the three new .../responses entry points are now mapped to openResponses in variant.ts. I checked that Variant.resolve now gives effort options for each of them, the core variant tests pass, and tsc --noEmit is clean for packages/core. The public API impact is the same as in my earlier review (additions only, backwards compatible). Nothing else to raise.

@rekram1-node
rekram1-node merged commit dd35fbc into dev Oct 28, 2026
22 checks passed
@rekram1-node
rekram1-node deleted the provider-responses-routes branch October 28, 2026 19:52
@amrkmn amrkmn mentioned this pull request Oct 29, 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