Chapter 20 of 28, in Part III, Repos Worth Reading
ccusage explained: how an agent cost dashboard prices your logs
ccusage read at commit 43af93d: where its dollar figures come from, how it removes duplicate log lines, its 5-hour blocks, and a script that re-runs the rules.
A dollar figure from a local usage dashboard is the output of four decisions: which log lines count, which price table applies, which rule turns tokens into dollars, and how time is cut into windows. ccusage, an open-source tool that reads coding-agent logs on your own machine, makes all four in code you can read. This piece reads it at one commit, names each decision with the file it lives in, and ships a standard-library script that re-runs the rules on the repository's own test transcripts and on a few constructed cases, so you can see how much each decision moves the total.
We did not build or run ccusage itself. Every number below comes from our script, run on 12 October 2026, and the script header says where it simplifies.
The repository on 2026-10-12
Read through the GitHub API on 12 October 2026, the repository had 18,974 stars, 871 forks and 5 open issues. Its old address, ryoppippi/ccusage, now redirects to ccusage/ccusage. The latest release was v20.0.30, published 11 October 2026, and the newest commit on main was 43af93d21c8f from 03:34 UTC on 12 October, titled "chore(pricing): update models.dev snapshot". The top language is now Rust: the tree at that commit holds 506 files, with a rust/adapters/ directory per agent and shared logic in rust/crates/ccusage-core/.
The README describes the tool in one line, "Analyze coding (agent) CLI token usage and costs from local data." The Claude Code guide lists fourteen other sources it reads the same way, from Codex and Gemini CLI to GitHub Copilot CLI. This piece follows the Claude Code path because it is the one with the most rules attached.
Which lines count
The Claude Code source README gives the file layout ccusage scans: projects/{project}/{sessionId}/{file}.jsonl and the older flat projects/{project}/{sessionId}.jsonl, under ~/.config/claude/projects/ and ~/.claude/projects/, or whatever CLAUDE_CONFIG_DIR names. Retention matters before anything else. Anthropic's Claude Code docs say the full transcripts in projects/<project>/<session>.jsonl are deleted once older than cleanupPeriodDays, and "The default is 30 days." A dashboard cannot report a month it no longer has.
The harder problem is that one response can appear on several lines. The README states the cases it handles. "Claude Code writes one line per content block for a single response," and "Claude Code can copy one response into multiple session transcripts." Side-question logs can replay a parent message, cache reads included. The rule in lib.rs, in the function usage_dedupe_hash, is short:
- with a request id, the key is message id plus request id, across all sessions;
- without one, the session id joins the key, and so does the timestamp, unless the message id starts with
msg_, the prefix of real Anthropic ids; - when two lines share a key, the one with more tokens is kept.
The code comment is candid about the third case: "The prefix is a heuristic: only a request ID proves two lines belong to one response." A proxy that reused a msg_-shaped id would see its responses merged. The README links the overcounting bug that produced the sidechain rule, issue #913.
To see the size of the effect, the script builds a seven-line log by hand: one response written as three lines, the same response copied into a second session, one distinct response, and a gateway that sends two responses under one reused id with no request id. The rule keeps 4 entries. Summing every line counts 1,734 output tokens; after de-duplication the count is 1,230. Priced at the snapshot's Sonnet 5.5 rates, the naive sum is $0.031382 against $0.020324, an overcount of 54.4%. The proportions come from a constructed log, so read them as a demonstration of the mechanism, not a measured rate for real sessions.
Three cost modes
cost.rs has three modes and the whole difference between them is four lines of Rust. display returns the stored costUSD or zero. auto, the default, returns the stored costUSD when the line has one and prices the tokens otherwise. calculate always prices the tokens. The docs page shows a log line carrying costUSD; the Claude Code docs we read do not say which current versions write that field, so how often auto falls through to pricing depends on your logs.
The consequence of auto is that a stored figure is trusted whole. The repository's two Claude Code test transcripts make the point, because their costUSD values are round test values. Five lines, 1,240 tokens in all buckets: display and auto both report $1.09, while pricing the same tokens from the snapshot gives $0.013979. Nothing is wrong with the fixtures, which exist to test parsing. The lesson is for real logs. If a stored cost and a priced cost disagree, --mode calculate and the --debug flag, which the docs say lists "Pricing mismatches between calculated and pre-calculated costs", are how you find out.
Where the prices come from
pricing.rs names two upstream documents: LiteLLM's model_prices_and_context_window.json on its main branch and the models.dev API. Both are also embedded at build time, and the update pricing workflow refreshes the committed snapshots on an hourly cron ('0 * * * *'), which is why the newest commit is a snapshot update. At the commit we read, the models.dev snapshot held 3,032 model entries, 76 of them starting with claude. LiteLLM's file at commit ac98cabae5ff, the newest touching it on 11 October, held 4,042. An online run reads LiteLLM's main at run time, so the same log priced on two days can give two answers; --offline uses the embedded copies.
The fast-mode multiplier comes from LiteLLM's provider_specific_entry first and a small overrides file second. LiteLLM's file carries fast: 2 for Claude Opus 5.5, Opus 5 and Opus 4.8, which matches Anthropic's pricing page today: Opus 5.5 fast mode at $8 and $40 per million input and output tokens against $4 and $20 standard, and Opus 5 and Opus 4.8 at $10 and $50 against $5 and $25.
The formula, and two kinds of long context
The token formula in calculate_cost_from_pricing prices input, output, 5-minute cache writes at the cache-write rate, cache reads at the cache-read rate, and 1-hour cache writes at the input rate times a constant, CACHE_CREATE_1H_INPUT_MULTIPLIER: f64 = 2.0. Anthropic's page gives the same rule, "1-hour cache write 2x base input price", and LiteLLM's own 1-hour field, which ccusage does not read, agrees for the two models we checked: writing 100,000 tokens to the 1-hour cache costs $0.80 on Opus 5.5 and $0.40 on Sonnet 5.5 by either route, against $0.50 and $0.25 for the 5-minute cache.
The interesting part is long context, because the file holds two semantics side by side. When a price entry came from models.dev with a context tier, the whole request switches once its context passes the tier size; the comment says the request's input "selects the tier and every bucket is billed entirely at that tier's rate", and context means uncached input plus cache reads and writes. When the higher rates came from LiteLLM's *_above_200k_tokens fields, tiered_cost applies them only to the tokens of each bucket beyond 200,000.
The script prices one constructed request, 60,000 fresh input tokens, 150,000 cache-read tokens and 2,000 output tokens, both ways with the snapshot's Sonnet 4 rates ($3 and $15 below 200,000 tokens of context, $6 and $22.50 above):
| Semantics | Context counted | Price |
|---|---|---|
| Whole request (models.dev tier) | 210,000 tokens, over the 200,000 tier | $0.495 |
| Per bucket, marginal (LiteLLM fields) | no single bucket over 200,000 | $0.255 |
Same tokens, same rates, and one answer is 94% higher than the other. Which one a dashboard uses depends on which source supplied the rates for that model id, and the code is explicit that it never mixes them: the fill step only copies tier rates into an entry that has none, because "each source's rates assume that source's own boundary".
For a model where Anthropic documents the rule, the whole-request reading is the right one. The pricing page says Claude Haiku 5.5 is priced by prompt length, that "A request's prompt length counts all of its input tokens, including cache reads and cache writes", and that each request is priced on its own. The snapshot's Haiku 5.5 entry carries a 100,000-token context tier at the page's prices, so the same constructed request costs $0.0425 at the higher prices, against $0.0085 if the tier were ignored. pricing.rs, which defines the LiteLLM fields ccusage reads, has no *_above_100k_tokens field at this commit, so for Haiku 5.5 the tier comes from the models.dev fill, which is the whole-request kind.
Five-hour blocks start on the hour
The blocks report groups usage into 5-hour windows. The docs say a block is "Triggered by your first message" and "Lasts exactly 5 hours from start time." The code in blocks.rs floors the first entry's timestamp to the hour, current_start = Some(floor_to_hour(entry.timestamp)), and closes a block when an entry is more than five hours past the block start or past the previous entry.
On a constructed day with messages at 10:47, 13:30, 15:20 and 21:00 UTC, the floored rule gives three blocks, 10:00, 15:00 and 21:00, and the 15:20 message opens the second one. Starting the window at the first message instead gives two blocks, with 15:20 inside the window that began at 10:47. The docs also say the windows match "Claude's internal session tracking"; we found nothing on Anthropic's pages we read that says whether a usage window starts on the hour, so treat block boundaries as ccusage's convention.
Running the script
python ccusage-cost-dashboard-explained.py
# sources models.dev snapshot entries 3032 models
# fixtures total cost, auto mode 1.09 USD
# fixtures total cost, calculate mode 0.013979 USD
# dedupe naive overcount 54.4 percent
# tiers claude-sonnet-4 whole-request price 0.495 USD
# blocks floored start: blocks 3 blocks
It downloads four files pinned by commit into ccusage_inputs/ on the first run and writes the CSV below. It never reads your own transcripts. To check a real log against the rules, point the de-duplication function at a copy of one session file and compare line count with kept entries.
The dataset
_README ccusage-cost-dashboard-explained.csv (35 rows, one per result printed by the script)
section sources | fixtures | dedupe | tiers | cache | blocks
item what was measured, in words
value the number (or "none" where a source has no field)
unit models, lines, tokens, entries, USD, percent, x, blocks or UTC
note "constructed" where the input was built by hand, or which rates priced it
Limits
The script re-implements four rules; it is not ccusage and will not match it to the cent. It prices from the models.dev snapshot alone, ignores the fast multiplier, and skips the sidechain-replay matching, the advisor records the README describes and the dated price schedules. The duplicate, long-request and block cases are constructed inputs; they show what each rule does, not how often it matters in real use. The repository moves several times a day, so every claim here is pinned to 43af93d21c8f.
The useful habit is the one the tool's own --debug flag encourages. A dashboard total is a reading of logs against a price table at a moment, and when two readings disagree, the four decisions above are where to look: the lines kept, the table used, the long-context rule, and the window boundaries. For costing a task before you run it, the method in what one coding task costs still applies; for deciding when a session has spent enough, see when to stop a coding agent, and for what the transcripts on disk hold, coding agent memory across sessions.