Skip to content

Configure OpenCode's Router One provider and model IDs

OpenCode is the open-source terminal coding agent from Anomaly, with a TUI, a desktop app and IDE extensions, and it takes custom model providers straight from opencode.json. One provider block on the @ai-sdk/openai-compatible package, pointed at https://api.router.one/v1, puts the Claude, GPT, Gemini, Grok and DeepSeek chat models in its /models picker behind one key. Each model turn, subagent task and session title then reaches Router One as POST /v1/chat/completions and shows up in Dashboard → Logs, and a maxSpend cap on the key is the hard stop for sessions that run unattended. A custom provider's models match nothing in OpenCode's models.dev catalog, so the block also has to say what each model can do: context and output limits, image input, and which model writes titles. Checked against OpenCode 1.18.33 (September 28, 2026, the version npm installs by default) on 2026-09-29, per the opencode.ai docs and the 1.18.33 source; re-checked on 2026-10-09 against 1.18.35 (October 6, 2026, npm's latest tag), whose provider and config source is unchanged; the last section covers the 2.0 pre-release line.

Install OpenCode and store the key with /connect

npm install -g opencode-ai installs 1.18.35, the version on npm's latest tag as of 2026-10-09. Create a Router One key for OpenCode alone and give it a maxSpend cap: every model turn of an agent session is a billed request, including turns that only return a tool result. Then store the key: run /connect in the TUI, scroll to Other, enter a provider ID such as router-one and paste the key. OpenCode writes it to ~/.local/share/opencode/auth.json and warns that this only stores a credential, so the provider itself still has to be defined in opencode.json, with the same ID. opencode auth list shows the stored credentials. If you would rather keep the key in the environment, skip /connect and use the {env:ROUTER_ONE_API_KEY} line in the configuration below.

terminal
npm install -g opencode-ai        # 1.18.35 on npm's latest tag, 2026-10-09
opencode                          # in the TUI: /connect → Other → router-one → paste the key
opencode auth list                # the stored credentials

Configure OpenCode to use the Router One base URL

Define the provider under provider in opencode.json: in ~/.config/opencode/opencode.json for every project, or in a project's opencode.json, which OpenCode merges on top of the global file. The key under provider is the provider ID and must match the one you gave /connect. npm selects the protocol package: the docs pair @ai-sdk/openai-compatible with /v1/chat/completions and @ai-sdk/openai with /v1/responses, so keep the former. options.baseURL takes the /v1 base, and each key under models is an exact catalog ID. When the key comes from /connect, leave options.apiKey out; {env:ROUTER_ONE_API_KEY} reads it from the environment instead, and becomes an empty string, not an error, when the variable is missing. Select a model as router-one/<catalog-id> in /models, or pin it with the top-level model field:

opencode.json
{
  "$schema": "https://opencode.ai/config.json",
  "model": "router-one/anthropic/claude-sonnet-5",
  "small_model": "router-one/anthropic/claude-haiku-4.5",
  "provider": {
    "router-one": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Router One",
      "options": {
        "baseURL": "https://api.router.one/v1",
        "apiKey": "{env:ROUTER_ONE_API_KEY}"
      },
      "models": {
        "anthropic/claude-sonnet-5": {
          "name": "Claude Sonnet 5",
          "attachment": true,
          "modalities": { "input": ["text", "image"], "output": ["text"] },
          "limit": { "context": 1000000, "output": 32000 }
        },
        "anthropic/claude-haiku-4.5": {
          "name": "Claude Haiku 4.5",
          "limit": { "context": 200000, "output": 16000 }
        }
      }
    }
  }
}

Tell OpenCode what each Router One model can do

For a model defined only in your config, OpenCode 1.18.33 fills in defaults in its source: limit.context and limit.output of 0, text-only input with attachments off, tool calling on, and no temperature parameter. A context of 0 turns automatic compaction off, because OpenCode checks for overflow only when it knows the window, so a session grows until a request is rejected for its length. An output of 0 falls back to OpenCode's cap of 32,000 tokens; with a value set, OpenCode sends the smaller of limit.output and 32,000 as max_tokens, which Router One's Chat Completions accepts as the output cap. Set limit.context from the model's page on /models, which shows the window rounded (1.05M, 200K), as a whole number rounded down: 1000000 for anthropic/claude-sonnet-5, openai/gpt-5.6-sol and deepseek-v4-flash, 200000 for anthropic/claude-haiku-4.5. For screenshots, add attachment: true and modalities with image in input, and only for IDs whose page lists image input; a text-only ID such as deepseek-v4-flash gets HTTP 400 with has no chat candidate that supports requested capabilities: vision. A larger window lets OpenCode send longer prompts: on a plan, a request whose input exceeds a model's long-context threshold draws more than one plan request (see /pricing).

Pick the title model with small_model

OpenCode's docs describe small_model as the model for lightweight tasks such as title generation: when it is unset, OpenCode looks for a cheaper model on the same provider and otherwise uses your main model. A custom provider's models carry no family metadata for that search (per the 1.18.33 source), so every session title falls back to the main model, often the most expensive ID you run. Set small_model to a cheaper catalog ID on the same provider, such as router-one/anthropic/claude-haiku-4.5, and title requests appear in Dashboard → Logs under that ID.

Claude IDs on /v1/messages, GPT IDs on /v1/responses (optional)

The OpenAI-compatible block already serves every chat model. For the native Messages API on Claude IDs, add a second provider under a different ID with npm @ai-sdk/anthropic and options.baseURL https://api.router.one/v1; that package appends /messages and sends the key in the x-api-key header, which Router One accepts. Router One serves Claude-family and DeepSeek V4 IDs on /v1/messages. To send one model to the Responses API instead, OpenCode's docs let a model override its package with provider.npm, for example @ai-sdk/openai on openai/gpt-5.6-sol; use that only for IDs whose model page lists /v1/responses (the GPT-family, DeepSeek and Grok chat models). A Claude-family ID sent there is rejected with HTTP 400 model '<id>' must be called via /v1/messages or /v1/chat/completions, and Gemini IDs are not served there either.

Run one prompt and match it in Logs

opencode models router-one lists the models OpenCode sees for the provider, which is the quickest check that the block loaded. opencode run -m router-one/anthropic/claude-sonnet-5 "Reply with OK" sends one non-interactive prompt with the model given as provider/model. In Dashboard → Logs the model turn appears under the catalog ID with its tokens, cost, status, total time and, for a streamed turn that produced output, time to first token (TTFT); a title request may follow under the small_model ID. A 401 usually means the key was not sent: an unset environment variable behind {env:…}, or a /connect provider ID that differs from the key under provider. A model that OpenCode calls unknown is missing from that provider's models map. Provider options also set timeout (300000 ms by default) and chunkTimeout, the longest gap allowed between streamed chunks (also 300000 ms), after which OpenCode aborts the request.

terminal
opencode models router-one
opencode run -m router-one/anthropic/claude-sonnet-5 "Reply with OK"

OpenCode 2.0 pre-release: what changes in the config

OpenCode tagged v2.0.12 through v2.0.26 between September 21 and October 8, 2026, while npm's default install stayed on 1.18.x (1.18.35 on 2026-10-09), so 2.x is a pre-release line; the fields below are from its current docs and are worth checking again when it becomes the default. Per the 2.0 migration guide, 1.x and 2.x both use the opencode command and are not installed side by side, and 2.x reads the same config locations and normalizes supported V1 fields in memory, so the block on this page keeps working. The native 2.0 shape renames provider to providers, npm to package (aisdk:@ai-sdk/openai-compatible, or the native @opencode/ai/providers/openai-compatible), options.baseURL to settings.baseURL, a model's id to modelID, and tool_call and modalities to capabilities; env lists the variables that can supply the key, small_model becomes agents.title.model, and 2.x ignores the V1 attachment field. For a model it does not know, 2.x assumes tool support, text and image input, and 200,000 context and 32,000 output tokens, so set capabilities and limit explicitly. Keep each provider entry entirely in one format, and keep a V1 copy while you test, because 1.x cannot read a file converted to the native 2.0 shape.

opencode.jsonc (2.0 native shape)
{
  "$schema": "https://opencode.ai/config.json",
  "model": "router-one/anthropic/claude-sonnet-5",
  "providers": {
    "router-one": {
      "name": "Router One",
      "env": ["ROUTER_ONE_API_KEY"],
      "package": "aisdk:@ai-sdk/openai-compatible",
      "settings": { "baseURL": "https://api.router.one/v1" },
      "models": {
        "anthropic/claude-sonnet-5": {
          "capabilities": { "tools": true, "input": ["text", "image"], "output": ["text"] },
          "limit": { "context": 1000000, "output": 32000 }
        }
      }
    }
  }
}

Which model ID should OpenCode send?

Copy the exact model ID from /models, preserving case, hyphens, and version suffixes; do not substitute a display name. Open its detail page and match the supported API endpoints, context window, and capabilities such as tool calling to the provider and features selected in OpenCode. A catalog listing does not mean the client can use every feature of that model. Give each client or application a dedicated API key with a maxSpend cap.

Which API protocol is OpenCode using?

OpenAI-compatible describes an interface format; it does not make Chat Completions (/v1/chat/completions), Responses (/v1/responses), and Anthropic Messages (/v1/messages) interchangeable. Check the installed client version, provider configuration, and actual request path against the model detail page and API compatibility fact sheet. A successful plain-text chat does not establish support for hosted tools, conversation state, or file-editing features.

Verify the OpenCode call in your request trace

Send a simple text request from OpenCode, then match its trace in Dashboard → Logs by time, model, and request_id: tokens, cost, latency, and status. Next, test streaming, tool calls, and multi-turn history separately. For failures, retain the actual request path, full error message, and request_id. If there is no matching log, either the request never reached the gateway (client configuration or connectivity) or the gateway refused it before calling a model — a 401 for the key, a 402 for funds, a 429 from your key's or account's limits, or a 400 or 404 for the model ID or endpoint; a streamed request that failed before any output may have no log either. The status and error message the client received tell which, so check them before attributing the error to the gateway or upstream.

FAQ

Why does OpenCode report the model as unknown or fail to authenticate?

Check three things in order. The provider ID in the model string must match the key you used under provider (router-one/<model-id>). Every model must be listed in that provider's models map with the exact ID from /models. And the key must resolve: stored under the same provider ID by /connect, or read with {env:ROUTER_ONE_API_KEY} from a variable exported in the shell that launches opencode, because an unset variable becomes an empty string rather than an error.

Why does a long session never compact, then fail on its length?

Because the model has no limit.context. For a model defined only in your config, OpenCode 1.18.33 uses a context of 0, and with 0 it never checks for overflow, so the session grows until a request is rejected. Add limit.context (the model page's window as a whole number, rounded down) and limit.output to each model.

Why do session titles run on my most expensive model?

Because small_model is unset and a custom provider gives OpenCode nothing cheaper to pick, so titles fall back to the main model. Set small_model to a cheaper catalog ID on the same provider, for example router-one/anthropic/claude-haiku-4.5.

Can OpenCode use Router One's Anthropic-compatible endpoint for Claude models?

Yes, optionally. The OpenAI-compatible block already serves the Claude family; for the native Messages API, add a second provider with npm set to @ai-sdk/anthropic and options.baseURL set to https://api.router.one/v1. The gateway serves Claude models natively on /v1/messages and accepts the same key in the x-api-key header that package sends. Keep the two providers under different IDs so their model lists stay separate.

Does this configuration work with OpenCode 2.0?

Per the 2.0 migration guide, yes: 2.x reads the same opencode.json locations and normalizes V1 provider blocks without rewriting the file. 2.x is still a pre-release line as of 2026-10-09, so if you convert to the native shape (providers, package, settings.baseURL, capabilities), keep a V1 copy, because 1.x cannot read the converted file.

Which OpenCode version is this guide for?

OpenCode 1.18.33 (September 28, 2026), checked on 2026-09-29 against the opencode.ai docs and the 1.18.33 source, plus the 2.0 pre-release docs for the last section. Re-checked on 2026-10-09 against 1.18.35. The model defaults described here come from the 1.18.33 source; check them again after an upgrade.

Which models can OpenCode use through the gateway?

Choose a current catalog model that supports both the endpoint and the features OpenCode uses. Check /models and the model detail page for the exact ID, current rates, and capabilities; a family name such as GPT or Claude is not a compatibility guarantee. A client that lists a model has only read the ID, from its own configuration or from GET /v1/models; verify an actual request too.

Models are listed, but requests fail with 400 or 404. What should I check?

Record the actual request path and error message, then check the exact model ID. A 400 can indicate invalid parameters, unsupported tools, or a model/endpoint mismatch; a 404 can indicate an incorrect path or missing resource, so it does not by itself establish that a model was retired. If the error says must be called via, use the named endpoint or select a model supported on the current endpoint. Do not add or remove /v1 or /chat/completions across all clients indiscriminately.

Does this work from Mainland China?

Yes. The gateway is reachable from Mainland China without a VPN, and the configuration is identical to the global setup.

How do I debug a 401/402/403/429?

Start from what your client received: the status, the error body and the X-Request-ID header. A 401 (missing, unknown, revoked or expired key), a 402 (wallet balance, or the key's maxSpend cap) and a 429 from your key's or account's rate limits are refused before any model is called, so they never appear in Dashboard → Logs — nor does a 400 or 404 for a wrong model ID or endpoint. For 401, check that the full key is sent and still active in Dashboard → API Keys; for 402, top up or raise the key's maxSpend; for a 403, keep the full error body. For 429, back off and retry later; a 429 that does show up in Logs came back from upstream after the gateway's retries. Keep the request_id and follow the error-codes reference.