claude-code-router with an OpenAI-Compatible Backend

In short: Claude Code speaks the Anthropic message format, so the router exists to translate rather than merely to redirect. Two config layouts circulate with different field names, and the router ignores keys it does not recognise. The routing table takes provider and model joined by a comma, and the transformer list is what makes tool calls work on a backend that does not already emit Anthropic-shaped ones.

Claude Code is hard-locked to Anthropic's API. It speaks the Anthropic message format and it will not be talked out of it by a config file. claude-code-router (CCR) sits in the middle and translates: Claude Code talks to a local port, and the router turns each Anthropic-shaped request into an OpenAI-shaped one for whatever backend you point it at.

That protocol gap is the whole reason the tool exists, and it also explains why its config is the least standardised of any tool on this site. Two layouts are in circulation, and the field names differ between them.

You need a key before the code below runs. Create an account, generate a key, and copy the base URL (https://aicomp.ai/v1). Create one free →
Check current rates → Free to sign up · $1 minimum top-up · No prepayment

Which layout your config uses

Both of the following appear in current guides, and they are not interchangeable:

LayoutProvider keyEndpoint fieldWhere it comes from
AProviders (array)api_base_urlthe mainstream @musistudio/claude-code-router line
Bprovider (object)base_urlforks, and some older builds

Layout A is the one that matches the npm package's own documentation. If your config file has a Providers array, you are on A. If it has a provider object with a default key inside it, you are on B, and copying a layout A file into it fails without an explanation.

Read your existing file before writing a new one. If the file does not exist yet, create ~/.claude-code-router/config.json and start from layout A.

Layout A, which is the one to use

{
  "LOG": true,
  "HOST": "127.0.0.1",
  "PORT": 3456,
  "APIKEY": "local-router-token",
  "API_TIMEOUT_MS": "600000",
  "Providers": [
    {
      "name": "gw",
      "api_base_url": "https://gw.example.com/v1",
      "api_key": "sk-your-gateway-key",
      "models": ["claude-sonnet-5", "deepseek-v4-pro", "glm-5.3"],
      "transformer": { "use": [] }
    }
  ],
  "Router": {
    "default": "gw,claude-sonnet-5",
    "background": "gw,glm-5.3",
    "think": "gw,deepseek-v4-pro",
    "longContext": "gw,claude-sonnet-5",
    "longContextThreshold": 60000,
    "webSearch": "gw,claude-sonnet-5"
  }
}

The Router block is a routing table, not a preference list. Claude Code issues several distinct kinds of request and CCR maps each to a model:

Route keyWhat Claude Code is doing
defaultordinary turns
backgroundcheap auxiliary calls, the ones worth a small model
thinkextended-reasoning turns
longContextturns past longContextThreshold tokens
webSearchturns that involve search

The value format is provider,model, a comma, not a colon. That single character is a frequent typo because most other tools use a colon.

Putting a cheap model on background is the one routing change that reliably pays for itself, because those calls are frequent and their answers are rarely read.

The version segment

Here the guidance in the wild conflicts, and the reason is that a gateway can be reached two ways.

What never works is pasting the full operation path (.../v1/chat/completions), because the router appends its own. That produces the doubled path failure, and it surfaces as a 404 alongside a message about the upstream provider rather than about the URL.

For an OpenAI-compatible gateway, use the version root.

Transformers decide whether tools work

Claude Code is an agent. It calls tools constantly, and a backend that returns tool calls in a different shape than Claude Code expects will appear to work for plain questions and fail the moment it tries to read a file.

The transformer.use array is where that is corrected. ["deepseek", "tooluse"] is the common pairing: one entry adapts the provider's response shape and the other normalises tool-call emission.

Leaving it empty is correct only for a backend already emitting Anthropic-shaped tool calls. For everything else, an empty array produces an agent that chats well and cannot act, which reads as a Claude Code bug rather than a config gap.

Launching it

The router is a server, and Claude Code needs two environment variables pointed at it. The ccr code subcommand starts the server if it is not running, exports both, and then launches Claude Code:

npm install -g @musistudio/claude-code-router
ccr code

The two variables it sets are worth knowing for the case where you launch the pieces by hand:

export ANTHROPIC_BASE_URL="http://127.0.0.1:3456"
export ANTHROPIC_API_KEY="placeholder"

The key here is not your gateway key. Claude Code performs a login check and rejects an unset value, so a dummy string satisfies it; the real credential is the api_key inside the provider block, which the router applies upstream. Setting this variable to a real Anthropic key changes nothing about where the traffic goes.

Switch models inside a session with the /model command using the same provider,model form as the routing table.

Why this setup costs less than it looks

Routing Claude Code to a gateway is mostly a cost decision, and the saving comes from two places.

Per-token rate. A gateway's rate for an equivalent model is lower than the first-party one. On an agent that re-sends the whole conversation on every tool call, that difference compounds over a long task rather than over a long message.

Routing. background calls are frequent and cheap to serve with a small model. think turns are rare and worth a strong one. Sending everything to one frontier model pays frontier rates for both.

A day on this page's workload is 300k input and 40k output tokens, or 6M input and 0.8M output across 20 working days. It is the highest input figure of any tool on this site, and it is high for the same reason the site exists: a coding agent reads far more than it writes.

Per 1M input / output tokens and a 20-day month
ModelGateway rate
in / out per 1M tokens
One day20 days
gpt-5.6-terra$1.00 / $6.00$0.54$10.80
claude-sonnet-5$1.00 / $5.00$0.50$10.00
deepseek-v4-pro$0.66 / $1.98$0.28$5.54
qwen3.8-max$1.00 / $3.00$0.42$8.40
glm-5.3$0.70 / $2.2$0.30$5.96

One day = 300k input + 40k output on this page's workload. 20 days = 6M input and 0.8M output. Rates checked 2026-10-14 (gateway rates move with upstream promotions), so verify the current number in your dashboard before committing to a budget.

Across 221 catalogue models that publish both rates, the median output rate is 4.0× the input rate, and 69% of them (152 models) charge at least 4× more for output than input. The other 624 raw catalogue entries are dropped by cleaning, test entries, dated snapshots and non-chat variants, not for a missing rate. This is why an agent workload is decided on the output column, not the input one everyone quotes.

Verifying the route

  1. Open your existing config file first and see whether it uses Providers or provider. Match the layout you find.
  2. Address each provider by the name in the config, because the routing table references providers by that name and a mismatch fails silently at routing time.
  3. Write routing values as provider,model with a comma.
  4. Set the version root in api_base_url, not the operation path.
  5. Fill transformer.use unless your backend already returns Anthropic-shaped tool calls.
  6. Launch with ccr code so the environment variables are set for you.
  7. Check /status inside the session to confirm which model is actually serving.

FAQ

FAQ

Why is my config file ignored?

Two schema layouts are in circulation. One uses a Providers array with api_base_url, the other a provider object with base_url. The router reads the layout it expects and ignores keys it does not recognise, so a mismatched file looks like an empty config.

Why does Claude Code ask me to log in?

It requires ANTHROPIC_API_KEY to be set and does not validate it. ccr code sets a placeholder. The value has no effect on routing; the real key lives in the provider block.

Why do plain questions work but file reads fail?

Tool calling. Claude Code expects Anthropic-shaped tool calls and your backend emits a different shape. Add the appropriate entries to transformer.use, typically the provider transformer plus tooluse.

Does api_base_url need /v1?

For an OpenAI-compatible gateway, yes (the version root, with nothing after it). The router appends the operation path, so a value ending in /chat/completions produces a doubled path.

Is the router key the same as my gateway key?

No. APIKEY in the config is a local token between Claude Code and the router. api_key inside the provider block is the credential the router sends upstream.

Which model should background routing use?

A small, cheap one. Those calls are frequent and their output is rarely read, which makes them the best target for a rate reduction.

Related

Get API access