0036. ai: team pools, grants and people's access
Status: accepted, 2026-10-30
Context
Every account and source in ai was the instance's, managed by superusers, and every key's call rotated
across all of them. Teams want to bring their own subscriptions and API keys without lending them to the
whole instance, give them to chosen teams, and decide which of their people may use AI, which models and
how much. Superusers want to add an account to the instance or to one team, explicitly.
CLIProxyAPI picks the credential itself and knows nothing of teams. A custom selector is replaced by a
built-in one whenever routing settings change, so it can't hold isolation; model prefixes can:
routing.force-model-prefix registers a prefixed credential's models only under prefix/model.
Decision
- Pools: an account or a source is the instance's or a team's (
pool_owners). A team's pool has a
fixed prefix (pools, t-<slug>) written on its credentials and source entries, with
force-model-prefix on: a bare name never reaches a team's account. Superusers choose the pool when
adding and may move an account or a source between pools; a team's admins and owners manage their own
(a personal team has none). A team's sources and proxies are public https addresses unless a superuser
adds them.
- Grants: a pool is given to a team (
pool_grants): the instance's by superusers, a team's by its
admins, with optional models and monthly tokens and dollars (usage rows carry the answering pool).
- Routing: the gate turns a bare model into the pools the key's team may use, in order — its own, other
teams' given to it, the instance's — keeping those that serve the model (CLIProxyAPI's model list, read
every minute). By default only the first is tried ("own only"); a team's
fallback moves on along them
on 429 or 5xx. A prefixed name goes only to that pool and only if the team may use it. /v1/models shows
a key the bare names it may call.
- Access: inside a team its admins give members access (
member_access): models, requests a minute and
a day, tokens and dollars a month. A member's own keys stay within it (an unset limit becomes the
access's) and the gate checks it on every call against the person's usage in the team (usage.user_id).
Admins and owners need none unless given one, nor does a person in their own team. Team keys stay the
developers' as before; the team's limits (ADR 0013) apply on top.
- Nothing breaks on upgrade: until the first seeding nothing is enforced; seeding (once, when id's
directory is first read) gives every existing team the instance's pool and every existing member
unlimited access. Teams and members that come later start with nothing until given.
Consequences
- A team's subscriptions serve only who it chooses; the instance's pool is something superusers give.
- Instance sources that already had a prefix answer bare names only through the gate's routing (it adds
their prefixed ids after the bare one), not CLIProxyAPI's own matching.
- Parking and quota alerts work per pool: a team's last account isn't parked because the instance has
others; its alerts go to the team.
- The gate rewrites the model wherever the call carries it (JSON body, Gemini's path, an image edit's form),
so no call reaches a pool under a name the team wasn't routed to.