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.