0055. ai: each credential its own prefix; the gate picks the account
Status: accepted, 2026-10-31
Context
Pools (ADR 0036) say which teams may use which accounts, but inside a pool every key used every account:
a team's pool had one prefix (t-<slug>) on all its credentials, and CLIProxyAPI chose the credential
behind it. Owners want a key to use only account A's quota and never B's while another key uses both, and
a cap per key and account ("this key takes at most 2M tokens a month from A").
CLIProxyAPI v8 can't be told which credential to use from outside: pinning a credential is internal to its
handlers, and a custom selector is replaced whenever routing settings change (ADR 0036). Prefixes are the
one isolation it keeps: with routing.force-model-prefix a prefixed credential answers only prefix/model.
Decision
- Every credential its own prefix: a local account
a-<8 hex of sha256(its file name)> (longer on a
collision), a source the prefix it had (if it was its own, not a team's) or s-<slug of its name>
(credentials). A remote CLIProxyAPI's accounts go with their source's. force-model-prefix stays on:
nothing answers a bare name. Moving a credential between pools changes pool_owners only.
- Pools stay the policy: a pool is the credentials a team owns ('' the instance's); grants and access
are as before.
t-acme/model still names the pool.
- The gate picks: candidates are the credentials of the pools the key's team may use (own, given by
other teams with the grant's models, the instance's), within the key's own list and its person's access,
that serve the model (CLIProxyAPI's
/v1/models every minute), minus those off or parked, and minus those
whose key-and-account or account-wide month is used. Order: pool by pool (own, given, the instance's);
inside a pool the routing policy's tiers (highest first) and weights (random by weight inside a tier);
credentials that just answered 429 or whose window is used up last; a session (as CLIProxyAPI reads it:
a session header, Claude's metadata.user_id, prompt_cache_key, the first messages) stays on the
credential that answered it for the policy's affinity TTL. The gate moves on at 429, 404 and 5xx inside
the pool always, to the next pool only with the team's fallback, at most 4 credentials a model.
- Keys choose accounts:
keys.accounts_mode all (every account the team may use) or listed
(only key_accounts); each key_accounts row may cap the key's tokens and dollars a month on that
account (usage.credential counts them). A member's access may name accounts (member_access.accounts):
their own keys stay within them. A credential may carry an account-wide monthly cap set by its pool's
admins. GET /api/v1/credentials?team= lists what a team's keys may be given: names, labels and
providers only.
- Usage and lists stay bare: usage rows,
X-Xvii-Model and /v1/models show the model without any
prefix, once.
- Rollout without a gap: at start aid gives every credential its prefix one at a time and tells the
gate as soon as CLIProxyAPI serves the new ids; until a pool's credentials serve under their own
prefixes, the gate routes it as before (the pool's prefix, the bare name).
Consequences
- A key can be bound to some accounts and capped on each; a member's access can be too.
- CLIProxyAPI no longer balances inside a pool: each prefix holds one credential and the gate does the
weighted choice, the tiers and session affinity. Its own round-robin, cooldowns and affinity only matter
inside one credential.
- Sticky sessions live in aid's memory: a restart loses them, and conversations land by weight again (the
prompt cache misses once).
- A call names one credential at a time: moving on is the gate's (up to 4 tries), so a pool of many busy
accounts may refuse sooner than CLIProxyAPI did.
- Clients that called
t-acme/model keep working; ones that called a source's own prefix too. A model
under a credential's prefix (a-1f2e3d4c/claude-…) reaches that account alone, if the key may use it.
- On the first start a credential's old id stops answering a moment (well under a second) before the gate
learns its new one; a call in that moment gets CLIProxyAPI's 404 and clients retry.