> Markdown mirror of https://router.one/blog/goose-vs-opencode-vs-qwen-code for AI assistants and crawlers. Router One is an OpenAI-compatible LLM API gateway.
> Published: 2026-09-15 · Author: Router One Team

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

_Goose, OpenCode, Qwen Code, Aider on one Router One key: what points each at the gateway, the protocol sent, what stays in the tool, and one trace per call._

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](https://router.one/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

| Tool | Install / where it runs | Point it at the gateway | Protocol sent | Families reached | Stays in the tool |
| --- | --- | --- | --- | --- | --- |
| Goose | CLI (Homebrew or download script) and a Desktop app | `OPENAI_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/responses` | Every chat model | Extensions (MCP servers), `GOOSE_MODE` tool approval, sessions, `GOOSE_MAX_TURNS` |
| OpenCode | Terminal 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` map | Chat Completions (`@ai-sdk/openai` would mean Responses) | Every chat model; an optional `@ai-sdk/anthropic` provider puts Claude ids on `/v1/messages` | Tools, `mcp` servers, `permission` rules, agents |
| Qwen Code | Node.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.json` | Chat Completions through the official OpenAI Node SDK (`openai-responses` posts to `/v1/responses`) | Every chat model; Responses natively for GPT-family and DeepSeek ids | Built-in tools, `/mcp`, `/approval-mode`, `/resume`, `/compress` |
| Aider | Python CLI (`aider-install`), run inside your repo | `OPENAI_API_BASE=https://api.router.one/v1`, `OPENAI_API_KEY`, `aider --model openai/<id>` | Chat Completions through LiteLLM | Every chat model | Repo 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.

```bash
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](https://router.one/integrations/goose).

## 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`.

```json
{
  "$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](https://router.one/integrations/opencode); against the vendor CLIs: [OpenCode vs Claude Code vs Codex CLI](https://router.one/blog/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.

```bash
# .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](https://router.one/integrations/qwen-code).

## 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.

```bash
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](https://router.one/integrations/aider).

## 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](https://router.one/facts/api-compatibility.md) has each endpoint's accept and reject lists, and the [error code page](https://router.one/llm-api-error-codes) 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](https://router.one/llm-streaming)). The ledger, field by field: [cost tracking](https://router.one/llm-cost-tracking); the vendor CLIs' two-variable setup: [CLI setup guide](https://router.one/docs/guides/cli-setup), [Claude Code from China](https://router.one/claude-code-china), [Codex CLI from China](https://router.one/codex-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](https://router.one/) and cap each; [/integrations](https://router.one/integrations) lists every guide and [one gateway behind every coding tool](https://router.one/use-cases/ai-coding-tools) 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](https://router.one/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.

## See also

- Canonical page: https://router.one/blog/goose-vs-opencode-vs-qwen-code
- Claude Code China: https://router.one/claude-code-china
- All blog posts: https://router.one/blog
- Models and per-model token rates: https://router.one/models (markdown: https://router.one/models.md)
- Pricing: https://router.one/pricing
- API docs (markdown): https://router.one/docs.md
- Company facts: https://router.one/facts/company.md
