Skip to content
Router One
Back to Blog

Goose vs OpenCode vs Qwen Code vs Aider on One API Key

PublishedByRouter One TeamHow we measure

Four open-source coding agents that live in the terminal, one question: what changes when their model calls go through Router One? The tools stay what they are — Goose runs extensions (MCP servers) and sessions from a CLI or a Desktop app; OpenCode treats model providers as configuration; Qwen Code switches providers and models from /auth and /model; Aider edits files and commits each change to git. What changes is the request underneath: one sk- key, exact model ids from /models, and one trace per model request in Dashboard → Logs with model, tokens, cost, latency, status and request_id. The gateway serves the requests; the tool owns the loop.

The verdicts. Goose: the built-in OpenAI provider with OPENAI_HOST on the host root and OPENAI_BASE_PATH at its default, or a custom-provider JSON file. OpenCode: one provider block in opencode.json on @ai-sdk/openai-compatible, model selected as router-one/<id>. Qwen Code: three OPENAI_* variables in .qwen/.env, or one modelProviders.openai entry per model. Aider: OPENAI_API_BASE, OPENAI_API_KEY and the openai/ prefix rule. All four send Chat Completions, so every chat model in the catalog is reachable from each.

Side by side

ToolInstall / where it runsPoint it at the gatewayProtocol sentFamilies reachedStays in the tool
GooseCLI (Homebrew or download script) and a Desktop appOPENAI_HOST=https://api.router.one, OPENAI_API_KEY, GOOSE_PROVIDER=openai, GOOSE_MODEL=<id>; or a JSON file in ~/.config/goose/custom_providers/Chat Completions (OPENAI_BASE_PATH defaults to v1/chat/completions); gpt-5, gpt-6 and o-series names go to /v1/responsesEvery chat modelExtensions (MCP servers), GOOSE_MODE tool approval, sessions, GOOSE_MAX_TURNS
OpenCodeTerminal TUI, desktop app or IDE extension (npm, Homebrew or install script)provider.router-one in opencode.json: npm @ai-sdk/openai-compatible, options.baseURL on /v1, options.apiKey {env:ROUTER_ONE_API_KEY}, a models mapChat Completions (@ai-sdk/openai would mean Responses)Every chat model; an optional @ai-sdk/anthropic provider puts Claude ids on /v1/messagesTools, mcp servers, permission rules, agents
Qwen CodeNode.js 22+ CLI (npm or Homebrew)OPENAI_API_KEY, OPENAI_BASE_URL=https://api.router.one/v1, OPENAI_MODEL in .qwen/.env; or modelProviders.openai[] in ~/.qwen/settings.jsonChat Completions through the official OpenAI Node SDK (openai-responses posts to /v1/responses)Every chat model; Responses natively for GPT-family and DeepSeek idsBuilt-in tools, /mcp, /approval-mode, /resume, /compress
AiderPython CLI (aider-install), run inside your repoOPENAI_API_BASE=https://api.router.one/v1, OPENAI_API_KEY, aider --model openai/<id>Chat Completions through LiteLLMEvery chat modelRepo map, git auto-commits, /undo, /diff, the --weak-model for commit messages

All four speak Chat Completions, and /v1/chat/completions serves every chat model in the catalog — GPT, Claude, Gemini, Grok and DeepSeek families — so the same anthropic/claude-sonnet-5 or google/gemini-3.8-flash id works in each config below, prefix included. The last column is the boundary: nothing in it runs on the gateway. The one exception is Goose: with the base path at its default, its OpenAI provider sends gpt-5, gpt-6 and o-series names to /v1/responses on its own, and Router One serves that endpoint natively for the currently listed GPT-family ids, so the setting does not change.

Goose: OPENAI_HOST on the host root, GOOSE_MODEL as the exact id

Goose's built-in OpenAI provider connects to "any OpenAI-compatible endpoint", and its docs spell out the split for proxies and gateways: OPENAI_HOST is the root with no trailing path, OPENAI_BASE_PATH is the path the endpoint serves, and its default v1/chat/completions is the right one — so the host root https://api.router.one is the whole setting. GOOSE_PROVIDER and GOOSE_MODEL override the config file for that process, and GOOSE_MODEL goes on the wire unchanged, so it takes the exact catalog id. One rule sits on top of that default: Goose's OpenAI provider builds Responses requests to /v1/responses (with store false) for names that match gpt-5, gpt-6 or the o-series, so openai/gpt-5.5 qualifies, and Chat Completions requests for every other id; a custom OPENAI_BASE_PATH containing responses would force every model onto Responses, where Claude, Gemini and Grok ids get HTTP 400, so leave the path alone.

export OPENAI_HOST=https://api.router.one
export OPENAI_API_KEY=sk-your-router-one-key
export GOOSE_PROVIDER=openai
export GOOSE_MODEL=<exact-model-id-from-/models>

goose session

goose configure persists the provider and model to ~/.config/goose/config.yaml and the key to the system keyring, but the docs note that it does not accept custom model names — a catalog id such as anthropic/claude-sonnet-5 is not in its list — so set GOOSE_MODEL in the environment or edit config.yaml. The alternative is a custom provider: a JSON file in ~/.config/goose/custom_providers/ with engine set to openai, base_url as the full chat-completions URL, api_key_env naming the variable that holds the key, and a models array; it then appears in the provider list, and goose run --provider <name> selects it for one run. Desktop has the same fields under Settings → Models → Configure providers → Add Custom Provider, type "OpenAI Compatible".

The gotchas. A 404 means OPENAI_BASE_PATH does not match the endpoint — leave it at the default. A 401 reading No api key passed in means the key was not loaded: goose does not read provider keys from config.yaml, and the environment variable takes precedence over stored secrets. Goose uses tool calling, so pick a model that supports it. Guide: Goose + Router One.

OpenCode: one provider block, model as router-one/<id>

OpenCode takes custom providers from opencode.json — in the project, or globally at ~/.config/opencode/opencode.json. The block names the npm package that speaks the protocol: the docs pair @ai-sdk/openai-compatible with /v1/chat/completions and @ai-sdk/openai with /v1/responses, so keep the former. options.baseURL carries the /v1 base, options.apiKey reads the key with the {env:VAR} syntax, and every model you want in the picker is a key in models — the exact catalog id — selected as provider-id/model-id.

{
  "$schema": "https://opencode.ai/config.json",
  "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": {
        "<exact-model-id-from-/models>": { "name": "<label shown in the picker>" }
      }
    }
  },
  "model": "router-one/<exact-model-id-from-/models>"
}

The /models command in the TUI lists what you declared; the top-level model field pins one. Everything else stays in OpenCode: MCP servers under mcp (a local command or a remote url), whose tools are "automatically available to the LLM alongside built-in tools", and permission rules that resolve each action — edit, bash, webfetch, task for subagents — to allow, ask or deny, agent rules first.

The gotchas. The provider id in the model string must match the key under provider; a model not declared under models does not exist as far as OpenCode is concerned; and {env:ROUTER_ONE_API_KEY} becomes an empty string, not an error, when the variable is missing from the shell that launches opencode. For the native Messages path on Claude ids, add a second provider on @ai-sdk/anthropic with the same /v1 base URL under a different id — the gateway accepts the x-api-key header that package sends. Guide: OpenCode + Router One; against the vendor CLIs: OpenCode vs Claude Code vs Codex CLI.

Qwen Code: three variables in .qwen/.env, or one modelProviders entry per model

Qwen Code's OpenAI-compatible auth type is openai, and it reads OPENAI_API_KEY, OPENAI_BASE_URL and OPENAI_MODEL (alias QWEN_MODEL). The base URL includes /v1, and requests go out through the official OpenAI Node SDK, so the protocol is Chat Completions. The docs recommend .qwen/.env in the project for these variables — isolated from other tools' variables, and kept out of git. Only the first .env file found is loaded (.qwen/.env, .env, then the same two under ~), and a shell export overrides all of them.

# .qwen/.env
OPENAI_API_KEY=sk-your-router-one-key
OPENAI_BASE_URL=https://api.router.one/v1
OPENAI_MODEL=<exact-model-id-from-/models>

To keep several catalog ids in the /model picker, declare them under modelProviders.openai in ~/.qwen/settings.json — each entry carries id (the model id sent to the API), a display name, envKey (the name of the variable holding the key, not the key itself) and baseUrl — with security.auth.selectedType: "openai" and a model.name matching one of the ids; a running session picks up the edit, and /model <model-id> switches immediately.

The gotchas. openai-responses is a different auth type that posts to /v1/responses over direct HTTP — native on Router One only for the currently listed GPT-family and DeepSeek ids; any other id gets HTTP 400 before a model runs, so keep Claude, Gemini and Grok ids under openai. Two entries with the same id and baseUrl collapse to the first with a warning. Retries from generationConfig.maxRetries are separate requests with separate traces. Approval modes are the tool's: /approval-mode yolo runs shell commands, file writes and network requests without confirmation, and the docs limit it to trusted or disposable environments. Guide: Qwen Code + Router One.

Aider: OPENAI_API_BASE, OPENAI_API_KEY and one openai/ prefix

Aider's documentation for OpenAI-compatible APIs is two variables and one flag: OPENAI_API_BASE on the /v1 endpoint, OPENAI_API_KEY, and aider --model openai/<model-name>. Aider connects through LiteLLM, and the openai/ prefix is the protocol selector — "speak the OpenAI-compatible protocol to OPENAI_API_BASE" — and is consumed, so the catalog id follows it unchanged.

export OPENAI_API_BASE=https://api.router.one/v1
export OPENAI_API_KEY=sk-your-router-one-key

aider --model openai/<exact-model-id-from-/models>

Because exactly one prefix is consumed, a GPT-family id is written twice — openai/openai/gpt-5.5 — and a Claude id once, with its own prefix — openai/anthropic/claude-sonnet-5. --openai-api-base and --openai-api-key do the same job on the command line, and the .env file Aider loads from the git root can hold the variables. What Aider does with the responses stays in Aider: a repo map with each change request (--map-tokens, 1k tokens by default), a git commit for every edit, /undo and /diff.

The gotchas. Expect Unknown context window size and costs, using sane defaults on start — Aider has no metadata for a gateway id, its docs say the warning can be ignored, and its cost figure is therefore not the bill. Commit messages are their own model requests: Aider sends the --weak-model the diffs and chat history, so give that flag the same openai/ form if those requests should use the same key and appear in the same Logs; --no-auto-commits turns the commits off. Guide: Aider + Router One.

What the gateway does and does not do

Router One serves the model requests. For each one it records a trace in Dashboard → Logs — model, input and output tokens, cost, latency, HTTP status, request_id — and applies the caps on the key: maxSpend, plus rateLimit and tokenLimitTpm for runaway loops. An id sent to an endpoint that does not serve it is rejected with HTTP 400 before any model is called; the API compatibility fact sheet has each endpoint's accept and reject lists, and the error code page decodes the rest.

It does not run tools, keep sessions or approve actions. Goose extensions, OpenCode's permission rules and MCP servers, Qwen Code's approval modes, Aider's git commits — all of it runs on your machine; the gateway sees one Chat Completions request per model turn and nothing in between. The trace does not show provider names or routing attempts, and an HTTP 200 alone does not prove a stream finished (streaming). The ledger, field by field: cost tracking; the vendor CLIs' two-variable setup: CLI setup guide, Claude Code from China, Codex CLI from China.

Budgeting a coding session

Every tool above has a loop bound, and none is a spend cap: GOOSE_MAX_TURNS limits turns without user input (1000 by default), OpenCode's doom_loop permission catches repeated identical calls, Qwen Code's maxRetries bounds retries per request. Retries and tool-call loops sit on top: every attempt that reaches the gateway is its own request, trace and charge, and each tool result the agent reads goes back to the model as a new request. Tool-side cost figures — Aider's estimate, anything a TUI shows — are not what you are billed.

The cap that stops spend is on the key: one key per tool, each with maxSpend, and a runaway session stops at the cap with a 402 while the wallet and the other keys are untouched. Reconcile by request_id — filter Logs by time window and exact model, match the count against the session's turns, and keep the request_id of any failed trace. Cancelled streams are neither a silent charge nor a silent zero: on POST /v1/chat/completions and POST /v1/responses a request the client cancels mid-stream — Ctrl-C halfway through a turn — is recorded as HTTP 499 client_cancelled and billed only for the usage the upstream reported.

Create one key per tool at router.one and cap each; /integrations lists every guide and one gateway behind every coding tool is the overview. A tool that still answers with a connection error after the switch has not actually changed endpoint — start with API connection troubleshooting.

FAQ

Which tool reaches the most model families on one key? All four, equally: each sends Chat Completions (Goose alone moves gpt-5, gpt-6 and o-series names to /v1/responses, which the gateway serves natively for GPT-family ids), and /v1/chat/completions serves every chat model in the catalog — GPT, Claude, Gemini, Grok and DeepSeek families. The difference is where the id goes: GOOSE_MODEL, a key in OpenCode's models map, OPENAI_MODEL or a modelProviders id in Qwen Code, after the openai/ prefix in Aider.

Can one key serve all four at once? Yes. The same Router One key works in every configuration on this page, and calls use the same wallet. Separate keys give each tool its own maxSpend cap, so one runaway session cannot spend what another tool needs.

Why does a GPT-family id look different in each tool? Each tool has its own naming rule for the same id on the wire: OpenCode prefixes its provider id (router-one/openai/gpt-5.5), Aider consumes one openai/ segment (openai/openai/gpt-5.5), Goose and Qwen Code send the catalog id as written (openai/gpt-5.5). The model column in Logs shows what actually went out; check that it is the full catalog id.

Does the gateway see the tools, MCP servers or sessions? No. Extensions, MCP servers, permissions, approval modes, sessions and git commits run on your machine. The gateway receives one Chat Completions request per model turn, records the trace, applies the key's caps and returns the response; tool definitions and tool results in the request body are metered as tokens, and nothing in them is executed.

Related canonical pages

This article belongs to the Claude Code China cluster. These pages are the commercial page, setup docs, evidence source, and trust references.

Related reads