English · 한국어
A terminal UI that shows the remaining quota of every account signed in to omo-ai (senpi), plus which account is being drained right now, on one screen.
Text version (the same demo data as the screenshot above)
omo-ai account usage accounts 7 · ok 5 · updated 14:00:00
────────────────────────────────────────────────────────────────────────────
anthropic-subscription
▶ alice (pinned) 5h ██████████████████░░ 88% 16:00 · in 2h
│ 7d ████████████░░░░░░░░ 61% 9/22 14:00 · in 3d
│ Fable ███████████████████░ 96% 9/22 14:00 · in 3d
│ in use · just now
● bob 5h ██████░░░░░░░░░░░░░░ 32% 18:00 · in 4h
│ 7d ██████░░░░░░░░░░░░░░ 31% 9/24 14:00 · in 5d
│ Fable ░░░░░░░░░░░░░░░░░░░░ 0% 9/24 14:00 · in 5d
│ last used 3h ago
● carol expired · senpi refreshes it on next use · no re-login
│ last used 14h ago
chatgpt-subscription
● ann (pro) 7d ██████████████░░░░░░ 71% 9/23 14:00 · in 4d
● dana (team) 5h ████████████████████ 100% 15:00 · in 1h
│ 7d █░░░░░░░░░░░░░░░░░░░ 4% 9/25 14:00 · in 6d
xai
● default 7d █████████████████░░░ 87% 9/22 08:00 · in 2d 18h
│ breakdown GrokBuild 12% · GrokImagine 1%
google
● google n/a · API key · no usage API
[r] refresh [q] quit
Once you run several Claude, Codex, xAI and Kimi accounts through senpi, you keep asking "how much is left on which account?" and "which one am I spending right now?" — and checking by hand every time gets old. omo-usage only reads the credentials and account-pool state senpi already stores, and shows the remaining percentage and reset time per account together with the account being drained right now. Tokens never appear on screen, in the output or in a log, and when a response cannot be interpreted it writes down the reason instead of inventing a number.
You need: Bun ≥ 1.2 and an omo-ai (senpi) login (~/.omo/agent/auth.json).
bun add -g github:orientpine/omo-usage # or: npm i -g github:orientpine/omo-usage
omo-usageUpdating is the same command again. To remove it: bun remove -g @orientpine/omo-usage.
Note
The code is Bun-only (Bun.file, Bun.argv, .ts run without a build step), so bun executes it even if you install through npm. senpi itself runs on bun, so senpi users have nothing extra to install.
~/.omo/agent/auth.json ─ read only ─▶ account roster ─▶ per-provider usage API ─▶ remaining bars · reset times
~/.omo/agent/credential-pool-state.json ─ read only ─▶ lastSuccessAt per slot ─▶ ▶ in use · last used
It opens the two files senpi uses and writes not a single byte. Token refresh and account selection are entirely senpi's job, so leaving omo-usage running changes nothing about how senpi behaves. Usage is re-fetched every 150 seconds (longer than Anthropic's 429 cooldown), and the in-use marker comes from one local file, so that is re-read every 5 seconds.
| Command | What it does |
|---|---|
omo-usage |
TUI. Usage refreshes every 150 s, the in-use marker (▶) every 5 s. r refreshes now, q / Ctrl-C quits |
omo-usage --once |
Fetch once, print one line per row, exit |
omo-usage --json |
The same result as JSON (no tokens) |
omo-usage --help |
Help |
omo-usage | cat |
Piped anywhere it prints a single frame instead of the TUI |
- One account = one block. The first line carries the account name after
●(Codex adds the plan,(pro)/(team); a slot pinned by auth.json gets(pinned)), and the windows below it are tied together by│. - The account being drained right now is
▶. If senpi completed a request on that account within the last 10 minutes, the marker turns into a green▶and the block ends within use · just now. Anything older stays●with a dimlast used 3h ago. senpi picks an account per session, so there can be more than one▶. senpi keeps no such record for Codex, xAI and kimi-coding, so the TUI compares the previous fetch against the current one and writes the drop asin use · -3% since poll 2m ago— different evidence, so different wording (it is not an exact timestamp but somewhere inside the 150-second fetch interval, and since Codex reports whole percents anything below 1 pp goes unnoticed). Once it goes stale:last drain seen 25m ago.--once/--jsonhave no previous fetch, so they show none of this. google has no usage API at all. - The bar and the percentage are what is left. 100% means nothing used yet. Green ≥ 50% · yellow ≥ 20% · red < 20%.
- Window names:
5hsession,7dweekly, and a model name such asFablemeans that model's weekly limit. - Reset times read
16:00when they fall today, otherwise9/22 14:00, followed by a relative time likein 2h. - xAI turns its single weekly credit pool into the bar, and the
breakdown GrokBuild 12% · GrokImagine 1%line below it is each product's share of that same pool (usage, not remaining). - When a reason shows up instead of a bar:
| Display | Meaning |
|---|---|
expired · senpi refreshes it on next use · no re-login |
Only the access token expired. senpi refreshes it with the refresh token the moment it uses that account |
expired · refresh failed · run /login <provider> in omo (name: <slot>) |
The refresh token is dead too. Re-authenticate with /login in the omo TUI |
rate limited (HTTP 429) · retry HH:MM |
The previous bar is kept as is, and the account is not called again before that time |
error · … |
HTTP error, timeout, or a response that cannot be parsed. No numbers are invented |
n/a · … |
A provider with no usage API (google) |
--json gives you an array of accounts. No tokens, no email addresses.
[
{
"provider": "anthropic-subscription",
"slot": "default",
"label": "alice",
"status": "ok",
"detail": null,
"plan": null,
"expiresAt": 1789840800000,
"windows": [
{ "label": "5h", "kind": "session", "remainingPercent": 88, "resetsAt": 1789830000000 },
{ "label": "7d", "kind": "weekly", "remainingPercent": 61, "resetsAt": 1790082000000 },
{ "label": "Fable", "kind": "scoped", "remainingPercent": 96, "resetsAt": 1790082000000 }
],
"pinned": true,
"lastUsedAt": 1789822760000
}
]status:ok·expired·error·unsupported.detailis the reason string (nullwhen there is none).windows[].kind:session(5h) ·weekly(7d) ·scoped(per model) ·other. All times are epoch ms.lastUsedAt: when senpi last succeeded with that account (epoch ms,lastSuccessAtfrom~/.omo/agent/credential-pool-state.json). Providers that keep no such record (Codex, xAI, kimi-coding, google) do not carry the key at all.pinned: trueappears only when auth.json pinned that slot. The drop detection for Codex, xAI and kimi-coding (drained) needs a previous fetch, so it exists in the TUI only and never in--json.- While a 429 keeps the previous values,
retryAtis added; the xAI per-product breakdown addsnote.
# example: just the 7d remaining per account
omo-usage --json | jq -r '.[] | select(.status=="ok") | "\(.provider)/\(.label)\t\(.windows[] | select(.label=="7d") | .remainingPercent)%"'
# example: accounts being drained right now (last 10 minutes)
omo-usage --json | jq -r --argjson now "$(date +%s000)" '.[] | select(.lastUsedAt != null and $now - .lastUsedAt < 600000) | .label'Can I tell which account senpi is spending right now?
The one marked ▶. Every time a request succeeds, senpi writes that slot's lastSuccessAt into ~/.omo/agent/credential-pool-state.json, and omo-usage marks every slot newer than 10 minutes as in use. The TUI re-reads only that file every 5 seconds, so it keeps up regardless of the 150-second usage fetch interval. There is a reason it refuses to declare "this one account": senpi picks an account per session by hash (preferring a pinned account when one exists), so with several sessions several accounts are drained at once. Hence more than one ▶ is possible, and the just now / 3m ago next to it is what you judge by. senpi keeps no such record for Codex, xAI and kimi-coding (stored/slots is empty for chatgpt-subscription and kimi-coding, and xai has no entry at all). For those three the TUI substitutes a comparison of remaining values between fetches — if the remainder dropped, ▶ … in use · -3% since poll 2m ago. That is not an exact timestamp but somewhere inside the 150-second fetch interval, and a rise caused by a reset does not count as drain. google has no usage API.
It says expired — do I have to log in again?
Usually not. An expired access token is normal, and as long as the refresh token is alive senpi refreshes it the moment it next uses that account. omo-usage only reads auth.json, so it cannot refresh on your behalf (refreshing directly would rotate the refresh token and corrupt senpi's stored copy) — it simply shows the expiry as it is. Re-login is needed only when refresh failed is attached, and then you run /login anthropic-subscription in the omo TUI and enter the existing slot name at the name prompt, which replaces it in place.
Is there no shell command like omo auth login?
There is not. omo auth only has check / print-api-key / print-bearer-token. Logging in is /login <provider> or /claude-account add inside the TUI.
rate limited (HTTP 429) keeps showing up on my accounts.
The Anthropic usage endpoint returns 429 for roughly 95 seconds after the last success per token, and it gets more frequent when it collides with senpi's own polling. On a 429 omo-usage keeps the previous bar and does not call that account again before the retry time, and the auto-refresh interval (150 s) is deliberately longer than that cooldown. The value is not gone — it just does not move for a while.
Why is the xAI per-product breakdown not a bar?
GrokBuild 12% · GrokImagine 1% are not each product's own limit but their shares of the same weekly pool, so drawing them as remaining bars would say something false. The only bar is the pool as a whole (creditUsagePercent).
Can a token leak somewhere?
auth.json is opened read-only and tokens are used in the fetch call and nowhere else. The screen state (AccountRow) never holds a token, so --json cannot expose one either. This tool does not write a single byte to auth.json.
| provider | What you see | Where it comes from |
|---|---|---|
anthropic-subscription (claude-sdk-oauth before senpi 2026.9.22) |
Per-account 5h / 7d / per-model weekly limits | GET https://api.anthropic.com/api/oauth/usage (anthropic-beta: oauth-2025-04-20) |
chatgpt-subscription (openai-codex before senpi 2026.9.22) |
Per-account 5h / 7d plus the plan | GET https://chatgpt.com/backend-api/wham/usage (ChatGPT-Account-Id is extracted from the access-token JWT) |
xai |
Weekly credit pool remaining plus the per-product breakdown | GET https://cli-chat-proxy.grok.com/v1/billing?format=credits — the same endpoint as Grok CLI's /usage. omo's xai token is issued by the same OIDC client as Grok CLI, so the bearer alone gets through |
kimi-coding |
Per-account 5h / 7d | GET https://api.kimi.com/coding/v1/usages — omo's Kimi subscription OAuth token (issued through the same device flow as Kimi CLI) is used as the bearer as is |
google |
— (n/a) |
No published usage API |
- Never invent a number. If the response is empty or shaped differently, show the reason instead of a bar.
- Never write to auth.json. Refreshing tokens is senpi's job.
- Expired ≠ dead. The re-login hint is attached only to slots that senpi's failover stamped with
blockReason: "auth_error". - Never declare a single "current account". senpi chooses per session, so every slot shows its own last-drain time and earns a
▶when that is recent (10 minutes). Different evidence, different wording (just nowvs-3% since poll). - A 429 never erases a bar. Keep the previous value, show the retry time, and do not call before it.
- No line ever exceeds the terminal width. Hangul and full-width glyph widths are measured by hand, so columns hold even with color applied.
Implementation notes (the traps in these response formats)
- Codex windows are told apart by length, not by position. Depending on the plan,
primary_windowcan be the weekly one or the 5-hour one, solimit_window_seconds(18000 / 604800) decides. - Claude's per-model weekly limits live only in
weekly_scopedinsidelimits[]. Top-level keys such asseven_day_opusare usuallynull. - The xAI response is proto3 JSON, so a zero drops its key. When
config.currentPeriodis present butcreditUsagePercentis missing, read it as 0 (100% remaining); whencurrentPerioditself is missing it is not a credits response at all, so it stays anerror.{"val":0}arrives as{}. - The Anthropic 429 window is about 95 seconds per token (measured 2026-09-18). Use
Retry-Afterwhen it is present, otherwise 120 seconds as the cooldown. - The xAI slot's label is
default. senpi stores no displayName for xai. - The drain record exists only in
credential-pool-state.json.providers.<provider>.lanes.stored.slots.<slot>.lastSuccessAtis updated on every successful request.leaseis a half-open probe lock (30 s), not an in-use signal. Stale slot names and account-id keys linger in the file, so matching happens only against the current auth.json slot names, and Codex and kimi-coding slots are missing from it entirely (xai has no provider entry at all). That is why those three fall back to the drop in remaining values between TUI refreshes —collect.tscompares the previous result'sremainingPercentper window label, keeps the largest drop asdrained, never treats a rise (a reset) as a drain, and holds on to the previous detection when there is no new one.
git clone https://github.com/orientpine/omo-usage && cd omo-usage
bun install
bun test # tests (fixtures are anonymized real responses)
bun run typecheck # tsc --noEmit
bun run src/main.ts # run locally (= bin/omo-usage.ts)src/
main.ts CLI entry: --once / --json / --help, otherwise the TUI
tui.ts alternate screen, key input, 150 s auto refresh, 5 s pool-state refresh, 429 retry scheduling
collect.ts per-provider fetch, 429 cooldown, drop detection against the previous fetch
parse.ts response -> UsageWindow (Claude / Codex / xAI / Kimi)
auth.ts auth.json -> account roster, expiry verdict and reason, pinned slots
pool.ts credential-pool-state.json -> last drain time per slot (the in-use marker)
credentials.ts token map (kept apart from the screen state)
render.ts frame rendering: width math, color, the ● / ▶ / │ guides
types.ts AccountRow, UsageWindow
