Quota CLI for agents - designed with AXI (Agent eXperience Interface).
Agents need quota state before they choose where work can safely run. Vendor dashboards are not shaped for shell automation, and local CLIs expose different windows, resets, and auth sources.
quota-axi reports local Claude, Codex, Cursor, GitHub Copilot, Grok, Kimi, Z.AI, Alibaba, OpenCode Go, Antigravity (agy), Command Code, MiniMax, MiMo, DeepSeek, OpenRouter, ElevenLabs, Devin, Muse, and Higgsfield quota windows in one AXI-shaped call.
It is data only: it never routes, recommends a provider, model, harness, credential, or route, proxies, intercepts, logs in, imports browser cookies, or mints or rotates a credential. Muse's only quota read also issues an API key server-side, which quota-axi discards unread and rate-bounds (Muse provider notes). When the same stored access token is expired, carries a refresh token, and is definitively rejected, quota-axi may delegate renewal to that vendor's own non-interactive CLI command and re-read the result (Delegated credential refresh). Default output has no ordering preference. The opt-in models --sort runway surface applies only its documented deterministic comparator to quota evidence, preserves all evidence and explicit ties, and is not a recommendation. It publishes one derived per-scope comparative selection signal, selection, as data computed from figures it already reports; the consumer, not quota-axi, does any routing or ranking with it.
- Official sources - quota-axi reads local provider auth sources and calls first-party quota, usage, billing, entitlement, local loopback, or read-only credential-liveness endpoints used by the local agents, with read-only CLI probes where applicable. Vendor-command boundaries and the explicit inference exception are documented under Safety guarantees.
- Local first - quota and auth reports run on the machine that holds the credentials; their network calls go to first-party provider endpoints, never a third-party relay.
The separate
updatecommand contacts npm only when the user runs it. - Token efficient - default stdout is compact TOON so agents spend fewer tokens parsing quota state, with
--jsonavailable when a caller needs the normalized model.
Credential-source note: Claude Code, the Cursor CLI (cursor-agent), Copilot CLI, and Muse Code keep live tokens in native secure stores on supported platforms. Linux cursor-agent stores its access token in ~/.config/cursor/auth.json (or the XDG/$CURSOR_CLI_CONFIG override); Copilot CLI uses Windows Credential Manager on Windows and the macOS Keychain on macOS; Muse Code on macOS stores the OAuth token in the login Keychain item its auth.json storage: "keychain" records point to.
quota-axi does not read native secure-store values until the user grants permission, so Claude quota, CLI-only Cursor auth, Copilot CLI auth, or Muse quota can stay stale or appear unavailable when no other usable credential exists. On Linux it reads only the Cursor auth file's accessToken and never its refresh token.
Run quota-axi --allow-keychain-prompt once and approve macOS Keychain access with "Always Allow"; on Windows the same flag permits the Copilot CLI Credential Manager read without an OS prompt.
After a successful read, future non-interactive quota calls reuse the corresponding account-scoped grant without requiring the flag. Claude grants are also profile-scoped; legacy Claude markers created before account-pinned lookup are not reused.
$ npx -y quota-axi --provider claude,codex,cursor,copilot,grok,kimi,zai,agy
bin: ~/.npm/_npx/.../quota-axi
description: Report local agent-provider quota windows for routing-aware agents
generatedAt: "2026-03-15T16:42:00.000Z"
quota[12]{provider,scope,effectivePercentRemaining,spendPriority,runway,confidence,limitedBy,resetsAt}:
claude,all_models,64,-0.3798,projected_exhaustion,established,seven_day,"2026-03-20T17:59:45.600Z"
claude,seven_day_opus,64,0.3218,projected_exhaustion,established,seven_day,"2026-03-20T17:59:45.600Z"
claude,"model:fable",64,-0.0932,projected_exhaustion,established,seven_day,"2026-03-20T17:59:45.600Z"
codex,all_models,47,-0.2383,projected_exhaustion,established,weekly,"2026-03-19T09:54:28.800Z"
codex,"model:gpt-5.1-codex",47,-0.1973,projected_exhaustion,established,weekly,"2026-03-19T09:54:28.800Z"
cursor,all_models,72,1.4067,through_reset,established,included_usage,"2026-04-01T00:00:00.000Z"
grok,all_products,67,0.5778,through_reset,established,week,"2026-04-01T00:00:00.000Z"
kimi,all_models,74,0.2484,through_reset,established,weekly,"2026-03-20T12:17:02.400Z"
zai,all_models,50,-1.0046,projected_exhaustion,established,weekly,"2026-03-20T16:42:00.000Z"
zai,tools,100,unknown,unknown,unknown,mcp_month,"2026-04-01T00:00:00.000Z"
agy,gemini,88,0.4102,through_reset,established,gemini_weekly,"2026-03-20T00:00:00.000Z"
agy,claude_gpt,90,0.3687,through_reset,established,claude_gpt_weekly,"2026-03-21T00:00:00.000Z"
exhaustion[6]{provider,scope,usableRunwaySeconds,projectedExhaustedAt,limitingWindowId}:
claude,all_models,298906,"2026-03-19T03:43:45.600Z",seven_day
claude,seven_day_opus,298906,"2026-03-19T03:43:45.600Z",seven_day
claude,"model:fable",298906,"2026-03-19T03:43:45.600Z",seven_day
codex,all_models,10365,"2026-03-15T19:34:45.428Z",five_hour
codex,"model:gpt-5.1-codex",10365,"2026-03-15T19:34:45.428Z",five_hour
zai,all_models,172800,"2026-03-17T16:42:00.000Z",weekly
attention[2]{provider,scope,kind,detail,remedy}:
copilot,all,unresolved_windows,chat + premium_interactions,none
zai,tools,unmeasurable,"mcp_month blocks runway + spendPriority",none
help[1]:
Run `quota-axi --full` for windows, pace, reserve, and account evidenceDefault TOON is decision-shaped: quota[] carries one fully populated row per measurable scope, and the sparse exhaustion[] and attention[] blocks carry the finite-runway and non-nominal facts. See Default report blocks.
--json emits the normalized model instead. Derivation inputs are demoted to --full; see Output tiers.
$ quota-axi --provider claude --json
{
"generatedAt": "2026-03-15T16:42:00.000Z",
"schemaVersion": 5,
"providers": [
{
"provider": "claude",
"accountKeys": ["default"],
"plan": "pro",
"windows": [
{
"id": "five_hour",
"label": "session",
"kind": "session",
"percentRemaining": 82,
"resetsAt": "2026-03-15T20:10:48.000Z",
"pace": {
"status": "behind",
"reservePercentPoints": 12.4,
"burnMultiple": 0.5921
}
},
{
"id": "seven_day",
"label": "week",
"kind": "weekly",
"percentRemaining": 64,
"resetsAt": "2026-03-20T17:59:45.600Z",
"pace": {
"status": "ahead",
"reservePercentPoints": -8.2,
"burnMultiple": 1.295
}
},
{
"id": "model:fable",
"label": "Fable week",
"kind": "model",
"percentRemaining": 71,
"resetsAt": "2026-03-20T08:25:12.000Z",
"pace": {
"status": "behind",
"reservePercentPoints": 4.5,
"burnMultiple": 0.8657
}
}
],
"state": {
"status": "fresh",
"stale": false
},
"quotaSemantics": {
"status": "known",
"effectiveAvailability": [
{
"scope": "all_models",
"status": "known",
"effectivePercentRemaining": 64,
"boundedBy": [
"five_hour",
"seven_day"
],
"limitingWindowIds": [
"seven_day"
],
"pace": {
"status": "mixed",
"aheadWindowIds": [
"seven_day"
],
"worstReservePercentPoints": -8.2,
"worstReserveWindowId": "seven_day"
},
"runway": {
"status": "projected_exhaustion",
"usableRunwaySeconds": 298906,
"projectedExhaustedAt": "2026-03-19T03:43:45.600Z",
"limitingWindowId": "seven_day",
"projectionConfidence": "established"
},
"selection": {
"status": "known",
"spendPriority": -0.3798
}
},
{
"scope": "model:fable",
"status": "known",
"effectivePercentRemaining": 64,
"boundedBy": [
"five_hour",
"seven_day",
"model:fable"
],
"limitingWindowIds": [
"seven_day"
],
"pace": {
"status": "mixed",
"aheadWindowIds": [
"seven_day"
],
"worstReservePercentPoints": -8.2,
"worstReserveWindowId": "seven_day"
},
"runway": {
"status": "projected_exhaustion",
"usableRunwaySeconds": 298906,
"projectedExhaustedAt": "2026-03-19T03:43:45.600Z",
"limitingWindowId": "seven_day",
"projectionConfidence": "established"
},
"selection": {
"status": "known",
"spendPriority": -0.0932
}
}
]
}
}
]
}$ quota-axi auth
bin: ~/.npm/_npx/.../quota-axi
description: Inspect local quota auth sources without printing secret values
auth[42]{provider,source,path,status,error}:
claude,oauth-file,~/.claude/.credentials.json,available,none
claude,keychain,none,skipped,keychain_prompt_required
codex,auth-json,~/.codex/auth.json,available,none
codex,pi:openai-codex,~/.pi/agent/auth.json,available,none
codex,cli-rpc,~/.local/bin/codex,available,none
cursor,state-vscdb,~/Library/Application Support/Cursor/User/globalStorage/state.vscdb,available,none
cursor,cli-keychain,~/.cursor/cli-config.json,skipped,keychain_prompt_required
copilot,apps-json,~/.config/github-copilot/apps.json,available,none
copilot,copilot-cli:keychain,~/.copilot/config.json,skipped,keychain_prompt_required
copilot,gh:hosts.yml,~/.config/gh/hosts.yml,available,none
grok,auth-json,~/.grok/auth.json,available,none
grok,pi:xai,none,missing,none
kimi,pi:kimi-coding,none,available,none
kimi,kimi-code-cli,none,available,none
zai,pi:zai,~/.pi/agent/auth.json,missing,none
zai,opencode:auth.json,~/.local/share/opencode/auth.json,available,none
agy,loopback,none,available,none
alibaba,bl-cli,none,available,none
opencode-go,opencode:opencode.db,~/.local/share/opencode/opencode.db,available,none
opencode-go,opencode:auth.json,~/.local/share/opencode/auth.json,missing,none
commandcode,pi:commandcode,~/.pi/agent/auth.json,missing,none
commandcode,env:COMMAND_CODE_API_KEY,none,missing,none
commandcode,env:COMMANDCODE_API_KEY,none,missing,none
commandcode,commandcode-cli,~/.commandcode/auth.json,missing,none
commandcode,omp:commandcode,~/.omp/agent/auth.json,missing,none
minimax,env:MINIMAX_API_KEY,none,missing,none
minimax,pi:minimax,~/.pi/agent/auth.json,available,none
minimax,minimax:config.json,~/.mmx/config.json,missing,none
mimo,env:MIMO_API_KEY,none,available,none
mimo,pi:xiaomi,~/.pi/agent/auth.json,missing,none
mimo,pi:xiaomi-token-plan-sgp,~/.pi/agent/auth.json,missing,none
mimo,pi:xiaomi-token-plan-cn,~/.pi/agent/auth.json,missing,none
mimo,pi:xiaomi-token-plan-ams,~/.pi/agent/auth.json,missing,none
deepseek,env:DEEPSEEK_API_KEY,none,missing,none
deepseek,pi:deepseek,~/.pi/agent/auth.json,available,none
openrouter,env:OPENROUTER_API_KEY,none,missing,none
openrouter,pi:openrouter,~/.pi/agent/auth.json,available,none
elevenlabs,env:ELEVENLABS_API_KEY,ELEVENLABS_API_KEY,missing,none
devin,env:WINDSURF_API_KEY,WINDSURF_API_KEY,missing,none
devin,file:credentials.toml,~/.local/share/devin/credentials.toml,missing,none
muse,muse:auth.json,~/.config/muse/auth.json,missing,none
muse,cli-keychain,Keychain ai.meta.dev.credentials,skipped,keychain_prompt_required
muse,env:META_API_KEY,META_API_KEY,missing,none
help[1]:
Run `quota-axi --allow-keychain-prompt auth` to permit native secure-store accessThis auth example's header count and the rows below it come from one capture and are internally consistent (the count equals the rows shown); a live auth run varies by the providers and credential stores actually present on the machine.
quota-axi requires Node.js 22.19 or newer.
Agent skill (recommended)
Install the skill in the Agent Skills format with npx skills:
npx skills add kunchenguid/quota-axi --skill quota-axi -gThe minimal skill points your agent to quota-axi's live CLI guidance through npx -y quota-axi, so nothing needs to be installed ahead of time and installed skill copies do not duplicate changing CLI instructions.
The skills CLI's -g option is intended to install for all projects (e.g. ~/.claude/skills/).
It may report PromptScript does not support global skill installation even when the skill was installed for other agents.
Check the relevant agent's skills directory for quota-axi/SKILL.md to confirm.
Drop -g for a per-project install (.claude/skills/), or use npm install -g quota-axi or npx -y quota-axi for the CLI.
Direct use
npx -y quota-axinpm
npm install -g quota-axiFrom source
git clone https://github.com/kunchenguid/quota-axi.git
cd quota-axi
pnpm install
pnpm run build
pnpm run devThe npm package includes skills/quota-axi/SKILL.md, the same installable skill recommended above.
It is generated from src/skill.ts as a minimal stub: discovery frontmatter, what quota-axi is, when to reach for it, and pointers to the live CLI (npx -y quota-axi, --help, --json / --full). CLI output is the source of truth. Do not restate CLI instructions or claim that quota-axi recommends or ranks. Update the skill with pnpm run build:skill and verify it with pnpm run build:skill -- --check.
┌────────────┐
│ quota-axi │
└─────┬──────┘
▼
┌───────────────┐
│ provider │
│ adapters │
└─────┬─────────┘
▼
┌───────────────┐ ┌──────────────┐
│ local auth or │ ───▶ │ first-party │
│ runtime │ │ APIs/loopback│
└─────┬─────────┘ └──────┬───────┘
▼ ▼
┌───────────────┐ ┌──────────────┐
│ CLI │ ───▶ │ normalized │
│ fallbacks │ │ quota model │
└─────┬─────────┘ └──────┬───────┘
▼ ▼
┌───────────────┐ ┌──────────────┐
│ stale cache │ ◀─── │ TOON/JSON/TUI│
└───────────────┘ └──────────────┘
- Live first - provider HTTP calls and Antigravity's structured print command use 15 second timeouts, Codex JSON-RPC and Antigravity loopback reads use shorter per-call timeouts, and stale cache fallback is per provider.
- Host network policy - Claude, Codex, Copilot, Cursor, Grok, Z.AI, Command Code, MiniMax, DeepSeek, OpenRouter, ElevenLabs, Devin, and Muse outbound HTTP calls honor standard
HTTP_PROXY,HTTPS_PROXY, andNO_PROXYenvironment variables (including lowercase forms). This only follows the user's configured egress path; quota-axi does not expose a proxy service or print proxy URLs. New remote fetches go throughsrc/lib/http.tsrather than globalfetch. The proxy path pairs the installedundicibuild'sProxyAgentwith that same build'sfetch, because Node's global fetch only accepts a dispatcher from the undici build Node bundles. - Opt-in secure-store access - macOS Claude, Cursor CLI, Copilot CLI, and Muse Keychain value reads, plus Windows Copilot Credential Manager reads, are skipped on plain calls until
--allow-keychain-promptsucceeds once for that source, then future quota calls reuse the corresponding grant. - Delegated refresh, never minted - when the same stored access token is expired, carries a refresh token, and is definitively rejected, quota-axi may run that vendor CLI's own smallest non-interactive refresh command and re-read the store the CLI rewrote. quota-axi never performs a refresh-token exchange itself. See Delegated credential refresh.
- Partial success is success - one provider can fail while another returns fresh or stale data, and the process still exits 0. Exit code 1 means every provider failed and the report is still rendered. Exit code 2 is a usage error (
VALIDATION_ERROR).quotais the implicit default command: a bare call or a flag-first call is read asquota, whileauth,update, a single-token--help, and a version flag stay with the SDK. Slow-path routing, help, exit framing, andupdatecome fromaxi-sdk-jsrunAxiCli; machine TOON/JSON stays insrc/render.tsand command bodies insrc/commands.ts. A bare-v/-V/--versionis answered byaxi-sdk-js/fast-pathplus the leafsrc/version.ts(node builtins only), which imports the CLI only on the slow path so the provider graph never loads. Keepsrc/version.tsleaf-clean. - No token equivalence - quota-axi does not claim that one provider percentage equals another provider percentage.
| Command | Description |
|---|---|
quota-axi |
Report supported local quota windows |
auth |
Report local auth-source availability, no values |
models |
Join curated model buckets with local quota evidence |
update |
Upgrade quota-axi to the latest published version |
update --check |
Report current vs. latest without installing |
| Flag | Description |
|---|---|
--provider claude,codex,cursor,copilot,grok,kimi,zai,agy,alibaba,opencode-go,commandcode,minimax,mimo,deepseek,openrouter,elevenlabs,devin,muse,higgsfield |
Scope providers; repeat to union in first-seen order (--provider zai --provider codex equals --provider zai,codex) |
--json |
Emit normalized JSON instead of TOON for quota, auth, or models |
--full |
Include audit and derivation details |
--tui |
Render the live human terminal report instead of TOON (quota only) |
--refresh 30s|5m|1h |
Live --tui refresh interval, default 5m (30s-24h) |
--once |
Render one --tui frame and exit instead of staying live |
--theme light|dark|system |
--tui palette; flag overrides QUOTA_AXI_THEME, default dark |
--all |
Draw not-set-up providers as full --tui cards |
--allow-keychain-prompt |
Permit native secure-store access that could prompt (macOS or Windows) |
--allow-claude-inference |
Spend one bounded native Claude inference to read env-token quota headers |
--no-credential-refresh |
Never run a vendor CLI's own non-interactive credential refresh |
--max-age 0|90s|2m |
Opt in to reusing a fresh reading up to this old instead of asking the vendor; 0 always asks |
--profile-only |
Read one explicitly selected Claude or Codex credential file (quota only) |
--intelligence high|medium|low |
Filter models by editorial intelligence bucket |
--sort runway |
Explicitly sort models by documented usable-runway evidence |
-h, --help |
Print terse AXI help |
-v, -V, --version |
Print version |
Profile-only mode is accepted only by quota, requires exactly one --provider selector, and supports only Claude and Codex. Claude requires an explicit nonblank CLAUDE_CONFIG_DIR; Codex requires an explicit nonblank CODEX_HOME. There is no default-location fallback in this mode, and --allow-keychain-prompt and --allow-claude-inference are rejected rather than ignored.
It reads only $CLAUDE_CONFIG_DIR/.credentials.json or $CODEX_HOME/auth.json. It never reads the macOS Keychain or Pi auth, invokes a CLI RPC or other credential fallback, delegates a refresh, or reads, writes, clears, or persists quota cache data. --full --json retains non-secret account identity, the top-level source, and source attempts for provenance. Tokens and credential-file contents remain excluded. Ordinary output remains redacted.
CLAUDE_CONFIG_DIR=/path/to/claude-profile quota-axi --provider claude --profile-only --full --json
CODEX_HOME=/path/to/codex-profile quota-axi --provider codex --profile-only --full --jsonA burst of back-to-back reads (a dispatcher that takes a fresh reading per decision, a test run) can trip a vendor's usage-endpoint rate limit and leave the provider unmeasured until the limit clears.
Fresh reuse lets such a caller serve the last successful reading instead of asking the vendor again.
It is opt-in: with neither --max-age nor QUOTA_AXI_MAX_AGE, every quota and models read asks the vendor.
--max-age <duration> enables it for one call, and QUOTA_AXI_MAX_AGE=<duration> enables it for a host; the flag wins, so --max-age 0 always asks the vendor.
Both accept 0, bare seconds, or a whole-unit duration up to one hour, and a QUOTA_AXI_MAX_AGE that does not parse fails the read with a validation error instead of being ignored.
Ninety seconds absorbs a burst and moves a five-hour window's elapsed time by half a percent.
A reading is reused only when all of these hold:
- It came from the same credential selection: every environment variable a provider consults to choose a profile, store, credential, CLI, or deployment (
CREDENTIAL_SELECTION_ENVinsrc/lib/reuse-context.ts) has the value it had, plus the same home directory and user. The values are hashed together and never stored. - No local file the reading was derived from has changed. Every file quota-axi opened for that provider, present or absent, is recorded with its identity, size, and modification times (never its contents), so a login that rewrites a credential store, or a Kimi Code configuration that now names another deployment, is read again. A store read through a vendor tool (the macOS Keychain, Cursor's SQLite database, a vendor CLI) is identified by the selection that names it, the same boundary the stale-cache context uses, so an in-place Keychain login switch can be reused for at most
--max-age. Muse is excluded from this path: its macOS credential lives in a Keychain item that is not a traced file, so a login switch cannot be detected here, and--max-agenever serves a Muse reading. The five-minute key-endpoint ledger remains the bound on those vendor calls. Higgsfield is excluded the same way: a CLI login switch does not change a traced file, and the status payload quota-axi already fetches has no stable non-email account discriminator (the email field is ignored and never stored), so--max-agenever serves a Higgsfield reading. - Every lane the provider reported in that reading was a fresh reading with windows, so part of an account-expanded report is never served, and a failed, stale, or window-less lane makes the next read ask the vendor.
- It is younger than
--max-age, and no window has reached its own reportedresetsAt, because a number that has stopped being true is never served (#257).
The cache keeps one slot per provider lane, so two profiles that alternate each read the vendor; each still reuses its own reading between switches.
--full is the audit tier and account identity and source attempts are never cached, so QUOTA_AXI_MAX_AGE does not reach it; it reuses only when --max-age is passed explicitly.
--profile-only never reuses, and readings the cache excludes (Claude native inference and Copilot native secure-store readings) are never reused.
With reuse enabled, a live --tui reuses on its first frame; a scheduled refresh reuses only a reading younger than the refresh interval, so it never repeats its own previous frame and still reads the vendor at any --refresh, unless --max-age is passed explicitly.
Pressing r always reads the vendor, because it is an operator asking for a new reading now.
With reuse enabled, processes that miss the cache together make one vendor read per provider and credential selection. The first creates a lock file under the cache directory, reads the vendor, and caches that provider's reading before releasing it; the others poll the cache and are answered by that reading through the same checks as any reuse. A waiter gives up after 30 seconds and reads the vendor itself, and a lock whose holder has exited, or that is older than two minutes, is taken over, so a crashed or wedged holder never blocks a read. If the holder's reading is not reusable (it failed, is stale, has no windows, or is a reading the cache excludes), nothing is cached and the waiters read the vendor together rather than one at a time, so a burst takes about one vendor round trip beyond the holder's, not one per waiter. Leaders of different providers merge their readings into the one cache file under a short lock of their own, so one leader's write never drops another's reading and leaves its waiters to read the vendor; if that lock is not free within five seconds the write goes ahead unlocked. The lock only saves vendor calls: a lost race costs an extra read, never a wrong one.
A new environment variable a provider reads to choose a credential must be listed in CREDENTIAL_SELECTION_ENV (src/lib/reuse-context.ts) or declared non-selecting in test/reuse-context.test.ts. A new provider file read goes through the src/lib/fs.ts readers or traceInput (src/lib/input-trace.ts). quota-axi's own cache is read untraced, because every write changes it. QUOTA_AXI_SNAPSHOT is the fixture surface on the same path.
A reused reading is a fresh reading, not a stale one: state.status stays fresh, stale stays false, and quota rows, pace, runway, and selection are derived as usual at the report's generatedAt.
It is marked honestly on every surface: --json adds state.reused: true and keeps its state.refreshedAt (the time the vendor answered) in the default tier, default TOON adds a reused attention row naming that time, and the --tui card title says reused 42s.
Reusing never rewrites the cache record, so its age keeps counting from the vendor's answer.
Serving it as fresh is honest because the caller chose the bound: a reused reading is the vendor's own answer at refreshedAt, no older than the --max-age the caller asked for, and never past a window's reset, so a consumer that needs a newer number reads refreshedAt or passes a smaller --max-age.
QUOTA_AXI_SNAPSHOT=<file> makes quota and models answer every provider from that file instead of any credential or vendor, for tests and fixtures of tools that consume quota-axi output.
The file uses the cache file format ({"schemaVersion": 3, "providers": [...]}), so a cache file captured from a real run is a valid fixture.
Each provider's snapshots are served as reused readings with sourcesTried: ["snapshot"], whatever their age; a provider the file does not name reports unavailable with not_in_snapshot, and one with a window past its own reset reports unavailable with snapshot_expired.
A file that is missing, does not parse as that format, or holds a provider record that does not parse fails with VALIDATION_ERROR (exit 2) rather than reporting that provider as not_in_snapshot.
Nothing is written to the cache, and the variable cannot be combined with --profile-only.
QUOTA_AXI_SNAPSHOT=test/fixtures/quota.json quota-axi --jsonquota-axi --tui renders the same redacted report as a live human terminal view instead of TOON: a two-up provider card grid with thin quota bars and a ┃ linear-pace marker whenever pace is known. It is presentation only and is not part of the machine-readable contract.
- On an interactive terminal the report stays up and refreshes every 5 minutes until you press
q(or Ctrl+C). Pressrto refresh immediately; the footer lists both controls.--refreshsets the interval (30s-24h) and--oncerenders a single frame. A non-TTY stdout or stdin (pipes, CI, screenshots) always renders one frame and exits. - Every refresh re-runs the same quota read as a bare
quota-axi, including delegated credential refresh when a stored session has expired in the meantime. Runquota-axi --tui --no-credential-refreshto keep the live report strictly read-only. - Live frames paint on the alternate screen and repaint immediately on terminal resize; quitting restores the screen and prints the final frame so the last report stays in scrollback.
- Height comes from the terminal too. When the report is taller than the terminal, the live view windows it instead of letting the alternate screen (which has no scrollback) push the header and first cards out of reach. The viewport accounts for physical rows after terminal-width wrapping: a full-width visible line can consume its own row without wrapping the optional scroll affordance, which is omitted when it cannot fit. At five or more rows the header stays pinned; when there is room, the last row carries a scroll affordance naming how many report lines are above and below. Below five rows the header scrolls with the other report content, and at one row the scroll affordance is omitted so content still remains visible. Use
j/k, the arrow keys,PgUp/PgDn,Space/b, org/Gto move the window.renderQuotaTuilays cards out for the width and returns every line that takes;scrollFrameinsrc/tui-viewport.tswindows that frame to the terminal, pins the first line, and clips oversized lines. The closing hint is composed outsiderenderQuotaTui, and the live paint has no trailing newline, so a frame that exactly fills the terminal does not scroll itself. Fold a provider only on positive evidence of absence. Declare an uncertain skip on the adapter that owns the source (isUncertainSkip) and a sibling-tool login as incidental (incidentalSources); do not special-case those in the TUI. Thetui.showandtui.accountLabelspreferences stay on the human path insrc/commands.tsandsrc/tui.tsand never change the model or machine output. Scrolling clamps at both ends, survives a refresh, and re-clamps on resize; growing the terminal back past the report's height restores the whole frame and the ordinaryPress r to refresh · q to quitfooter.--once, non-TTY output, the final frame echoed on quit, and the TOON and JSON surfaces are all unaffected by terminal height. - Each live card with a combinable bound leads with the effective-availability rollup (min across bounding windows), colored by headroom: >=50% healthy, 20-50% tight, <20% critical. Per-window rows, including per-model breakouts, are the supporting detail.
- Percentages and bars show what is left by default. To have every provider's headline, window rows, and bars show what has been used instead, set
tui.showtousedin the user config file~/.config/quota-axi/config.json($XDG_CONFIG_HOME/quota-axi/config.jsonwhenXDG_CONFIG_HOMEis set):{ "tui": { "show": "used" } }. That file is the only place the preference is read - there is no environment variable or flag for it - and it is read once when--tuistarts, so restart a live report to pick up a change. Only the exact valuesusedandremainingare recognized; a missing, unreadable, or malformed file, or any other value, keeps the default remaining view. The used view rounds the raw consumed percentage independently of the remaining view, so rounded figures need not sum to 100% (48.5% used / 51.5% remaining displays 49% used / 52% remaining). It labels each headline49% used · week. Its bar fill is consumption, the┃marker moves to the elapsed share of the window, and fill running past the marker means burning ahead of the reset clock. Health colors, runway verdicts, used-share rows, raw credit balances, and?rows read the same in both views. It is a human display preference only: quota-axi never reads the file outside--tui, so the default TOON,--json,--full, and the cache are byte-identical whatever it says. - Account lanes can be named for people. Each lane of a provider that reports more than one account shows its opaque account key under the card title, so several
claudeprofile cards look alike. Settui.accountLabelsin the same user config file to an object from a lane'saccountLocator.path(the Claude config directory, written as an absolute or~/path) or from its account key to a name, and that lane's card title readsclaude · posaminstead ofclaude, with the account key still on the line below:{ "tui": { "accountLabels": { "~/.claude": "personal", "~/.claude-work": "posam" } } }. A claude-swap lane has no locator, so name it by the account key its card shows:{ "tui": { "accountLabels": { "claude-swap:9a1c20d6b88a9bcec745fb70": "team" } } }. Paths are compared after~expansion and symlink resolution, so a trailing slash or a linked directory names the same lane; any other entry must equal the account key exactly, so a relative path names nothing. A label is trimmed and cut to 20 columns; a non-string or blank label, or a malformed setting, is ignored. Only a lane that reports an account key can carry a label, so a provider reporting a single lane keeps its plain title. Folded not-set-up lanes readclaude/posaminstead ofclaude/profile:…. Liketui.show, the file is read once when--tuistarts, and the labels never reach TOON,--json,--full, or the cache. - A window with
shareOfis a used-share, not an independent remaining pool: the row printsN% of <parent>(orshare of <parent>whenpercentUsedis absent) instead of a remaining bar or?, so it cannot be read as missing data or as its own headroom. - The headline is labeled with the window it actually is: the minimum across bounding windows always equals at least one named window, so the label names the
limitingWindowIdswindow (week,session,credits) and changes per provider and over time. Tied limiting windows readcredits + grok build, compacting tocredits +2when the names do not fit; a model- or product-scoped headline appends its scope, and any unresolved limiter falls back to the scope wording (all models). - In the default view, the bar fill is current headroom; the
┃marker sits at the binding window'space.timeRemainingPercent, the fill position of exactly linear burn. The headline marker therefore matches the correspondinglimitingWindowIdssub-bar even when another window supplies the finite-runwayempty inverdict. Fill ending left of the marker means burning faster than the reset clock. The marker is omitted when that window's pace is unknown. - Pace is shown by the bar and marker alone, never as a numeric burn multiple. The runway verdict on the headline reads
on pace ✓forthrough_resetandempty in 7h 21mforprojected_exhaustion. Two-up rows keep both card bottoms aligned by padding the shorter card inside its border. The TUI does not display the per-scope selection signal; that signal remains on the JSON and TOON machine surfaces. Those surfaces also keep thethrough_resetvocabulary, while--full --jsonexposes the completepaceobject. The TUI renders from the complete in-memory model, so--jsontiering never removes anything it draws. - A provider whose window relationships are wholly unknown (Copilot or Antigravity, with every window unresolved) has no combined effective percentage, pace, or runway to show, so its card replaces the headline block with a single
per-window usage · no combined boundline and leads straight into its real per-window rows. Partially understood providers keep the effective-unknown headline. No combined headroom, pace, or runway number is invented. - Providers are grouped by what quota-axi found on this machine, and the header always counts all four groups in that order, for example
4 live · 1 stale · 2 need attention · 10 not set up, including the ones that are empty. Every count is kept even in the narrowest supported terminal: a header that would not fit gives up its timestamp - the time zone first, then the date, then the clock - never a count. A fresh reading is live: an accented●and a full border. A stale cached reading is its own lane, counted apart from live. Its card uses a◐glyph and a dim border, and notes name when the snapshot was last refreshed and the error from the live read that failed, includingfetch failedwhen the failure was the usage fetch. A provider without a reading whose sources found something (a credential that is expired, rejected, or waiting on a Keychain prompt, an installed tool that failed, or a request that failed) needs attention: it stays a dimmed full card right after the live and stale cards, with its remedy, and is excluded from the live total. A provider is not set up only when every credential source it checks was skipped as absent (no file, environment variable, CLI, or running app). A provider that recorded no sources at all still needs attention, so absence is never assumed, and so does one whose skip could not establish absence either way, such as a Copilot CLI account whose Keychain value still awaits--allow-keychain-promptor a configuration that names no account quota-axi can confirm, a Kimi Code configuration quota-axi could not read to the environment it names, or an Antigravity CLI that is installed but timed out. A GitHub CLI login alone, or one Copilot refused, is not evidence of Copilot access and leaves Copilot not set up rather than broken; aghstore that could not be read, or a request through it that failed transiently, keeps Copilot in view. - Providers that are not set up fold into one dim footer line that names each of them and points at
quota-axi authfor where each one is read. Pressain the live report to draw them as dimmed full cards under a○ not set up · Nlabel, and press it again to fold them; while any are folded or expanded the closing line names the key, including when the report scrolls.--allstarts the report expanded and is how a--onceframe shows them. A provider named with--provideris never folded. - Width comes from the terminal, clamped to 80-120 columns; below the two-up width the grid reflows to one column. Color honors
NO_COLOR,TERM=dumb, and non-TTY stdout (the glyph skeleton is kept), re-enables withFORCE_COLOR, and uses truecolor whenCOLORTERMadvertises it, falling back to 256-color then ANSI-16. - The truecolor and 256-color palettes come in a dark (Catppuccin Mocha) and a light (Catppuccin Latte) theme.
--theme light|dark|systempicks one or follows the OS appearance; when the flag is absentQUOTA_AXI_THEME(same values) applies, and the default isdark. Light and system modes are opt-in only.systemreads macOS appearance, Windows default app mode (AppsUseLightTheme), or GNOME’scolor-schemeon Linux once per quota refresh (--oncereads it once). The read has a 500 ms timeout; missing tools, errors, unsupported platforms, and unknown settings (including GNOME’sdefault) fall back todark. A blankQUOTA_AXI_THEMEis the same as unset. An invalid flag or variable value is a usage error, not a silent fallback. The ANSI-16 path is theme-independent and always uses the terminal's own colors. --tuicomposes with--providerscoping,--all, and--full(account identity and source-attempt footers, including for folded providers). It is mutually exclusive with--jsonand only supported by thequotacommand;--allis only supported with--tui.
The reporting unit is the verified underlying subscription, not each credential route that can read it.
A normal invocation reports every Codex ChatGPT subscription it can discover from sibling entries in one Pi auth.json.
Claude reports more than its one selected profile only when QUOTA_AXI_CLAUDE_PROFILES (Claude profiles) or QUOTA_AXI_CLAUDE_SWAP (claude-swap accounts) opts in.
It keeps each account's quota windows, resets, plan, effective availability, runway, and spendPriority separate.
Each account gets its own TUI card, naming its key on an account <key> line under the card title, including accounts whose quota cannot be read.
The default key an expanded report fills in for single-account providers is a schema artefact, so the TUI leaves it out of both the card and the --full footer; TOON and JSON still publish it.
quota-axi --provider codex --json
quota-axi --provider codex --tui --no-credential-refresh
quota-axi --provider codex --full --json # adds vendor identity and source attempts per keyPi's built-in provider id is openai-codex.
pi-codex-accounts stores additional logins under ids such as openai-codex-work in the same file.
quota-axi enrolls those already-present keys; it does not read codex-accounts.json, copy tokens, launch Pi, or change the active account.
Discovery order is the built-in openai-codex entry, then other openai-codex-* keys in lexical order.
Two keys that carry the same stored accountId are the same ChatGPT account and are not reported as extra capacity.
The later key stays a credential fallback until a probe succeeds or every candidate is rejected.
The lane keeps the first key as its accountKey, while source names the key that answered.
See Account keys and compatibility for how consumers join folded keys to a row.
A key whose identity cannot be compared is left as its own lane so the uncertainty stays visible.
When only the built-in Pi entry (or none) is present, Codex keeps its existing single-winner path: native $CODEX_HOME/auth.json, then openai-codex, then the CLI fallback.
That row's accountKeys names the key of the credential that produced it: codex-home for the native login or CLI fallback, openai-codex for the built-in entry. A failed row names the credential it speaks for, and a stale row names the one that produced its cached snapshot. The other key is added when the native login and the built-in entry store the same accountId. A --profile-only row is always codex-home.
When siblings are present and a native $CODEX_HOME/auth.json exists, that login stays first as its own codex-home lane, read from auth.json and then the CLI fallback.
The built-in openai-codex entry whose stored accountId matches the native login is not a separate lane; it stays the native lane's fallback, as it was before.
Without a native auth.json, an installed Codex CLI fallback is probed once as the codex-home lane.
The lane is left out only when the app-server's account/read positively reports no ChatGPT login (account: null or a non-ChatGPT account).
A reading without the optional accountId stays its own lane with no identity.
A failed CLI reading is shown as stale or unavailable only when account/read confirmed a ChatGPT login or a codex-home snapshot is cached; a probe that fails before that evidence adds no lane, and auth still shows the cli-rpc source.
A native login (from auth.json or the CLI) for the same account as a Pi lane is not a second lane.
The account is compared by the vendor accountId a fresh reading reports, or else the stored one; email, tokens, and key names are never used as identity.
That Pi lane keeps its own reading when fresh, and shows the native reading when its own is expired, rejected, or stale and the native one is fresh, or when only the native one has stale cached windows.
A proven sign-out retires only cached Codex snapshots whose stored account identity matches the rejected credential; a native login that coalesces into a Pi lane no longer removes the shared codex-home slot by name, so another account's snapshot cannot be discarded.
--profile-only still reads one native Codex file and never opens Pi auth.
Claude account lanes are opt-in. When QUOTA_AXI_CLAUDE_SWAP is not enabled and QUOTA_AXI_CLAUDE_PROFILES is absent, blank, or lists no entry, Claude reads only the profile this process selects, and the report, its schema, and the cache slot it uses are exactly what they are without these features: one keyless lane, quota schema 5, accountKeys: ["default"]. quota-axi never searches the home directory for other Claude profiles.
QUOTA_AXI_CLAUDE_PROFILES="$HOME/.claude-work:$HOME/.claude-team" quota-axi --provider claude --json
QUOTA_AXI_CLAUDE_PROFILES="$HOME/.claude-work:$HOME/.claude-team" quota-axi --provider claude --full --json # adds accountLocator per lane- The value lists Claude config directories separated by the platform path delimiter (
:on macOS and Linux,;on Windows). Write each one exactly as you would setCLAUDE_CONFIG_DIR: Claude Code hashes the raw path to name a profile's Keychain item, so quota-axi does not trim entries, expand~, or resolve relative paths. Empty entries are ignored, and a relative entry fails the read withVALIDATION_ERROR(exit 2) wheneverclaudeis in scope, like a malformedQUOTA_AXI_MAX_AGE. - Lanes are reported in this order: the process-selected profile first (
CLAUDE_CONFIG_DIR,CLAUDE_SECURESTORAGE_CONFIG_DIR,CLAUDE_CODE_OAUTH_TOKEN; exactly the lane a run without the variable reports), then the default profile (~/.claudewith noCLAUDE_CONFIG_DIR, whose macOS Keychain service is the unsuffixedClaude Code-credentials), then each listed directory in order. Profiles that read the same credential store are reported once, at their first position: on macOS the same config directory and Keychain service, elsewhere the same real directory. - The selected profile and every listed directory always get a lane, because the user named them, including a listed directory that is also the default profile. A listed directory with no credential reports that as its own reading through the same rules as the selected profile (for example
auth_requiredwithcredentials_missing, markednotSetUp: truein JSON and drawn as an attention row in TOON) instead of silently disappearing. The default profile was not named, so it joins only on positive evidence that it holds a credential: its native.credentials.json(off macOS, or with no secure-storage selector), or its exact Keychain item, found from metadata without reading a value or prompting. - Each lane reads only its own store. Only the selected lane consults
CLAUDE_CODE_OAUTH_TOKENand may run the delegatedclaude doctorrefresh; every other lane is read-only and never uses a sibling's token. A lane never takes another listed profile's Keychain item for its own opaque default item. - Keychain consent stays per item: a lane reads its Keychain value only under its own grant marker or
--allow-keychain-prompt. When a sibling lane needs a grant, itsstate.remedyCommandselects exactly that profile, for exampleenv -u QUOTA_AXI_CLAUDE_PROFILES CLAUDE_CONFIG_DIR='/Users/me/.claude-team' quota-axi --provider claude --allow-keychain-prompt(the default profile's command unsetsCLAUDE_CONFIG_DIRinstead), so one approval records that one item's marker. - A sibling lane whose stored session expired with a refresh token present reports
state.reason: credentials_expiredwithstate.remedyCommandnaming that profile's own refresh, for exampleCLAUDE_CONFIG_DIR='/Users/me/.claude-team' claude doctor, plus an agent-directed JSONhelpentry. A secure-storage selector set in the environment is unset in both commands, because it would override the profile. - Each lane's key is
profile:plus 24 hexadecimal characters of a one-way digest of the profile's own store (on macOS the config directory and Keychain service, elsewhere the real config directory). It contains no path, token, or email, does not move whenCLAUDE_CODE_OAUTH_TOKENis exported, and names the lane's cache slot. --fulladdsaccountLocator: { kind: "config-dir", path, keychainService, delegateEligible }to each expanded lane, so a consumer can map a lane to theCLAUDE_CONFIG_DIRthat selects it, and the default lane bykeychainService: "Claude Code-credentials". One exception: on macOS, when the selected lane's Keychain service comes fromCLAUDE_SECURESTORAGE_CONFIG_DIR,pathalone is not a complete selector, because setting only thatCLAUDE_CONFIG_DIRnames a different Keychain item; the locator does not publish the secure-storage selector, only the resultingkeychainService.keychainServiceis present on macOS only, anddelegateEligibleistrueonly on the process-selected lane.--fullTOON adds the same fields toaccounts[]aslocatorKind,locatorPath(home shown as~),keychainService, anddelegateEligiblecolumns whenever any row has a locator, and the--tui --fullfooter shows the lane's path. The locator is never in default output, and a single surviving lane keeps the legacy keyless shape with no locator.- Each lane is cached and served stale under its own profile context, and a definitive sign-out retires only that lane's snapshot; a sibling's stays.
- Two lanes signed in to the same Anthropic account and organization are one subscription and coalesce as described below. Claude identity for this purpose is the verified account id plus the organization the bearer is scoped to, because one login can hold seats in several organizations, each with its own limits; a reading whose organization is not reported stays its own lane.
- The
--tuireport can name each lane by its config directory or account key withtui.accountLabels(Human terminal report); agent output keeps only the account key. --profile-onlyignores the variable and keeps reading exactly one file.
QUOTA_AXI_CLAUDE_SWAP=1 (or true) is an opt-in for people who run the third-party claude-swap to hold several Claude accounts. Instead of reading Claude's own stores, quota-axi then runs claude-swap's non-interactive cswap list --json (fixed argv, resolved on PATH) and reports one lane per account it lists. Only the real executable is launched directly; a Windows .cmd or .bat launcher is reported as claude_swap_unavailable with remedyCommand: pipx install claude-swap, and can instead be replaced by the cswap.exe installed through uv. Unset or any value other than 1 or true, Claude behaves exactly as it does without the variable and cswap never runs.
QUOTA_AXI_CLAUDE_SWAP=1 quota-axi --provider claude --json
QUOTA_AXI_CLAUDE_SWAP=1 quota-axi --provider claude --full --json # adds source attempts and claude-swap's account labels- quota-axi takes no credential or token from claude-swap: it reads only the account labels and usage that command prints, and sends no request of its own for these lanes. The child receives only set values from this environment allowlist:
HOME,PATH,USER,LOGNAME,SHELL,TMPDIR,TMP,TEMP,LANG,LC_ALL, everyLC_*variable,TERM,XDG_CONFIG_HOME,XDG_DATA_HOME, andXDG_CACHE_HOME; Windows additionally allowsSystemRoot,USERPROFILE,APPDATA,LOCALAPPDATA,PATHEXT, andCOMSPEC, matched case-insensitively. Claude, Anthropic, other provider, and quota-axi variables are not inherited. The command is claude-swap's own; it may fetch usage from Anthropic and renew credentials it manages under its own polling policy. For that reason,--no-credential-refreshdoes not runcswapand reports Claude unavailable withclaude_swap_refresh_disabled. quota-axi otherwise waits up to 30 seconds and then walks away without signalling it, like a delegated refresh. Command output goes to a private0600temporary file so a timed-out child keeps a writable destination without keeping quota-axi alive; completed output is read within a 1 MiB bound and then removed. A timed-out child's output is removed when it exits, or when quota-axi exits first. - It reads claude-swap's JSON schema 1 (verified against claude-swap 0.26). Every account row must carry a non-empty
email, stringorganizationUuid(which may be empty), non-emptyusageStatus, andusageas an object or null;organizationNameis optional but must be a string when present, and an object-valuedusagerequires a parseableusageFetchedAt. The complete source is malformed if any listed row violates that schema or produces a repeated lane key.usage.fiveHourandusage.sevenDay, when present, require a finite numericpct;resetsAtmust parse when present, and claude-swap omits it for a window that has not started. Their percentages are claude-swap's copies of the Anthropic usage endpoint'sutilization, which is percent used, so they map through the native Claude normalizer tofive_hourandseven_daywindows with the same trusted durations.resetsAtpasses through, and a window without one is treated like a native untriggered window. Nothing else is read:usage.scoped,active,number,alias,spend, and claude-swap's pace and projection fields are not published. - Each lane's key is
claude-swap:plus 24 hexadecimal characters of a one-way digest of the account's email and organization UUID, so it survives slot renumbering and contains no email. Lanes keep claude-swap's order, which carries no preference. - A lane's
refreshedAtis claude-swap'susageFetchedAt, and a window whose reset has passed since then is dropped. An account whoseusageStatusis notok, or that has no window left, is its ownunavailablelane withstate.error: claude_swap_<usageStatus>(orclaude_swap_usage_unavailable) rather than disappearing; quota-axi does not treat claude-swap's status as a sign-out. --fullreportssource: cliwith aclaude-swapattempt, andaccountwith claude-swap'semailandorganizationNamemarkedidentityStatus: unverified. With no verified account id, lanes never coalesce: two seats are never combined.- These lanes are not cached and never served stale; claude-swap keeps its own last-good usage.
authnever runscswapand continues to inspect Claude's selected native credential. - When
cswapis missing, exits nonzero, outruns the wait, or prints anything but schema 1 whose complete account list passes the schema above with unique lane keys, Claude reports unavailable with aclaude_swap_*reason and never falls back to native Claude. - The variable takes precedence over
QUOTA_AXI_CLAUDE_PROFILES, and--profile-onlyignores it.
After a provider's lanes have been read, collection coalesces readings that share a provider-supported subscription identity.
That identity is account.accountId when it is present and not marked identityStatus: unverified; Claude narrows it to that account in the organization the bearer is scoped to, and a Claude reading without an organization has no comparable identity.
Email, token, credential path, profile label, and runner name do not establish equality or distinctness.
Missing or incomparable identity stays its own lane so the uncertainty stays visible: quota-axi does not guess a merge, and it does not invent extra capacity by summing those lanes.
A successful response that supplies comparable identity missing from the local store is enough to coalesce the resulting cards.
When two readings share an identity, the usable one is published (fresh over stale over a rejected source) and the other is not a second card.
The published row's accountKeys lists every coalesced lane, the winner's key first, so a consumer still binds either credential route to that one row.
Between two stale readings, the one refreshed more recently is published.
Windows are never summed, averaged, or concatenated across those readings.
Every lane keeps its own cache slot: a coalesced lane that also read fresh has its snapshot saved in the same cache write as the published reading, and a lane that served its own stale snapshot keeps it.
So when that route fails on a later run, its snapshot still joins the other lane's reading instead of reappearing as a separate unavailable report, and when every route fails the subscription is still served once.
A cached snapshot also records a SHA-256 hash of the verified subscription identity it was read from (never the id itself), so a stale reading served from one lane still coalesces with a fresh reading of the same subscription from another.
Machine-readable output and the TUI share the same coalesced provider list.
Codex still discovers and folds its own native and Pi lanes as described above; this collection step is the shared contract any multi-account provider goes through.
When a provider expands to multiple accounts, the report uses quota schemaVersion: 6 (auth and models use version 2).
Every provider record then has an accountKey; providers still using one selected account use the literal default.
Every quota-axi output quota row, in schema 5 and schema 6 alike, has accountKeys: the credential keys that one row covers, own accountKey first, then any keys folded into it in the order the provider recorded them. The exported ProviderQuota.accountKeys type remains optional so package consumers can construct reports without it; quota-axi output sets it on every quota row.
A row covering one credential lists just its own key. A consumer that holds a credential key binds the row whose accountKeys contains that key. Matching accountKey alone misses a key that was folded into another row.
A provider without account discovery lists accountKeys: ["default"], the same literal as its schema 6 accountKey filler. A discovering provider that stays on one row, such as two Pi keys for one account with no native login, keeps schema 5 and no accountKey, and its accountKeys starts with that lane's key.
Membership mirrors quota-axi's own grouping exactly: every key the provider grouped into the row, and no other. That grouping relies on stored account identity when no fresh live reading confirms it, so a fold is published as applied even when a later live reading names a different account.
accountKeys is a quota JSON field. Auth still lists each discovered lane on its own, because a fold that depends on the quota reading has not happened there. TOON does not add a column: its flat blocks already name the published lane by accountKey, and the membership list is the JSON account row's join field.
The shared account collector puts each discovered lane's key first; an adapter adds folded keys to its quota reading's ProviderQuota.accountKeys, including keys learned during the read. Codex and, when QUOTA_AXI_CLAUDE_PROFILES or QUOTA_AXI_CLAUDE_SWAP opts in, Claude are the adapters that discover accounts today; other providers report default. The list itself is not cached; Codex reconstructs a stale row's originating key from the cached credential source.
Every flat TOON block adds accountKey immediately after provider, and the quota/exhaustion/attention join becomes provider + accountKey + scope.
Models and model sort ties use provider + accountKey + id.
Models unmatchedWindowIds entries gain the same key, so an unmapped window reads provider/accountKey/scope instead of provider/scope; the key keeps two accounts of one provider from reporting the same unmapped window indistinguishably.
Declaration order remains non-preferential; quotas are never combined across accounts, and never summed across coalesced routes of one subscription.
A Codex Pi lane's key is the auth.json provider id (openai-codex, openai-codex-work); the native Codex lane's key is codex-home.
It is stable across refreshes and discovery order and contains no token, email, or path, and it also names the account's cache slot.
A lone lane keeps the legacy keyless slot, which the single selected account uses too, so the snapshot itself records the stored ChatGPT account id of the credential that produced it (see Cache).
A key the report cannot publish (malformed or repeated) costs only its own lane: the lanes with usable keys still expand, so one unreadable entry never hides the accounts beside it.
The provider normally falls back to its single selected account only when no usable lane remains; a source-owned failure lane, such as enabled claude-swap being unavailable, keeps that source's failure instead.
--full adds the vendor identity the usage endpoint supplied, when any.
If no provider expands, existing fields retain their shape and order, while quota JSON adds accountKeys: quota schema 5, auth/models schema 1, and no account column.
A sole discovered Pi sibling uses that legacy representation.
Expansion follows the lanes discovered rather than the rows published, so when a native login and a Pi sibling turn out to be one account the single surviving row still carries its key and the report stays schema 6.
Consumers must honor the schema version; a legacy keyless row means the single selected lane, and keys must never be inferred from row position.
Account collection is shared in src/providers/accounts.ts.
Adapters can implement ProviderAdapter.discoverAccounts with stable keys and bound quota/auth readers; collection preserves each account's success or failure.
Codex Pi sibling entries are the first discovery implementation on this tree, opt-in Claude profiles the second, and opt-in claude-swap accounts the third. An adapter may also name each lane's local selector as ProviderAccount.locator; the collector publishes it as accountLocator on an expanded lane's row, in --full only.
Other adapters retain their existing source-selection behavior.
The quota command's --json emits schemaVersion: 5, or 6 when a provider expands to multiple accounts.
The package publishes TypeScript declarations from its package root, so consumers can use import type { QuotaAxiResponse, ModelsResponse } from "quota-axi". The adapter contract is ProviderAdapter in and normalized ProviderQuota out: adapters report observed quota data, never rank, mint credentials, or retain raw responses. The narrowly bounded vendor-owned renewal path is documented under Delegated credential refresh.
schemaVersion is command-specific. Additive fields do not bump it. A semantic or incompatible shape change does. The legacy single-account quota report is version 5, auth is version 1, and models is version 1. When account discovery expands a provider, those versions are 6, 2, and 2 respectively. For quota row membership, see Account keys and compatibility.
Default TOON is organized by the reading agent's decision rather than by quota-axi's data structures:
| Block | Rows |
|---|---|
quota[] |
One row per measurable scope: provider, optional accountKey, scope, effectivePercentRemaining, spendPriority, runway, confidence, limitedBy, resetsAt. Every column is populated on every row. limitedBy is the scope's limitingWindowIds, and resetsAt is that binding window's own reset. |
exhaustion[] |
Sparse. One row per scope with a finite exhaustion point: usableRunwaySeconds, projectedExhaustedAt, limitingWindowId. exhaustion[0]: means nothing is projected to run out. |
attention[] |
Sparse. Every non-nominal fact: provider, optional accountKey, scope, kind, detail, remedy. |
A quota[] row whose runway is projected_exhaustion or exhausted_now has exactly one matching exhaustion[] row, joined on provider + scope (plus accountKey in an account-expanded report). A row with through_reset or unknown has none, by definition: through_reset deliberately has no deadline and unknown has none to state.
attention[] kinds:
kind |
scope |
Meaning |
|---|---|---|
stale |
all |
The report is stale diagnostic data. detail names the last refresh, fetch failed plus state.error when a usage fetch failed, and any state.reason; no scope gets a quota[] row. |
auth_required, rate_limited, unavailable, error |
all |
The provider state status. detail is state.error, any state.reason, plus the retry-after instant when state.retryAfter is set. |
no_quota |
all |
The provider reported no measurable scope and no raw credit balance. Emitted when nothing else names it or when needed to preserve state.authStatus. |
credits |
all |
The provider reported a raw credit balance but no measurable scope. detail states that balance verbatim; no percentage or bound is derived from it. |
jobs |
all |
A PHI-safe sample of vendor job outcomes. detail is sampled N · completed N · failed N · other N. JSON also carries structured jobs.{sampled,completed,failed,other}. |
unresolved_windows |
all |
quotaSemantics.unresolvedWindowIds: unfamiliar vendor windows not folded into any bound. |
untrusted_windows |
all |
state.untrustedWindowIds: limits that could not be parsed authoritatively. |
share |
all |
A window is a used-share of another window, not an independent allowance. detail is <id> of <parent> plus · <percentUsed> when that figure is present. It bounds no scope. |
headroom_unknown |
scope | The scope reports no effective percentage for a reason other than a bound conflict. detail names the windows that block it and any finite runway verdict with its limiting window. |
bound_conflict |
scope | A window the scope only inherits reads zero while the scope's own windows still report allowance. detail names both sides. The scope gets no quota[] row and no exhaustion[] row. |
unmeasurable |
scope | Headroom is known but a bound blocks runway, spendPriority, or both. detail names which. |
degraded_source |
all |
A source was broken or unreadable while a sibling source answered: a superseded credential source, or Higgsfield's failed auxiliary account transactions / generate list read. detail is <source> · <error>. One row per source, only on a fresh reading. |
reused |
all |
A fresh reading was reused instead of asking the vendor again. detail is last refreshed <refreshedAt>. See Fresh reuse. |
remedy carries state.remedyCommand when one exists, and situational agent-directed advice is still prepended to help.
Two invariants hold for every report:
- Every requested provider appears at least once, in
quota[]orattention[], except providers with positive evidence of absence, which default TOON omits and counts in a help line;--fulland--providername them. A provider that remains in the report with noquota[]row always states itsstate.authStatus- including a positiveusable- as(auth <status>)in itsattention[]detail. In an account-expanded report a provider is omitted only when every one of its lanes is absent. quota[]rows stay in provider-declaration order, never sorted by any metric. A compact table with aspendPrioritycolumn must never read as a published ranking.
The omission help line sits after any situational advice and before the tier hint:
10 providers not set up are omitted; run `quota-axi --full` to list them
One omitted provider uses the singular (1 provider, is, it). A machine with nothing set up still prints quota[0], exhaustion[0], and attention[0] plus that line, and still exits 1. --full prints the omitted rows and no omission line. Exit codes are otherwise unchanged.
An unknown or stale scope deliberately gets no quota[] row: the absence of a number is the correct encoding of "no number", and the scope is named in attention[] instead.
--full adds; it never subtracts. Default TOON carries the three decision blocks; --full TOON adds the providers[], windows[], scopeAudit[], accounts[], and attempts[] audit blocks. Default --json carries the normalized model with derivation inputs demoted; --full restores them with no renames and no re-nesting - a demoted field is simply absent until --full, in the exact position and under the exact name it has there.
Demoted to --full in --json |
|---|
providers[].label, providers[].source |
state.refreshedAt, state.sourcesTried |
windows[].percentUsed (kept when the window carries shareOf, where it is the only figure), windows[].startsAt, windows[].windowSeconds |
windows[].pace.timeRemainingPercent, elapsedPercent, cycleBasis, cycleSeconds, projectedExhaustedAt, projectionConfidence |
quotaSemantics.description |
effectiveAvailability[].pace.behindWindowIds, onPaceWindowIds |
Account identity (account), account-lane accountLocator, and per-source attempts |
Everything a consumer branches on stays in the default tier: accountKey and accountKeys on an account row; state.status, stale, authStatus, error, reason, remedyCommand, retryAfter, untrustedWindowIds, and degradedSources, plus reused and a reused reading's refreshedAt; window pace.status, reason, reservePercentPoints, burnMultiple, and shareOf together with that share window's percentUsed; quotaSemantics.status and unresolvedWindowIds; every scope's effectivePercentRemaining, boundedBy, limitingWindowIds, boundConflict, runway, selection, and pace aheadWindowIds / unknownWindowIds / worstReservePercentPoints; provider jobs counts; and sparse notSetUp. credits also stays, so a consumer can avoid misreading it as exhaustion.
notSetUp: true is present only on a provider lane that has positive evidence of absence (the same providerPresence classification the human report uses). Every provider stays in providers[] on both default --json and --full; omitting entries would change downstream consumers that look a provider up by id. The field is additive, so schemaVersion stays 5, or 6 when a provider expands to multiple accounts. Default TOON is the only output that omits these providers.
--tui renders from the complete in-memory model, so demotion never changes what the human report draws. Demotion happens only in the renderer (quotaJsonReport and renderQuotaToon in src/render.ts). Presence for an absence marker and for a TOON omission is classified in quotaCommand before redaction.
| Object | Fields |
|---|---|
| Quota report | providers |
| Provider report | provider, optional accountKey, accountKeys (every quota row), windows, quotaSemantics, state, optional plan, optional credits, optional jobs, and optional notSetUp |
Provider report with --full |
Also label, source, optional account identity, optional accountLocator, and per-source attempts |
Account identity (--full) |
Optional email, organization, accountId, and identityStatus |
Account identity and per-source attempts are omitted unless --full is passed.
Claude identityStatus is verified only when Anthropic returns an authoritative account identifier; email and organization are display-only and must not be used for duplicate detection.
| Field | Description |
|---|---|
status |
Provider status |
stale |
Whether the provider report is stale |
sourcesTried |
Sources tried for the provider (--full) |
refreshedAt |
Optional refresh timestamp (--full) |
error |
Optional error |
retryAfter |
Optional retry-after state |
reason |
Optional reason |
remedyCommand |
Optional remedy command |
untrustedWindowIds |
Optional identifiers for limits that could not be parsed authoritatively |
authStatus |
Optional machine-readable local auth usability: usable, expired_refreshable, or unusable. Distinct from quota freshness and from human error prose. |
degradedSources |
Optional { source, error? } list of sources that failed or were superseded while a sibling answered, on a fresh reading; see the degraded_source row. |
reused |
Optional true when this fresh reading was reused from the cache; refreshedAt then stays in default --json |
When stale or unavailable quota is likely fixable by a one-time native secure-store grant, state.reason is keychain_access_required, state.remedyCommand supplies the runnable grant command (Copilot's includes --provider copilot), and for Claude, Cursor, and Muse JSON also includes an agent-directed help entry. That remedy is offered only for keychain_prompt_required; backend-specific consent behavior is described in Provider notes. A denied or timed-out Keychain outcome is reported as keychain_access_denied or keychain_prompt_timeout and stays silent no longer, but it does not advertise --allow-keychain-prompt. For Claude, macOS security exit 44 is cannot-reach (keychain_unreachable), not item-absent: it is not signed-out, and it does not retire the Claude cache. A successful metadata listing covering every keychain in the search list, with no matching or unrecognized Claude item, is the only evidence that establishes a missing Claude Keychain source; an empty, partial, or failed listing proves nothing and falls back to the exact profile-and-account read.
When no Grok credential establishes usability but at least one still has a valid literal refresh token, state.authStatus is expired_refreshable and state.status is unavailable (not auth_required). Stored-expired bearers are first tested with bounded read-only liveness attempts; this classification stands only after the credential's own model catalog definitively rejects it or cannot decide, and an empirically live bearer reports fresh quota or usable instead. The grok remedy (state.reason: credentials_expired, state.remedyCommand: grok, plus an agent-directed JSON help entry telling the user to run the Grok CLI once) is emitted only when the same refreshable CLI candidate is rejected by both consumer quota and the official Grok Build model catalog. It survives the delegated grok models refresh, so it names the case where that delegate could not run or did not recover the session (see Delegated credential refresh). A transient failure does not trigger the remedy. If Grok CLI OIDC is refreshable and Pi is not usable, state.error is Grok access token expired. If only Pi xai OAuth is refreshable, state.error is Pi xAI access token expired and no Grok CLI remedy is emitted because Grok cannot refresh Pi-owned credentials. A Kimi soft expiry uses the same credentials_expired reason; its pi or kimi remedy is described in the Kimi provider notes. Default JSON exposes authStatus; for a provider shown in compact TOON without a quota[] row, a defined auth status appears as (auth <status>) in attention[]. Providers omitted because they are not set up follow Default report blocks. Source-appropriate advice is included only when a remedy exists. Full output shows the attempts: model_auth_probe_live means the credential's official model catalog accepted it but exposed no numeric quota. attempts[].error: credentials_expired marks a stored-expired credential that was not attempted.
True Grok sign-out or definitive remote rejection uses state.authStatus: unusable with state.status: auth_required and state.error: Grok sign-in required (no credentials_expired reason). For official Grok Build OIDC and Pi OAuth, a rejection from the consumer Usage-page operation alone is not that verdict because those access tokens can have a different audience; the matching first-party model catalog must reject the same bearer too. authStatus: unusable by itself only means that no source established usability; for example, a Pi credential-resolution failure instead has state.status: error. Callers must branch on authStatus, status, and reason, not on human error prose alone, and must not treat expired_refreshable as logged out.
When Grok Build or Pi xai OAuth passes its model-catalog probe after consumer-quota rejection, or a local Pi xai API key establishes model-only usability, and no sibling supplies consumer quota, state.authStatus is usable, windows stay empty, and state.error is Grok model access available; quota unavailable rather than sign-in required. Only transient consumer-quota failures and expired_refreshable soft expiry remain eligible for same-source stale fallback; a definitive sign-out (auth_required) retires the cached snapshot and reads the same with or without a cache. Model-only success does not inherit old quota windows. This is authenticated-but-unmeasurable, not a percentage or evidence of unlimited capacity.
A provider with more than one credential source consults them in its fixed declared order and reports the one that works, so a broken store never speaks for a provider whose sibling store still answers. A store's own expiry field is advisory within that source, never a verdict or a reason to reorder sources: a stored-expired credential is still probed in its source's declared position because only the provider's own endpoint can establish that a credential is dead. The superseded source is not discarded: state.degradedSources names it on the fresh reading, and default TOON emits a degraded_source attention row for it. Pi auth readers reserve missing for an absent file or provider property; malformed stores and present non-object or structurally invalid entries are invalid. Handover is limited to credential problems - a transport, decoding, or server failure is about the request rather than the credential, so it is reported as-is instead of being retried on a second credential. When no source works the provider still reports its auth problem, and a source that only failed transiently outranks a definitive rejection so a rejected credential is never reported as a sign-out while a sibling's outage is unresolved. A present-but-unusable source is a failed credential rather than an absence: a set environment variable that is not a usable literal secret, or a Pi entry of the wrong shape, names a credential that exists, so it outranks an earlier plain absence in state.error and an unavailable diagnostic never names it. preferCredentialFailure in src/providers/env-pi-credential.ts owns that order for the env-plus-Pi adapters, and MiniMax and Z.AI apply the same rule in their own pipelines. New or migrated adapters route multi-credential selection through src/providers/credential-selection.ts instead of skipping a stored-expired credential. Codex, Kimi, Copilot, and Devin call it once per source so each provider's ownership-stability order stays authoritative. An expired resolution carries the stored token for the probe only; it is never logged or rendered. Pi entries are classified through classifyPiAuthEntry in src/lib/pi-auth-store.ts: only an absent file or provider property is missing, and a present non-object entry is invalid. Literal secrets for a new adapter go through usableLiteralSecret in src/lib/secret.ts. Claude's env source has distinct handover and failure-priority rules; see Claude provider notes.
Adding a credential source:
- Enumerate every source in an ownership-stability order declared by a named constant. Keep
absent,structurally_invalid,unsupported,read_error, andresolved(stored-valid or stored-expired) distinct, and derivecredentialPresentonce from that resolution. - Separate local resolution from the bounded read-only liveness probe. Enroll stored-expired credentials instead of skipping them, never infer liveness from presence, and classify probe outcomes as
usable,live_no_quota,definitively_rejected, ortransient. Only a first-party 401/403 is an auth verdict. - Hand over only after definitive credential failure. An absent source is never degraded. A present-but-broken source superseded by a working sibling is degraded on fresh readings only. Inspect refresh-token presence only, and never read or exchange its value, except the Pi usability boolean described under Safety guarantees.
- Add adapter regressions for primary healthy, stored-expired plus live sibling, structurally invalid present, absent source, all rejected, refreshable expired, and transient stops handover. Extend the cross-provider invariant table in
test/credential-contract.test.ts. Env-plus-Pi adapters route throughsrc/providers/env-pi-credential.ts.
Claude credential failures without a usable access token preserve the precise credentials_missing or credentials_invalid error. A usage response with HTTP 401 reports Claude sign-in required only when it definitively establishes account authentication failure; the Claude provider notes own the withheld-Keychain exception and cache effects. HTTP 403 is not sufficient authentication evidence because network policy and WAF denials use it too; it stays unavailable or stale rather than claiming sign-out.
| Field set | Fields |
|---|---|
| Required | id, label, kind |
| Optional | Percentages, shareOf, startsAt, reset fields, windowSeconds, credit-spend fields, and derived pace |
Credit-spend fields include spentUsd and limitUsd for USD spend windows, and limitCredits, usedCredits, remainingCredits, and creditUnit for non-USD credit caps. The latter four appear only on the Codex spend_control window: limitCredits is the workspace-imposed cap in vendor credits, usedCredits is the amount consumed, remainingCredits is the amount left, and creditUnit is the vendor-supplied unit name (e.g. credit).
Do not interpret a model window's percentage in isolation. quotaSemantics.effectiveAvailability reports the effective percentage for each understood scope, the complete boundedBy window set used to compute it, the currently limiting window IDs, an effective runway aggregate, and a per-scope selection signal. all_models applies to any model without a more specific scope; whether a matching model:* scope also includes account bounds is provider-specific. Grok uses the analogous all_products and product:* scopes.
A model-specific scope names the model window or the shared model prefix when multiple period windows describe one Codex or MiniMax model.
An inherited bound is only ever as good as the evidence that the vendor enforces it. When a scope's own windows all still report allowance while a window the scope merely inherits from a broader scope reports zero remaining, the two readings contradict each other, and quota-axi publishes that contradiction as effectiveAvailability[].boundConflict instead of resolving it. The scope's status stays unknown with no effectivePercentRemaining, and its runway and selection are unknown with both sides of the conflict named in unmeasurableWindowIds. This is a disclosure of uncertainty, not a claim that the scope is available - and it is deliberately not the opposite assertion either, because one live call proves the inherited bound was not enforced at that moment, not that it never binds. A zero on one of the scope's own windows is not a conflict: that is the scope's own meter reporting exhaustion, and it stands as exhausted_now, whether or not any bound reports a reset (see Effective usable runway).
Codex is the provider this applies to today. The vendor reports a named model limit under additional_rate_limits / rateLimitsByLimitId - a separately metered budget alongside the base limit - and a base window at zero has been observed alongside that model's own windows visibly drawing down under successful calls. Claude, Grok, and Alibaba model scopes keep their established account bounds unchanged.
quotaSemantics.status is known only when quota-axi understands the relationships needed for the reported scopes. A non-definitive availability entry omits effectivePercentRemaining. Unfamiliar vendor windows produce partial or unknown semantics and are named in unresolvedWindowIds; an empty provider report is unknown without inventing an unresolved window.
Cursor's IDE windows (included_usage, auto_usage, api_usage, and optional spend_limit) all draw on the same plan billing cycle, so quota-axi treats them as jointly bounding and reports an all_models effective remaining equal to the lowest of them. That is the conservative reading: it never overstates headroom. Grok Bot weekly usage is a separate Cursor-account meter reported as its own grok_bot scope, so it never lowers IDE headroom and IDE windows never mask Grok Bot exhaustion. An unfamiliar Cursor window is not folded into either bound and does not create a bound of its own - it stays named in unresolvedWindowIds and turns the provider's semantics partial while the recognized-window bounds remain. GitHub Copilot's window relationships are still unknown, so it reports no effective remaining.
Z.AI's five_hour and weekly usage windows jointly bound model usage and are reported as one all_models scope, while the mcp_month tool window is a separate resource reported as its own tools scope; a tool window near exhaustion therefore never lowers model headroom, and model windows never mask tool exhaustion. An unfamiliar or untrusted Z.AI window is not folded into either bound: it stays named in unresolvedWindowIds, turns the provider's semantics partial, and leaves both scopes non-definitive because it could add a bound to either.
Kimi's weekly, five-hour, and monthly-total windows jointly bound model usage at all_models scope.
The /usages payload gives month_total a reset time but no duration.
Kimi documents the cycle itself: Kimi Code shares the Kimi membership's monthly total quota and is frozen at it "until the monthly quota resets" (Kimi Code Membership Benefits), and membership credits "refresh monthly according to your subscription cycle, not by calendar month", for monthly and annual memberships alike (Kimi Help Center: Membership Credit Updates and Usage Rules).
So month_total gets a startsAt one calendar month before its reported reset, with the day of month clamped into a shorter month (a 31 March reset starts on 28 or 29 February), which makes its pace, and with it the all_models runway and spendPriority, measurable.
A month_total with no reported reset gets no start, so its pace stays missing_cycle; month_code is a share and gets no cycle either.
The monthly code window is the code-typed share of that monthly total as the vendor serves it, not a cap of its own, so it is reported as a used share with no percentRemaining and bounds no scope: monthly headroom for code work is month_total's.
That window carries shareOf: month_total so default TOON can name it as an attention[] share row and --tui can print the used percent of the parent instead of a remaining bar or ?. shareOf is the single share rule for interpretation, TOON, TUI, and --json: keep percentUsed because it is that window's only figure, never derive a remaining from a share, and never special-case a window id.
share is an additive attention kind; schemaVersion is unchanged.
An unfamiliar legacy limit, a declared map entry that carries no usable ratio, and an unparsed principal usage next to a valid map all stay named in unresolvedWindowIds - as limit:<index>, usages:<key>, or usage - and turn the provider's semantics partial.
When one answer carries both the usages map and the principal usage with limits[], a window that both shapes report takes the more-used figure, so all_models is bounded by the week that binds.
An unrecognized usages key is not reported as a window.
Alibaba's account weekly window is reported at all_models scope, while each model:* limit is kept only at its named model scope; a model limit never becomes an account-wide bound. OpenCode Go's rolling, weekly, and monthly windows are stacked plan caps ($12 per rolling 5 hours, $30 per week, $60 per month) that jointly bound Go-plan usage at all_models scope, so effective remaining is the minimum across them. A zeroed plan window blocks Go-plan requests, but the vendor's free-model fallback or an opted-in Zen balance may still serve past it, which the usage endpoint does not report. quota-axi reports no Zen balance, so that zeroed window still reads plan-exhausted and names the fallback only in prose. Only a payload-supplied 18,000-second rolling duration promotes the window to five_hour; a plan-declared duration stays rolling. Cursor, OpenCode Go, ElevenLabs, and Kimi renewal dates all step back through calendarMonthsBefore in src/lib/time.ts. Command Code's five_hour and weekly windows jointly pace included monthly credits and are reported as one included_credits scope only when both windows are present and trusted; extra pay-as-you-go credits can bypass those windows, so they are never an unconditional all_models bound. Devin's daily and weekly windows bound included_quota the same way: paid extra usage continues past them, the daily window reuses the session kind, and only an explicit hideDailyQuota: true omits the daily cap. A missing flag leaves the bound unresolved. limited:false is credit-only: leftover five-hour or weekly fields are omitted rather than treated as a bound. An unfamiliar or untrusted Command Code limit, including a non-empty organization spend-limit list whose schema is not independently established, stays named in unresolvedWindowIds, turns the provider's semantics partial, and withholds the included_credits scalar. MiniMax model-remains windows are kept at their named model scopes and never become an account-wide bound. MiMo has no quota windows or effective model scope until a provider-owned quota source exists. DeepSeek and OpenRouter expose balances or key caps as raw credit evidence; quota-axi does not infer model headroom or effective remaining from those amounts. ElevenLabs' single characters window is the subscription plan's included character allowance and is reported as an included_characters scope, never all_models and never a model scope: plans whose can_extend_character_limit is set bill usage past that allowance, so a zeroed window means the included allowance is spent rather than that requests are refused. Muse's five_hour and weekly subscription windows jointly bound every model at all_models, so effective remaining is the minimum across them. If either expected window is absent while the other remains - including a five-hour reset that has already passed, which is dropped rather than published live - that missing bound stays named in unresolvedWindowIds and the remaining window does not publish a definitive all_models percentage. A window of any other length, or an entry that carried no usable percentage, is likewise unresolved and turns the provider's semantics partial rather than being folded in. An omitted or null subs_usage on an active subscription is an established empty reading (plan, no windows), not an untrusted field. An inactive subscription reports no window. Higgsfield remaining credits are a generation-credit allowance at included_credits only when the first account transactions page shows a subscription-credits grant as its only positive-credit entry and the balance still reconciles with that grant plus the on-page entries newer than it (fractional credits included, within the vendor's 0.01-credit rounding precision); otherwise the raw credits.remaining balance is reported with no percentage. The vendor status payload has no reset, so no cycle is assumed: pace stays unknown, and runway reads exhausted_now at zero balance and unknown otherwise, naming credits in unmeasurableWindowIds. Job outcomes from generate list are published as counts only (jobs.sampled / completed / failed / other).
Antigravity groups its Gemini windows (gemini_5h, gemini_weekly) and its Claude/GPT windows (claude_gpt_5h, claude_gpt_weekly) into two independent scopes, gemini and claude_gpt. Within each group the 5-hour and weekly windows jointly bound that group, so its effective remaining is the minimum across its named windows, and neither group's usage lowers the other's headroom. Gemini 4 Argon is its own quota on paid Pro plans, so its buckets are reported under their own gemini_argon_* ids (for example gemini_argon_weekly) and are not folded into the gemini bound. An unfamiliar Antigravity window - including a recognized group's unrecognized bucket such as gemini_unknown or an Argon bucket - is not folded into either bound: it stays named in unresolvedWindowIds and turns the provider's semantics partial, while a reading that exposes only model-config model:* windows resolves no group and stays unknown. Buckets identified as 5-hour or weekly by their vendor window, bucket ID, or display name receive fixed windowSeconds (18000 or 604800). With usable usage and vendor resetsAt evidence, these support cycle-average pace, runway, and selection; unknown-kind and model-config model:* windows receive no cycle length. A used window keeps a fixed reset from its first request; an untouched one reports a reset one full window ahead, which reads as zero elapsed time and zero usage. No history-based burn rate is reported; see Pace signals for the calculation.
For every stale provider report, raw windows remain available for diagnostics but effective availability is always unknown and omits effectivePercentRemaining and limitingWindowIds. Window pace is unknown with reason stale, and each effective pace summary, effective runway, and selection is also unknown with its unmeasurable bounds named. Routing agents must not treat a stale raw percentage as current headroom.
Each window may include a derived pace object that compares cumulative usage to elapsed cycle time using the response generatedAt clock:
timeRemainingPercent = 100 * (resetsAt - generatedAt) / cycleDuration
reservePercentPoints = percentRemaining - timeRemainingPercent
reservePercentPoints |
Meaning |
|---|---|
| Negative | Usage is ahead of the reset clock (burning faster than linear); conserve |
| Positive | Usage is behind the reset clock |
| Within ±1.0 | on_pace deadband for API rounding noise |
| Pace field | Meaning |
|---|---|
status |
ahead, on_pace, behind, or unknown |
reason |
Why pace is unknown (stale, missing_usage, missing_cycle, invalid_cycle, future_cycle_start, expired_reset, unsupported_period) |
timeRemainingPercent / elapsedPercent |
Cycle progress from generatedAt |
reservePercentPoints |
Signed residual capacity vs the linear clock |
burnMultiple |
percentUsed / elapsedPercent when elapsed > 0 |
projectedExhaustedAt |
Linear cycle-average exhaustion timestamp when defined |
projectionConfidence |
early when elapsed < 10% of the cycle; otherwise established |
cycleBasis |
starts_at_resets_at when both boundaries are trusted; otherwise window_seconds with resetsAt |
cycleSeconds |
Trusted cycle duration used for the math |
Pace is calculated only from trusted cycle evidence:
- Prefer trusted
startsAt+resetsAtpairs (Grok's provider-reported current period; Cursor's monthly billing cycle, whose start comes from the payload's cycle start or the previous renewal date). - Otherwise use provider-owned
windowSecondswithresetsAt(Codex durations; Claude fixed 5h/7d; Kimi, Z.AI, and Antigravity fixed 5h/weekly). - Do not infer monthly, rolling, or unlabeled periods.
Every projection quota-axi publishes is cycle-average. There is deliberately no projectionBasis field: its absence means cycle_average, and a future non-cycle-average basis would name itself.
Default TOON keeps token cost low: quota[] puts spendPriority immediately after effective headroom and carries the runway verdict, its confidence, and the binding window's reset, while per-window rows and raw numeric reserve live in --full. Default --json keeps pace.status, reason, reservePercentPoints, and burnMultiple, and demotes the cycle-progress inputs those are derived from. Pace, runway, and selection are recomputed on every report from generatedAt and are not written to the quota cache.
Each effectiveAvailability entry also carries a compact pace summary over every bounding window for that scope (not only the current lowest-remaining limiter): per-status window lists, including aheadWindowIds and unknownWindowIds, plus worstReservePercentPoints / worstReserveWindowId (most negative signed reserve among known-pace windows). Different windows keep their own reset horizons; quota-axi does not invent one synthetic reset for a scope. This is factual inspectable data, never a provider/model routing recommendation.
pace.worstReservePercentPoints stays a single-window diagnostic and is deliberately not a scope-level comparative signal. The published per-scope comparative signal is selection, which aggregates every bounding window instead of reporting one extreme.
effectiveAvailability[].runway is an optional, additive field derived from every authoritative boundedBy window using the report's single generatedAt clock. It is completion-risk evidence, not a score or recommendation.
runway.status |
Meaning |
|---|---|
exhausted_now |
A bounding window reports zero remaining now. usableRunwaySeconds is 0; limitingWindowId names that bound. Not reported when that zero is only an inherited bound the scope's own live windows contradict - see boundConflict. A vendor-reported zero stands even when no bound reports a resetsAt; the resetless rule below suppresses only reset-implying verdicts. |
projected_exhaustion |
Every bound is measurable and one or more cycle-average projections exhaust before their own resets. The earliest one supplies usableRunwaySeconds, projectedExhaustedAt, limitingWindowId, and projectionConfidence. |
through_reset |
Every measurable bound reaches its own current-cycle reset before projected exhaustion. There is deliberately no synthetic finite deadline or combined reset timestamp. |
unknown |
A stale, missing, malformed, or otherwise unmeasurable authoritative bound prevents a sound aggregate conclusion, as does a disclosed boundConflict. unmeasurableWindowIds names the blockers. |
In default TOON the finite-runway detail moves to exhaustion[]; runway and projectionConfidence stay as the runway and confidence columns of the scope's quota[] row, and unmeasurableWindowIds becomes an attention[] row naming the blocked signals.
usableRunwaySeconds is nonnegative and is present only for finite results. projectionConfidence is early or established. Zero observed usage with a valid current cycle proves through_reset under that same cycle-average basis. Named model or product windows are additional bounds only for their applicable scopes, so they can become the effective limiting window without changing other scopes.
A bounding window with no resetsAt at all has not been triggered yet (e.g. a Claude five_hour window before its first request this window) rather than being a data gap. When that untriggered window also reports zero usage (100% remaining, 0% used), it is treated as fully available rather than a data gap, so while any bounding window of the scope reports a reset it stays excluded from unmeasurableWindowIds and never forces runway.status: unknown by itself; the report's other bounding windows still determine the aggregate. When no bounding window reports a resetsAt at all, the scope has no current cycle to derive a reset-implying verdict from, so runway.status is unknown instead of through_reset or projected_exhaustion, every bound is named in unmeasurableWindowIds, and only effectivePercentRemaining still publishes. A bound reporting zero remaining still makes the scope exhausted_now, because that verdict needs no cycle. The same runway treatment applies to a fresh named-model window when its scope's enclosing account bounds independently establish runway and it explicitly reports 0% used and 100% remaining with a valid reset no more than its declared window duration plus five minutes ahead, making its inferred cycle start slightly later than the report snapshot (pace.reason: future_cycle_start): that unopened model window has no exhaustion projection yet. The five-minute ceiling bounds request latency and clock skew; a reset beyond it is implausible cycle evidence and remains unmeasurable. Its 100% can still contribute to effectivePercentRemaining as a headroom bound. quota-axi never synthesizes a resetsAt or starts the countdown client-side. A missing resetsAt paired with any other usage shape (unknown usage, or nonzero usage without an active clock) is a real data gap, not "not yet triggered," and still fails closed into unmeasurableWindowIds - alongside stale data, missing usage percent, an expired or malformed resetsAt that is actually present, and a missing projection when usage is nonzero and the cycle is known. Likewise, a future cycle remains unmeasurable unless it is a named-model window with independently measurable enclosing account bounds, both usage fields explicitly prove that it is wholly unused, its positive declared duration is available, and its reset falls within that bounded cycle-ahead interval.
effectiveAvailability[].selection is an optional, per-scope object published for every scope quota-axi reports, including unknown and stale ones. It is the primary published selection signal: one scalar per scope, comparable across scopes, providers, and accounts. Consumers that need to distinguish accounts can request the optional account identity with --full.
In default TOON the scalar is the spendPriority column of the scope's quota[] row - there is no separate selection[] block, at any tier, because the column already carries it. An unmeasurable scalar renders the literal unknown, never 0: 0 is exact utilization, a completely different claim.
| Field | Meaning |
|---|---|
status |
known when at least one bounding window is measurable, every other bound is an untriggered zero-use window, and no boundConflict is disclosed; otherwise unknown |
spendPriority |
The clamped scope scalar. Present only when status is known |
unmeasurableWindowIds |
The bounding windows that blocked the scalar. Present only when a named bound made the scope unknown |
For each bounding window w of the scope:
S_w = percentRemaining_w - burnMultiple_w * timeRemainingPercent_w
gap_w = S_w / timeRemainingPercent_w
scopeMetric = SUM(gap_w * cycleSeconds_w) / SUM(cycleSeconds_w)
S_w is the percentage points of that window's paid allowance projected to reach reset unused if the observed burn continues. Dividing by timeRemainingPercent_w makes windows on different reset clocks comparable, and weighting by cycleSeconds_w keeps a short session window from dominating a weekly or monthly one. The result is clamped to [-100, +100].
spendPriority |
Meaning |
|---|---|
| Positive | Paid allowance is on track to reach reset unused, so spending here reclaims allowance that would otherwise be forfeited |
0 |
Exact utilization: the scope is projected to finish its cycle with nothing left over and nothing overdrawn |
| Negative | Overdrawn against the reset clock |
A higher spendPriority therefore marks the scope where spending recovers the most paid allowance that would otherwise expire unused. At burnMultiple 1, S_w reduces exactly to that window's reservePercentPoints; the metric generalizes reserve to projected forfeiture at the observed burn pace.
Any bounding window without usable pace makes the whole scope unmeasurable: status is unknown, no scalar is emitted, and unmeasurableWindowIds names the blockers. An unknown window is never assumed healthy and never treated as zero. A window whose remaining cycle time has effectively run out is unmeasurable rather than infinite. The one case where an absent burnMultiple is not a gap is a window with zero elapsed cycle time and zero usage: nothing can have been consumed yet, so its observed burn is 0 and the scope stays measurable.
A bounding window with no resetsAt at all has not been triggered yet (e.g. a Z.AI five_hour window before its first request this window, whose vendor omits the reset while the window is idle) rather than being a data gap. Under the same rule as the runway aggregate, when that untriggered window also reports zero usage (100% remaining, 0% used) it is treated as fully available and excluded from the weighted mean instead of blocking the scalar, so the scope's other bounding windows still determine spendPriority. When every bounding window is untriggered there is no measurable cycle to weight, so the scope publishes no scalar and spendPriority renders the literal unknown. A missing resetsAt paired with any other usage shape (unknown usage, or nonzero usage without an active clock), and a present-but-unparseable resetsAt, remain real data gaps that fail closed into unmeasurableWindowIds.
selection is derived per report from the same generatedAt clock as pace and runway, and is not cached. summarizeEffectiveSelection in src/pace.ts is its only implementation, and SELECTION_SCALAR_KEY in src/types.ts is the only spelling of the published field name. worstReservePercentPoints is a single-window diagnostic and is not this scalar.
This is data, not routing. quota-axi still never routes, ranks a winner, orders providers preferentially, proxies, logs in, or changes provider quota state. selection is a derived comparative data signal computed entirely from figures quota-axi already reports; any routing, ranking, or preference is the consumer's decision. It is also advisory only: it never overrides runway, which remains the hard completion-risk evidence a consumer checks against its task horizon.
| Name | Values |
|---|---|
| Provider statuses | fresh, stale, unavailable, auth_required, rate_limited, or error |
| Provider sources | oauth, pi:openai-codex, pi:openai-codex-* sibling keys, cli-rpc, cli, api, web, cache, or unavailable |
| Current provider adapter sources | oauth, pi:openai-codex, pi:openai-codex-* sibling keys, cli-rpc, cli, api, web, cache, and unavailable |
| Window kinds | session, weekly, monthly, model, credits, or unknown |
| Window pace statuses | ahead, on_pace, behind, or unknown |
| Effective pace statuses | ahead, on_pace, behind, mixed, or unknown |
| Effective runway statuses | exhausted_now, projected_exhaustion, through_reset, or unknown |
| Effective selection statuses | known or unknown |
| Pace projection confidence | early or established |
| Pace cycle basis | starts_at_resets_at or window_seconds |
| Quota relationship statuses | known, partial, or unknown |
| Source attempt statuses | success, failed, or skipped |
Source attempts can include credentialPresent when a source is not genuinely absent, including when a read failure prevents a more precise classification. They can also carry an explicit degraded flag that overrides the derived classification in either direction: false when a non-success attempt is not a broken credential source, and true when a skipped source was itself unreadable without establishing that a credential is present. Otherwise fresh reports derive state.degradedSources from failed attempts and skipped attempts with credentialPresent.
| Provider | Windows and capabilities |
|---|---|
| Claude | Can report five_hour, seven_day, optional seven_day_opus, and optional extra_usage windows. Claude's OAuth usage utilization values, and scoped-limit percent values, are percent used; quota-axi stores them as percentUsed (while extra_usage.utilization remains its vendor-defined used-credit percentage). Trusted session/weekly/model windows emit fixed windowSeconds (18,000 or 604,800) for pace; extra_usage does not invent a monthly duration. |
Claude scoped limits |
When the account's usage response includes a scoped limits list, quota-axi surfaces every active window it describes instead, including model-scoped ones (e.g. Fable) as a model:<slug> window with the same trusted weekly duration. |
| Codex | Identifies exact 18,000-second and 604,800-second periods as five_hour and weekly, regardless of source slot; periods without a duration retain their positional identity. Additional model- or feature-scoped limits use model:<id>:5h / model:<id>:7d, and code-review limits use code_review_five_hour / code_review_weekly. Unfamiliar durations remain honest <hours>h windows instead of being classified as known periods. Duplicate derived IDs are preserved with _2, _3, and later suffixes. Workspace-imposed credit caps from spend_control.individual_limit appear as spend_control (credits, “workspace credit cap”), even without plan windows. Used and remaining percentages derive from the reported credit totals when the limit is positive and both totals are valid; otherwise the reported percentages are retained and missing values stay unknown. spend_control.reached: true marks this cap exhausted. When the cap is the sole quota evidence, it bounds all_models. When rate-limit windows also appear, the cap remains published but is excluded from effective availability: included usage and subsequent credit-funded usage are not modeled as a simultaneous minimum or a combined allowance. Its reported reset is retained without inventing a cycle duration. Optional credit balance data can also appear. |
| Cursor | Can report included_usage, auto_usage, api_usage, and optional spend_limit windows from GetCurrentPeriodUsage, plus an optional grok_bot weekly window from the same dashboard service's GetSandUsageStatus RPC. Their effective-availability interpretation is documented in Quota windows. Monthly labels alone are not trusted cycle evidence, but the billing cycle is: the monthly windows take startsAt from a reported billingCycleStart, or - with only billingCycleEnd - from the previous renewal date one calendar month earlier (clamped to the last day of that month), so pace uses starts_at_resets_at. With neither field the cycle stays unresolved; no fixed 30-day duration is invented. The Grok Bot window uses the sand payload's own currentPeriodStart / nextResetTimestampUtc pair when present, and is omitted when that RPC is missing, non-finite, or a pooled enterprise allowance. |
| GitHub Copilot | Can report quota snapshot windows such as chat, completions, and premium_interactions; when the first-party endpoint exposes entitlement but no numeric quota windows, quota-axi reports a fresh provider state with an empty windows list rather than inventing percentages. Pace stays unknown without trusted cycle boundaries. |
| Grok | With a consumer-compatible Grok session or Pi xai OAuth bearer, can report the shared credits-identified current-period window (kind weekly/monthly from the decoded period type, otherwise credits; labeled week/month accordingly), optional product-scoped product:<slug> windows with the same kind, the current-period startsAt and reset, and optional prepaid credit balance from the consumer Usage-page operation. Official Grok Build OAuth and Pi OAuth can remain valid for their model catalogs while that separate operation rejects their audience; in that case quota-axi reports usable auth with no windows rather than inventing a SuperGrok percentage. A Pi xai API key likewise establishes model usability only. Top-level credits.remaining is prepaid/on-demand balance only; it is not subscription remaining and does not bound the period windows used for effective availability. Pace prefers the startsAt/resetsAt pair. |
| Grok proto3 zero | For the exact consumer operation only, an omitted usage float is the official proto3 zero when a valid weekly or monthly current period proves the config is present; quota-axi reports 0 used and 100 remaining rather than deriving usage from money. |
| Kimi | Reports every window the /usages payload actually supplies: the current usages map (five_hour, weekly, month_total, month_code from limit_5h/limit_7d/limit_month_total/limit_month_code), the legacy principal weekly object plus every valid self-described limits[] entry in wire order, or both. Kimi Code can send both shapes in one answer, and their figures for the same window can disagree: a live answer carried the principal usage at 92% used and limit_7d at 0% for the same week. When both shapes report a window, quota-axi reports the more-used figure, so the window never overstates headroom. A declared principal usage that this reader cannot parse next to a valid map is named usage in state.untrustedWindowIds, so the reading is partial and the principal is never dropped silently. Trusted 5h/weekly durations are 18,000s and 604,800s; month_total has the monthly cycle. Only a legacy limit whose normalized duration is exactly 18,000s is identified as five_hour; future limits remain limit:<index> unknown windows. The map's ratio is read from used_ratio only. Absent map entries stay absent, a declared map entry with no usable ratio is named usages:<key> in state.untrustedWindowIds rather than dropped, and an unrecognized map key is not a window. month_code stays a distinct window but carries only percentUsed plus shareOf: month_total, because it is a share of month_total rather than its own budget. An authenticated /usages body that declares no quota-bearing field at all (usages absent/null/{}, usage absent/null, limits absent/null/[], no key besides the vendor's own goods_version/boosterWallet) is a Free-tier account's established-empty reading, not schema_invalid: it still consults the sibling Kimi Code CLI source, and if nothing yields windows, quota-axi reports fresh with no windows and a usable auth status instead of an error. A body that declares a quota field this reader cannot parse still fails as schema_invalid. |
| Z.AI | Can report the Coding Plan five_hour and weekly usage windows (with trusted 18,000s and 604,800s durations) plus the mcp_month tool window, whose duration is not invented. Token and credit limits are both identified by the endpoint's own unit/number magic values (3/5 and 6/1) rather than array position; any other limit, or a repeat of an already reported one, degrades to an untrusted limit:<index> unknown window named in state.untrustedWindowIds. |
Antigravity (agy) |
On macOS and Linux, can report gemini_5h, gemini_weekly, claude_gpt_5h, claude_gpt_weekly, and gemini_argon_* buckets from the installed CLI's structured agy -p "/quota" --output-format json response (a bounded read, not an agent session), falling back to an already-running Antigravity app or agy loopback quota summary when the CLI is unavailable. If only model config quota is exposed, quota-axi reports model-scoped model:<slug> windows instead of inventing grouped windows. See Quota windows for grouped bounds and cycle evidence. |
| Alibaba | Reads the local bl CLI's Alibaba Coding Plan Token Plan usage; reports the plan name and weekly remaining percentage and reset time from the CLI's JSON output, plus any named model limits as separate model:<name> windows. Repeated limits for the same model remain separate with suffixed IDs such as model:<name>:2. |
| OpenCode Go | Using the credential sources documented in Security Posture, reports the provider's rolling, weekly, and monthly usage windows as stacked plan caps that jointly bound Go-plan usage at all_models scope. It applies the plan's declared cycle lengths ($12 per rolling 5 hours, $30 per week, $60 per calendar month) when the payload carries no duration of its own: rolling and weekly windows gain trusted 18,000s and 604,800s durations, and the monthly window derives a start one calendar month before its reported reset (day-of-month clamped); a payload-supplied duration still wins, so pace, runway, and selection are measurable. A zeroed plan window blocks Go-plan requests, but the vendor's free-model fallback or an opted-in Zen balance may still serve past it. |
| Command Code | Reads local Command Code API keys and reports the exact aggregate credit balance (monthlyCredits + purchasedCredits + freeCredits, unit credits) plus five_hour and weekly rolling windows from Command Code's undocumented first-party GET /alpha/whoami?limits=1 and GET /alpha/billing/credits endpoints. Those windows use trusted 18,000s and 604,800s durations and jointly bound the included_credits scope, not all_models, only when both are present and trusted. limited:false omits leftover rolling fields so the report stays credit-only with empty windows. No monthly percentage or 30-day reset is invented. Pay-as-you-go/Enterprise-style responses with no rolling windows stay fresh and auth-usable with empty windows, and their balance is surfaced as a credits attention row rather than no_quota. The /alpha account endpoints are not a documented public API; unrecognized limits fail closed as untrusted/partial. |
| MiniMax | Reports Token Plan model_remains windows, or a balance in credits.remaining with unit usd on the global api.minimax.io host and cny on the China api.minimaxi.com host. Model-window IDs are model:<slug>:5h or model:<slug>:7d for the trusted 18,000-second and 604,800-second durations, and model:<slug>:window:<label> for any other or undetermined duration; all share the model:<slug> scope. Absent numeric observations remain absent, and no account-wide bound is inferred. A model_remains row with no recognized window fields degrades to an untrusted limit:<index> unknown window named in state.untrustedWindowIds. |
| MiMo | Reads the local MIMO_API_KEY first, then Pi's auth.json Xiaomi entries (xiaomi plus the xiaomi-token-plan-sgp/cn/ams clusters) as one pi:<entry> source each, so the report names the entry that answered and keeps a present-but-unusable entry visible as a credential rather than an absence. No first-party read-only quota endpoint is established, so a usable credential reports an empty fresh window set rather than fabricated model headroom. |
| DeepSeek | Reports credits.remaining from the USD total with unit usd, or, when no representable USD total exists, from the CNY total with unit credits; quota-axi does not infer model headroom or effective remaining from those amounts. |
| OpenRouter | Reads a local OpenRouter/Pi API key and the first-party key endpoint. A finite key cap is reported as a raw key-limit credit amount; unlimited keys are reported without an invented cap, and no model headroom is inferred. |
| ElevenLabs | Reads the deliberately exported ELEVENLABS_API_KEY and reports one characters window from the subscription plan's included allowance (character_count / character_limit) via the documented first-party GET /v1/user/subscription. resetsAt comes from next_character_count_reset_unix; the cycle start is that reset stepped back by the vendor's declared character_refresh_period, and an unrecognized period leaves the cycle - and therefore pace, runway, and selection - unknown rather than assuming one. Only monthly_period maps onto the monthly window kind; the longer declared periods stay unknown. A zero or absent character_limit reports no window instead of a percentage, and so does a response whose reported reset has already passed: those counters describe a finished cycle, so the live reading fails closed the same way the stale-cache path does. The window bounds included_characters, never all_models. |
| Devin | Reads WINDSURF_API_KEY when it is non-blank, otherwise $XDG_DATA_HOME/devin/credentials.toml (~/.local/share/devin/credentials.toml, or %APPDATA%\devin\credentials.toml on Windows). One read-only Connect-JSON POST to https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetUserStatus reports weekly and, when planInfo.hideDailyQuota is explicitly false, daily remaining percents with the vendor's reset timestamps. An absent flag leaves the daily cap unresolved, not omitted as on Max. The daily window reuses the existing session kind, so the published window-kind enum is unchanged: its id is daily, its label is day, and windowSeconds is 86,400. Weekly is weekly / week / 604,800. Both bound included_quota, because paid extra usage continues past a zeroed window. overageBalanceMicros is the extra-usage USD balance and is never turned into a percentage. A body whose billingStrategy is not BILLING_STRATEGY_QUOTA publishes no windows. An unrecognized api_server_url is not contacted. |
| Devin proto3 zero | For GetUserStatus only, an omitted dailyQuotaRemainingPercent or weeklyQuotaRemainingPercent is 0 remaining when that window's reset timestamp is present, because proto3 JSON omits zero-valued scalars. When both the percent and reset are missing on a quota plan with an expected cap, that window is untrusted and the combined bound is withheld. |
| Muse | Reads the Muse CLI's sign-in credential - auth.json's providers.meta.access_token on Linux, the macOS Keychain item its storage: "keychain" records point to - then an exported META_API_KEY, and reports the subscription's five_hour (subs_usage.window when window_duration_mins is 300) and weekly (subs_usage.weekly) windows from used_percent and epoch-second resets_at, via POST https://api.meta.ai/muse-code/key - the request the Muse CLI makes at startup. Their effective-availability interpretation is documented in Quota windows. A window of any other length is named window with its own duration; an entry without a usable used_percent is named in untrustedWindowIds; an omitted or null subs_usage on an active subscription stays absent (plan still published) rather than untrusted or filled with 0%; an inactive subscription reports no window. Because that endpoint also issues an API key, requests are rate-bounded per credential; see Muse provider notes. |
| Higgsfield | Reads the local higgsfield CLI: account status --json for plan and remaining credits, account transactions --json for a subscription-credits grant that supplies the allowance only when it is that page's sole positive credit and the balance reconciles with it plus the newer on-page entries, and generate list --json for a PHI-safe job-status rollup. Status has no limit or reset field, so a percentage is published only from a matching grant whose page also reconciles the balance, and pace stays unknown. Email on the status payload is ignored and never published. Job ids, prompts, and URLs are discarded; only jobs.sampled / completed / failed / other counts remain. The credits window bounds included_credits, never all_models. |
quota-axi models [--intelligence high|medium|low] [--sort runway] [--provider ...] [--json|--full] joins a reviewed catalog of native Claude, Codex, Grok, Kimi, MiniMax, and MiMo models to the provider's effective quota evidence. MiMo has catalog names for discovery, but no quota windows are established, so its model rows remain unevidenced. It queries catalog-backed providers by default and accepts only those providers in an explicit models scope. Cursor, Copilot, Z.AI, Alibaba, OpenCode Go, DeepSeek, OpenRouter, Antigravity, Command Code, Devin, Muse, and Higgsfield report quota but have no reviewed catalog entries yet, so they are not models providers. ElevenLabs meters speech characters rather than model usage, so it is not a models provider by construction.
Catalog buckets are coarse editorial classifications relative to the current frontier, not scores. They are curated from public provider material and public leaderboards, including Artificial Analysis as an informing source, with a documented provenance string so a stale catalog is detectable rather than silently refreshed. quota-axi does not reproduce Artificial Analysis scores, has no runtime Artificial Analysis dependency, and never commits an Artificial Analysis key. scripts/refresh-model-kb.ts is a maintainer-only review aid: it may use a private AA_API_KEY to suggest changes, but it never writes the catalog.
Every models response includes catalog.version and catalog.provenance; callers must treat catalog freshness and unmapped unmatchedWindowIds as explicit unknowns, never as missing evidence. A model row exposes the applicable effective quota scope and provider state. When no model-specific scope is known, the provider account scope remains the evidence rather than an invented model limit.
Default model order is deterministic and non-preferential: provider, then accountKey in an account-expanded report, then model ID. --sort runway is an explicit, evidence-preserving comparator only: finite usableRunwaySeconds descend, then through_reset, then exhausted_now, with unknown evidence last. Equal evidence appears in sort.tieGroups; no hidden score or model, provider, harness, credential, or route recommendation is implied. The comparator registry is intentionally extensible for a future separately sourced cost comparator, which is not shipped in v1.
| Object | Fields |
|---|---|
| Auth report | generatedAt, schemaVersion (1, or 2 when account-expanded), and auth |
| Provider auth report | provider, optional accountKey, and sources |
| Auth source entry | source, optional path, status, and optional error |
Auth source entries can include credentialPresent when a source is not genuinely absent, including when a read failure prevents a more precise classification.
| Name | Values |
|---|---|
| Auth source statuses | available, missing, invalid, expired, skipped, or error |
| Auth source names | env, oauth-file, keychain, auth-json, auth-env, apps-json, copilot-cli:keychain, gh:hosts.yml, state-vscdb, cli-keychain, cli-authfile, cli-rpc, pi:openai-codex, pi:openai-codex-* sibling keys, pi:kimi-coding, pi:xai, pi:zai, pi:commandcode, kimi-code-cli, pi:opencode-go, opencode:opencode.db, opencode:auth.json, minimax:config.json, pi:minimax, env:MINIMAX_API_KEY, env:MIMO_API_KEY, pi:xiaomi, pi:xiaomi-token-plan-sgp, pi:xiaomi-token-plan-cn, pi:xiaomi-token-plan-ams, pi:deepseek, env:DEEPSEEK_API_KEY, pi:openrouter, env:OPENROUTER_API_KEY, bl-cli, loopback, env:COMMAND_CODE_API_KEY, env:COMMANDCODE_API_KEY, commandcode-cli, omp:commandcode, env:ELEVENLABS_API_KEY, muse:auth.json, env:META_API_KEY, env:WINDSURF_API_KEY, file:credentials.toml, and higgsfield-cli |
| Provider | Credential sources read |
|---|---|
| Claude | CLAUDE_CODE_OAUTH_TOKEN when it is set to a usable literal token; $CLAUDE_CONFIG_DIR/.credentials.json or ~/.claude/.credentials.json (on macOS, not read when a nonempty secure-storage selector is set); on macOS, the discovered Claude Code Keychain value for the selected profile, pinned to Claude Code's validated current-user account, with --allow-keychain-prompt or, after a service-and-account-scoped non-secret access marker exists, on plain calls |
| Codex | $CODEX_HOME/auth.json or ~/.codex/auth.json, then Pi's $PI_CODING_AGENT_DIR/auth.json openai-codex subscription OAuth entry and any sibling openai-codex-* entries in that same file (default ~/.pi/agent/auth.json), before the read-only CLI fallback; $QUOTA_AXI_CODEX_BINARY can pin that fallback to an absolute executable path |
| Cursor | Cursor editor: $CURSOR_STATE_DB when set or the platform Cursor state database path. Cursor CLI (cursor-agent), macOS: identity from $CURSOR_CLI_CONFIG or ~/.cursor/cli-config.json plus the cursor-access-token / cursor-user Keychain value with --allow-keychain-prompt or an account-scoped marker; Linux: only accessToken from $CURSOR_CLI_CONFIG or ${XDG_CONFIG_HOME:-~/.config}/cursor/auth.json |
| GitHub Copilot | $GITHUB_COPILOT_APPS_JSON when set or the local Copilot apps auth file; on macOS and Windows, the Copilot CLI's secure-store item for the default ~/.copilot/config.json selection (Keychain service copilot-cli, or Credential Manager target <host>:<login>.copilot-cli) with --allow-keychain-prompt or a selection-scoped marker; then the selected host's oauth_token from the GitHub CLI's hosts.yml ($GH_CONFIG_DIR, else $XDG_CONFIG_HOME/gh, else %AppData%\GitHub CLI on Windows, else ~/.config/gh): github.com unless QUOTA_AXI_COPILOT_HOST names a *.ghe.com host |
| Grok | Grok CLI session auth from $GROK_AUTH_JSON, inline $GROK_AUTH, $GROK_AUTH_PATH, or $GROK_HOME/auth.json / ~/.grok/auth.json, plus Pi's independent $PI_CODING_AGENT_DIR/auth.json xai entry (default ~/.pi/agent/auth.json) for OAuth or literal API-key model auth |
| Kimi | Pi's $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent/auth.json) for a literal kimi-coding API key or OAuth access token first, then an official Kimi Code CLI access token from the slot $KIMI_CODE_HOME/config.toml names (default $HOME/.kimi-code/), falling back to credentials/kimi-code.json when it names none. Stored-expired tokens are probed; rejected refreshable expiry is soft, not sign-out |
| Z.AI | Pi's $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent/auth.json) for a literal Coding Plan api_key entry under zai, then opencode's auth.json ($XDG_DATA_HOME/opencode/auth.json when set, %LOCALAPPDATA%\opencode\auth.json on Windows, otherwise ~/.local/share/opencode/auth.json) for a literal key under zai-coding-plan, zai, z-ai, z.ai, zhipu, or zhipuai |
| Antigravity | No credential files; prefers the installed CLI's bounded, noninteractive structured /quota read (agy -p "/quota", a bounded read, not an agent session), falling back to discovering already-running Antigravity or agy processes and reading their 127.0.0.1 read-only loopback endpoints |
| Alibaba | The local bl CLI (bl usage token-plan --output json); quota-axi never reads Alibaba credential files or exchanges refresh data |
| OpenCode Go | OpenCode 2.x's SQLite database ($XDG_DATA_HOME/opencode/opencode.db when set, %LOCALAPPDATA%\opencode\opencode.db on Windows, otherwise ~/.local/share/opencode/opencode.db; OPENCODE_DB overrides the file name, resolved against that directory) first, read-only, for the credential table's opencode-go row OpenCode itself would select (active, then newest) when its value is a literal {type: "key"} key; source opencode:opencode.db. OAuth and external values are invalid. Then, for 1.x installs and as the fallback, the same directory's auth.json for a literal opencode-go key with opencode fallback; source opencode:auth.json. A non-release OpenCode channel's opencode-<channel>.db is reached through OPENCODE_DB. When QUOTA_AXI_OPENCODE_GO_PI_AUTH is 1 or true, Pi's $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent/auth.json) literal opencode-go api_key entry is read before both. This is opt-in so an unscoped run does not read an unrelated Pi store. The database is read with sqlite3 -readonly; quota-axi never writes it or launches OpenCode. If sqlite3 is unavailable, that source is reported as skipped with sqlite3_unavailable (credential present) when the database file exists, and auth.json is still tried; if that key is then rejected, the result is the sqlite3_unavailable error rather than a sign-out. Do not install sqlite3 or any other system package to fill that gap. |
| Command Code | Pi's $PI_CODING_AGENT_DIR/auth.json commandcode entry first (default ~/.pi/agent/auth.json), then $COMMAND_CODE_API_KEY, legacy $COMMANDCODE_API_KEY, production ~/.commandcode/auth.json top-level apiKey, then ~/.omp/agent/auth.json commandcode/command-code. Stored expiry is ignored: the credential is a non-expiring API key. |
| MiniMax | MINIMAX_API_KEY, then $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent/auth.json) for a literal minimax key, then $MMX_CONFIG_DIR/config.json (default ~/.mmx/config.json); MINIMAX_BASE_URL is accepted only for MiniMax first-party hosts |
| MiMo | MIMO_API_KEY, then Pi's $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent/auth.json) literal type: "api_key" entries under xiaomi, xiaomi-token-plan-sgp, xiaomi-token-plan-cn, and xiaomi-token-plan-ams, each reported as its own pi:<entry> source; quota-axi does not read browser or dashboard state |
| DeepSeek | DEEPSEEK_API_KEY or $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent/auth.json) for a literal deepseek key |
| OpenRouter | OPENROUTER_API_KEY or $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent/auth.json) for a literal openrouter key |
| ElevenLabs | $ELEVENLABS_API_KEY only, when it holds a usable literal key. That is the credential ElevenLabs' own CLI documents for non-interactive use and sends as the xi-api-key header. Stored expiry does not apply: the credential is a non-expiring API key. |
| Devin | $WINDSURF_API_KEY when it is non-blank (optional $WINDSURF_API_SERVER_URL), otherwise $XDG_DATA_HOME/devin/credentials.toml, ~/.local/share/devin/credentials.toml, or %APPDATA%\devin\credentials.toml on Windows. The token does not expire and has no refresh token. A blank variable selects nothing. The token is sent only to https://server.codeium.com. |
| Muse | The Muse CLI's ${XDG_CONFIG_HOME:-~/.config}/muse/auth.json providers.meta.access_token first, then - on macOS, where the file records storage: "keychain" - the login Keychain item ai.meta.dev.credentials / meta, then $META_API_KEY, each only when it holds a usable literal credential. Only the access token is read: an auth.json sibling refresh_token and the Keychain bundle's minted api_key are dropped while parsed, a bundle refresh_token is checked for presence only. The Keychain value read follows the --allow-keychain-prompt gate (presence checks never prompt). quota-axi auth never fetches the bundle unless that flag is on, even when a grant marker exists. Neither store carries a stored expiry quota-axi reads, so each is probed in declared order. |
| Higgsfield | The local higgsfield CLI (higgsfield account status --json, plus read-only account transactions and generate list). quota-axi never reads Higgsfield credential files, never runs higgsfield auth token, and never publishes the account email the status payload includes. |
The Claude and Codex rows describe default discovery; --profile-only narrows each to the one selected credential file.
Claude
- quota-axi mirrors Claude Code's Keychain account selector: nonempty
USER, otherwise the operating-system username, validated against Claude Code's safe account pattern with the sameclaude-code-userfallback. Metadata discovery filters to that account, and exact presence/value reads require it plus the selected service. There is no ambiguous service-only fallback. CLAUDE_CODE_OAUTH_TOKENis an explicit credential source and is preferred over every discovered store, because Claude Code resolves that variable before it opens any credential store: a token there names the account a live session is actually using, so reading a store instead would report a bystander account. It is an access token alone, so it has noexpiresAtto order it by, is never advisory-expired, is never eligible for the delegatedclaude doctorrefresh, and is never written to a credential store or a cache. Absent, empty, and whitespace-only values select nothing and leave stored-credential discovery exactly as it was; a non-blank value that is not a usable literal bearer is reported as anenvsource problem rather than dropped silently. An environment token also needs no Keychain grant, so--allow-keychain-promptis irrelevant to it. Because such a token selects an account the profile path and Keychain service do not describe, a reading taken with one carries its own cache identity, so its snapshot is never served as stale once the variable is gone; profiles with no environment token keep the identity they already cached under. A generic non-definitiveenvrejection can still fall through to a stored source. If that fallback fails, a stored source's definitive rejection outranks the earlier generic env failure. The specifically recognized usage-endpoint denial for missinguser:profileis different: it establishes that the selected env bearer cannot use that quota surface without asserting sign-out, so quota-axi reportsstate.status: unavailablewithstate.authStatus: usableand stops rather than letting a bystander stored credential relabel the selected session. A genuine env HTTP 401 remainsauth_requiredeven when unrelated Keychain discovery fails; it neither falls through to a stored credential nor retires its cached snapshot.- When that exact scope denial is the final Claude failure and the native fallback was not attempted,
state.reasonisinference_opt_in_required,state.remedyCommandisquota-axi --provider claude --allow-claude-inference, and JSON includes an agent-directedhelpentry naming the startup-plus-inference cost. That is advice only: it never runs the fallback, changesauthStatus, or persists consent, and a run that already passed the flag gets no such advice. --allow-claude-inferenceis the explicit numeric fallback for that specific env-token scope boundary. It launches the installed native Claude Code client once from a new empty temporary directory with safe mode enabled and tools, MCP/browser integration, permission prompts, slash commands, and session persistence disabled; the fixed prompt asks for exactlyOK, sets an inference budget, and may spend normal startup plus inference tokens. The child inherits the selected credential and route environment. SDK debug output is bounded in memory, never persisted or rendered, and reduced to validated unified five-hour/seven-day utilization and reset headers. The report uses sourcecli;--fullrecords aclaude-native-inferenceattempt. Cache eligibility is defined under Cache. Both allowlisted windows must validate before any windows are returned; an incomplete pair remains unmeasurable. Stdout and stderr are parsed independently after stripping terminal control sequences; the latest debug response in each stream is considered without joining fragments across streams. One complete observation is accepted, identical complete observations agree, and conflicting complete observations are rejected. An incomplete bare 429 in the other stream cannot displace complete evidence. The child disables native retries and the retry watchdog without changing the parent environment. Ordinary SIGINT/SIGTERM cancellation terminates the inference process group, escalating after a bounded grace period, and preserves the caller’s signal behavior. Unknown debug formats, missing headers, unavailable/incompatible Claude, timeouts, and process failures report unavailable rather than inventing quota. A native 429 reportsrate_limitedwith its bounded Retry-After and keeps any validated unified five-hour/seven-day windows that response itself carried inwindows; such a windows-carrying report uses sourcecliso those observed numbers still produce their ordinaryquota[]andexhaustion[]rows beside therate_limitedattention row, while therate_limitedstatus keeps the reading out of the cache. That observation outranks a later execution or output bound only when it is complete - a validated Retry-After or windows - so a block truncated to bare status digits, like a run with no observed rate limit, stays a timeout or output-limit failure; a complete 200 reading cut off by a bound is deliberately not rescued. Because the env token is consulted first and that scope denial ends discovery, a skipped Keychain candidate on such a reading never produceskeychain_access_requiredadvice, whether the denial or a failed native run is the final error. A nonblankANTHROPIC_API_KEYorANTHROPIC_AUTH_TOKENin the environment also reports unavailable (claude_native_api_key_presentorclaude_native_auth_token_present) before anything is launched, because the native client could bill that key instead of measuring the selected OAuth session; quota-axi checks only whether these values are nonblank and never retains, logs, or unsets them. Claude may still perform its normal transient startup cache/log writes. The flag is off by default, is rejected byauth,models, and--profile-only, and requires--oncewith--tuiso it cannot become a recurring paid probe.- The plaintext credential file is
$CLAUDE_CONFIG_DIR/.credentials.jsonor~/.claude/.credentials.json. A nonemptyCLAUDE_SECURESTORAGE_CONFIG_DIRselects the Keychain service; on macOS, where that Keychain item is the sibling source, the plaintext file is left unread so two profile selectors can never answer for one reading. Off macOS there is no Keychain sibling, so the configured file is still read. An empty value falls through toCLAUDE_CONFIG_DIR. A nonempty selector gives exactlyClaude Code-credentials-<sha256(NFC selector path).slice(0,8)>; with neither selector set, the service is the unsuffixedClaude Code-credentials. The hash uses the raw NFC-normalized path, without resolving relative paths, expanding~, or trimming it, matching Claude Code's selector. - quota-axi reads
security list-keychains, then lists metadata from those paths in onesecurity dump-keychain <search-list paths...>call (no values, raw data, or ACLs). Each command is bounded to five seconds and 16 MiB. It accepts only generic-password records for the validated current-user account whose service is the exact selected service, or - for a default selection only - a singleClaude Code-credentials-<eight lowercase hex>item (see below). The exact selected service is preferred when both exist. Duplicate matching items follow Keychain search-list order, independently of modification time or dump order; each credential pass reads at most one value, pinned to the located keychain, service, and account. - A default selection cannot re-derive the opaque suffix Claude Code gave its own item, so with no selector set quota-axi accepts a
Claude Code-credentials-<eight lowercase hex>item - but only when complete, conclusive metadata covers the entire search list and exactly one such item exists for this account, since the same naming shape can represent an explicit profile or an MCP OAuth store. Two or more leave the reading inconclusive and none is opened; recency never breaks the tie. Any other suffix shape is never accepted. Missing or unparseable records, unfamiliar Claude services, and Claude items owned by another account prevent opaque fallback; exact selected-service reads remain available. An explicit selector accepts only its own exact service: it never falls back to the unsuffixed item, to an opaque item, or to another profile's item. If the configured service cannot be located and any other Claude-prefixed generic-password item remains - including one owned by a different account name - the result stays inconclusive and eligible stale cache is preserved. - Metadata discovery runs again on each credential pass, including every TUI refresh. A replacement under the selected service, a changed search list, or a changed storage selector is picked up without restarting. A second opaque suffix appearing alongside the first leaves both unconfirmed; recency cannot establish ownership. A newly selected service still requires its own access marker or
--allow-keychain-prompt. - When the search list or metadata cannot be read completely, quota-axi falls back only to the exact selected service/account read across the search list. Empty or partial metadata and
securityexit 44 never establish sign-out. Only a parsed listing covering every searched keychain and containing no matching or unfamiliar Claude-prefixed item establishes absence. Denied and timed-out value reads retain their errors and never trigger reads of other services. - quota-axi records the non-secret access marker after any successful pinned Keychain value read. The marker is keyed by the account and the selected service, so a grant is never assumed for a differently suffixed item that Claude Code wrote later. That key changed shape in this release, so a marker recorded by an earlier version no longer authorizes a plain call; run
quota-axi --provider claude --allow-keychain-promptonce to record the current one. - When that marker exists, plain calls read the pinned Keychain value again so an already-approved "Always Allow" grant keeps live Claude quota fresh. Markers written by earlier versions remain untouched but do not authorize a value read.
- Without the flag or the current marker, quota-axi may perform a non-secret pinned Keychain item presence check so it only suggests Keychain access when the selected Claude credential item exists.
- In
--fulloutput, Claude usage attempts identifyoauth-fileorkeychainas the credential discovery source. They never include the Keychain account. - When an access token exists, local
expiresAtmetadata is advisory. quota-axi tests that token with Anthropic's existing read-only usage request; success returns fresh quota, while HTTP 401 definitively rejects that bearer. HTTP 403 alone is non-definitive because Anthropic's network-policy and WAF denials can use the same status for a valid bearer. HTTP 429 is ordinarily standard Retry-After rate limiting, but in default discovery, when the token was already stored-expired, quota-axi confirms that reading against the read-only OAuth profile endpoint: an explicit 401 there reports a non-definitive credential expiry instead, discarding the Retry-After, while a live, transient, or unclear profile answer leaves the rate-limited failure and its Retry-After untouched. A confirmed expiry allows any remaining stored credential candidate to be tried; a successful sibling returns fresh quota. If the confirmed expiry remains the final failure, it reportsClaude credential expiredas unavailable or eligible stale quota, without retiring the Claude cache or qualifying that token for delegated refresh. The confirming probe appears as anoauth-profileattempt in--fulloutput. It is not a credential source, so its outcome neither triggers nor suppresses Keychain access advice. - When that stored token was expired, carried a refresh token, and was definitively rejected, quota-axi may run
claude doctorand retry once against the store Claude Code rewrote, subject to the best-effort live-process check and residual race described under Delegated credential refresh.claude doctoris Claude Code's own installation health check: it starts no session, sends no model request, spends no quota, and does not connect to configured MCP servers. quota-axi never exchanges the Claude refresh token itself; it only checks that one is present, because Anthropic rotates it on use and a second exchange would sign Claude Code out. On macOS a withheld Keychain value read suppresses the delegate and keeps the Keychain remedy instead, because the CLI would rewrite a store quota-axi still could not read. An HTTP 401 on a stored-expired token that carries refresh-token presence is soft expiry, not a sign-out: it reportsstate.authStatus: expired_refreshablewithstate.status: unavailableor stale-cache fallback (Claude access token expired) and never retires the Claude cache. That covers delegation being suppressed (Claude Code already running, refresh disabled with--no-credential-refresh,claudenot found, an unlistable process table, or a withheld Keychain read) and a delegated run that outran its wait. That includes a delegatedclaude doctorthat genuinely ran and left the same refreshable token rejected: that read stays soft too, and additionally carriesstate.reason: credentials_expired,state.remedyCommand: claudeand an agent-directed JSONhelpentry telling the user to run Claude Code once (the Claude counterpart of Grok's remedy). When the Claude CLI was not found or could not be started, the same remedy names that limitation and the help instead says quota-axi could not run the Claude CLI. The remedy is not emitted while Claude Code is running or the delegate outran its wait. Among resolved (non-transient) rejections the highest-priority source's verdict wins, so a leftover non-refreshable.credentials.jsoncannot override a refreshable Keychain session listed first.auth_requiredtherefore requires a 401 on a non-refreshable or stored-valid candidate that leads the source order, or absence proven from complete metadata. A denied or unreachable Keychain read still surfaces as the Keychain error rather than a sidecar's soft expiry. The confirmed-expiry 429 path above uses the sameexpired_refreshablestatus when its token carries a refresh token, under its ownClaude credential expiredmessage. - Missing or invalid credentials without a usable access token and usage HTTP 401 (except the refreshable stored-expired case above) normally bypass and best-effort retire Claude cache. For stored credentials, when macOS Keychain discovery fails or its value read is denied/unreachable, a 401 from an oauth-file sidecar rejects only that candidate: quota-axi reports the Keychain failure, does not classify Claude as signed out, and preserves any eligible stale cache. Only complete search-list metadata with no matching or unfamiliar Claude item reports a missing source; every exact-read failure, including exit 44, remains a Keychain failure that preserves the cache. Stored-credential HTTP 403, timeout, network, rate-limit, server, and response-compatibility failures may use only a formerly fresh Claude snapshot less than seven days old that was captured for the same locally selected Claude configuration context (see Cache). Reset-expired windows are removed; resetless session, monthly, and credit windows expire after five hours, resetless weekly and model windows expire after seven days, and resetless unknown windows are rejected. Env-selected failures never serve stale quota. A stale Claude attention row names
fetch failedplus the usage-fetch error rather than a bare stale label. - After a successful usage read, quota-axi queries Anthropic's first-party OAuth profile endpoint with the same credential. Its authoritative root
account.uuidis exposed asaccount.accountIdonly in--fulloutput; if that field is absent,identityStatusisunverifiedinstead of deriving an identity from email, organization data, or cached account metadata.
Codex
-
Codex checks native
$CODEX_HOME/auth.jsonor~/.codex/auth.jsonOAuth first.If that does not return quota, it checks Pi ChatGPT subscription OAuth entries in
$PI_CODING_AGENT_DIR/auth.json(default~/.pi/agent/auth.json) before the CLI fallback. The built-in key isopenai-codex. Sibling keys such asopenai-codex-workfrompi-codex-accountsare enrolled from that same file when they are already present. Two or more distinct Pi entries become independent Codex account lanes; one account's failure or exhaustion does not hide or substitute another. A successful Pi-backed probe reports sourcepi:<provider-id>(pi:openai-codex,pi:openai-codex-work, …). quota-axi never readscodex-accounts.json, copies tokens, or changes Pi's active account. -
Native Codex
auth.jsonsupport is OAuth-token only; API key values such asOPENAI_API_KEYare treated as invalid for quota usage calls and are not sent to ChatGPT usage endpoints. -
Access-token JWT usability is authoritative for the native OAuth bearer probe. An expired
id_tokenalone does not markauth-jsonexpired or skip OAuth; identity-token expiry is diagnostic metadata only. A stored-expiredaccess_tokenis still probed in native OAuth's first declared source position rather than skipped, so only the usage endpoint's own rejection is an authentication verdict. A missingaccess_tokenskips OAuth and preserves the Pi and read-only CLI fallbacks. -
The Pi broker opens
auth.jsonread-only with a strict 64 KiB cap and guaranteed descriptor cleanup. It accepts only Pi's literal ChatGPT subscription OAuth shape: nonempty, control-byte-freeaccessandaccountIdstrings plus a numeric millisecondexpiresvalue. Strings containing$or beginning with!are rejected rather than resolved. Piapi_keyentries are unsupported because platform API billing is not ChatGPT subscription quota. Malformed, unsupported, expired-refreshable, and expired-non-refreshable states remain distinct diagnostics. -
quota-axi checks only whether Pi's refresh property is present; it never reads or retains the refresh-token value. It never performs a refresh-token exchange, refreshes Pi OAuth, launches Pi, or writes credential state. Pi owns refresh; quota-axi reads the current entry and sends its access token and account ID only to the existing bounded, read-only ChatGPT usage probe. Stored expiry is advisory, so the probe may test a stored-expired access token and only the endpoint's rejection marks it dead. Refresh token values are never logged, rendered, cached, or sent. Access token values are never logged, rendered, or cached.
-
It may run
codex -s read-only -a never app-serverfor Codex JSON-RPC fallback. That probe is also Codex's delegated refresh: the Codex CLI renews its own expired OAuth session and rewritesauth.jsonbefore answering, so an expired stored token still reports live quota without quota-axi touching the refresh token or spawning a second command. Codex rotates the refresh token on use, which is why the exchange stays the vendor's. -
Set
QUOTA_AXI_CODEX_BINARYto an absolute executable path when the fallback must use a specific Codex installation. Auth inspection and the app-server probe resolve the same path, and an invalid override fails closed instead of consultingPATH.
Cursor
- The Cursor editor and the Cursor CLI keep credentials in different stores, so both are independent sources and Cursor auth is usable when either one is. For quota fetching, the editor
state-vscdbsource is tried first because it never prompts; the platform CLI source is tried when the editor has no usable token or its token is rejected. Theauthcommand reports both sources. - Cursor Desktop is not required. On macOS, a CLI-only machine can refresh from the CLI Keychain token after the one-time Keychain grant described below; that quota attempt is named
cli-keychaininsourcesTried. On Linux,cursor-agentquota uses the read-onlycli-authfilesource fromauth.json; itsaccessTokenis only a bearer for the existing dashboard RPCs. The editor-credential fetch keeps its historicalapiattempt name. When credential discovery cannot produce a token, an unavailable source known to hold a credential takes precedence over a merely absent store, so a signed-incursor-agentuser sees the applicable source state rather thanCursor sign-in required. - Editor source: it uses
sqlite3 -readonlyto readcursorAuthvalues and calls Cursor's first-party dashboard RPCs. Ifsqlite3is unavailable, that source is reported as skipped withsqlite3_unavailablewhen the state database exists or its path cannot be checked, and as missing only when it does not exist, so an absent editor store never holds back a sign-out verdict from the CLI source. Do not installsqlite3or any other system package to fill that gap. - CLI source: on macOS,
cli-config.jsonholds sign-in identity only and is never a token; itsauthInfosupplies the reported account email, and the access token is read from the login Keychain itemcursor-access-token/cursor-useronly under--allow-keychain-promptor an existing account-scoped non-secret access marker. On Linux,cli-authfilereads onlyaccessTokenfrom$CURSOR_CLI_CONFIGor${XDG_CONFIG_HOME:-~/.config}/cursor/auth.json; missing, unreadable, malformed, or empty files are unavailable. The sibling refresh token is never read. - quota-axi never refreshes Cursor credentials, and Cursor has no delegated refresh. Neither the Linux auth-file refresh token nor the macOS
cursor-refresh-tokenKeychain item is read, and no non-interactivecursor-agentcommand was observed to rotate the stored session, so an expired or rejected CLI access token is a sign-out:auth_required, and any cached snapshot is retired. A transient probe failure still falls through to stale reporting. Recovery requirescursor-agent loginoutside quota-axi. Cursor never classifiesexpired_refreshablebecause it does not read refresh tokens. Cursor CLI session tokens are long-lived (about sixty days observed), so this costs little in practice. This is a known limitation, not a silent gap. - The token value is used only as the bearer of Cursor's read-only dashboard RPCs (
GetCurrentPeriodUsage,GetPlanInfo, andGetSandUsageStatus). It is never logged, cached, or included in any output. quota-axi does not call Grok Bot trial, banked-reset, or machine-registration methods.
GitHub Copilot
- It calls GitHub's first-party Copilot user endpoint.
- It checks three credential stores in order. Copilot's own
apps.jsonis first, and a workingapps.jsonreads exactly as before. The standalone Copilot CLI sign-in in the platform secure store (copilot-cli:keychain) is second. The GitHub CLI (gh) login inhosts.ymlis third, because current Copilot sign-ins no longer writeapps.jsonand the Copilot CLI accepts aghOAuth token as a Copilot credential. A reading from the Copilot CLI sign-in uses top-level sourcecli; a reading from the GitHub CLI login namesgh:hosts.ymlinsourcesTried. - A GitHub Enterprise Cloud account with data residency (
<subdomain>.ghe.com) is read only by opt-in: setQUOTA_AXI_COPILOT_HOST=<subdomain>.ghe.com(case-insensitive; the value is lowercased). Quota is then read fromhttps://api.<host>/copilot_internal/user, and only tokens that belong to that host are sent: matchingapps.jsonentries and theghhosts.ymllogin for that host. Agithub.comtoken never goes to the GHE host, and a GHE token never goes toapi.github.com. The Copilot CLI sign-in is github.com-only, so it is skipped for that host, and cached snapshots are scoped to the host so switching it never replays the other host's reading. Full URLs,api.-prefixed hosts,github.com, and any other value that is not a*.ghe.comhost are ignored, so a token never goes to an arbitrary host. - quota-axi hands over to the next store only when the previous one is absent, cannot be used, or its token gets HTTP 401 or 403 that is not a rate limit. A server, network, decoding, or rate-limit failure stops the search. When the GitHub CLI login answers, a present
apps.jsonthat did not answer shows as a degraded source. A rejectedapps.jsontoken keeps its attempt nameapi, as before. The stores are independent:apps.jsonor theghlogin can belong to a different account from the Copilot CLI's selected one. - The Copilot CLI source is supported on macOS and Windows for the default profile only (
~/.copilot/config.json, or%USERPROFILE%\.copilot\config.json), and only for an exactlastLoggedInUserwhose host ishttps://github.com. On macOS it reads the Keychain item with servicecopilot-cliand account<host>:<login>. On Windows it reads one generic credential (type 1) throughCredReadW, with target<host>:<login>.copilot-cliand username<host>:<login>; theLegacyGeneric:target=prefix thatcmdkeyshows is display notation, not part of the target. The source namecopilot-cli:keychaincovers both backends. After the value read, the selection is read again, and a changed selection discards the token. quota-axi never picks another logged-in user, never reads the CLI's plaintextcopilotTokensfallback, and never launchescopilot, refreshes, or migrates storage. - These bindings were checked empirically with Copilot CLI 1.0.87-0 on macOS and 1.0.86 on Windows x64. GitHub's authentication documentation documents the service and multiple-account behavior but not the item schema, so they are not a stable vendor contract. Selections quota-axi cannot bind with certainty are reported rather than guessed: a non-default
COPILOT_HOME(copilot_home_unsupported; metadata is inspected there, and no default profile is substituted), another host spelling or an enterprise host (selected_host_unsupported), an absent or malformed selection (selected_account_unconfirmed), and Linux or another platform without a supported store (secure_store_unsupported). A non-blankCOPILOT_GITHUB_TOKEN,GH_TOKEN,GITHUB_TOKEN,COPILOT_GH_HOST, orGH_HOSTreportsenvironment_selection_unsupported, because the CLI would select through the environment instead; only presence is checked, and a blank value selects nothing. - Consent follows the Claude and Cursor CLI Keychain sources. Plain macOS calls check item metadata only until
--allow-keychain-promptsucceeds; approve the Keychain request with "Always Allow". On Windows,CredReadWreturns the secret together with its metadata, so plain calls inspect only the CLI config, and the same flag permits the read without promising an OS prompt. A successful read records a non-secret marker, hashed over the config path, service, and account (0600on POSIX), so later quota calls for that same selection read without the flag. Ordinaryauthstays metadata-only even with a marker. The prompt remedy (state.remedyCommand: quota-axi --provider copilot --allow-keychain-prompt) is offered only forkeychain_prompt_required; macOS exit 44 iskeychain_item_unavailable, not a sign-out. - The Windows read runs the fixed system Windows PowerShell path with no profile, non-interactively, with a 10-second timeout and bounded output. It checks the returned target, username, and type, and accepts only a bounded literal GitHub token.
credential_not_found,credential_logon_session_unavailable,credential_access_denied,credential_binding_mismatch,credential_format_unsupported,credential_read_timeout, andcredential_read_failedstay distinct unmeasured outcomes and never imply sign-out. A service or SSH session without the desktop user's logon session cannot establish that the credential is absent. Windows ARM64 has not been checked. - A rejected Copilot CLI token reports
GitHub Copilot credential rejected or quota access deniedon its attempt and hands over to theghlogin. Copilot CLI readings are never written to the cache or reused as stale. When that source could have answered (its config selects an account on a supported platform, including an item still awaiting--allow-keychain-prompt, or the config cannot be read with certainty), an olderapps.jsonorghsnapshot is withheld too, so another account's quota is never served while the selected account is unmeasured. An item awaiting the opt-in reportsunavailablewithkeychain_prompt_requiredand the prompt remedy, never a sign-out, and does not use an older stale snapshot. A missing config, no selection, or an unsupported platform keeps the existing stale fallback, and also keeps the Copilot CLI source from defining the verdict. - From
hosts.yml, quota-axi reads only the selected host's ownoauth_token(github.comunlessQUOTA_AXI_COPILOT_HOSTnames a*.ghe.comhost), which is the tokenghitself uses for that host. It uses a narrow block-mapping reader, not a YAML library, and it keeps no other value. Other hosts, per-user token copies, and all other keys are parsed past and not kept. A file shape it cannot read with certainty is reported ascredentials_invalid, not guessed. - For the
ghlogin, quota-axi never reads the OS keyring and never runsghorcopilotto get a token. Whenghkeeps the selected host's token in the keyring,hosts.ymlholds no token for that host. The source then reportscredentials_keyring_storageas a skipped attempt. Ahosts.ymlthat cannot be read or parsed is also only a skipped attempt. Aghlogin does not prove Copilot access, so these skipped attempts never change the verdict. Aghtoken request that fails with a status other than HTTP 401 or 403, or with a network, decoding, or rate-limit failure, is a failed read. That error stays the provider verdict and is not reported as a sign-out. When every store that quota-axi can read is absent, unusable, or rejected, the provider reportsGitHub Copilot sign-in required, as before. - The values of
GH_TOKEN,GITHUB_TOKEN, andCOPILOT_GITHUB_TOKENare not read. - A
ghtoken can have wider scopes than a Copilot app token. quota-axi sends it only to that host's Copilot user endpoint (api.github.com, orapi.<host>whenQUOTA_AXI_COPILOT_HOSTopts into a*.ghe.comhost), the hostghalready sends it to, and only as the bearer of the read-only Copilot user request. - Unset, it only sends tokens associated with public GitHub hosts to
api.github.com; host-specific GitHub Enterprise tokens are treated as unavailable there. WithQUOTA_AXI_COPILOT_HOST, only tokens for that host are sent toapi.<host>. Redirects are rejected. - No Copilot store is refreshed, and Copilot has no delegated refresh.
Grok
- It checks two independent usability sources: Grok CLI session auth and Pi's
xaicredential in$PI_CODING_AGENT_DIR/auth.json(default~/.pi/agent/auth.json). Grok is locally usable when either source is usable, including asymmetric cases where the other source is absent, malformed, stale, or expired. True sign-out requires every applicable source to be unavailable or definitively rejected;authStatus: unusablecan also accompany an indeterminate local credential-resolution failure, but that failure remainsstate.status: errorrather thanauth_required. - Grok CLI session-scoped auth and Pi
xaiOAuth are eligible, read-only, for Grok's consumergrok_api_v2.GrokBuildBilling.GetGrokCreditsConfigoperation. The CLI source is attempted first; fresh quota makes a Pi request unnecessary, authenticated-but-unmeasurable auth falls through to Pi for numeric quota, and a transient failure stops sibling handover. CLI session-scoped entries are preferred over API-key entries. Observed Grok CLI OIDC access tokens are short-lived (about six hours on current CLI sessions) while a refresh token remains present for CLI-owned recovery. - Session-scoped Grok auth includes web/session scopes and official Grok Build OIDC records scoped to
auth.x.aiwithauth_modeorauthModeset tooidc, including scope keys with::<client id>suffixes. A consumer-operation 401/403 for official Build OIDC is followed by a GET tohttps://cli-chat-proxy.grok.com/v1/models; success proves subscription-backed model auth but that catalog carries no numeric quota, so its body is discarded. - Pi
xaiauth follows Pi's auth-file contract:type: "oauth"with literalaccess/ optionalrefresh/expires, ortype: "api_key"with a literalkey. Environment, template, and command references are not resolved. AmbientXAI_API_KEYis not a quota-axi credential source. A Pi OAuth bearer is first offered to consumer credits; if that operation rejects it,https://api.x.ai/v1/modelsis the corresponding read-only liveness check. A Pi API key establishes model usability locally (authStatus: usablewith empty windows when it cannot expose consumer quota), not grok.com credits. - The Grok CLI owns OIDC access-token refresh and rewrites
~/.grok/auth.json; Pi owns refresh of its ownauth.jsonOAuth entries. quota-axi never exchanges a refresh token, launches an agent session, or writes either auth file. Expired-session classification and recovery fields are documented under Providerstate. - When the Grok CLI session was stored-expired, carries a refresh token, and grok.com definitively rejected it, quota-axi runs
grok modelsand retries once against the file the CLI rewrote (see Delegated credential refresh).grok modelsprints the account's model list and exits: it starts no agent, opens no TUI, and spends no model quota. The delegate is skipped for a relocated store (GROK_AUTH,GROK_AUTH_JSON,GROK_AUTH_PATH), because the CLI would rotate its own default file instead. Pi'sxaientry has no delegated refresh: no non-interactive Pi command was observed to renew it, so it stays read-only. - Stored expiry is advisory within a source, never a verdict or a reason to reorder sources. Shared credential selection (
src/providers/credential-selection.ts) empirically tests stored-expired credentials in that source's declared position. A consumer-compatible bearer wins with fresh quota. An official Build/Pi OAuth bearer rejected only by consumer billing is tested against its own first-party model catalog; catalog success produces usable, unmeasurable auth instead of false expiry or sign-out. - HTTP 401/403 and auth-class gRPC codes are definitive only for the credential surface receiving them. A transient network/rate-limit failure never switches credentials or becomes an auth verdict (the stored classification stands). Such failures remain stale-cache eligible for same-source web snapshots.
- It does not send browser cookies, perform OAuth, exchange a refresh token, retain model-catalog or raw quota response bodies, or derive usage from model presence, rate-limit headers, or monetary fields. The only Grok process it ever starts is the declared
grok modelsrefresh delegate.
Kimi
- It opens Pi's
$PI_CODING_AGENT_DIR/auth.json(default~/.pi/agent/auth.json) read-only with a strict 64 KiB cap and guaranteed descriptor cleanup. It accepts only the exactkimi-codingentry, eithertype: "api_key"with a nonempty, control-byte-free literal stringkey, ortype: "oauth"with such anaccesstoken and an optional numericexpires. A stored-expired OAuth token remains eligible for the bounded read-only liveness probe in Pi's first declared source position; only the endpoint's rejection marks one dead, and a rejected expired record that still carries itsrefreshproperty reportsauthStatus: expired_refreshablewithstatus: unavailable, not sign-out. A missing file orkimi-codingentry ismissing; malformed or oversized stores, invalid entry shapes, and unsafe environment, template, or command references areinvalid; unknown credential types areunsupported; and expired OAuth isexpiredwith refresh-path presence preserved. None of these states resolves references, exposes values, or mutates Pi auth state. - The second source reads the official Kimi Code CLI credential from the environment
$KIMI_CODE_HOME/config.tomldescribes, defaulting to$HOME/.kimi-code/. Kimi Code keeps one credential per environment and records which one is current, so quota-axi reads[providers."managed:kimi-code".oauth]for that pointer rather than scanning or guessing: it takes onlystorage,key, andoauth_hostfrom that table andbase_urlfrom its parent (accepting either the snake_case or the camelCase spelling of the two URL keys), applies Kimi Code's own key-to-file-name rule plus its containment check, and openscredentials/<name>.json. It accepts only a non-emptyaccess_tokenwith a Unix-secondsexpires_at(a JSON number or numeric string). A token within 60 seconds of expiry is classifiedexpired, refreshably so when the record also carries arefresh_token; either way it is still tested by the bounded read-only probe in the CLI source's declared position. A successful probe returns fresh quota, a rejected refreshable record reports the softexpired_refreshableverdict, and a rejected non-refreshable record remains a hardauth_requiredfailure that retires its cache. - When
config.tomlnames no OAuth reference, quota-axi assumes only what Kimi Code itself guarantees: the unsuffixed slotcredentials/kimi-code.jsononhttps://api.kimi.com/coding/v1, the pair a mainland-China login persists, so that reading is unchanged. A file that is absent, unopenable, past the 256 KiB cap, or unparseable before its reference is reached names no reference either, and those states deliberately share the one verdict because they share their evidence - in each, quota-axi does not know what the file says. Should such a file still have recorded a non-defaultbase_url, the default slot cannot pair with it, so the result iskimi_code_cli_region_unrecognizedand no request is sent. A suffixed slot is never given an assumed deployment: with no endpoint left to place it, it iskimi_code_cli_config_invalid. - Those two states are not equally confident, and only the confident one produces a reading. No
config.tomlat all means the only slot Kimi Code could have written is the default one, so an absent file keeps the full default reading. A file that exists and could not be walked to its reference may name any slot, so quota-axi opens none: it reportskimi_code_cli_credential_unconfirmed, sends no request, and hands over to the Pi source. Nothing at a slot nobody named is read either way - not its emptiness or expiry as a sign-out, and not its contents as the current account's quota, which is what a credential left behind by a deployment the user has since left would otherwise be reported as. - A credential quota-axi cannot reach is never reported as one the user does not have: a
keyringslot iskimi_code_cli_credential_storage_unsupported, endpoints that are not one of Kimi Code's own deployments arekimi_code_cli_region_unrecognized, and configuration naming an unusable slot, or a suffixed slot with no deployment left to place it, iskimi_code_cli_config_invalid. - The Pi source always has priority. Ambient API-key environment variables are not a credential source. The CLI source is consulted whenever Pi cannot yield quota windows - no supported credential, an unreadable store, a Pi credential the provider definitively rejected, or an authenticated established-empty response as described under Provider windows - so a broken or quota-empty Pi source does not hide a working Kimi Code CLI session. Transport, decoding, timeout, cancellation, and server failures do not trigger credential switching.
- It sends one redirect-disabled
GETto<base URL>/usageswith a 15 second total deadline and a 262,144-byte decoded-body cap. That deadline also bounds how long quota-axi waits for the one reading ofconfig.tomltaken before the request, so a configuration file that never answers cannot outlast the operation; a run that cannot read it in time reports through the Pi source with no Kimi Code cache identity to reuse or stamp. The base URL travels with the credential: a Kimi Code CLI token is sent only to the deployment its own environment names, and a Pi-brokered credential, which records no deployment, keepshttps://api.kimi.com/coding/v1. A configured environment is accepted only when the endpoints it records - its OAuth host, its base URL, or whichever of the two it names - identify one of Kimi Code's two published deployments (.commainland China,.aiglobal). Slot and deployment must then agree in both directions, because Kimi Code derives the unsuffixedkimi-codeslot for the default deployment and a suffixed slot for every other: neither may appear with the other's counterpart. Configuration therefore cannot redirect the bearer to an arbitrary origin or pair one deployment's token with another's host, and an unrecognized environment sends no request at all. - It checks
refresh_tokenpresence only and never reads, renders, or exchanges its value. It never accepts an arbitrary Kimi origin, launches Pi or Kimi, makes a model request, refreshes or writes credentials, creates a device ID, imports cookies, sends device identity, retains raw responses, or exposes account, plan, token, or fingerprint data. Kimi has no delegated refresh: no non-interactive Pi or Kimi Code CLI command was established as renewing either store, so an expired record stays read-only. A soft expiry (authStatus: expired_refreshable, statusunavailableorstale) publishesstate.reason: credentials_expired,state.remedyCommandofpiwhen Pi'skimi-codingstore defines the reading orkimiwhen the Kimi Code CLI store does, and an agent-directedhelpentry telling the user to use a Kimi model inpionce, or to run a Kimi Code session withkimionce, because each CLI refreshes its own Kimi token only when that token is used. quota-axi does not launch either command. A hard sign-out staysauth_requiredwith no remedy. - Definitive credential absence or rejection retires Kimi cache data, but only when it is the verdict that speaks for the provider: a sibling source that failed transiently outranks it, so the cache survives, and a refreshable soft-expired verdict (
expired_refreshable) is never definitive, so it also survives. Transient fallback serves only windows the shared stale fallback bound still allows. - Because a reading can come from either Kimi Code deployment, or from a Pi credential that names neither, Kimi snapshots are cache-scoped like Claude's: each carries an opaque SHA-256 identifier for what produced it, and stale fallback reuses a snapshot only when the source whose failure is being reported asks for the identity it would have read under. A Kimi Code CLI reading is identified by the environment it was taken in; a Pi-brokered reading, which records no deployment and always asks
https://api.kimi.com/coding/v1, is identified by that source and endpoint, so its numbers are never served back as a Kimi Code deployment's stale reading. An environment quota-axi assumed rather than established is its own identity, distinct from the confirmed environment naming the same slot and host, so an unreadable or unparseableconfig.tomlreuses no snapshot at all: a mainland reading is never served to a global login that fell back to the default slot. That state withholds the cached numbers without retiring them, so the environment that produced them keeps them. Legacy context-less Kimi records are not reused. The environment identifier is derived from the resolved slot and base URL, never fromconfig.toml's bytes, so no credential in that file contributes to it and an edit that selects the same environment keeps the cache usable. It also comes from the one reading ofconfig.tomla run takes before it requests anything, never from a second reading taken afterwards, so a login that switches deployments mid-run cannot file one deployment's numbers under the other's identity. Both identifiers discriminate configuration, source, and endpoint rather than accounts, so neither distinguishes two Pi credentials.
Z.AI
- It checks two independent stores for a literal Coding Plan API key: Pi's
$PI_CODING_AGENT_DIR/auth.json(default~/.pi/agent/auth.json) first, then opencode'sauth.json($XDG_DATA_HOME/opencode/auth.jsonwhen set, otherwise~/.local/share/opencode/auth.json;%LOCALAPPDATA%\opencode\auth.jsonon Windows). Z.AI is locally usable when either source is usable. A successful Pi-backed probe reports sourcepi:zai; a successful opencode probe reportsopencode:auth.json. - Each store accepts only a nonempty, control-byte-free literal string key under a known Coding Plan provider id. In opencode's store it is taken from
key,apiKey,api_key,token,accessToken, orauth_token, or from a bare string entry. In Pi's store it must be thekeyof Pi'szaiapi_keyentry, which resolves toapi.z.ai, and a present Pizaientry of any other shape is reported asinvalidrather than missing. Environment, template, and command references are not resolved or executed, so a key that holds one is never sent as a header value. quota-axi never writes or manages Pi or opencode state. - In opencode's store, the
zai-coding-plan,zai,z-ai, andz.aiids resolve toapi.z.ai, andzhipu/zhipuairesolve toopen.bigmodel.cn; ambient API-key environment variables are not a credential source. - It sends one redirect-disabled
GETto that host's/api/monitor/usage/quota/limitwith the key in a BearerAuthorizationheader, a 15 second total deadline, and a 262,144-byte decoded-body cap. Direct requests take the host's default dual-stack path first and retry once over IPv4 only when that attempt fails to connect, because the vendor advertises IPv6 addresses that an IPv4-only host cannot reach. A configured HTTP(S) proxy still takes precedence. The endpoint is undocumented, so normalization is deliberately schema-tolerant rather than positional. A definitive rejection for one store, either HTTP 401/403 or the vendor's HTTP 200{"code":1000,"success":false}authentication-failed body, falls through to the next store; transport, timeout, rate-limit, and server failures stop without credential switching. - Z.AI snapshots are identified by a domain-separated one-way SHA-256 digest of the key that actually answered, never by the credential file or source name. Rotating a stored key cannot reuse the previous account's quota, and legacy snapshots without that identity are withheld. Timeout, network, 408, 429, 5xx, and oversized-response failures may reuse only the same key's formerly fresh snapshot under the shared stale fallback bound. Cache data is retired only when every store ends in a definitive credential failure: absence, an unparseable file or invalid entry, or a definitive rejection. The report keeps the highest-priority store's real failure, so a rejected or invalid Pi key is never relabeled as absence by a missing opencode entry. An unreadable auth file reports
state.status: errorand preserves the snapshot, but serves no stale quota because it identifies no account. This deliberately replaces the former stale-on-unreadable-file behavior, as decided in issue #213. - It never launches Pi or opencode, refreshes or writes credentials, sends cookies, retains raw responses, or exposes the account's key or plan identity beyond the plan label the endpoint reports. The Coding Plan key does not expire, so there is nothing to renew and Z.AI has no delegated refresh.
MiniMax
- It tries
MINIMAX_API_KEY, then Pi auth, then MiniMax CLI config. Within CLI config, OAuth access is tried before a co-stored API key. Candidate handover follows the Providerstaterules; it never resolves templates, commands, cookies, or refresh tokens. - It sends one redirect-disabled
GETper attempted credential to/v1/token_plan/remainsfor bearer-style keys, or/account/query_balanceforsk-api-keys. Each request has a 15 second deadline and a 262,144-byte decoded-body cap. HTTP 401/403 and application codes 1004/2049 are credential rejection; HTTP 429 and application code 1002 are rate limits, not permission to try another credential. Window normalization is documented under Provider windows. - It never launches the MiniMax CLI, performs a refresh-token exchange, writes credentials, or infers a quota from an inference request. Non-definitive failures - timeout, network, rate-limit, oversized or malformed responses, a missing quota payload, and unreadable credential stores - may reuse a formerly fresh snapshot that was captured for the same credential source and deployment host (see Cache); definitive provider-wide auth failure retires that cache only after candidates are exhausted.
MiMo
- It reads the literal
MIMO_API_KEYenvironment value first, then Pi's$PI_CODING_AGENT_DIR/auth.json(default~/.pi/agent/auth.json), where Xiaomi stores these keys under the vendor's own entry names rather thanmimo:xiaomi(pay-as-you-go) and thexiaomi-token-plan-sgp,xiaomi-token-plan-cn, andxiaomi-token-plan-amsToken Plan clusters, consulted in that order. Each entry is its own source -pi:xiaomi,pi:xiaomi-token-plan-sgp,pi:xiaomi-token-plan-cn,pi:xiaomi-token-plan-ams- so attempts,sourcesTried, andauthname the exact entry that answered, while an absent entry never speaks for a sibling. Only a literaltype: "api_key"record with a usablekeyis a credential; a present entry of any other shape reports as present but unusable, never as an absence, and markscredentialPresentin both the quota attempts and theauthsource rows - as does an auth store that exists but could not be read. Environment, template, and command references are rejected rather than resolved. - MiMo's provider-owned setup exposes model authentication but no established read-only quota operation. A usable key therefore yields
authStatus: usablewithwindows: []; the adapter sends no request at all, so quota-axi never probes an inference endpoint, sends cookies, or claims a model scope.
DeepSeek
- It reads only a literal
DEEPSEEK_API_KEYor the literaldeepseekentry in Pi's$PI_CODING_AGENT_DIR/auth.json(default~/.pi/agent/auth.json). Environment, template, and command references are rejected rather than resolved. - It sends one redirect-disabled
GETper attempted credential to DeepSeek's first-party/user/balanceendpoint with a bearer token, trying the environment key before Pi under the credential handover rules. The balance representation is documented under Provider windows. - It never launches DeepSeek tooling, refreshes or writes credentials, sends cookies, or retains raw responses.
OpenRouter
- It reads only a literal
OPENROUTER_API_KEYor the literalopenrouterentry in Pi's$PI_CODING_AGENT_DIR/auth.json(default~/.pi/agent/auth.json). Environment, template, and command references are rejected rather than resolved. - It sends one redirect-disabled
GETper attempted credential to OpenRouter's first-party/api/v1/keyendpoint with a bearer token, trying the environment key before Pi under the credential handover rules. Key-cap representation is documented under Provider windows; reset text comes only from the endpoint. The endpoint'sdata.labelis a per-key nickname rather than an account identifier, so it is exposed asaccount.accountIdwithidentityStatusunverifiedin--fulloutput. - It never launches OpenRouter tooling, refreshes or writes credentials, sends cookies, or retains raw responses.
Antigravity
- It never restarts, signs in to, configures, or mutates Antigravity or
agy. It reads no credential store and no process memory, and it never contacts an unowned port, so it has no delegated refresh. It prefers runningagy -p "/quota" --output-format jsononce with a 15 second bound; this is the vendor's noninteractive read-only usage command (a bounded read, not an agent session or credential operation). quota-axi resolves the CLI executable first, then prepends controlled denials for the desktop openers (xdg-openandopen) plus the current Node runtime to that invocation's otherwise unchanged command search path: authenticated/quotalaunchers still resolve Node even under a restricted inherited path, while the signed-out CLI's PATH-based desktop-opener attempt is denied before it can launch an interactive sign-in browser. When the CLI is unavailable or fails, it falls back to existing loopback access. A cached reading is retired rather than served when both the CLI is absent and no Antigravity process is running; an installed CLI that is temporarily stopped or unreachable retains the stale-cache fallback. As a deliberate accuracy decision, a discovered Antigravity process with no accessible endpoint needs attention rather than reading as not set up. Loopback HTTP 401 carryingmissing CSRF tokenis treated asunavailable(not signed-out) and retains the cached record. Any reading whoseresetsAtis in the past is marked stale. - On macOS and Linux, it discovers only the current user's processes and their owned listening ports, then sends read-only POST requests with operation-specific bodies to local endpoints on
127.0.0.1. On macOS, an app language server qualifies only when the executable path itself is inside an.appbundle whose name containsAntigravity, including bundle names with spaces; wrapper or prompt text that merely mentions such a path does not qualify. An app-advertised extension port is eligible only when the same process owns its listener. - For loopback access, it uses
RetrieveUserQuotaSummarywith the vendor's read-onlyrequest: {}/forceRefresh: falseenvelope, forwards a runtime CSRF token only when an app process advertises one, usesGetUserStatusfor plan identity and account identity exposed only behind--full, and can fall back to model quota data fromGetUserStatus/GetCommandModelConfigswhen grouped quota summary is unavailable. Conversation text, token counters, and subprocess error output are discarded. - Percent remaining and reset times come only from vendor
remainingFraction/resetTimefields. It does not invent windows, resets, or percentages. - For Antigravity's cycle evidence and the limits of its pace reading, see Quota windows.
Command Code
- It consults local API-key sources in a fixed order: Pi's
$PI_CODING_AGENT_DIR/auth.jsoncommandcodeentry first (default~/.pi/agent/auth.json), then$COMMAND_CODE_API_KEY, legacy$COMMANDCODE_API_KEY, production~/.commandcode/auth.jsontop-levelapiKey, then~/.omp/agent/auth.jsoncommandcode/command-code. A process--api-keyis not observable and is not guessed. Environment, template, and command references are rejected rather than resolved. Staging/local Command Code auth files are not read, and the API origin is fixed athttps://api.commandcode.ai. - Pi's Command Code login stores a non-expiring API key in OAuth-shaped fields. quota-axi ignores stored expiry, never reads the duplicate refresh value, and never labels the credential
expired_refreshable. A stored-expired access value is still probed in Pi's declared position. - It sends two redirect-disabled
GETs through the shared proxy-aware transport:/alpha/whoami?limits=1then/alpha/billing/creditswithorgIdonly whenwhoami.org.idis a non-empty string. A 15 second total deadline and a 262,144-byte decoded-body cap apply. OnlywhoamiHTTP 401 is a definitive auth rejection for that key; HTTP 403 and transport failures are not sign-out. A livewhoamikeepsauthStatus: usableeven when the credits call fails. - The
/alphaaccount endpoints are undocumented first-party surfaces (the public Provider API documents model routes only, which quota-axi never calls). Normalization fails closed: missing credit components omitcreditsrather than treating them as zero;limited:falseis credit-only and leftover five-hour/weekly fields are omitted from trusted windows;limited:truewith a missing expected window emits an untrusted placeholder; unknownwindowLimitsentries and non-empty organization spend limits are untrusted/partial until their contract is independently established. - Extra pay-as-you-go credits can bypass the rolling windows, so effective availability is published only for
included_credits. No monthly percentage, USD unit, 30-day reset, or unlimited status is invented. A credit-only reading (no rolling windows) still reports its raw balance: default TOON names it as acreditsattention row and--tuistates it on the card, instead of ano_quotarow that would contradict the same run'scredits. - Command Code has no delegated refresh.
ProviderOptions.refreshCredentialsis ignored. quota-axi never runs Pi,cmd, or Command Code, never writes credentials, and never prints token values. - Because several sources can point at different accounts, Command Code snapshots are cache-scoped: each carries an opaque SHA-256 of the winning source plus the account identity
whoamiestablished. Stale fallback is available only after a successfulwhoamimatches that context. A credits failure after a confirmed identity may reuse pruned same-context windows, and it defines the report over an earlier context-less source failure so that fallback stays reachable; a failed or unidentifiedwhoamipreserves the old snapshot without serving it and neither caches nor clears that provider's existing record. Fresh identified credit-only success (windows: []) clears an old window snapshot. Definitive all-source auth failure retires the cache.
ElevenLabs
- ElevenLabs' character allowance is a subscription allowance with a reset date, not metered platform billing, which is why it is reported here at all. It is a speech allowance rather than a coding-agent lane; it is published as its own
included_charactersscope and never bounds a model. - The one credential source is
$ELEVENLABS_API_KEY, and it is read as an explicitly supplied credential rather than as an ambient one. It is the variable ElevenLabs' own CLI documents for non-interactive use (export ELEVENLABS_API_KEY=xi-..., sent as thexi-api-keyheader), so a value there names the account the user put there on purpose - the same reasoning that makes$COMMAND_CODE_API_KEYand$CLAUDE_CODE_OAUTH_TOKENcredential sources. Absent, empty, and whitespace-only values select nothing; a non-blank value that is not a usable literal secret is reported as anenvsource problem rather than sent as a header. elevenlabs auth loginstores an OAuth bundle in the OS keyring, falling back to<config dir>/elevenlabs/auth-keyring.json. That store is deliberately not read: its entry is an opaque token bundle whose shape this tree has not established, and guessing at one would risk sending the wrong string as a credential. A user who wants quota-axi to see that account exports the key instead.- It sends one redirect-disabled
GET https://api.elevenlabs.io/v1/user/subscriptionthrough the shared proxy-aware transport, authenticated with thexi-api-keyheader and never withAuthorization. A 15 second deadline and a 262,144-byte decoded-body cap apply. The key leaves the process only as that header: it is never printed, logged, cached, or written into a fixture. - HTTP 401 is a definitive auth rejection unless its bounded JSON body carries
detail.status: missing_permissions. That recognized permission error and HTTP 403 are not sign-out: ElevenLabs keys carry scope restrictions (the subscription read needs theuser_readpermission) and an optional IP allowlist, either of which rejects this one operation while the key stays live. It is reported aselevenlabs_user_read_deniedwithauthStatus: usable, and it preserves the cache. Provider error messages are never retained. - ElevenLabs has no delegated refresh and no vendor rotation command.
ProviderOptions.refreshCredentialsis ignored, and the key has no expiry field, so no candidate is ever skipped before the endpoint itself has answered. - Because the key is the account, ElevenLabs snapshots are cache-scoped by an opaque SHA-256 of the answering source plus a one-way digest of that key. A different key gets a different identity, so a rotated or second key never inherits the previous subscription's windows, and the key itself never enters the cache. A definitive rejection retires only a snapshot whose identity matches the rejected key; absent or locally invalid keys leave the snapshot intact.
Devin
- Devin stores a non-expiring session token in
credentials.toml(windsurf_api_key) or in$WINDSURF_API_KEY. The environment variable is the session identity when it is non-blank; a blank value selects nothing and leaves the file. There is no refresh token and no delegated refresh. quota-axi never launchesdevinand never callsauth loginorauth logout. Sign-out advice is the textdevin auth login, offered only after a first-party 401/403; a present but malformed environment value or credentials file is a failed source (status: error), never a sign-out. - The token and
api_server_urltravel together. The only host the token is sent to ishttps://server.codeium.com. Any other origin is a skippedunsupported_serverattempt: the token is not sent, the attempt is not a sign-out, and the human report keeps Devin in view. - The read is one redirect-disabled Connect-JSON
POSTofSeatManagementService/GetUserStatusthrough the shared proxy-aware transport. quota-axi identifies itself asideName/extensionNamequota-axiwith its own semver. The token is the JSONmetadata.apiKeyand is never printed, logged, or cached. A 15 second deadline and a 1,048,576-byte body cap apply. - HTTP 401 and HTTP 403 are definitive auth rejection and retire only a snapshot whose identity matches the rejected token. HTTP 400, 5xx, timeouts, and decoding failures are transient: they preserve the cache and do not switch sources. An authenticated body with no quota-bearing fields is fresh with
windows: []. - Windows are published only for
planInfo.billingStrategyBILLING_STRATEGY_QUOTA.weeklyis kindweekly.dailyreuses the existingsessionkind (iddaily, labelday,windowSeconds86,400) so the published window-kind enum stays unchanged. OnlyhideDailyQuota: trueomits the daily cap, which is how Max reports no daily cap;falserequires a daily window. If the flag is absent, the daily cap is unresolved even when a daily figure is present, because that figure might be unenforced. An expected weekly or daily window whose figure is missing or has an elapsed reset is likewise named inunresolvedWindowIds; these cases withhold theincluded_quotascalar. A body that carries quota fields without abillingStrategy, or in which no expected window is usable, isschema_incompleteand keeps the cached snapshot. Remaining percent is the vendor field; proto3 omission of a zero percent with a present reset is 0 remaining.overageBalanceMicros / 1e6iscredits.remainingin USD and is omitted when the field is absent rather than reported as$0. Enterprise ACU fields and any non-quota billing strategy publish no windows. - The two windows bound
included_quotajointly when both are trusted andhideDailyQuotais false, or weekly alone whenhideDailyQuotais explicitly true. Free models do not draw on them, and paid extra usage continues past a zeroed window, so they are not anall_modelsbound. Organization and administrator limits are not in these fields. - Snapshots are cache-scoped by an opaque SHA-256 of the answering source, host, and a one-way digest of the token. A new login is a cache miss. A reading with no resolved credential neither caches nor clears the record.
Muse
- Muse (Meta's Muse Code CLI) exposes subscription usage only on
POST https://api.meta.ai/muse-code/keywith{"onboard": false}andx-api-version: 1.0.0, the request the Muse CLI itself sends at every startup. quota-axi sends that same request with the same bearer and readssubs_usage.window,subs_usage.weekly,is_subs_active, andsubs_tier_name(asplan). An active subscription whose mint body omitssubs_usageor returns it asnullkeeps that plan and publishes no windows: the field is absent, not untrusted, and 0% is not invented. A live Muse Code 1.4.0-R4161.1Muse Code Power Usagemint body (2026-09-25, JSON key paths only, no values) carriedis_subs_activeandsubs_tier_nameand omittedsubs_usage,window,weekly,used_percent, andresets_at. - That endpoint also returns an API key in its response. quota-axi never keeps it: a JSON reviver drops every response key outside the usage fields while the body is parsed, so the returned key and any account fields (email, name, payment method) never reach an object, an error, the cache, or any output mode. No response body is ever quoted in an error. quota-axi never writes that key, or anything else, to Muse's
auth.json. - Verified against a live Muse Code 1.3.0 subscription (2026-09-24): two consecutive calls returned the identical
api_key, and that key equals the one the CLI minted at a session startup, so repeated reads do not mint or rotate a fresh credential - the endpoint serves the account's standing Model API key - and a CLI session started before those calls kept working through and after them (a multi-round headless session straddling both calls completed normally). - Because the endpoint issues a key on first use and its rotation cadence is not documented, requests are still bounded per credential: at most one request per credential per five minutes, whatever its outcome, across every quota-axi process sharing the cache directory. The request is recorded as
pendingin the key-endpoint ledger before it leaves, so a timeout or crash still counts. Inside the interval nothing is sent: the reading that request produced is served from its own context-matched snapshot asstatus: freshwithsource: cacheand its originalrefreshedAt; an empty successful observation (omitted or nullsubs_usage, an inactive subscription, or every reportedresets_atalready passed) is replayed from a non-secret ledger marker as the samestatus: freshwithsource: cache, the sameplanif present,windows: [], and its originalrefreshedAt; a rejection is replayed as a rejection; and anything else reads statusunavailablewith errormuse_read_deferredand aretryAfternaming when the next request may leave, falling back to that credential's older snapshot as stale when one is still inside its windows. A cache miss after a windowed quota outcome is not turned into an empty reading. The interval equals the--tuidefault refresh, so a live report sends at most one request per refresh, and a burst of agent calls sends one in total.quota-axi authnever contacts the endpoint. The ledger check andpendingclaim are one locked step, so two processes sharing the cache directory cannot both POST inside the interval. If that claim cannot be recorded (unwritable cache directory, or the lock cannot be taken), the request does not leave and the report ismuse_read_unrecorded. auth.jsonis read forproviders.meta.access_tokenonly; a siblingrefresh_tokenis checked for presence and its value is dropped while the file is parsed. An unreadable or malformed file with no working fallback is an operational failure (status: error), notauth_required. On macOS, records withstorage: "keychain"point at the login Keychain item (serviceai.meta.dev.credentials, accountmeta) whose JSON bundle holds the OAuthaccess_tokenbeside the CLI's minted Model API key; the bundle is parsed with its own key allowlist, so the minted key never survives parsing, and the value read is gated like the Claude and Cursor CLI Keychain sources (presence check only on plain quota calls,--allow-keychain-promptor the recorded grant marker for the value).quota-axi authnever fetches that bundle unless--allow-keychain-promptis on, even when a grant marker exists, because the report only emits status and the bundle carries the mintedapi_key. HTTP 401 on a credential stored beside a refresh token is soft expiry (authStatus: expired_refreshable, statusunavailable, stale fallback served); without one it is sign-out. A present Keychain source skipped without being probed (keychain_prompt_required,keychain_access_denied, orkeychain_prompt_timeout) is never a sign-out: the blocked read is reported with that error and statuserror, and the cache is preserved even when a later sibling is rejected. HTTP 403 is not sign-out evidence. Muse has no delegated refresh, andProviderOptions.refreshCredentialsis ignored.- Snapshots are cache-scoped by an opaque SHA-256 of the answering source plus a one-way digest of its credential. That identity holds on the adapter path: interval replay and stale fallback require it to match, so a token the Muse CLI has since refreshed is a cache miss, never a cross-attribution. Muse is excluded from
--max-age/QUOTA_AXI_MAX_AGEfresh reuse, because a macOS Keychain login switch does not change a traced file and must not be served under the new identity; the five-minute key-endpoint ledger remains the bound on vendor calls.
Higgsfield
- Higgsfield quota is read only through the installed vendor CLI. The fixed argv are
higgsfield account status --json(plan and remaining credits),higgsfield account transactions --json --size 100(subscription-credits grant used as the allowance), andhiggsfield generate list --json --size 20(job status counts). quota-axi never runshiggsfield auth token, never reads Higgsfield credential files, never drives a browser, and never imports cookies. - The status payload may include an account email. That field is ignored: it is never copied onto
account, never logged, never cached, and never written into a fixture. - Status does not include a credit limit or reset. A
creditswindow with percentages is published only when the fetched first transactions page (--size 100, never paginated) includes agrantwhosedisplay_nameisSubscription Creditsas that page's only positive-credit entry, and the remaining balance reconciles with that grant plus the sum of the on-page entries newer than it (fractional credits included, within the vendor's 0.01-credit rounding precision) - so residue the page cannot show can never inflate the percentage. No 6,000-credit Ultra cap is hardcoded. When the grant is off the page, when any other positive credit is on it, or when the balance does not reconcile with it, remaining credits stay a rawcredits.remainingbalance. Without a vendor reset, pace staysunknown, and runway readsexhausted_nowat zero balance andunknownotherwise, namingcreditsinunmeasurableWindowIds. - Job list entries are reduced to status counts (
jobs.sampled,jobs.completed,jobs.failed,jobs.other), which default TOON names as ajobsattention row and--tuiprints on the card. Prompts, URLs, job ids, and model names never enter the snapshot. Those counts are stored on the snapshot the same waycreditsis, and a cached entry that never carried jobs stays without them rather than inventing zeros. Higgsfield is excluded from--max-agereuse: a CLI login switch is not a traced file, and the status payload has no stable non-email account discriminator, so a reused reading would be able to attribute another seat's credits and jobs. The auxiliary transactions and list commands run concurrently; a failure in either omits that auxiliary rather than failing a successful status reading, and a failed transactions or list read is additionally named as adegraded_sourceattention row on the fresh report (higgsfield-transactionsorhiggsfield-jobs), so a broken read is distinguishable from a first page whose grant has scrolled past or an empty job list. - Higgsfield has no delegated refresh.
ProviderOptions.refreshCredentialsis ignored. The CLI is the credential source: inspectAuth reports it present or missing without probing quota.
quota-axi reports quota; it is not an auth app. It never mints a credential, never rotates one, and never performs a refresh-token exchange over HTTP. Those refresh tokens rotate on use, so a second holder performing the exchange would spend the vendor's own single-use token and sign the user out of the harness being measured.
Instead, when the same stored access token is expired, carries a refresh token, and is definitively rejected, quota-axi may run the vendor CLI's own smallest non-interactive command that already owns rotation, then re-read the store that CLI rewrote and retry the same read-only quota request once. Rotation is always the vendor's; quota-axi only reads the result. A provider gets a delegate only when that command's rotation is established from the vendor's own CLI. Leaving a provider read-only is always allowed. Route any new delegate through src/providers/delegated-refresh.ts rather than spawning ad hoc. ProviderOptions.refreshCredentials gates it: --no-credential-refresh turns it off, and the read-only auth command always passes false. Tests pass it explicitly so no test can spawn a vendor CLI by accident.
| Provider | Vendor-owned recovery path | Store the vendor rewrites |
|---|---|---|
| Claude | claude doctor delegate |
the Claude Code Keychain item, or .credentials.json |
| Codex | existing app-server quota probe |
$CODEX_HOME/auth.json |
| Grok | grok models delegate |
$GROK_HOME/auth.json |
| Cursor, GitHub Copilot, Kimi, Z.AI, Alibaba, OpenCode Go, Antigravity, Command Code, MiniMax, MiMo, DeepSeek, OpenRouter, ElevenLabs, Devin, Muse, Higgsfield | none | read-only; see the per-provider notes below |
The Claude and Grok delegated runs are bounded the same way:
- Fixed argv declared in this repository, resolved through
PATH(or a provider's absolute binary override), never through a shell and never assembled from provider responses, configuration, or user input. - No interactive surface: the child gets no stdin, so a vendor command that would prompt exits instead of waiting;
TERM=dumb,NO_COLOR=1, and the vendors' ownNO_BROWSER/NO_OPEN_BROWSERopt-outs are forced. No delegate opens a browser, starts a session, or spends the quota being measured. - A wall-clock budget that bounds how long quota-axi waits, never how long the vendor may run. quota-axi never signals a delegated run: the command is the vendor performing a single-use OAuth refresh-token exchange against its own store, and interrupting one part way through is the sign-out this whole design exists to prevent. When the budget expires quota-axi stops waiting, leaves the vendor running, and reports the refresh as unconfirmed. The delegate also runs in its own process group, so a Ctrl+C that quits a live
--tuicannot land on a vendor mid-exchange. - At most one delegated refresh per credential source per quota read. That bound is per read rather than per process, so a long-running
--tuistill recovers from a session that expires while it is up. - Vendor output is discarded at the operating system, never read. A credential is never parsed out of a vendor's stdout; the refreshed value only ever comes from re-reading the vendor's own store.
- It runs only for soft expiry: a stored-expired credential that carries a refresh token and was definitively rejected. Transient failures, missing or malformed stores, stored-valid credentials the server revoked, and relocated stores the vendor would not rewrite all stay read-only.
- Process-table discovery (Antigravity loopback endpoints and this Claude check) shells out to
psthroughcurrentUserProcessListArgsinsrc/lib/process.ts. Linux procps rejects the BSD-xselector alongside-u, so a new process-table read goes through that helper. Tests mocksrc/lib/running-processes.tsso the machine's real process table never decides a test. - Claude adds a best-effort check before delegating: the process list must show no Claude Code process. Claude Code owns that session and refreshes it on its own schedule, so
claude doctoralongside a live session is at best redundant and at worst a second holder racing a single-use refresh token. This also means a detachedclaude doctorthat outlives quota-axi's wait is visible to the next read, which stays read-only instead of stacking another refresh on it. Not knowing counts as not safe: where the process list cannot be read (Windows, no effective uid, nops), quota-axi stays read-only rather than guessing. The check and spawn are not atomic, so a Claude Code session starting after the check or another concurrent quota-axi read can still overlap the delegate. This narrows the common repeated five-minute--tuiversus live-session collision and, together with never signaling the delegate, is strictly safer than force-killing without adding a failure mode beyond the pre-existing vendor-owned race. --no-credential-refreshdisables it entirely, the read-onlyauthcommand never delegates a refresh, and neither does--profile-only.
A Claude or Grok delegated run appears in --full output as its own attempt (claude-cli-refresh, grok-cli-refresh). Its error says what happened, so a report shows why no refresh took place:
| Attempt error | Meaning |
|---|---|
refresh_command_not_found |
The vendor CLI is not installed, so there was nothing to delegate to (skipped). |
refresh_spawn_failed |
The vendor command could not be started. |
refresh_live_vendor_process |
Claude Code is already running and owns its own refresh (skipped). |
refresh_vendor_processes_unknown |
quota-axi could not read the process list, so it stayed read-only (skipped). |
refresh_timed_out |
The vendor outran quota-axi's wait and was left running; the outcome is unknown. |
refresh_exit_status |
The vendor ran and exited non-zero; the store was still re-read. |
A refresh_timed_out run is never treated as a credential verdict. Claude reports that read as unmeasured (claude_refresh_unconfirmed), falling back to a stale cached snapshot when one applies, and keeps the cached snapshot rather than retiring it. On Windows, a resolved .cmd or .bat command shim runs through the platform command interpreter without enabling Node's shell mode, preserving the no-shell argument boundary. Quota accuracy and the no-shell safety guarantee are unchanged. Codex needs no extra spawn: its existing read-only cli-rpc app-server probe both refreshes auth.json and returns the rate limits, so an expired Codex token already reports live quota through the vendor CLI.
Providers with no established non-interactive rotation command stay read-only on purpose. That is a documented limitation rather than a reason to force an unsafe path: Cursor's CLI token is long-lived and no non-interactive cursor-agent command was observed to rotate it, GitHub Copilot has no established non-interactive rotation command for its supported sources, Z.AI uses a non-expiring API key, Alibaba is accessed through the read-only bl usage command, OpenCode Go has no vendor-owned rotation command, Pi-owned OAuth entries (openai-codex, xai, kimi-coding, commandcode) have no non-interactive Pi refresh command, Command Code API keys do not expire, ElevenLabs API keys do not expire either, Devin's session token does not expire and has no refresh token, Muse exposes no rotation command beyond the key endpoint its quota read already uses, Higgsfield is accessed through read-only higgsfield account / generate list commands, and Antigravity exposes no credential store at all.
- Quota and auth HTTP requests go only to first-party provider usage, quota, billing, entitlement, or read-only credential-liveness endpoints with the user's local credentials; quota-axi's direct Antigravity requests stay on 127.0.0.1 loopback.
- The user-initiated
updatecommand is the only outbound non-provider network surface, and it is not part of quota measurement. - It sends credential values only to the first-party provider request they authenticate.
- It never prints, logs, or caches credential values.
- It never mints, rotates, or writes a credential, and never performs a refresh-token exchange. Credential renewal is always delegated to the vendor CLI that owns the store (see Delegated credential refresh). Muse is the one provider whose only quota read, the Muse CLI's own startup request, also returns an API key; that read is verified to return the account's standing key rather than mint a fresh one, quota-axi discards the key while parsing, never stores or prints it, and bounds how often it sends the request (see Muse provider notes).
- It never retains, prints, logs, renders, caches, sends, or exchanges a refresh token's value. The Pi credential brokers read a stored refresh value only to derive a usability boolean - whether it is a usable literal secret rather than absent or an environment, template, or command reference - and discard it immediately; elsewhere only its presence is checked, as evidence that the vendor can still recover.
- It never launches the Cursor, GitHub (
gh), Copilot, Pi, Kimi, MiniMax, OpenCode, Command Code, ElevenLabs, Devin, or Muse CLIs. It runs the read-only Alibabablusage command, the read-only Higgsfieldaccount status/account transactions/generate listcommands, the declared read-only Codex app-server probe, Antigravity's noninteractive structured/quotaread (agy -p "/quota"), preferred ahead of its loopback access, claude-swap'scswap list --jsononly under theQUOTA_AXI_CLAUDE_SWAPopt-in (claude-swap accounts), and the two declared refresh delegates (claude doctor,grok models); none starts an agent session or spends the quota being measured. The one explicit exception is--allow-claude-inference, whose fixed native Claude request spends inference quota and is documented above. - It never signals or kills a delegated refresh. A vendor that outruns quota-axi's wait is left to finish its own token exchange, and quota-axi reports an unconfirmed refresh instead of a credential verdict.
- It never routes, ranks a winner, or orders providers preferentially. Derived comparative signals, including
effectiveAvailability[].selection, are published as data for the consumer to act on.
| Item | Behavior |
|---|---|
| Quota cache | Lives at ~/.cache/quota-axi/quotas.json or under $XDG_CACHE_HOME/quota-axi/ when XDG_CACHE_HOME is set. |
| Quota cache permissions | Uses 0600 file permissions. |
| Quota cache contents | Stores normalized non-secret snapshots only. |
| Claude Keychain access marker | Lives alongside the quota cache as claude-keychain-access-granted-<service-hash>-account-<account-hash>, where the service hash is eight hexadecimal characters and the account hash is sixteen. It uses 0600 file permissions, contains no credential material, raw account name, or raw service name, and markers written by earlier versions are ignored rather than deleted. |
| Cursor CLI Keychain access marker | Lives alongside the quota cache as cursor-cli-keychain-access-granted-account-<account-hash>, where the account hash is sixteen hexadecimal characters. It uses 0600 file permissions and contains no credential material or raw account identity. |
| Muse Keychain access marker | Lives alongside the quota cache as muse-keychain-access-granted-<hash>, where the hash is the SHA-256 of the JSON array [service, account]. It uses 0600 file permissions and contains no credential material, raw account name, or raw service name. |
| Cached reports | Only fresh provider snapshots with windows are cached. Cache schema 3 slots one snapshot per provider plus accountKey; a snapshot with no key holds that provider's default slot, and the filler default key an expanded report stamps on single-account providers is never written, so a stale reading can never expand a report in which nothing expanded. Each lane reads its own slot, except the codex-home compatibility lane may also read the legacy keyless Codex slot, and a slot name alone cannot say which account signed in there, so a provider whose slot can be rebound stamps the snapshot with its own account identity (see Codex cache identities below). Schema 1 and 2 files without accountKey remain readable. A context-scoped snapshot for Claude, Copilot, Kimi, Command Code, MiniMax, ElevenLabs, Devin, or Muse is written only when the current reading supplies its context identity. A reading served from the cache is never written back. |
| Fresh provider reports with no windows | Clear any cached snapshot for that provider/account lane, so entitlement-only reports do not leave stale quota windows behind. Claude, Kimi, Command Code, MiniMax, ElevenLabs, Devin, and Muse skip both cache write and clear when the current reading has no context identity. |
| Reports and details not cached | Failed providers, stale providers, account identity, and source attempts are not cached; a snapshot keeps only a hashed subscription stamp (see Same subscription across sources). PHI-safe job counts (jobs.sampled / completed / failed / other) are stored with the snapshot the same way credits is; a snapshot that never carried jobs stays without them, and a malformed stored jobs rollup rejects the whole cached record rather than serving its credits without jobs. Higgsfield is excluded from --max-age fresh reuse (a CLI login switch is not a traced file, and the status payload has no stable non-email account discriminator). Claude native inference readings and Copilot native secure-store readings (source: cli) are excluded from both cache writes and clears, leaving existing stored-session snapshots untouched. |
| Definitive sign-out | A definitive sign-out reports auth_required whether or not a snapshot exists, and retires that snapshot so a later failure cannot serve it. Transient failures and expired_refreshable soft expiry keep the stale fallback, and so does a sign-in verdict reached while a present store was skipped without being probed (unreadable, malformed, or awaiting a secure-store grant): only a credential the vendor rejected, or no credential at all, retires the snapshot. A Copilot sign-out still retires its snapshot beside a GitHub CLI login that is keyring-stored or unreadable, because that login is not evidence of Copilot access. Grok, Cursor, Copilot, and Codex share staleUnlessSignOut for that split. Cursor and Copilot have no expired_refreshable verdict. A Codex credential the endpoint rejects is a sign-out, and retires every Codex snapshot stamped with that stored account id, unless the rejected credential was stored-expired and its store holds a refresh token (presence only, never its value): that is expired_refreshable with status unavailable, and the account's stale snapshot stays. Unstamped snapshots and snapshots stamped for another account remain. A Codex CLI fallback that then reports the account signed out does not replace that sign-out, and neither does one that cannot read the limits after the endpoint rejected the auth.json login the CLI shares; after a rejected Pi entry, the CLI reads a different login, so its failure to read limits is its own error and keeps the stale fallback. |
| Fresh reuse stamp | A snapshot from a provider reading whose every lane was cacheable also carries a reuse stamp for Fresh reuse, except Muse and Higgsfield (see Muse cache provenance and Higgsfield cache provenance below): a hashed credential-selection identifier, the paths of the local files the reading was derived from and a hash of their state (never their contents), the reading time and lane position, and that reading's accountKeys, accountLocator, authStatus, and degradedSources. Stale fallback never reads the stamp, so a stale report carries none of that state forward. |
| Stale fallback bound | Every provider with a stale cache fallback serves only cached windows that can still be true, through one shared rule. A window whose own resetsAt is at or before the moment of the fallback is dropped, because the vendor has already started a new cycle. A window without a usable reset is kept only while the snapshot is younger than the shortest cycle it admits, because after that long a reset has certainly happened: its own windowSeconds, else five hours for session, seven days for weekly, and 28 days (the shortest month) for monthly. A resetless credits, model, or unknown window with no declared duration has no such bound and is never served, and neither is a resetless window from a snapshot whose refreshedAt cannot be read. A snapshot whose refreshedAt is later than the moment of the fallback (written while the clock ran ahead) serves no window at all, because its age cannot be known. A stale report keeps a cached untrustedWindowIds entry only when it names a window that survived or never named a cached window. When no window survives, the transient failure is reported as the failed read, with the same status, windows, and exit code as a run with no cache. Provider rows below add provenance and identity checks on top, and a provider's own shorter resetless bound still applies to the windows the shared rule keeps: Claude also withholds a snapshot older than seven days and caps a resetless session or monthly window at five hours and a resetless weekly or model window at seven days, Kimi caps every resetless window except weekly at five hours, and ElevenLabs never serves a resetless window. |
| Claude cache fallback | Follows the Claude provider rules, including the denied-Keychain exception. Eligible fallback uses a formerly fresh snapshot from the same selected Claude configuration context, with a seven-day provider bound on top of the shared stale fallback bound. Its opaque SHA-256 context identifier includes the configuration directory and the selected Keychain service, which already encodes any secure-storage selector. Each lane, including the one lane of a run without Claude profiles, is stamped with its own profile's context; a definitive sign-out retires only snapshots written under that context, and when several slots share a context the fallback uses the most recently refreshed one. Legacy context-less records, snapshots from the earlier broad suffix-discovery context, and Claude snapshots written by 0.1.50 with inverted usage percentages are withheld without deleting them. |
| Codex cache identities | Cached Codex windows are accepted only when ID, label, kind, duration, and duplicate suffix order agree; stale snapshots with mismatched identities are rejected. A snapshot also records an opaque SHA-256 identifier of the ChatGPT account id the credential that produced it stored, and is withheld when the credentials a failed reading actually tried store only other accounts' ids, because a slot name alone cannot say whose windows it holds: a credential that reading never tried does not vouch for the snapshot, and a probe that fails transiently is vouched for only by its own credential. The vendor's response id is not the stamp: it can differ from the store while the same token is live, and a failed probe only has the store. A snapshot without that identifier, or a run whose tried credentials name no account, cannot establish ownership and is withheld from stale fallback. |
| Grok cache provenance | Only snapshots produced by the current web consumer operation can be used as Grok stale fallback; legacy api billing-proxy snapshots are rejected. |
| Kimi cache provenance | Follows the Kimi provider rules: a snapshot is reused only when its opaque SHA-256 identifier still matches the source that produced it - the selected Kimi Code environment, or the Pi source and its default endpoint - and legacy context-less Kimi records are not reused. A reusable snapshot written before shareOf existed stores month_code with percentUsed alone, so reading it restores the shareOf: month_total marker; a stale fallback after upgrading therefore still presents that window as a used share rather than as missing data. |
| Command Code cache provenance | Follows the Command Code provider rules: a snapshot is reused only after the current whoami identifies the account and the opaque SHA-256 source-plus-account identifier still matches; a failed or unidentified whoami cannot select stale data from an unknown account and does not cache or clear the existing record. |
| MiniMax cache provenance | Follows the MiniMax provider rules: a snapshot is reused only when its opaque SHA-256 identifier still matches the credential source and deployment host that produced it, and legacy context-less MiniMax records are not reused. |
| ElevenLabs cache provenance | Follows the ElevenLabs provider rules: each snapshot carries an opaque SHA-256 of the answering source plus a one-way digest of the key that produced it, and stale fallback requires that identity to match. A reading with no usable key neither caches nor clears the record, and a cached window whose own reported reset has already passed is never served. As a deliberate exception to the shared stale fallback bound, a resetless ElevenLabs window is never served at all: the character allowance is not given a shelf life the vendor never reported. |
| Context-scoped identity | A provider whose slot name cannot fix the account, or whose local configuration selects the account, stamps snapshots through CONTEXT_SCOPED_PROVIDERS in src/cache.ts rather than a per-provider field. The stamp describes the source that answered. A context read from a file the vendor rewrites is taken before the request and is not re-derived at write time. Kimi publishes that identity through src/providers/kimi-cache-context.ts. |
| Devin cache provenance | Follows the Devin provider rules: each snapshot carries an opaque SHA-256 of the answering source, host, and a one-way digest of the token that produced it, and stale fallback requires that identity to match. A reading with no resolved credential neither caches nor clears the record. A definitive rejection retires only a snapshot whose identity matches the rejected token. |
| Muse cache provenance | Follows the Muse provider rules: each snapshot carries an opaque SHA-256 of the answering source plus a one-way digest of the credential that produced it, and both interval reuse and stale fallback require that identity to match. Muse is not stamped or served through --max-age fresh reuse. A reading with no usable credential neither caches nor clears the record. |
| Higgsfield cache provenance | Higgsfield is not stamped or served through --max-age fresh reuse. The installed CLI login is not a traced file, and the account status --json payload quota-axi already fetches has no stable non-email account discriminator (email is ignored and never stored), so a reused reading would be able to attribute another seat's credits and jobs. Higgsfield has no stale fallback: a transient CLI failure reports the failure and reads no snapshot. |
| Muse key-endpoint ledger | Lives alongside the quota cache as muse-key-reads.json (0600). It maps each Muse credential's opaque context digest to the time and outcome class (pending, quota, rejected, transient) of the last key-endpoint request. A successful reading with no windows also stores a non-secret empty-quota marker (plan if present, untrustedWindowIds if any, original refreshedAt) so interval replay can return that observation; it never stores an api_key, token, email, payment field, or quota figure. The ledger holds no credential material or account data, and drops entries older than the five-minute interval on each write. |
pnpm install # Install dependencies
pnpm run build # Compile TypeScript to dist/
pnpm run lint # Run ESLint
pnpm run format:check # Check Prettier formatting
pnpm test # Run fixture parser and CLI tests
pnpm run build:skill -- --check # Verify the generated skill is current
pnpm run dev # Run the CLI with tsxProvider tests use synthetic credential stores and mock Keychain, vendor-process, and HTTP boundaries, including Copilot CredReadW and the Claude process list. Never validate against a developer's live credentials, invoke the real security binary, refresh authentication, or call provider APIs. Adapter behavior is clean-room from each vendor's own OAuth/HTTP behavior. quota-axi carries no vendored third-party adapter code. The Z.AI adapter is the one attribution exception (derived from opencode-glm-quota, MIT), credited in src/providers/zai.ts; no third-party code is copied. The committed pnpm-lock.yaml is Prettier-formatted. After changing dependencies, run pnpm exec prettier --write pnpm-lock.yaml.
See CONTRIBUTING.md for the no-mistakes PR workflow, generated-file rules, and release-please conventions.
MIT