Skip to content

Draft: point an agent at OpenRouter, with the user's key and never ours - #127

Draft
Amitgb14 wants to merge 3 commits into
mainfrom
feat/openrouter-gateway
Draft

Draft: point an agent at OpenRouter, with the user's key and never ours#127
Amitgb14 wants to merge 3 commits into
mainfrom
feat/openrouter-gateway

Conversation

@Amitgb14

Copy link
Copy Markdown
Owner

OpenRouter and sandbox-cli routing solve different problems — that one switches
whole agents when one stops working, this one switches what a single agent talks
to — and they compose. gateway: is that composition.

# ~/.config/sandbox/config.yaml — user config only, never a project's
gateway:
  agents: [opencode]
  key_env: OPENROUTER_API_KEY        # a NAME. The value is yours.
  base_url: https://openrouter.ai/api/v1

The rule this is built around

sandbox-cli supplies no key and holds no account. key_env is a name; the
value arrives the way every other credential does — forwarded from your
environment if set, or resolved by the broker from secrets: — and never passes
through the code that renders the argv. A run configured for a gateway with
nothing to read is refused, because both silent alternatives are wrong:
reaching the gateway unauthenticated, or falling through to the vendor on the
agent's own credential and spending the wrong account against the wrong endpoint.

Four facts, resolved together

The base URL, the key variable, the probe host and the egress domain have to
agree or the run is worse than unconfigured. The last two fail quietly:

  • probing the vendor while talking to a gateway makes routing skip that agent
    exactly when the vendor is down — the outage a gateway is bought to survive;
  • an allowlist that does not permit the gateway fails as a connection error from
    the agent, naming nothing.

Two refusals rather than best-effort support

  • An agent that speaks its vendor's own API shape (claude, gemini, droid) is
    refused, not pointed at an OpenAI-shaped endpoint: a base URL cannot translate a
    protocol, and the failure would land in a container as a parse error blamed on
    the model. The table lists opencode (its own feature, verified) and codex
    (documented shape, marked unverified).
  • A plaintext base_url is refused: the credential and every prompt cross it.

gateway: is user-config only, refused from a project .sandbox.yaml — it names
the host every prompt travels through and the credential that pays for it, which
is providers:'s three objections at once plus one of its own, since a gateway
reads the work.

Why draft

  • The codex pairing is inference from its documented environment, not a run.
    It should be exercised against a real key before this is presented as supported.
  • No Studio surface yet: the Launch screen neither shows that a run will go
    through a gateway nor lets one be chosen. The config path is complete and tested.
  • Worth deciding: whether an unverified pairing should warn at launch, and whether
    gateway: belongs in sandbox-cli doctor.

Tests cover the key never being ours, the shape refusal naming what would work,
per-agent opt-in, the plaintext refusal, probe-follows-the-gateway with
providers: still winning, and end to end through BuildSpec: refusal with no
key, base URL in the container, key forwarded by name with no value in the spec,
and the host joining the allowlist.

🤖 Generated with Claude Code

OpenRouter and this tool's routing solve different problems — that one switches
whole agents when one stops working, this switches what a single agent talks to —
and they compose. `gateway:` in the user's own config is that composition:
name the agents, and their API calls go to an OpenAI-shaped gateway instead of
the vendor.

**sandbox-cli supplies no key and holds no account.** `key_env` is a *name*; the
value arrives the way every other credential does, forwarded from the user's
environment or resolved by the broker from `secrets:`, and never passes through
the code that builds the argv. A run configured for a gateway with nothing to
read is refused, because both silent alternatives are wrong: reach the gateway
unauthenticated, or fall through to the vendor on the agent's own credential and
spend the wrong account against the wrong endpoint.

Four facts have to agree or the run is worse than unconfigured, so one function
resolves them together — base URL, key variable, probe host, egress domain. The
last two are the ones that would otherwise be got wrong quietly: probing the
*vendor* while talking to a gateway makes routing skip the agent exactly when the
vendor is down, which is the outage a gateway is bought to survive; and an
allowlist that does not permit the gateway fails as a connection error from the
agent, naming nothing.

Two refusals rather than best-effort support. An agent that speaks its vendor's
own API shape is refused, because a base URL cannot translate a protocol and the
failure lands in a container as a parse error blamed on the model — so the table
lists opencode (its own feature; verified) and codex (documented shape; marked
unverified) and deliberately not claude, gemini or droid. And a plaintext
base_url is refused outright.

`gateway:` is user-config only, refused from a project file: it names the host
every prompt travels through and the credential that pays for it.

Draft because the codex pairing has not been run end to end, and because the
Studio surface for this is not built — the config path is complete and tested.
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sandbox-cli Ready Ready Preview Aug 17, 2026 8:11am

The Routing screen could not have shown this correctly from what the daemon
reported: `host` is what routing probes, and for a gateway agent that is
openrouter.ai — so a picture built from it would draw the gateway as the agent's
vendor. `ProviderStatus.Gateway` is the separate fact, and the two are equal today
only by coincidence.

The graph puts it below the ring rather than in it, and the shape carries the
argument. Agents in the ring are alternatives — the chain falls from one to the
next — while agents on a gateway are the opposite of alternatives: they share one
credential, one bill and one point of failure that no chain can route around. If
the gateway is down, every agent on it is down together, which is precisely what
a ring of interchangeable nodes would deny.

Their nodes take a dashed ring so the row and the node are recognisably one fact,
the providers list says *via <host>*, and the health shown is the gateway's own —
the vendor behind it being unreachable is the case a gateway is bought to survive,
so measuring the vendor would skip the agent that still worked.
…cues

Three panels, and a correction that had to come with them.

**If you launched now** resolves each chain against the probe results already on
the page. That join — provider list says claude is down, chain list says claude
falls back to codex — was left to the reader, which is a strange thing to leave
to a reader on the one screen whose subject is what happens automatically. It
keeps the daemon's rule that unprobed is not down: an agent nothing was asked
about is taken rather than skipped, since skipping it acts on a measurement
nobody made.

**Why they fired** ranks the reasons in the words the runs recorded — grouping
them would be this screen inventing a taxonomy the log does not have, and a
column of "provider answered 503" and a column of "exited 1 having changed
nothing" call for different actions. **Asked for, and ran** puts two bars per
agent, because the interesting number is the gap: an agent finishing work it was
never asked for is doing it under its own login and its own bill.

The correction is that the third panel would have been drawn on a lie. A detached
run's audit line is written when the container launches — there is no exit code
to wait for — so it carries 0 whatever happens, and *every* Studio run is
detached. The rescue rate on this screen was therefore 100% by construction and
the trend's still-failed series unreachable. An episode ending in a detached
attempt now reports **not recorded**: its own counter, its own band, its own row
badge. The same rule the rest of the tool keeps about absent readings — a missing
measurement is not a good one.
Amitgb14 added a commit that referenced this pull request Aug 21, 2026
Twenty-one user-facing entries in the sixty-five commits since beta.15:
Studio's projects registry and file browsing, conversation resume and
cross-agent handoff, the routing screens, a published TypeScript SDK, the
terminal fix that cost goose its banner, and today's two submodule bugs.

One entry moved back to Unreleased rather than shipping: the `gateway:`
block that points an agent at OpenRouter lives in #127, which is still a
draft. Its changelog text had ridden onto main with an unrelated Routing
commit, so the release would have announced a config key nobody could
use. What main actually has is OPENROUTER_API_KEY in the env allowlists —
your own key forwarded by name, which is a much smaller claim.

The version is pinned in two places and both move together: version.go,
which the build stamps, and web/src/lib/site.ts, which the site prints.
Amitgb14 added a commit that referenced this pull request Aug 22, 2026
Three entries: the podman network reaper (issue #77), the SDK's
sequential-run fix, and the travel-planner example.

The OpenRouter `gateway:` entry stays in Unreleased for the second
release running — #127 is still a draft, so `main` has no such config key
and shipping the note would announce something nobody can use.
Amitgb14 added a commit that referenced this pull request Aug 23, 2026
Five entries: the Python SDK, `addProject(init: true)` and the
empty-worktree refusal, and the two SDK bugs the Python review found in
both clients — a failover reported as a failure, and a misspelled run
option ignored.

The OpenRouter `gateway:` entry stays in Unreleased for the third release
running. #127 is still a draft and now conflicting, so main has no such
config key and shipping the note would announce something nobody can use.
Amitgb14 added a commit that referenced this pull request Aug 26, 2026
Six entries: cline reaching Studio and both SDKs, a Devin adapter, the
Python client's start() and its two examples, and the distribution
rename to sandbox-cli-sdk.

The OpenRouter `gateway:` entry stays in Unreleased for the fourth
release running. #127 is a conflicting draft, so main has no such config
key and shipping the note would announce something nobody can use.
Amitgb14 added a commit that referenced this pull request Aug 27, 2026
Two entries: six agents removed and a Kilo Code adapter added. The roster
is twelve.

`agent: droid` in an existing fleet.yaml is refused from this version on,
which is the one thing in here that can break a file somebody already has.
Droid shipped in beta.19 yesterday, so anyone on that release meets the
refusal on upgrade rather than reading about it — hence the release note
leading with it rather than with what was added.

The OpenRouter `gateway:` entry stays in Unreleased for the fifth release
running: #127 is still a conflicting draft, main has no such config key,
and shipping the note would announce something nobody can use.

Also corrects the site's meta description, which said "Claude Code, Codex,
Gemini and 12 more agents" — fifteen, and the count nobody grep'd because
the number is in prose rather than beside the word "adapter". It is the
description search results show, so it was the roster claim with the widest
reach and the least chance of being noticed here.

Neither SDK is republished. The only change under sdk/ since beta.19 is
four lines of doc comment in contract.ts, so the published packages differ
from this tree by prose that names droid and nothing else; spending a
version number on that would make the next real SDK release harder to
find. It is worth knowing the published contract.d.ts says droid until
whatever ships next.
Amitgb14 added a commit that referenced this pull request Aug 27, 2026
Twenty pre-releases is enough. The isolation boundary, the profiles, the
agent roster and the Studio contract have all been stable for several
releases; continuing to ship "beta.N" says more about the habit than about
the software. Nothing in the boundary changes here — this is beta.20 plus
the two fixes it carries.

The number stays conservative deliberately. 0.0.1 is the first stable tag,
not a claim of feature-completeness: open-items.md is still a live backlog
and the agent still holds raw credentials inside the container. What
"stable" is claiming is narrower — that the contracts above do not move
without a changelog entry saying so.

Three things this changes beyond the number:

goreleaser's `prerelease` goes false, which its own comment has been
waiting for since the first beta ("Set this to false for a stable
release"). GitHub stops labelling the release Pre-release. The install
script is unaffected either way — it reads the releases list newest-first
rather than /releases/latest, precisely so a repository whose releases were
all pre-releases still resolved.

The site's go-install note had to narrow rather than stay put. It said the
tags "are not valid semver", which was true of 0.0.1beta.8 — no `-` before
the pre-release — and stops being true of 0.0.1, which is a perfectly valid
version. Go still cannot pin it, for the one reason left: a module tag
needs a `v` prefix and ours has none. Adopting v-prefixed tags would fix
that and is a real improvement, but it renames every release asset and
every --version example, so it is its own change and not a side effect of
this one.

The OpenRouter `gateway:` entry stays in Unreleased for the sixth release
running, for the sixth time for the same reason: #127 is a conflicting
draft and main has no such config key.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant