@route_33/opencode
v0.4.6
Published
opencode provider plugin for the route33.ai AI gateway — key-scoped model discovery, cost/limit enrichment, auth, and the key-setup wizard.
Maintainers
Readme
@route_33/opencode
An opencode provider plugin for the route33.ai AI gateway: model discovery, cost/limit metadata, and a key-setup wizard in the TUI.
Install
opencode plugin @route_33/opencodeThe installer patches both configs: the server plugin into opencode.json and the key-setup wizard (TUI plugin) into tui.json. To install manually instead, declare both entries yourself:
// opencode.json — server plugin (model discovery, cost/limit enrichment, auth)
{
"plugin": ["@route_33/opencode"]
}// tui.json — TUI plugin (the key-setup wizard)
{
"plugin": ["@route_33/opencode"]
}This plugin never writes your opencode.json or opencode.jsonc: opencode has no TUI-plugin auto-discovery, so without a tui.json entry the wizard simply never appears — the server plugin works regardless. The only files the plugin ever writes inside your repo are an optional .opencode/route33.key (git-ignored automatically) and your own .opencode/route33-overrides.json (yours to commit or ignore).
Auth
Run opencode auth login and choose route_33, or set the ROUTE33_API_KEY environment variable. Without a key, the TUI wizard offers to save one: per-project inside a git workspace, global otherwise. A globally stored key applies only outside git workspaces — inside a workspace, keys resolve from the environment, the workspace config, or .opencode/route33.key (per-project keys are the metering boundary). Skipping the project-key wizard snoozes it for 7 days — "Skip" for that workspace only, "Skip for all workspaces" everywhere (both live in opencode's TUI kv store; remove project-key-prompt.dismissed-at* from ~/.local/state/opencode/kv.json to re-arm sooner). ROUTE33_BASE_URL overrides the default gateway URL; ROUTE33_TUI_PROMPT=0 disables the wizard.
Your key is never written into the resolved provider configuration: authentication is attached per request at fetch time, so opencode debug config output contains no credential material. One inert exception: the options carry the non-secret marker __route33_fetch_shim_auth__ as apiKey, because the native per-model SDKs used for anthropic/, openai/, and mistralai/ models (@ai-sdk/anthropic, @ai-sdk/openai, @ai-sdk/mistral) refuse to construct a request without one — the marker satisfies that null check, never authenticates anything (the gateway reads only the request-time Authorization header, which the fetch shim injects), and no vendor env var (ANTHROPIC_API_KEY etc.) is needed or consulted. The marker is skipped by key resolution and never overrides an apiKey you authored yourself; it is visible in config dumps precisely because it is not a secret.
Model catalog
The catalog is discovered dynamically. The plugin calls GET {baseURL}/models with your resolved API key, caches the response on disk, and injects only the models that key can see. The refresh ladder is:
- Return the fresh on-disk cache if it has not expired.
- Fetch live from the gateway and update the cache.
- If the live fetch fails, return the stale cache.
- If there is no cache and no live response, preserve existing generated entries and inject no replacement models.
The plugin does not embed or serve a static catalog. The on-disk cache is keyed by gateway URL and API key, so the same key pointed at a different gateway (ROUTE33_BASE_URL, e.g. a local cluster) never shares a cache with production. Catalog cache envelopes are versioned; cache files from older plugin versions are ignored after an upgrade and repopulated from the gateway.
If discovery yields no catalog, the plugin logs exactly one warning:
route33: no model catalog available — 0 models injected (reason). route33 is unusable without a catalog: resolve an API key (opencode auth login / ROUTE33_API_KEY) — the key dictates which models are available.Set ROUTE33_DISCOVERY=0 to opt out of discovery entirely. This is logged at info and injects zero models without the warning.
Metadata provenance
Models are decorated with metadata from the route33 gateway broadcast; models.dev is used only as a backfill source.
- Cost: the gateway's broadcast pricing wins;
models.devfills in when the broadcast carries no pricing. - Limits: the gateway's broadcast
limitswin;models.devfills in when the gateway broadcasts none. Limits are omitted entirely when neither source knows them; the plugin never fabricates them. - Modalities: gateway
modalities.input/modalities.outputare passed through to opencode. Invalid modality values and malformed modality objects are omitted per model. - Attachment: gateway
attachmentwins whenever present.models.devsupplies it only when the gateway omits the field. - Capabilities: the broadcast
reasoningandtool_supportflags win (tool_supportis mapped totool_call);models.devalways backfills any capability flag left undefined. - Variants: R33 broadcast variants are passed verbatim (for example,
kimi-k3'smaxreasoningEffortvariant).
The normative broadcast contract is docs/gateway-limits-variants-broadcast.md.
Managed model entries
Gateway-discovered provider.route_33.models entries are fully managed: every startup rebuilds the map from the current broadcast, then deep-merges your overrides on top. The ownership rule is the whole design — in the broadcast (or in your overrides) ⇒ kept; anything else ⇒ dropped and logged — so retired gateway models disappear automatically and no bookkeeping file is needed. (Versions ≤0.4.3 kept a .opencode/route33-generated.json ledger for this; the plugin deletes that obsolete file on first run.)
Keep customizations in .opencode/route33-overrides.json, because the installed OpenCode ProviderConfig does not allow unknown provider-level keys such as modelOverrides. Overrides deep-merge onto matching generated entries and can add custom models:
{
"models": {
"openai/gpt-5.5": { "variants": { "fast": { "temperature": 0 } } },
"custom/internal": { "name": "Internal model" }
}
}If the gateway fetch fails with no usable cache, regeneration is skipped entirely: existing entries are never pruned on zero information.
ROUTE33_ENRICH_CAPS has been removed; capability backfill from models.dev is now always on.
Model-reference tolerance
Model references in your config that differ from the served ID only by digit-boundary dots versus dashes in the tail resolve automatically. For example, anthropic/claude-opus-4.8 resolves to the served anthropic/claude-opus-4-8 and is injected with the served wire ID. Arbitrary renames, such as gpt5.5 for openai/gpt-5.5, do not resolve.
Environment variables
| Variable | Effect |
|---|---|
| ROUTE33_API_KEY | Gateway API key; highest precedence. See Auth. |
| ROUTE33_BASE_URL | Override the default gateway base URL. See Auth. |
| ROUTE33_TUI_PROMPT | Set to 0 to disable the key-setup wizard. See Auth. |
| ROUTE33_DISCOVERY | Set to 0 to disable dynamic catalog discovery. |
| ROUTE33_STRICT | Set to 0 to downgrade unknown-model reference errors to a warning plus a stub entry. |
| ROUTE33_CACHE_DIR | Override the directory used for the on-disk catalog cache. |
The plugin reads only these variables. ROUTE33_ENRICH_CAPS is no longer supported.
Exhaustion retry
Starting with v0.4.6, the plugin wraps live chat/compat requests in an exhaustion retry layer. It triggers only on an HTTP 429 whose error code is PROVIDER_LANES_EXHAUSTED (all provider lanes exhausted). It reads the gateway's Retry-After response header and falls back to a client-side schedule of 5·2^(n−1) seconds capped at 1200 when the header is missing. It waits through the delay while respecting the request's abort signal, then re-issues the identical request. It keeps retrying indefinitely until the request succeeds or the user cancels it.
Non-exhaustion errors (network failures, 4xx responses from the gateway other than the exhaustion code, etc.) are passed through untouched.
Each wait is logged with the attempt number, waited seconds, and the gateway's correlationId:
route33: exhausted; waiting <seconds>s before retry <attempt> (correlationId=<correlationId>)The provider option headerTimeout is set to 1_500_000 ms (25 minutes). This is higher than opencode's own 300-second fetch default so that scheduled waits are not aborted while waiting for a lane to cool down. It is safe because the gateway caps provider hangs at 120 s TTFB and 240 s first-event timeouts, so the connection is never silent for anywhere near 25 minutes.
License
Proprietary — "SEE LICENSE IN LICENSE". Install and use as-is via npm; copying, modification, and redistribution are not permitted. Provided AS-IS, without warranty or liability.
