Data sources
OpenCode DB / Claude Code logs / Codex logs / Copilot store / Gemini telemetry / Local-model journal
-> one source registry -> background collectors -> TUI
\-> one-shot read -> JSON / CSV / budgets
Every source is a file on this machine, read in place. This section is the
paths and the switches; what each collector parses, how it keys an event, and
what it deliberately leaves unread are in
docs/provider-support.md.
OpenCode
Automatic. The database is opened read-only and only assistant-message usage
metadata is read. The default path follows XDG_DATA_HOME when set, otherwise:
~/.local/share/opencode/opencode.db
Select another database with --db PATH, the db config setting, or
OPENCODE_DB_PATH.
Claude Code
Automatic. Session logs are tailed from:
~/.claude/projects/<project>/<session-id>.jsonl
Select another directory with --claude-dir PATH, the claude_dir config
setting, or CLAUDE_PROJECTS_DIR. Only the usage block of each assistant
message is parsed; the transcripts also hold prompts, completions, file
contents and anything a tool printed, and none of that is read or retained.
Billing. Claude Code and Codex write identical transcripts on an API key and on a
subscription, and nothing on a usage line says which — so a plan’s traffic priced at list
rates would read as real spend and trip budgets on money that was never charged. The
collector decides how the account pays before pricing runs, from the billing setting, the
environment, the agent’s own config document and Omarchy’s record, in that order. Override
it with --claude-billing / --codex-billing or [collectors.<id>] billing. Full rules and
the exact signals: docs/provider-support.md.
The decision is printed on the source line, and by --doctor, so a wrong guess
is visible rather than silent: · subscription Max 20x, · api billing, or
· billing unknown — set [collectors.claude_code] billing.
Codex CLI
Automatic. Session logs (“rollouts”, one JSONL file per thread) are tailed from both of:
~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<thread-id>.jsonl
~/.codex/archived_sessions/...
Select another Codex home with --codex-dir PATH, the codex_dir config
setting, or CODEX_HOME. Rollouts the CLI has compressed to .jsonl.zst
are read too. Only the session metadata, the model in force and
each token_count event are read; prompts, tool output and reasoning
summaries are not. The same events carry the plan’s rate-limit windows, which
feed the Limits panel. Rows are openai / PAID, priced estimated from the
bundled table, and a model absent from it stays unavailable — no rate is
invented. Token conventions and event identity follow the CLI’s own arithmetic:
Codex CLI.
GitHub Copilot
Automatic. Copilot’s CLI keeps a SQLite store under its home, and the collector reads the per-request table in it:
~/.copilot/session-store.db (also tried: session.db, data.db)
~/.copilot/session-state/<session-id>/events.jsonl
The store is chosen by schema, not filename — whichever candidate actually
has an assistant_usage_events table wins — because the name has moved between
Copilot releases. Where no such table exists, the legacy session log is read
instead, and only its session.shutdown aggregates. Select another home with
--copilot-dir PATH, the copilot_dir config setting, or COPILOT_HOME.
Only the usage columns are read. The turns table and the log’s
user.message / assistant.message records hold prompts, completions and tool
arguments, and none of it is parsed or retained.
Billing. A Copilot seat bills premium requests against a plan, not tokens,
so rows are quota: no cost, and the list-rate figure carried as
API-RATE EQUIV. instead. Override with --copilot-billing or
[collectors.copilot] billing if you are billed some other way.
What it will not do. Copilot records nothing durable for a session that has not shut down, and the per-message records carry no usable input count. Those sessions are reported as unmeasured rather than reconstructed — see Why there is no Cursor collector. Event identity and token conventions: GitHub Copilot.
Gemini CLI
Opt-in, and it needs a setting in Gemini CLI itself. Gemini CLI persists
no usage anywhere by default; its only durable record is its OpenTelemetry
log, which is off until you add this to ~/.gemini/settings.json:
{ "telemetry": { "enabled": true, "target": "local", "outfile": "~/.gemini/telemetry.json" } }
ai-usage-tui --doctor prints that line for you when the file is missing. This
tool never edits Gemini’s settings itself. Point elsewhere with
--gemini-dir PATH, or set Gemini’s own GEMINI_TELEMETRY_OUTFILE and it is
read from there. Only the api_response records are parsed, and only their
token counts, model, timestamps and identifiers.
Local models
Opt-in, and the one source this tool is given rather than finding: a local server keeps no usage store to read, so a completed response has to be piped into the journal.
Ollama speaks its own format — pipe a completed response, single or streamed,
into --record-ollama:
curl -s http://localhost:11434/api/generate \
-d '{"model":"qwen3-coder:30b","prompt":"hello","stream":false}' \
| ai-usage-tui --record-ollama
Everything else that speaks OpenAI’s /v1/chat/completions — llama.cpp’s
llama-server, LM Studio, vLLM — goes through --record-usage PROVIDER, which
records the response under the provider you name:
curl -s http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"hello"}],"stream":false}' \
| ai-usage-tui --record-usage llamacpp
The provider is a required argument, not a guess: it is what decides whether the row is local work at a genuine zero or paid work at a price this tool would then have to look up.
A streamed response reports its token counts only when the request asked for
them with stream_options: {"include_usage": true} — without that, nothing is
recorded and the command says so, rather than journaling a row of zeros. Raw
server-sent events pipe in directly; the data: prefixes and [DONE] are
handled.
Replaying the same response is a no-op — each one is keyed by the server’s own
id — so a wrapper that records twice cannot inflate a total.
contrib/codecompanion/ wires this into
CodeCompanion, which drives llama.cpp from Neovim.
Any other tool — an agent, a gateway, a script of your own — goes through
--record-event, provided it logs its own token counts. There is no response to
parse here: a few lines of jq or a script turn the tool’s log into this tool’s
own terms, one JSON object per line, and pipe them in.
# a tool whose log has lines like
# {"id":"r1","ts":1758000000,"model":"claude-sonnet-5","in":1200,"out":300,"cwd":"/work/app"}
jq -c '{provider: "mytool", model, event_id: .id, created: .ts,
input_tokens: .in, output_tokens: .out, project: .cwd}' ~/.mytool/usage.jsonl \
| ai-usage-tui --record-event
provider, model, input_tokens and output_tokens are required, and so is
one of event_id (the tool’s own id for the request, the better choice) or
created (unix seconds) — with neither, recording the same log twice would
count it twice. Optional: reasoning_tokens, cache_read_tokens,
cache_write_tokens, project (the working directory), session_id, and the
two statements about money an adapter may make. "cost": 0.0123 is a figure the
tool itself recorded, kept as reported and never re-estimated;
"billing": "subscription" says the work was billed against a plan, which makes
the row quota with the list-rate figure beside it, as for Claude Code on a Max
plan. Say neither and the row is priced from the rate tables like any other. No
other cost_status can be supplied: this tool will not vouch for arithmetic it
did not see.
It is strict, because the exit status is all an adapter’s author has to go on. A
key it does not know, a count that is not a whole number, a line that is not
JSON: each refuses the whole batch, by name, before the journal is touched.
And it records measured counts only — an event without its token counts is
refused, never stored as zero, so a tool that keeps none cannot be journaled by
estimating them (why). Do not feed it a
tool that already has a collector here: the two would be counted twice.
ai-usage-tui --agent-guide tells an LLM agent how to write the adapter.
The journal defaults to:
~/.local/share/ai-usage-tui/usage.db
Override it with --journal PATH, the journal config setting, or
AI_USAGE_JOURNAL_PATH.
The journal is the only copy of what was recorded into it, so nothing deletes from it on its own:
not the dashboard, not a recorder, not a timer. --doctor shows its size, its routing events and
its oldest row, and ai-usage-tui --prune-journal DAYS deletes rows older than DAYS and hands
the space back (VACUUM), saying how many rows of how many went. It refuses fewer than 31 days
and never reaches into the current month, because a monthly budget still reads those rows. It
keeps the routing events of a Claude Code session that is still recording, since the hook counts
a session’s earlier rows to know which requests it has already attributed. And it keeps the single
highest-numbered usage row, so an open dashboard misses nothing recorded afterwards. Copy the
file first if you are unsure; a dashboard open across a prune keeps what it had read until it is
restarted.
Pricing tables
When a provider reports no cost, it is estimated from the tables bundled in the binary — no network needed:
| Table | Coverage |
|---|---|
pricing/litellm.tsv |
~3,450 keys across 88 providers, generated from LiteLLM’s community table |
pricing/zen.toml |
~60 curated models: OpenCode Zen ids, stealth models, and anything the community table gets wrong |
Together they price 1,491 distinct model names. The curated table is applied on top of the community one, and a refreshed cache on top of that, so a hand-checked rate always wins. Where providers charge differently for the same model name, the rate follows the provider on the usage row and no bare key is published: Pricing tables.
Every figure is US dollars at list price; nothing is converted. A rate is a
fact as of a date, and the tables’ dates are printed by --doctor and carried in
--summary-json (pricing.community_table_date, pricing.curated_table_date).
Once the bundled tables are more than 90 days old the tool says so — on the
dashboard’s status line, in --doctor and in the summary’s pricing.warnings —
because an old install otherwise prices at old rates without a word. Upgrading
refreshes both tables; --refresh-pricing updates the curated Zen rates only.
These optional network commands update local caches and exit:
# Scrape the current Zen pricing table
ai-usage-tui --refresh-pricing
# Cache the OpenCode Zen model catalog
ai-usage-tui --refresh-zen
# Ask GitHub for the latest release tag and cache it for the dashboard header
ai-usage-tui --check-update
The model catalog is informational. Refreshing it does not create usage data.
Automatic pricing refresh is disabled unless enabled in the collector config.
The release check is never automatic: the dashboard only reads what an earlier
--check-update or opted-in --doctor cached. To keep that current without
running anything by hand, schedule the command — contrib/systemd/user/ has a
daily user timer — so the request is made by the schedule you installed, never
by the dashboard process.