quota-axi
v0.1.47
Published
AXI CLI that reports local agent-provider quota windows without routing or credential minting
Maintainers
Readme
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), and Command Code 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. 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 for Alibaba and Codex where applicable. The only other vendor commands it runs are the declared credential-refresh delegates.
- 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.
Quick Start
Credential-source note: Claude Code and the Cursor CLI (cursor-agent) keep live tokens in the macOS Keychain; Linux cursor-agent stores its access token in ~/.config/cursor/auth.json (or the XDG/$CURSOR_CLI_CONFIG override).
quota-axi does not read macOS Keychain values until the user grants permission, so Claude quota can stay stale and CLI-only Cursor auth can appear unavailable when no other usable credential exists. On Linux it reads only the auth file's accessToken and never its refresh token.
Run quota-axi --allow-keychain-prompt once and approve Keychain access with "Always Allow".
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
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,credits,"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,unknown,unknown,unknown,gemini_weekly,"2026-03-20T00:00:00.000Z"
agy,claude_gpt,90,unknown,unknown,unknown,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[4]{provider,scope,kind,detail,remedy}:
copilot,all,unresolved_windows,chat + premium_interactions,none
zai,tools,unmeasurable,"mcp_month blocks runway + spendPriority",none
agy,gemini,unmeasurable,"gemini_5h + gemini_weekly blocks runway + spendPriority",none
agy,claude_gpt,unmeasurable,"claude_gpt_5h + claude_gpt_weekly 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",
"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[17]{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,gh:hosts.yml,~/.config/gh/hosts.yml,available,none
grok,auth-json,~/.grok/auth.json,available,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:auth.json,~/.local/share/opencode/auth.json,available,none
help[1]:
Run `quota-axi --allow-keychain-prompt auth` to permit macOS Keychain accessInstall
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.
-g installs the skill for all projects (e.g. ~/.claude/skills/); drop it to install for the current project only (.claude/skills/).
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 devAgent Skill
The npm package includes skills/quota-axi/SKILL.md, the same installable skill recommended above.
It is generated from src/skill.ts; update it with pnpm run build:skill and verify it with pnpm run build:skill -- --check.
How It Works
┌────────────┐
│ quota-axi │
└─────┬──────┘
▼
┌───────────────┐
│ provider │
│ adapters │
└─────┬─────────┘
▼
┌───────────────┐ ┌──────────────┐
│ local auth or │ ───▶ │ first-party │
│ runtime │ │ APIs/loopback│
└─────┬─────────┘ └──────┬───────┘
▼ ▼
┌───────────────┐ ┌──────────────┐
│ read-only │ ───▶ │ 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, and Command Code's 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. - No first-run Keychain prompt - macOS Claude and Cursor CLI Keychain value reads are skipped on plain calls until
--allow-keychain-promptsucceeds once for that source, then future plain 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 2 means a usage error.
- No token equivalence - quota-axi does not claim that one provider percentage equals another provider percentage.
CLI Reference
| 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 |
Flags
| Flag | Description |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| --provider claude,codex,cursor,copilot,grok,kimi,zai,agy,alibaba,opencode-go,commandcode | Scope providers |
| --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 |
| --allow-keychain-prompt | Permit macOS provider Keychain access that could prompt |
| --no-credential-refresh | Never run a vendor CLI's own non-interactive credential refresh |
| --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 quota reads
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 is 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 --jsonHuman terminal report (--tui)
quota-axi --tui renders the same redacted report as a live human terminal view instead of TOON: a two-up provider card grid with thin headroom 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), with aPress q to quitfooter hint.--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. 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 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.
- 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). - 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. - Signed-out and failed provider/account lanes stay visible as dimmed cards and are excluded from the fleet totals in the header.
- 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. --tuicomposes with--providerscoping and--full(account identity and source-attempt footers). It is mutually exclusive with--jsonand only supported by thequotacommand.
Multiple accounts
A normal invocation reports every Codex ChatGPT subscription it can discover from sibling entries in one Pi auth.json.
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.
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.
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, or a native login that coalesces into a Pi lane with a fresh reading on either side, removes the cached codex-home snapshot so a later failed probe cannot bring that account back.
--profile-only still reads one native Codex file and never opens Pi auth.
Account keys and compatibility
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 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.
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 falls back to its single selected account only when no usable lane remains.
--full adds the vendor identity the usage endpoint supplied, when any.
If no provider expands, output stays byte-compatible in shape and field order: 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.
Other adapters retain their existing source-selection behavior.
Output Model
The quota command's --json emits schemaVersion: 5, or 6 when a provider expands to multiple accounts.
Normalized schema contract
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 optional 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.
Default report blocks
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 for a rate limit. |
| 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. |
| 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. |
| 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 credential source was superseded: it was broken or unreadable while a sibling source answered. detail is <source> · <error>. One row per source, only on a fresh reading. |
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[]or both. A provider is never silently absent, and a provider with noquota[]row always states itsstate.authStatus- including a positiveusable- as(auth <status>)in itsattention[]detail. quota[]rows stay in provider-declaration order, never sorted by any metric. A compact table with aspendPrioritycolumn must never read as a published ranking.
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.
Output tiers
--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, windows[].startsAt, windows[].windowSeconds |
| windows[].pace.timeRemainingPercent, elapsedPercent, cycleBasis, cycleSeconds, projectedExhaustedAt, projectionConfidence |
| quotaSemantics.description |
| effectiveAvailability[].pace.behindWindowIds, onPaceWindowIds |
| Account identity (account) and per-source attempts |
Everything a consumer branches on stays in the default tier: state.status, stale, authStatus, error, reason, remedyCommand, retryAfter, untrustedWindowIds, and degradedSources; window pace.status, reason, reservePercentPoints, burnMultiple; quotaSemantics.status and unresolvedWindowIds; and every scope's effectivePercentRemaining, boundedBy, limitingWindowIds, boundConflict, runway, selection, and pace aheadWindowIds / unknownWindowIds / worstReservePercentPoints. credits also stays, so a consumer can avoid misreading it as exhaustion.
--tui renders from the complete in-memory model, so demotion never changes what the human report draws.
Quota report shape
| Object | Fields |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Quota report | providers |
| Provider report | provider, optional accountKey, windows, quotaSemantics, state, optional plan, and optional credits |
| Provider report with --full | Also label, source, optional account identity, 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.
Provider state
| 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 credential sources a working sibling superseded on a fresh reading |
When stale or unavailable quota is likely fixable by a one-time macOS Keychain grant, state.reason is keychain_access_required, state.remedyCommand is quota-axi --allow-keychain-prompt, and JSON includes an agent-directed help entry. That prompt remedy is offered only when a Keychain prompt can help (keychain_prompt_required). 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. 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. Default JSON exposes authStatus; when a provider has no quota[] row, compact TOON preserves a defined auth status as (auth <status>) in attention[]. 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 OAuth, Pi xai OAuth, or a Pi xai API key establishes model usability but consumer credit windows cannot be read, state.authStatus is usable, windows stay empty, and state.error is Grok model access available; quota unavailable rather than sign-in required. 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. Claude's env plus stored-credential fallback is the one exception: because env is consulted first and only its own non-definitive failure hands over to a stored source, a later definitive rejection from that stored source outranks the earlier transient env failure instead of being masked by it (see Claude provider notes).
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.
Quota windows
| Field set | Fields |
| --------- | ----------------------------------------------------------------------------------------------- |
| Required | id, label, kind |
| Optional | Percentages, startsAt, reset fields, windowSeconds, credit-spend fields, and derived pace |
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 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.
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 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.
An unfamiliar legacy limit, and a declared map entry that carries no usable ratio, both stay named in unresolvedWindowIds - as limit:<index> or usages:<key> - and turn the provider's semantics partial.
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. 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. 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.
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. An unfamiliar Antigravity window - including a recognized group's unrecognized bucket such as gemini_unknown - 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. Antigravity v1 snapshots carry no cycle history, so each group reports headroom while its pace, runway, and selection stay unmeasurable.
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.
Pace signals
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 and Z.AI 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.
Effective usable runway
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. |
| 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 and excluded from unmeasurableWindowIds, so it never forces runway.status: unknown by itself; the report's other bounding windows still determine the aggregate. 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
