The Zenith routing engine: how kRouter picks an account
Zenith scores every account on success rate, latency and remaining quota before each request. The exact formula, what it fixes and what it does not.
If you route AI traffic across several accounts on the same provider, something has to decide which account takes the next request. Before v0.5.75, kRouter's answer was mostly "the first one that still works": it used an account until it returned a 429, then moved to the next. That order ignores everything kRouter already knows about the accounts -- which ones have been failing, which are slow, which are nearly out of quota.
Zenith is the scoring step that replaced it. This post describes the formula as the code implements it.
What changed in this post
The first version of this post gave a weighted TTFB-and-quota formula, a squared penalty below 30% quota, an example table, and promised you would never hit a 429. None of that matched the code. The formula below does, and it does not prevent 429s; it makes them less likely and keeps them from cascading.
The score
Before each request, kRouter collects the accounts for the provider that are not locked for the requested model, then scores each one.
1. Health, from 0 to about 1,000. kRouter keeps an in-memory record per account of recent outcomes and request time:
health = success_rate × 1000 − (average_request_ms ÷ 100)The average is an exponentially weighted moving average of total request time (each new request counts for 30%). Success and failure counts decay with a five-minute half-life, so an account that had a bad patch recovers on its own. An account kRouter has never used starts from a neutral record -- five successes and 3,000 ms -- so new accounts get tried rather than starved.
2. The quota penalty. If kRouter tracks quota for the provider and the account has less than 30% left for that model, the score is multiplied by the remaining percentage divided by 30, with a floor of 0.1:
if remaining < 30%: score × max(0.1, remaining ÷ 30)An account at 15% keeps half its score; one at 3% or less keeps a tenth. An account whose provider reports no quota is not penalised at all. (Before v0.5.164 it was scored as if it had none left, which cut every API-key account to a tenth of its score.)
3. List position. The account at the top of your list gets 40 points, the second 30, the third 20 and the fourth 10; the rest get none. On a score of about a thousand, that only settles near-ties. (Before v0.5.164 the bonus ran the other way, 10 points per position down the list, so together with the quota bug above the bottom account often won.)
The highest score wins.
For chat there is one exception. Once an account has answered a conversation, kRouter sends that conversation's next turns back to the same account until it has been idle for 15 minutes, so the provider's prompt cache stays warm. If that account fails, the retry goes through normal Zenith selection. Round robin switches this off, because spreading traffic is the point of choosing it.
A worked example
Two hypothetical accounts on the same provider, with numbers chosen to show the arithmetic:
| Account A | Account B | |
|---|---|---|
| Success rate | 100% | 90% |
| Average request time | 2,000 ms | 1,000 ms |
| Health | 1000 − 20 = 980 | 900 − 10 = 890 |
| Quota left | 15% | 60% |
| After quota penalty | 980 × 0.5 = 490 | 890 |
B gets the request. Without the quota term, A would have won and been driven into its limit.
Notice how little latency moves the score: every 100 ms costs one point out of a thousand, so even a two-second difference is worth 20 points. Success rate and remaining quota do the real work.
Where the quota number comes from
The penalty needs a quota figure, and kRouter only has one for providers whose usage it can read: Claude Code, Antigravity, Kiro, Qoder, GitHub Copilot, Codex, Kimi Coding, Ollama, Gemini CLI, GLM, MiniMax and Vercel AI Gateway (plus the China-region GLM and MiniMax variants). You can see the same figures on the dashboard's Quota Tracker page.
What it does not do
It does not prevent 429s. A per-minute limit can trip on an account with plenty of daily quota left. When that happens, kRouter cools the account down for that model -- starting at 2 seconds and backing off up to 5 minutes, or, when the provider reports a real quota reset time, until then (capped at six hours) -- and retries the request on the next account.
It does not choose between models. Zenith picks an account for one provider. Choosing between models or providers is the job of a combo, which has its own strategies: fallback, round-robin and fusion.
It does not need the database on the hot path. The account list and its locks are kept in memory and re-read from SQLite every 10 seconds, so moving to the next account after a failure does not wait on a disk write. The RAM layer post covers that part.
Turning it on
You already have it. The account strategy lives in Settings under Routing Strategy. In v0.5.163 its default is still labelled Fill First, but since v0.5.77 that setting runs Zenith. (v0.5.75 added Zenith; v0.5.77 wired it into the default path.) The same selection applies to chat, embeddings, image, video, speech, transcription, search and fetch requests.
The other choices are:
- Round Robin, which rotates accounts and stays on each one for 3 calls by default. Antigravity uses it by default, because its per-minute limits punish sending a burst to one account.
- P2C (power of two choices), which picks two accounts at random and keeps the one with the higher Zenith score.
- Random.
npm install -g @sifxprime/krouter@latest
krouter -v # 0.5.77 or newerCommon questions
Does Zenith stop me hitting rate limits?
No. It makes the router prefer healthy accounts with quota left, which makes 429s rarer and keeps them from cascading across every account. When one happens, kRouter cools that account down and retries on another.
Why does the setting say Fill First if it runs Zenith?
The label predates Zenith. Since v0.5.77 the fill-first setting runs Zenith scoring, and as of v0.5.163 the label had not caught up.
Does latency matter much in the score?
Less than you might expect. Every 100 ms of average request time costs one point out of roughly a thousand, so success rate and remaining quota decide most choices.
Do I need to configure anything?
No. Zenith is the default. Moving an account higher in the list nudges it when scores are close, and you can switch to round robin, P2C or random in Settings.
Related
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 kRouterRelated posts
- How kRouter worksOne API for AI video: routing Grok Imagine through kRouterkRouter proxies xAI's Grok Imagine video jobs on the same local endpoint as chat, audio and images. How submit-and-poll works, and what it does not do yet.
- How kRouter worksClaude Cowork with other models: third-party inference setupClaude Desktop can run Cowork through any Messages API gateway. Setting it up with kRouter, what the app assumes about your models, and what you give up.
- How kRouter worksKilo Code codebase indexing: picking an embedding providerWhat Kilo's indexer sends, which embedding model to use for code, why changing it means a full re-index, and how to route it all through one endpoint.
Relevant docs
- Core conceptsProviders, combos, account routing, token savers, MITM mode, quota tracking, the response cache and remote access: the eight ideas behind kRouter.
- The Zenith Routing EngineHow kRouter picks which of your accounts serves a request: Zenith scoring, conversation stickiness, cooldowns, and the round-robin, P2C and random options.
- Token savers: RTK, Caveman, Ponytail, Headroom, PXPIPEFive ways kRouter can shrink a request before it reaches the provider. Only RTK is on by default. What each saver does, what it needs, and its limits.