Skip to main content

aiusage

CI

One screen for every AI coding agent's rate limits. How much of each limit is spent, and when it resets — for all your Claude accounts, Codex and Antigravity at once, refreshed every minute.

Pure Python standard library: no dependencies, no services, no telemetry, no network calls except to the providers' own endpoints.

 AI USAGE   peak 89%                                        next 52s   12:18:54

 ╭─ claude work ─────────────────────────── team ╮  ╭─ claude personal ──────────────────────── pro ╮
 │  5h ███████░░░░░░░░░░░░░░░  31%  ↻ 4h01m      │  │  5h ████████████████████░░  89%  ↻ 2h11m      │
 │  wk ████████░░░░░░░░░░░░░░  35%  ↻ 4d5h       │  │  wk ██████████████████░░░░  82%  ↻ 1d4h       │
 ╰───────────────────────────────────────────────╯  ╰───────────────────────────────────────────────╯

 ╭─ codex ───────────────────────────────── team ╮  ╭─ agy ─────────────────────────────────────────╮
 │  wk ████░░░░░░░░░░░░░░░░░░  20%  ↻ 5d11h      │  │  gemini ───────────────────────────────────── │
 ╰───────────────────────────────────────────────╯  │  5h █░░░░░░░░░░░░░░░░░░░░░   6%  ↻ 2h44m      │
                                                    │  wk █░░░░░░░░░░░░░░░░░░░░░   6%  ↻ 4d20h      │
                                                    │  claude/gpt ───────────────────────────────── │
                                                    │  5h ░░░░░░░░░░░░░░░░░░░░░░   0%  ↻ 4h58m      │
                                                    │  wk ░░░░░░░░░░░░░░░░░░░░░░   0%  ↻ 6d22h      │
                                                    ╰───────────────────────────────────────────────╯
 q quit   r refresh   poll 4.1s

5h is the five-hour window, wk the weekly one. Percentages are consumed, ↻ is the time until reset. Green below 70%, yellow from 70%, red from 90%. peak in the header is the hottest number across every agent — the one thing to look at.

Cards are packed shortest-column-first, so a two-line card never gets stretched to match a tall neighbour, and the grid folds to a single column on a narrow terminal.

Why

Every agent shows its own limits in its own place, in its own format, and only while you are inside it. When you split work across three CLIs and two Claude accounts, "which one still has room?" becomes a question you cannot answer without opening all of them.

Two design consequences worth knowing before you install:

  • The agent list is not hard-coded. The dashboard scans the machine for logged-in CLIs, so a new account appears without editing anything. See Auto-discovery.
  • Polling is deliberately slow. The Claude usage endpoint throttles hard, per account. The refresh windows here come from measurements, not guesses. See Polling and rate limits.

Requirements

Python 3.9+ and at least one logged-in CLI: claude, codex, or agy (Antigravity). Anything you are not logged into simply does not appear.

Linux and macOS. Windows is untested — the curses TUI will not work there, though --json and --table should.

Install

Single file — no pip, no venv, no root:

curl -L https://github.com/sharaxlex/aiusage/releases/latest/download/aiusage \
     -o ~/.local/bin/aiusage && chmod +x ~/.local/bin/aiusage

As a package, if you have uv or pipx:

uv tool install aiusage-tui                                 # from PyPI
uv tool install git+https://github.com/sharaxlex/aiusage.git   # or straight from the repo

The distribution is called aiusage-tui because plain aiusage was already taken on PyPI by an unrelated project. The command and the import name are both aiusage.

From a clone, for development:

git clone https://github.com/sharaxlex/aiusage.git
cd aiusage
make install     # uv tool / pipx / pip --user, whichever is available
make run         # or just run it out of the clone, without installing

That is the whole setup. Run it:

aiusage

The first run has no config to read, so it scans the machine, keeps what it found in ~/.config/aiusage/config.json, and goes straight to the dashboard — every later run reads that file and scans nothing. Log into a new profile later and aiusage init picks it up; see Auto-discovery.

If the shell says aiusage: command not found, ~/.local/bin is not on your PATH:

export PATH="$HOME/.local/bin:$PATH"

Usage

aiusage                      # full-screen TUI, refreshed every minute
aiusage --once               # one snapshot as cards, then exit (good under `watch`)
aiusage --json               # one snapshot as JSON
aiusage --table              # compact table, one line per limit window
aiusage --cols 4             # more cards per row (default 2)
aiusage --once --width 100   # fix the grid width instead of using the terminal's
aiusage --only codex --only agy
aiusage --interval 30        # seconds between screen refreshes
aiusage --force              # refresh now, ignoring the freshness window
aiusage --no-cache           # skip the on-disk cache (debugging; may earn a 429)
aiusage --no-refresh         # never rotate OAuth tokens

aiusage init                 # find agents and append the new ones to the config
aiusage init --yes           # no questions, for scripts and provisioning
aiusage init -n              # dry run: show what would be written
aiusage init --prune         # also drop agents whose files are gone
aiusage init --reset         # rewrite the agent list from the scan

aiusage uninstall            # remove the files the dashboard wrote

In the TUI: q or Esc to quit, r or space to refresh now (this bypasses the freshness window, but not the backoff — see below).

When stdout is not a terminal the TUI is skipped automatically and one snapshot is printed instead, so piping and watch work without extra flags.

Where the numbers come from

Agent Source Latency
claude GET api.anthropic.com/api/oauth/usage, token from ~/.claude/.credentials.json ~0.3 s
claude personal the same, from ~/.claude-personal/.credentials.json — and likewise for any ~/.claude-* profile ~0.3 s
codex GET chatgpt.com/backend-api/wham/usage, token from ~/.codex/auth.json; on failure, the last token_count from a recent rollout log (shown as from log HH:MM) ~0.5 s
agy agy --print /usage — the CLI itself prints a TSV table of the Gemini and Claude/GPT quotas ~4–5 s

Agents are polled in parallel, so the slow one (agy shells out to a CLI) never holds up the fast ones, and the TUI stays responsive while it happens.

Polling and rate limits

The Claude usage endpoint throttles aggressively, and separately from everything else. Measured against claude 2.1.239, one IP, two accounts:

  • The limit is per account, not per IP and not per application. At the same moment from the same machine: work token → 429, personal token → 200 with fresh numbers.
  • It is usage specifically. Same token, same second: GET /api/oauth/profile → 200, GET /api/oauth/usage → 429. Authentication and token rotation have nothing to do with it.
  • The floor is roughly two minutes per account. A call 30 s after a good one is always refused. A steady 70 s cadence succeeds about every other time (6 of 11). After a refusal, access returns after roughly a minute of silence.
  • Bursts are punished for a long time. A run of back-to-back requests once earned a 429 lasting ~24 minutes, including six-minute pauses inside it.
  • Retry-After is useless — it comes back as 0. The server will not say when to return.

Claude Code itself (fetchUtilization in the binary) makes one request with a 5 s timeout and no retries; on 429 it falls back to a seed value from ordinary API response headers or its own ~/.claude.json → cachedUsageUtilization. It saves a new snapshot at most every 5 minutes and treats a saved one as good for up to an hour. Five-minute staleness is normal by design.

So the dashboard works like this:

  • A freshness window per agent — 10 minutes for Claude (6 requests per hour per account, comfortably inside the measured limits), 2 minutes for Codex and agy. The screen still ticks every minute to recompute the countdowns, but only genuinely stale agents hit the network.
  • An on-disk cache at ~/.cache/aiusage/state.json, shared by every process, so watch -n60 aiusage --once does not hammer the API either. Data older than 90 seconds makes the card show when it was fetched: ╭─ claude work ──── team · 12:41 ╮.
  • Exponential backoff on 429 and 5xx: 5 → 10 → 20 → 30 minutes, or longer if the server did send a Retry-After. The card does not go blank — it keeps showing the last known numbers plus retry in 4m. One success clears the backoff.
  • No retries inside a single poll, exactly like the client: a 429 means back off.

Tune it with "min_interval" on an individual agent. To wipe the state entirely, rm ~/.cache/aiusage/state.json.

OAuth tokens and privacy

The dashboard reads the credential files the CLIs already keep, and talks only to the providers' own endpoints. There is no telemetry, no analytics, no third-party host in the code — there are exactly four outbound URLs in the source (two usage endpoints, two token endpoints), all official:

api.anthropic.com/api/oauth/usage        platform.claude.com/v1/oauth/token
chatgpt.com/backend-api/wham/usage       auth.openai.com/oauth/token

agy is not on that list: it is a local subprocess, and whatever network access it needs is its own business.

Claude access tokens live 8 hours; Codex has its own lifetime. When one has expired, the dashboard refreshes it with the stored refresh token and atomically rewrites the same file the CLI reads (~/.claude*/.credentials.json, ~/.codex/auth.json), preserving the format and 0600 permissions. That is deliberate: if the dashboard kept its own copy, the two would rotate independently and eventually fight. Concurrent runs are guarded by a lock file.

If you would rather it never wrote to a credentials file, use aiusage --no-refresh (or "refresh": false on an agent). Expired tokens then show as token expired — run claude to re-authenticate instead of numbers.

Uninstall

aiusage uninstall lists everything the dashboard has written and removes it after one question:

$ aiusage uninstall

aiusage wrote these files:

  ~/.config/aiusage/config.json  412 B
  ~/.cache/aiusage/state.json    3.1 KB

Credentials of claude and codex are not touched.

Remove them? [y/N] y

Removed 2 files.

To remove the command itself:
  uv tool uninstall aiusage-tui

Add --yes to skip the question. That really is the whole footprint — no services, no logs, and nothing outside those two files except a short-lived ~/.claude*/.credentials.json.aiusage-lock held during a token refresh, which uninstall also sweeps up if a killed process left one behind.

Credentials are deliberately out of scope: they belong to the CLIs, and deleting them would log you out of claude or codex. Rotated tokens stay in those files, where they belong — removing the dashboard breaks no login.

The command cannot delete the program it is running from, so it prints the right line for the way you installed it (uv tool uninstall aiusage-tui, pipx uninstall aiusage-tui, rm ~/.local/bin/aiusage for the zipapp, or make uninstall from a clone).

Auto-discovery

The agent list is not in the source. The dashboard looks for the files the providers would read anyway, with no network access:

Type Where it looks What proves a login
claude ~/.claude*, ~/.config/claude*, plus $CLAUDE_CONFIG_DIR a .credentials.json inside
codex ~/.codex*, ~/.config/codex*, plus $CODEX_HOME an auth.json inside
agy PATH, then ~/.gemini/antigravity-cli/bin/agy, ~/.local/bin/agy, /usr/local/bin/agy, /opt/homebrew/bin/agy an executable file

Card names come from the directory name: leading dot dropped, - and _ become spaces. ~/.claude → claude, ~/.claude-personal → claude personal, ~/.codex_backup → codex backup. Names must be unique, because the name is the cache key, so a clash gets a disambiguator (claude work and claude work (config)).

Plan and e-mail are read from the same files, but only ever printed by init, to tell one account from another. They are not written to the config: a plan changes with the subscription, and a name has to stay stable.

Profiles that physically cannot report usage — an API key in the credentials instead of OAuth — are marked [-] and skipped rather than added as a permanently red card. A profile with a dead refresh token is added, flagged token expired; logging in again is enough.

Re-running init

init is meant to be run whenever: log into a new profile, run aiusage init, and it appends the agent. Everything already in the config is left exactly as it is — renamed cards, enabled: false, a custom min_interval. Rows are matched by type and path, not by name, so a renamed card is not duplicated, and a re-run with nothing new prints Nothing to change and does not touch the file.

Command Effect on the config
aiusage init append new agents only
aiusage init --prune append new, remove those whose files are gone
aiusage init --reset rewrite the agent list from the scan

--prune is for deleted profiles, which otherwise sit in the config forever as a no credentials — log in first card. It only removes rows that lost their evidence of a login; hidden-but-alive agents are kept, as are agents of a type the dashboard does not know — it will not judge someone else's provider.

--reset means "go back to what is visible on the machine", and everything the rewrite costs is shown rather than applied silently:

The agent list will be rewritten from the scan:

  [+] claude                    ~/.claude           team
  [+] claude personal           ~/.claude-personal  pro

Dropped:

  [x] claude old profile        ~/.claude-gone      not found on this machine
  [x] claude by hand            ~/work/profile      still works, but the scan cannot find it
  [!] claude at work            ~/.claude           name 'claude at work', enabled: false, min_interval: 900

Note the second [x]: a profile configured by hand outside the search patterns is alive, but the scan cannot see it, so --reset would drop it. That is why it gets its own reason, and why [!] spells out which manual settings would be lost. interval, columns and any key this version does not know about survive a --reset — only the agent list is rewritten.

The first run

With no config on disk, aiusage scans, saves what it found, and says so once in the footer: saved 3 agents to ~/.config/aiusage/config.json. Installing and running is therefore the whole setup — nothing to configure before the dashboard is useful.

Saving is what makes the next run cheap and predictable: as long as the config lists agents, the dashboard does not stat a single extra path, and the file is there to rename a card, hide one with enabled: false, or give one its own min_interval. Noticing a new profile and running init is then your call — nothing rescans behind your back.

The first run refuses to save in exactly two cases. An empty scan is not written, because recording "nothing installed" would stop every later run from looking again — you get no agents found — run aiusage init instead. And an existing config is never touched, whatever is in it: a file with no agents in it means someone edited it that way, and rewriting it is init's job, not a side effect of starting the dashboard. A config directory that cannot be written to is not an error either — the dashboard just runs off the scan and says auto-detected — run aiusage init to save.

Configuration

The first run writes one (see above), and aiusage init rewrites it. You only need to edit it by hand to change something the scan cannot know — a card's name, whether it is shown, its refresh window:

{
  "interval": 60,
  "columns": 2,
  "agents": [
    {"name": "claude",          "type": "claude", "config_dir": "~/.claude"},
    {"name": "claude personal", "type": "claude", "config_dir": "~/.claude-personal"},
    {"name": "codex",           "type": "codex",  "home": "~/.codex"},
    {"name": "agy",             "type": "agy"}
  ]
}

Per-agent fields: enabled: false hides the card, refresh: false forbids token rotation, min_interval overrides the freshness window in seconds. codex takes home; agy takes bin and timeout. A third Claude account is one more row with a different config_dir — or just log in and re-run aiusage init.

Environment: AIUSAGE_CONFIG overrides the config path (default ~/.config/aiusage/config.json), AIUSAGE_CACHE the cache path (default ~/.cache/aiusage/state.json).

Troubleshooting

What you see What it means
no credentials — log in first no .credentials.json — start that CLI and log in
refresh rejected — run claude to log in the refresh token is dead, or refreshTokenExpiresAt has passed
from log HH:MM on codex the API was unreachable; these numbers come from the last session log
unparsed /usage (...) on agy agy changed its output format, or is not logged in — check agy --print /usage
timeout after 45s on agy the CLI did not answer; raise "timeout" in the config
rate limited · retry in 4m the API refused (429); the last cached numbers are shown, with a countdown to the next attempt
team · 12:41 in a card header the data is cached, fetched at 12:41 (fresh data shows no time)
saved 3 agents to ~/.config/aiusage/config.json the first run froze what it found; later runs read the file
auto-detected — run aiusage init to save in the footer running off the scan without saving — usually a config directory that cannot be written to
no agents found — run aiusage init nothing logged in was found — log into claude or codex and retry
[-] api key mode — no usage endpoint in init the credentials hold an API key, not OAuth: limits are not exposed that way, so the profile is skipped
[x] not found on this machine in init --reset/--prune the configured profile is gone from disk; the row can go
[x] still works, but the scan cannot find it a live profile outside the search patterns: --reset would drop it, --prune would not

Development

pyproject.toml    build (hatchling), the `aiusage` entry point, zero dependencies
Makefile          run / test / install / zipapp / clean
tests/            no network: providers are stubbed, discovery runs against a fake $HOME
src/aiusage/
  __main__.py     CLI: flags, the init subcommand, mode selection (TUI / --once / --json)
  config.py       defaults, ~/.config/aiusage/config.json, merging with the scan
  discover.py     scanning $HOME and PATH, names from paths
  initcmd.py      the `aiusage init` wizard: show, ask, write
  uninstall.py    `aiusage uninstall`: list what was written, remove it
  collector.py    parallel polling: what is stale, what is cached, what is backing off
  model.py        Report / Window — the one shape all data takes
  cache.py        last-good values plus backoff, shared by every process
  cards.py        cards: frames, responsive grid, styled segments
  render.py       bars, percentages, time formatting, the --table mode
  tui.py          the curses screen, background polling, the timer
  httpx.py        JSON over HTTPS on the standard library
  credfile.py     the credential files the CLIs keep: atomic rewrite, JWT claims
  providers/
    claude.py     OAuth usage + token rotation
    codex.py      wham/usage + the rollout-log fallback
    agy.py        parsing `agy --print /usage`
make test        # or: PYTHONPATH=src python3 -m unittest discover -s tests
make run         # run from the clone without installing
make zipapp      # build the single-file executable into dist/ (~80 KB)

CI runs the tests on 3.9 through 3.14 on Linux plus one macOS job, and separately builds the wheel, the sdist and the zipapp and runs each of them. The test job installs nothing at all — if it ever needs to, the "runs on a bare python3" promise is gone.

Across the matrix with uv:

for v in 3.9 3.10 3.11 3.12 3.13 3.14; do
    PYTHONPATH=src uv run --no-project --python $v python -m unittest discover -s tests
done

This is a src/ layout: import aiusage does not work from the repository root, so the tests always exercise what actually ships rather than the directory next door. The single-file build is possible only because there are no dependencies at all.

Adding a provider

A provider is a function fetch(spec) -> Report | list[Report] that never raises: failures go into Report.error. Register it in providers/REGISTRY and add a row to the config.

To make it discoverable too, add a scanner in discover.py (_claude / _codex / _agy are the models) and one line to discover.LAYOUT: the spec field holding its path, the default the provider assumes, and the file that proves a login. That single table is what init uses to tell agents apart and what --prune uses to decide whether one is still there. Without it the provider simply is not discovered — configured by hand, it still works.

Disclaimer

Unofficial and unaffiliated with Anthropic, OpenAI or Google. It reads local credential files and calls the providers' own endpoints; it is not built on any documented, stable API, so a provider can change a response shape and break a card at any time.

License

MIT.

Metadata

Release files for aiusage-tui 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aiusage-tui 0.1.0
File Size Uploaded
aiusage_tui-0.1.0.tar.gz 48.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aiusage-tui 0.1.0
File Interpreter ABI Platform
aiusage_tui-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 98.8 kB

Release files / aiusage_tui-0.1.0.tar.gz

Download URL aiusage_tui-0.1.0.tar.gz
Size 48.1 kB
Tags Source
SHA-256 checksum
How to use checksums
ade1647fc5185e195e695769caa21ca1aa2f2844cff8679c0b37b9f79ecfadf1
BLAKE2b-256 checksum
How to use checksums
bec22d100773b34157bc98b98dc9aaa49f246b01d0845c1dd0aaed63e7446cbf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / aiusage_tui-0.1.0-py3-none-any.whl

Download URL aiusage_tui-0.1.0-py3-none-any.whl
Size 50.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7442910d36b7002af34d38df63beeb2180869dcbc1a6bce463f50ddbb9bdf380
BLAKE2b-256 checksum
How to use checksums
923d369e87a491340026a49917003e5d36726469fcdf0d3140d75a6f0faf2609
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page