Skip to main content
kRouter
All posts
How kRouter works

Inside a kRouter request: Claude Code and Codex traced

One Claude Code request and one Codex request, followed through kRouter: key check, model and combo, account, translation, RTK, the call and fallback.

Kodelyth · The team behind kRouter
· Updated
9 min read

The architecture page lists the stages a request passes through. This post follows two requests through them, one from Claude Code and one from Codex, sent to the same kRouter. They share most of the path and part ways at three points: what their model name means, whether they need translating, and whether the token savers touch them.

Everything here describes kRouter v0.5.166.

The two requests

Claude Code   Anthropic Messages   model: always-on
Codex         OpenAI Responses     model: cx/gpt-6.1-sol
 
Combo "always-on"
  1. cc/claude-sonnet-5-5    your Claude Code subscription
  2. glm/glm-5.3             a GLM API key

Claude Code speaks Anthropic's Messages API; Codex speaks OpenAI's Responses API, because kRouter's CLI Tools page sets wire_api = "responses" in its config.toml. That page gives both tools http://localhost:20128/v1 as their base URL. Codex posts to /v1/responses; Claude Code adds its own /v1/messages, and kRouter serves the doubled path as its Messages endpoint.

Claude Code and Codex requests entering kRouter and passing eight steps, from access check through model ordering to reply, with an error loop from the executor back to account choice
The happy path, top to bottom. The red loop is the error path, drawn in full further down.

1. The door: is this caller allowed?

Before the body is read, a guard decides whether the caller may use /v1. A request from kRouter's own machine passes without a key. That is decided from the connection's peer address, never the Host header, and a request carrying X-Forwarded-For or X-Real-IP, as a reverse proxy adds, counts as remote. Every other caller needs a kRouter key, sent as Authorization: Bearer or x-api-key, or gets a 401. With Docker's default networking your host's requests do not arrive on loopback, so they need a key too.

Two switches remove the loopback exemption: the REQUIRE_API_KEY=true environment variable, and Require API key on the dashboard's Endpoint page, where keys are created. Turn one on if a proxy in front of kRouter forwards without those headers, because its requests would otherwise look local. PII redaction, if you turned it on, also runs here, before routing.

If the Claude Code request's model name carries Claude Code's [1m] suffix, which selects a 1M-token context window, kRouter strips it so the name still resolves. kRouter also answers a few Claude Code housekeeping requests itself when it recognises them, such as a Warmup probe or a conversation-title request.

2. What the model name means

kRouter reads the model field in a fixed order:

  • A name with a slash is provider/model. Everything before the first / names the provider, so cx/gpt-6.1-sol is the Codex provider and model gpt-6.1-sol. No combo lookup happens.
  • A name without one is looked up as a combo first, then as a model alias you defined. If it is neither, kRouter guesses the provider from the name: claude- goes to the Anthropic API-key provider, gemini- to Gemini, and gpt- and most unrecognised names to OpenAI.

So always-on becomes a list of two models, while Codex's request stays one model: it still gets account fallback, but nothing comes after its accounts.

3. Ordering the combo

Before each request kRouter reorders a copy of the combo:

  1. With the round-robin strategy the starting entry rotates; the default fallback strategy leaves it alone.
  2. If the current user turn carries an image, PDF, audio or video, the list is split in two: entries whose model can read it, then the rest. If none can, kRouter puts a model from the capacity adapter's pool in front, as it does for a single model that cannot read it. The pool is on by default for images only, but it starts empty: until you put a model in it the adapter adds nothing.
  3. Entries whose provider reports remaining quota for that exact model go ahead of the rest, most quota first. This runs inside the split step 2 made, not across it: each group is sorted on its own, so an entry that cannot read your attachment never ends up in front of one that can. With no attachment there is no split and quota sorts the whole list.

Claude Code and GLM report quota per time window, not per model, so neither entry has a per-model figure and always-on keeps the order you wrote. The combos docs cover the strategies, including fusion.

4. Picking an account

Each model now needs one of your accounts. kRouter keeps them in memory, re-read from its database every 10 seconds, and drops any that are cooling down for this model, locked as a whole, or reported at 2% or less of their quota for it. From the rest:

  • The conversation's account comes first. kRouter fingerprints a conversation from its model, the start of its system prompt, its tool names and its first user message. If an account answered this conversation in the last 15 minutes and is still available, the request goes back to it, so the provider's prompt cache stays warm. The binding is only recorded after a success.
  • Otherwise the strategy picks. The default, labelled Zenith in Settings since v0.5.164, scores accounts on recent success rate, latency, remaining quota and their place in your list; the Zenith post has the formula. The alternatives are P2C, random and round robin, which switches the conversation binding off and is Antigravity's default.

An OAuth token close to expiry is refreshed before use. With several accounts, a long session normally stays on one account while that account keeps answering.

5. Translation

kRouter knows the client's format from the path and the provider's from its configuration, sometimes per model. One of three things happens.

Native pairs pass through. Claude Code calling the Claude Code or Anthropic provider, and Codex calling the Codex provider, skip the translators, so the body keeps the client's shape. Both requests' first choices take this path. The list is short and exact, because passing through means no translator runs at all: Codex is native to cx/ and to nothing else, and a Codex body sent anywhere else is translated like any other.

Same format, different provider. If always-on falls through to glm/glm-5.3, GLM's endpoint also speaks Messages, so nothing is converted; kRouter only tidies the body: cache markers, empty messages and the order of tool results.

Different formats go through the hub. Everything else is converted to Chat Completions, then to the provider's format. Pointing Codex at openai/gpt-5.4 takes one hop, Responses to Chat Completions, because OpenAI's API wants messages and Codex sends input. Pointing it at cc/claude-sonnet-5-5 would take two, Responses to Chat Completions to Messages, and the reply would make the same two hops back. Messages to Kiro is the one pair with a direct translator. The format translation post shows what each hop changes.

Grid of client formats against provider formats showing whether each pair needs no conversion, one hop or two hops through Chat Completions
Native pairs skip this table. Cursor and Ollama providers follow the Gemini row.

6. Token savers

The savers run on the body about to be sent. RTK is on by default: it shortens tool results of 500 characters or more that it recognises, such as diffs, git status, search results, listings and long logs, and never makes one longer. Caveman, Ponytail, Headroom and PXPIPE are off unless you turn them on. This is the third place our requests differ:

  • Claude Code on cc/: all savers are skipped. Anthropic's prompt cache only hits when the cached part of the prompt is identical, so trimming earlier tool results would turn hits into misses.
  • The same request on glm/, after a fallback: RTK runs on its tool results.
  • Codex on cx/: RTK runs on the tool results in its function_call_output items.

Any client can skip every saver for one request by sending X-9Router-Token-Saver: off.

7. The executor

The executor is the provider-specific code that makes the call:

  • An account slot. For some providers kRouter caps the requests in flight per account: 5 for Claude Code, 4 for Kiro, 3 for Codex, fewer for a few others. A request that cannot get a slot in time (15 seconds for Claude Code and Codex) moves on to another account.
  • Provider fixes. The Codex executor, for example, forces streaming and store: false, removes item ids its backend cannot look up, drops fields the Codex API does not accept, and adds a prompt_cache_key when the request has none.
  • A little of your tool's own request. The executor also receives a copy of a few of the headers your tool sent: User-Agent, anthropic-beta, x-claude-code-session-id and anything starting x-opencode-. OpenCode Go passes your own session id upstream instead of inventing one, Cursor reads the user agent and forces Agent mode when the caller is Claude Code, and Kiro reads anthropic-beta: interleaved-thinking as a request for a thinking budget. It is a fixed list, not the whole set: your kRouter key, your cookies and the proxy's forwarding headers are not in it, and the copy belongs to this request alone.
  • The call itself, through a fetch that honours HTTPS_PROXY and per-account proxies. For this example's providers, an attempt with no response headers after 60 seconds is aborted and retried like a 502.

8. The reply

As soon as the provider answers with a success status, kRouter commits to it: it clears that account's error state, binds the conversation to it and starts streaming. Chunks are converted to the client's format as they arrive; for both our requests they pass straight through. Usage is recorded when the stream ends.

A stream that goes silent for 60 seconds is closed (three minutes for Codex and some other slow providers). The status line went out as 200 long ago, so the failure has to travel inside the stream: your tool gets an event: error in Messages, or a data: frame carrying an error before [DONE] in the OpenAI formats, and a Responses stream that never sent its final event gets a response.failed. No finish reason is invented, so a reply cut short never reads as a finished one.

When something fails

Fallback is three loops, one inside the other, and all of it happens before your tool has received a byte.

Three nested loops: retries on one account, the account loop with its four outcomes including a 2xx that is passed over when a non-final combo entry answers empty, and the combo loop that moves to the next entry
Inner to outer: the same account, the next account, the next combo entry.

The same account. For most providers, a 502, 503 or 504 is retried on the same account up to three times, a few seconds apart, unless the provider says to wait more than a minute. A 401 or 403 on an OAuth account triggers a token refresh and one retry. Codex also retries a reply that arrives with a 200 status but opens by saying the server is overloaded.

The next account. Most other errors put that account on a cooldown, usually for that model only, and the loop picks again. A reset time the provider actually names wins over every guess: the account waits until then, capped at six hours. Without one, a 429 worded as a rate limit whose cached daily quota still reads healthy gets 90 seconds, on the assumption that it is a per-minute window; other 429s and "overloaded" errors get a backoff starting at 2 seconds and doubling to a 5-minute ceiling. A "verify your account" ban or a monthly cap holds the whole account, and since v0.5.164 that includes Anthropic's monthly spend cap. 429 is not one error takes each of those apart, and the combos docs list the rules under When kRouter moves on. Cooldowns reach memory and the database before the next pick, so the retry skips that account and a restart does not clear it. A provider that returns ten 5xx errors in a row is skipped for five minutes.

Some errors leave the loop early. A prompt or request that is too large comes straight back to you, with no cooldown and no fallback, even in a combo; so does a reply saying the provider has no capacity available. A model that does not exist upstream is not tried on your other accounts. And a 4xx that none of the rules claims, meaning not a 401, 402, 403, 404 or 429 but a body or a parameter the provider rejected, is read as being about the request rather than the key: no cooldown is written and no other account is tried, because every account would be told the same thing. A combo still advances to its next entry there, since that is a different model and may accept what this one refused. Cursor is the exception, because its executor reports every failure as a 400: there an unclaimed 4xx keeps the old short cooldown and the account loop carries on.

The next combo entry. When an entry has no account left, the combo moves on. A missing model first tries up to three siblings if it is in kRouter's short list of model families (some Gemini, Claude and GPT-5 models; neither model here). An entry can also be passed over after answering: a non-final entry that returns 200 with nothing usable in it, meaning no text, no tool call, no reasoning and no finish reason saying the model was done, is treated as no answer at all, and the next entry gets the request. Only a complete JSON reply is inspected that way, never a stream, and the last entry's answer is always handed on as it arrived. When every entry has failed, your tool gets the error. Codex's single-model request has no next entry: it gets the last error, and if some of its accounts were skipped because they were cooling down, a Retry-After header saying when the first is free.

Common questions

Does kRouter retry a reply that fails halfway through?

No. Fallback only happens before the reply starts. Once kRouter is streaming to your tool it is committed to that account, and if the stream breaks, trying again is up to your tool. It is told, at least: the break reaches it as an error event inside the stream rather than as a silent stop, so a truncated reply is not mistaken for a complete one.

Can a Codex request end up on Claude?

Yes, if its model name or a combo entry points at a Claude model. It is translated from Responses to Chat Completions to Messages, and the reply back again, so Codex still receives Responses events.

Why did a request fail without trying my other accounts?

A few errors stop the account loop on purpose: a prompt that is too long, which every account would reject, a model the provider says does not exist, a reply saying it has no capacity available, and any other 4xx the rules do not recognise, which is read as a complaint about the request rather than the credential. Rate limits, 401s, 402s, 403s and most server errors move on to the next account.

Kodelyth · The team behind kRouter

Published by Kodelyth, the team that builds kRouter. Posts are drafted with AI assistance and reviewed by a person before they go out. kRouter is free and MIT licensed.

Install kRouter