Skip to main content
kRouter
All posts
How kRouter works

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.

Kodelyth · The team behind kRouter
· Updated
5 min read

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, leaves out any it has already tried for this request or knows to have 2% quota or less left, then scores each one. The three steps below fit together like this:

Flow from the candidate accounts through three steps: a health score from success rate and average request time, multiplied by a quota factor, plus a list-position bonus of 40, 30, 20 or 10 points; the highest total wins
kRouter v0.5.165. Round robin and random ignore the score; P2C compares it for two random accounts.

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 successful request counts for 30%; a failed one leaves it unchanged). 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, and one at 2% or less is not scored at all, because the quota check has already left it out. 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.)

Line chart of the quota factor: 1 at 30% quota left and above, falling in a straight line to 0.5 at 15% and 0.1 at 3%, with accounts at 2% or less dropped before scoring and accounts without quota data at 1
The factor scales health only. The list-position bonus is added afterwards, unscaled.

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 AAccount B
Success rate100%90%
Average request time2,000 ms1,000 ms
Health1000 − 20 = 980900 − 10 = 890
Quota left15%60%
After quota penalty980 × 0.5 = 490890

B gets the request. Without the quota term, A would have won and been driven into its limit.

The table stops before the last step. Adding the list-position bonus, with A at the top of the list, does not change the result:

Bars for accounts A and B: health 980 and 890, after the quota factor 490 and 890, after the list-position bonus 530 and 920, so B gets the request
Bar lengths are to scale. The bonus moves each total by 30 or 40 points.

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, 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, and from v0.5.164 the default option reads Zenith (default). Through v0.5.163 the same option was labelled Fill First; only the label changed, and the value kRouter stores is still fill-first on every version. The setting has run Zenith since v0.5.77. (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.164 or newer for the scoring above

Zenith has been the default since v0.5.77, but the two bugs fixed in v0.5.164 -- the unknown-quota penalty and the reversed list-position bonus -- changed which account won. On anything older, expect the pick described here to come out differently.

Common 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 my Settings page say Fill First instead of Zenith?

You are on v0.5.163 or older. The option was named before Zenith existed and the label lagged the code from v0.5.77 until v0.5.164, which renamed it to Zenith (default). Either way the stored value is the string fill-first, so that is what a settings export or an API response shows, and the behaviour has been Zenith since v0.5.77.

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.

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