cctrack 0.6.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ cctrack-py is a zero-dependency Python CLI that scans Claude Code and Codex CLI log files and reports token usage and estimated cost. It's a Python port of the Go-based [cctrack dashboard](https://github.com/ksred/cctrack), stripped down to a single command that parses logs and prints a cost report.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Install in development mode
13
+ uv pip install -e .
14
+
15
+ # Run the tool
16
+ python -m cctrack
17
+ # or after install:
18
+ cctrack
19
+
20
+ # Run all tests
21
+ python -m pytest tests/
22
+
23
+ # Run a single test
24
+ python -m pytest tests/test_cctrack.py::test_name
25
+
26
+ # Build distribution
27
+ uv build
28
+ ```
29
+
30
+ ## Architecture
31
+
32
+ This is a single-module CLI tool. All logic lives in `src/cctrack/__init__.py`:
33
+
34
+ 1. **Rate card** (`RATES`, `_an`/`_oa` row builders, `_family_matches`, `get_rates`, `calculate_cost`, `is_known_model`) - Anthropic and OpenAI prices. **Every model version needs its own row**: `_family_matches` lets a row absorb only the exact id, dated snapshots (`-20250514`, `-2025-08-07`) and non-version suffixes (`-codex`) — never a short-number minor version (`claude-opus-5` does NOT match `claude-opus-5-5`) or a size-tier word (`mini`, `nano`, `pro`, `max`, `luna`, `terra`, `sol`, `astra`). This is deliberate: prefix inheritance silently mispriced opus-4-8 (3×) and opus-5-5 (84%) before. Unknown models are flagged `?` and fall back to Sonnet (Claude) or GPT-5 (OpenAI) rates. `get_rates` takes the usage date (time-limited `intro` pricing) and the request's prompt size (`long_context` tiers: Haiku 5.5 >100K, several OpenAI models >272K). Re-verify against the official pricing pages when adding rows; don't infer a price from a neighbouring model.
35
+ 1a. **Energy model** (`ENERGY_WH_PER_MTOK`, `estimate_energy_wh`) - Opt-in (`--energy`) order-of-magnitude estimate keyed off each rate entry's `tier`. Deliberately NOT a function of price: both cache-write TTLs use one figure because a 1h write costs more money but the same compute.
36
+ 2. **File discovery** (`default_dirs`, `discover_files`) - Walks `~/.claude/projects/` and `~/.sandy/sandboxes/` for `.jsonl` files.
37
+ 2a. **Codex** (`discover_codex_files`, `parse_codex_lines`, `parse_codex_events`, `fetch_remote_codex`) - Reads `$CODEX_HOME` (default `~/.codex`) `sessions/` + `archived_sessions/` rollouts, incl. `.jsonl.zst` (stdlib `compression.zstd` on 3.14+, else the `zstd` CLI, else skipped and counted). Lines are `{timestamp, type, payload}`. Prefer `token_usage_record` items (per-response, dedup by `response_id` — forks replay parent history); fall back to legacy `event_msg`/`token_count` only before a file's first record, counting `last_token_usage` only when `total_token_usage.total_tokens` moved (it's re-emitted unchanged). **`input_tokens` includes `cached_input_tokens` and `cache_write_input_tokens`; `output_tokens` includes reasoning** — uncached input = input − cached − cache_write. Model/cwd from the latest `turn_context`; thread id + git branch from `session_meta`. Codex events carry `agent="codex"`.
38
+ 3. **Remote aggregation** (`fetch_remote_jsonl`, `parse_remote_events`, `fetch_remote_hook_data`, `parse_remote_hook_data`) - SSHs to hosts, cats all JSONL logs plus statusline hook data (statusline files, monthly summaries, session baselines) with file separators, parses locally.
39
+ 3a. **Hook parsing** (`parse_hook_rows`, `compute_hook_deltas`) - The hook gives **cost only**. Its `input_tokens`/`output_tokens`/`cache_*` fields are context-window SNAPSHOTS (`input_tokens` ≈ `cache_read + cache_write`; `output_tokens` is just the last message's), not cumulative — never use them for token counts. `cost_usd` is a session-lifetime cumulative that is unreliable in two ways handled by `compute_hook_deltas`: (1) it can already be large the first time a session is observed (a long-running/unattended session that only rendered its statusline partway through), so the **first observation is a baseline that is NOT counted** — booking a session's pre-existing cumulative onto its first-seen day is what produced a bogus $3,370 day; (2) it **drops** on resume/compaction (or when parallel processes share one session id), so a drop **rebaselines without counting the new level** — otherwise a day's booked spend can exceed the counter's own peak, which is impossible for real money. Net rule: **book only observed upward increments**; a rolled-up baseline seeds `prev` so already-counted days aren't recounted.
40
+ 4. **JSONL parsing** (`parse_lines`, `parse_events`, `dedup_events`) - Extracts `assistant` events with token usage. Deduplicates by `requestId` across all of a host's files (last event wins) — resumed sessions (`--resume`/`--continue`) repeat events with the same `requestId` in new files. Events without `requestId` are never deduped.
41
+ 5. **Aggregation** (`aggregate`) - Buckets events into daily and monthly summaries with token counts and costs.
42
+ 6. **Output** (`print_report`) - Prints current month-to-date summary with projections, daily breakdown, and previous month summaries. Cost is composed **per source** (one source per machine): hook cost where a source has statusline coverage, accuracy-factor-adjusted JSONL cost otherwise, summed per date. The accuracy factor is measured per source over dates strictly after that source's hook install day.
43
+ 6a. **Explore** (`print_explore`, `--explore DATE`) - Drills into one day: dollar decomposition by token class, then by model / project / session / machine. Each JSONL event carries its `project` (from `cwd`), `session_id`, `source` (host) and `git_branch` for attribution. Per-session JSONL estimate is shown against the hook's authoritative per-session cost; a >15% day-total divergence is flagged so an inflated estimate (incomplete logs, unknown model) is distinguishable from real spend.
44
+
45
+ Entry point: `cctrack:main` (registered in `pyproject.toml` as a console script).
46
+
47
+ ## Key Design Decisions
48
+
49
+ - Zero external dependencies - stdlib only (json, argparse, subprocess, etc.)
50
+ - Deduplication is global per host by `requestId` (last wins) - resumed sessions replay history into new files and must not double count. Cross-host `requestId` collisions can't occur (distinct API calls).
51
+ - Hook data is authoritative for **cost only**; every token count comes from the JSONL logs. Never mix the two in one figure - the hook's token fields are snapshots and would double count against JSONL cache columns.
52
+ - Never let one machine's hook coverage suppress another machine's JSONL cost - compose per source.
53
+ - Each coding agent is its own source. Codex sources are `exact`: priced straight from their logs, never replaced by the Claude hook and never scaled by the (Claude-derived) accuracy factor. In `--explore`, hook comparisons use only the Claude slice of the JSONL total.
54
+ - Costs are API list prices. For subscription users (ChatGPT plans, Claude subscriptions) they're API-equivalent values, not invoices.
55
+ - Cumulative counters from the hook are not monotonic and don't start at zero when first observed. `compute_hook_deltas` books only observed upward increments (first observation and post-drop levels are baselines, never counted), so a day's hook cost can never exceed the counter's peak and a session's pre-observation history is never mis-dated onto its first-seen day. Test fixtures must start sessions at ~$0 to mirror real statusline behavior.
56
+ - `session-baselines.json` carries each session's rolled-up cumulative across the 90-day retention boundary so straddling sessions aren't re-counted from zero.
57
+ - Build system is Hatch (`hatchling`)
58
+ - Requires Python 3.10+
cctrack-0.8.0/PKG-INFO ADDED
@@ -0,0 +1,212 @@
1
+ Metadata-Version: 2.5
2
+ Name: cctrack
3
+ Version: 0.8.0
4
+ Summary: Claude Code cost tracker — scans JSONL logs and reports token usage + estimated cost
5
+ Project-URL: Homepage, https://github.com/rappdw/cctrack
6
+ Project-URL: Repository, https://github.com/rappdw/cctrack
7
+ Project-URL: Issues, https://github.com/rappdw/cctrack/issues
8
+ Author: Dan Rapp
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: anthropic,claude,claude-code,cost,tokens
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Classifier: Topic :: System :: Monitoring
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+
26
+ # cctrack
27
+
28
+ A lightweight CLI tool that scans [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and [Codex CLI](https://github.com/openai/codex) logs and reports token usage and estimated cost. Zero dependencies, runs anywhere Python 3.10+ is available.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ # Run directly (no install needed)
34
+ uvx cctrack
35
+
36
+ # Or install globally
37
+ uv tool install cctrack
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ # Scan local logs and print report
44
+ cctrack
45
+
46
+ # Last 7 days only
47
+ cctrack --days 7
48
+
49
+ # Aggregate with remote machines via SSH
50
+ cctrack --remote dgx macbook-air
51
+
52
+ # Custom log directories
53
+ cctrack --dirs ~/.claude/projects ~/.sandy/sandboxes
54
+
55
+ # Only one coding agent (default: all found)
56
+ cctrack --agent claude
57
+ cctrack --agent codex
58
+ ```
59
+
60
+ ## Example output
61
+
62
+ ```
63
+ cctrack — Claude Code Cost Report
64
+ ══════════════════════════════════
65
+
66
+ April 2026 — month to date (day 1, 1 active)
67
+ ───────────────────────────────────────
68
+ Input tokens: 23,381
69
+ Output tokens: 199,069
70
+ Cache read: 30,862,977
71
+ Cache write: 1,051,023
72
+ Total tokens: 32,136,450
73
+ Total cost: $25.85
74
+ Avg/day: $25.85
75
+ Projected/mo: $775.50 (based on 1-day avg)
76
+
77
+ Daily breakdown:
78
+ Date Input Output Cache R Cache W Cost
79
+ ──────────── ──────────── ──────────── ──────────── ──────────── ──────────
80
+ 2026-04-01 23,381 199,069 30,862,977 1,051,023 $ 25.85
81
+ ```
82
+
83
+ ## What it does
84
+
85
+ 1. Walks `~/.claude/projects/` and `~/.sandy/sandboxes/` for JSONL log files
86
+ 2. Parses `assistant` events with token usage
87
+ 3. Deduplicates by `requestId` (last event wins, matching Claude Code's semantics)
88
+ 4. Calculates cost using Anthropic's published rates per model
89
+ 5. Prints daily breakdown with input/output/cache token splits and monthly summaries
90
+
91
+ ## Codex CLI
92
+
93
+ cctrack also reads [Codex CLI](https://github.com/openai/codex) usage from `$CODEX_HOME` (default `~/.codex`) — `sessions/` and `archived_sessions/` — and with `--remote`, from each host. When both agents have spend, the month-to-date block and `--explore` show a per-agent split.
94
+
95
+ How it's counted (verified against the Codex source, not inferred):
96
+
97
+ - **Per-response usage.** Newer Codex versions log a `token_usage_record` for every API response with a unique `response_id`; those are counted and deduplicated across files, so forked and resumed threads (which replay their parent's history) aren't double counted. Older versions only log `token_count` events, which are re-emitted with unchanged totals — only events whose running total moved are counted.
98
+ - **Cached tokens are inside `input_tokens`.** Codex reports cached reads and cache writes as parts of `input_tokens`, and reasoning tokens as part of `output_tokens`. Billable uncached input is `input − cached − cache_write`; reasoning is never added on top.
99
+ - **Its own cost source.** Codex spend is priced directly from its logs and is never replaced by, or scaled with, Claude Code's statusline-hook figures.
100
+ - **Compressed rollouts.** Codex compresses older rollouts to `.jsonl.zst`. Python's standard library only reads zstd from 3.14, so on older Pythons cctrack uses the `zstd` command if it's installed, and otherwise skips those files and says how many it skipped.
101
+
102
+ Costs are at OpenAI's **API list prices**. If you use Codex through a ChatGPT plan, that's an API-equivalent value, not what you're billed — the same is true of Claude Code on a Claude subscription.
103
+
104
+ ## Explore a day
105
+
106
+ When a day's cost looks wrong or surprisingly high, `--explore` breaks it down:
107
+
108
+ ```bash
109
+ cctrack --explore 2026-08-03 # a specific day
110
+ cctrack --explore yesterday # or 'today'
111
+ cctrack --explore yesterday --remote air dgx # across machines
112
+ ```
113
+
114
+ It reports, for that day:
115
+
116
+ - **Where it went** — the day's cost decomposed by token class (output, 1h/5m cache writes, input, cache reads), so a cache-write blowup or a runaway output loop is obvious at a glance.
117
+ - **By model / by project / by machine** — cost and share for each, so you can see which model, which repo, and which host drove it.
118
+ - **Top sessions** — ranked by cost, each with its project and model, and — where the statusline hook covered that session — its **authoritative** cost next to the JSONL estimate.
119
+
120
+ The day's JSONL-estimated total is shown next to the hook-authoritative total. If they diverge by more than ~15% the report says so: a big gap means the estimate is off (incomplete logs, or a model the rate card doesn't know), not that you actually spent that much. This is the fastest way to tell a real expensive day from an accounting artifact.
121
+
122
+ The project label is the basename of each session's working directory (`cwd`). When that's an unfamiliar name — a container path like `/workspace/team-inbox`, or a directory you don't recognize — trace it to its real location:
123
+
124
+ ```bash
125
+ cctrack --explore 2026-08-03 --project team-inbox
126
+ ```
127
+
128
+ This restricts the view to that project and prints each session's full `cwd`, git branch, and the log file on disk (which reveals the host directory or the specific Sandy sandbox it came from). The unfiltered view flags this automatically when a `cwd` looks like a container path. `scripts/trace_project.py` and `scripts/diagnose_session_cost.py` are standalone versions of the trace and per-session cost diagnostics.
129
+
130
+ ## Remote aggregation
131
+
132
+ With `--remote`, cctrack SSHs to each host and streams back both the JSONL logs and the host's statusline hook data (`~/.claude/cctrack/`) for local parsing, so authoritative hook costs from every machine are summed per day. Requires SSH key auth.
133
+
134
+ ```bash
135
+ # Aggregate this machine + DGX server + laptop
136
+ cctrack --remote dgx macbook-air
137
+
138
+ # For accurate aggregate costs, install the hook on each host too
139
+ cctrack --install-hook --remote dgx macbook-air
140
+ ```
141
+
142
+ ## Rate card
143
+
144
+ Prices per million tokens. Claude prices are from [Anthropic's pricing page](https://platform.claude.com/docs/en/about-claude/pricing); cache writes are priced per TTL (5-minute and 1-hour).
145
+
146
+ | Claude model | Input | Output | Cache Read | Cache Write 5m | Cache Write 1h |
147
+ |---|---|---|---|---|---|
148
+ | Fable 5.1 / Mythos 5.1 | $10.00 | $50.00 | $0.25 | $12.50 | $20.00 |
149
+ | Fable 5 / Mythos 5 | $10.00 | $50.00 | $1.00 | $12.50 | $20.00 |
150
+ | Opus 5.5 | $4.00 | $20.00 | $0.20 | $5.00 | $8.00 |
151
+ | Opus 5 / 4.8 / 4.7 / 4.6 / 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | $10.00 |
152
+ | Opus 4.1 / 4 | $15.00 | $75.00 | $1.50 | $18.75 | $30.00 |
153
+ | Sonnet 5.5 | $2.00 | $10.00 | $0.10 | $2.50 | $4.00 |
154
+ | Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | $4.00 |
155
+ | Sonnet 4.6 / 4.5 / 4 / 3.7 | $3.00 | $15.00 | $0.30 | $3.75 | $6.00 |
156
+ | Haiku 5.5 (prompt ≤ 100K) | $0.10 | $0.50 | $0.01 | $0.125 | $0.20 |
157
+ | Haiku 5.5 (prompt > 100K) | $0.50 | $2.50 | $0.05 | $0.625 | $1.00 |
158
+ | Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | $2.00 |
159
+ | Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 | $1.60 |
160
+ | Haiku 3 | $0.25 | $1.25 | $0.03 | $0.30 | $0.50 |
161
+
162
+ OpenAI prices (for Codex) are from [OpenAI's pricing page](https://developers.openai.com/api/docs/pricing), Standard tier. OpenAI has a single cache-write price, listed only for some models; elsewhere cache writes bill as ordinary input.
163
+
164
+ | OpenAI model | Input | Cached input | Output | Cache write | Over 272K input |
165
+ |---|---|---|---|---|---|
166
+ | gpt-6-astra | $10.00 | $1.00 | $50.00 | $12.50 | $20 / $2 / $75 / $25 |
167
+ | gpt-6.1-sol | $2.00 | $0.10 | $10.00 | $2.50 | $4 / $0.20 / $15 / $5 |
168
+ | gpt-6-sol | $2.00 | $0.20 | $10.00 | $2.50 | $4 / $0.40 / $15 / $5 |
169
+ | gpt-6-luna | $0.10 | $0.01 | $0.50 | $0.125 | $0.20 / $0.02 / $0.75 / $0.25 |
170
+ | gpt-5.6-sol | $4.00 | $0.40 | $20.00 | $5.00 | $8 / $0.80 / $30 / $10 |
171
+ | gpt-5.6-terra | $2.00 | $0.20 | $12.00 | $2.50 | $4 / $0.40 / $18 / $5 |
172
+ | gpt-5.6-luna | $0.20 | $0.02 | $1.20 | $0.25 | $0.40 / $0.04 / $1.80 / $0.50 |
173
+ | gpt-5.5 | $5.00 | $0.50 | $30.00 | — | $10 / $1 / $45 |
174
+ | gpt-5.5-pro / gpt-5.4-pro | $30.00 | — | $180.00 | — | $60 / — / $270 |
175
+ | gpt-5.4 | $2.50 | $0.25 | $15.00 | — | $5 / $0.50 / $22.50 |
176
+ | gpt-5.4-mini | $0.75 | $0.075 | $4.50 | — | |
177
+ | gpt-5.4-nano | $0.20 | $0.02 | $1.25 | — | |
178
+ | gpt-5.3-codex / gpt-5.2 | $1.75 | $0.175 | $14.00 | — | |
179
+ | gpt-5.2-pro | $21.00 | — | $168.00 | — | |
180
+ | gpt-5.1 / gpt-5 | $1.25 | $0.125 | $10.00 | — | |
181
+ | gpt-5-pro | $15.00 | — | $120.00 | — | |
182
+ | gpt-5-mini | $0.25 | $0.025 | $2.00 | — | |
183
+ | gpt-5-nano | $0.05 | $0.005 | $0.40 | — | |
184
+
185
+ Codex variants without their own price (e.g. `gpt-5.1-codex`) are priced as their base model, consistent with the one variant OpenAI does list (`gpt-5.3-codex` = `gpt-5.2`). Long-context and Haiku 5.5 prompt-length tiers are applied per request.
186
+
187
+ **Every model version has its own row.** A model only matches a row if it is that version or a dated snapshot of it — never a newer minor version. So a newly released model (say `claude-opus-5-7`, or `gpt-5.1-codex-mini`) shows as unknown and is flagged with `?` in the Model column, rather than silently inheriting its predecessor's price. Unknown Claude models are estimated at Sonnet rates, unknown OpenAI models at GPT-5 rates. Pricing is date-aware, so any time-limited introductory price applies only to usage on the days it was in effect.
188
+
189
+ ## Energy estimate
190
+
191
+ `cctrack --energy` adds an estimated inference energy column (Wh/kWh):
192
+
193
+ ```bash
194
+ cctrack --energy
195
+ ```
196
+
197
+ This is a **rough estimate, not a measurement** — Anthropic does not publish per-token energy figures or model sizes. It is modelled from logged tokens using the published full-stack per-prompt figures the major labs have disclosed (Google: 0.24 Wh median Gemini text prompt; OpenAI: ~0.34 Wh average ChatGPT query — both include accelerator, host, cooling and idle capacity), apportioned across token classes by relative compute cost and scaled by model tier:
198
+
199
+ - **output tokens dominate** — decode is sequential and memory-bandwidth bound
200
+ - **input and cache writes** are prefill: parallel, far cheaper per token
201
+ - **cache reads** skip prefill entirely (~0.1× input)
202
+ - both cache-write TTLs use the same figure — a 1-hour write costs more money but the same compute
203
+
204
+ Treat results as ±1 order of magnitude. It covers datacenter inference only: not model training, and not your own machine. Scale the whole model with `CCTRACK_ENERGY_SCALE=1.5` if you have better numbers for your workload.
205
+
206
+ ## Origins
207
+
208
+ This is a Python rewrite of the Go-based [cctrack dashboard](https://github.com/ksred/cctrack). The Go version provides a full web dashboard with real-time updates, session explorer, and project breakdown. This Python version strips it down to the essentials: a single command that parses logs and prints a cost report. The JSONL parsing logic, deduplication strategy, and rate card are ported directly from the Go implementation.
209
+
210
+ ## License
211
+
212
+ [MIT](LICENSE)
@@ -0,0 +1,187 @@
1
+ # cctrack
2
+
3
+ A lightweight CLI tool that scans [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and [Codex CLI](https://github.com/openai/codex) logs and reports token usage and estimated cost. Zero dependencies, runs anywhere Python 3.10+ is available.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # Run directly (no install needed)
9
+ uvx cctrack
10
+
11
+ # Or install globally
12
+ uv tool install cctrack
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ # Scan local logs and print report
19
+ cctrack
20
+
21
+ # Last 7 days only
22
+ cctrack --days 7
23
+
24
+ # Aggregate with remote machines via SSH
25
+ cctrack --remote dgx macbook-air
26
+
27
+ # Custom log directories
28
+ cctrack --dirs ~/.claude/projects ~/.sandy/sandboxes
29
+
30
+ # Only one coding agent (default: all found)
31
+ cctrack --agent claude
32
+ cctrack --agent codex
33
+ ```
34
+
35
+ ## Example output
36
+
37
+ ```
38
+ cctrack — Claude Code Cost Report
39
+ ══════════════════════════════════
40
+
41
+ April 2026 — month to date (day 1, 1 active)
42
+ ───────────────────────────────────────
43
+ Input tokens: 23,381
44
+ Output tokens: 199,069
45
+ Cache read: 30,862,977
46
+ Cache write: 1,051,023
47
+ Total tokens: 32,136,450
48
+ Total cost: $25.85
49
+ Avg/day: $25.85
50
+ Projected/mo: $775.50 (based on 1-day avg)
51
+
52
+ Daily breakdown:
53
+ Date Input Output Cache R Cache W Cost
54
+ ──────────── ──────────── ──────────── ──────────── ──────────── ──────────
55
+ 2026-04-01 23,381 199,069 30,862,977 1,051,023 $ 25.85
56
+ ```
57
+
58
+ ## What it does
59
+
60
+ 1. Walks `~/.claude/projects/` and `~/.sandy/sandboxes/` for JSONL log files
61
+ 2. Parses `assistant` events with token usage
62
+ 3. Deduplicates by `requestId` (last event wins, matching Claude Code's semantics)
63
+ 4. Calculates cost using Anthropic's published rates per model
64
+ 5. Prints daily breakdown with input/output/cache token splits and monthly summaries
65
+
66
+ ## Codex CLI
67
+
68
+ cctrack also reads [Codex CLI](https://github.com/openai/codex) usage from `$CODEX_HOME` (default `~/.codex`) — `sessions/` and `archived_sessions/` — and with `--remote`, from each host. When both agents have spend, the month-to-date block and `--explore` show a per-agent split.
69
+
70
+ How it's counted (verified against the Codex source, not inferred):
71
+
72
+ - **Per-response usage.** Newer Codex versions log a `token_usage_record` for every API response with a unique `response_id`; those are counted and deduplicated across files, so forked and resumed threads (which replay their parent's history) aren't double counted. Older versions only log `token_count` events, which are re-emitted with unchanged totals — only events whose running total moved are counted.
73
+ - **Cached tokens are inside `input_tokens`.** Codex reports cached reads and cache writes as parts of `input_tokens`, and reasoning tokens as part of `output_tokens`. Billable uncached input is `input − cached − cache_write`; reasoning is never added on top.
74
+ - **Its own cost source.** Codex spend is priced directly from its logs and is never replaced by, or scaled with, Claude Code's statusline-hook figures.
75
+ - **Compressed rollouts.** Codex compresses older rollouts to `.jsonl.zst`. Python's standard library only reads zstd from 3.14, so on older Pythons cctrack uses the `zstd` command if it's installed, and otherwise skips those files and says how many it skipped.
76
+
77
+ Costs are at OpenAI's **API list prices**. If you use Codex through a ChatGPT plan, that's an API-equivalent value, not what you're billed — the same is true of Claude Code on a Claude subscription.
78
+
79
+ ## Explore a day
80
+
81
+ When a day's cost looks wrong or surprisingly high, `--explore` breaks it down:
82
+
83
+ ```bash
84
+ cctrack --explore 2026-08-03 # a specific day
85
+ cctrack --explore yesterday # or 'today'
86
+ cctrack --explore yesterday --remote air dgx # across machines
87
+ ```
88
+
89
+ It reports, for that day:
90
+
91
+ - **Where it went** — the day's cost decomposed by token class (output, 1h/5m cache writes, input, cache reads), so a cache-write blowup or a runaway output loop is obvious at a glance.
92
+ - **By model / by project / by machine** — cost and share for each, so you can see which model, which repo, and which host drove it.
93
+ - **Top sessions** — ranked by cost, each with its project and model, and — where the statusline hook covered that session — its **authoritative** cost next to the JSONL estimate.
94
+
95
+ The day's JSONL-estimated total is shown next to the hook-authoritative total. If they diverge by more than ~15% the report says so: a big gap means the estimate is off (incomplete logs, or a model the rate card doesn't know), not that you actually spent that much. This is the fastest way to tell a real expensive day from an accounting artifact.
96
+
97
+ The project label is the basename of each session's working directory (`cwd`). When that's an unfamiliar name — a container path like `/workspace/team-inbox`, or a directory you don't recognize — trace it to its real location:
98
+
99
+ ```bash
100
+ cctrack --explore 2026-08-03 --project team-inbox
101
+ ```
102
+
103
+ This restricts the view to that project and prints each session's full `cwd`, git branch, and the log file on disk (which reveals the host directory or the specific Sandy sandbox it came from). The unfiltered view flags this automatically when a `cwd` looks like a container path. `scripts/trace_project.py` and `scripts/diagnose_session_cost.py` are standalone versions of the trace and per-session cost diagnostics.
104
+
105
+ ## Remote aggregation
106
+
107
+ With `--remote`, cctrack SSHs to each host and streams back both the JSONL logs and the host's statusline hook data (`~/.claude/cctrack/`) for local parsing, so authoritative hook costs from every machine are summed per day. Requires SSH key auth.
108
+
109
+ ```bash
110
+ # Aggregate this machine + DGX server + laptop
111
+ cctrack --remote dgx macbook-air
112
+
113
+ # For accurate aggregate costs, install the hook on each host too
114
+ cctrack --install-hook --remote dgx macbook-air
115
+ ```
116
+
117
+ ## Rate card
118
+
119
+ Prices per million tokens. Claude prices are from [Anthropic's pricing page](https://platform.claude.com/docs/en/about-claude/pricing); cache writes are priced per TTL (5-minute and 1-hour).
120
+
121
+ | Claude model | Input | Output | Cache Read | Cache Write 5m | Cache Write 1h |
122
+ |---|---|---|---|---|---|
123
+ | Fable 5.1 / Mythos 5.1 | $10.00 | $50.00 | $0.25 | $12.50 | $20.00 |
124
+ | Fable 5 / Mythos 5 | $10.00 | $50.00 | $1.00 | $12.50 | $20.00 |
125
+ | Opus 5.5 | $4.00 | $20.00 | $0.20 | $5.00 | $8.00 |
126
+ | Opus 5 / 4.8 / 4.7 / 4.6 / 4.5 | $5.00 | $25.00 | $0.50 | $6.25 | $10.00 |
127
+ | Opus 4.1 / 4 | $15.00 | $75.00 | $1.50 | $18.75 | $30.00 |
128
+ | Sonnet 5.5 | $2.00 | $10.00 | $0.10 | $2.50 | $4.00 |
129
+ | Sonnet 5 | $2.00 | $10.00 | $0.20 | $2.50 | $4.00 |
130
+ | Sonnet 4.6 / 4.5 / 4 / 3.7 | $3.00 | $15.00 | $0.30 | $3.75 | $6.00 |
131
+ | Haiku 5.5 (prompt ≤ 100K) | $0.10 | $0.50 | $0.01 | $0.125 | $0.20 |
132
+ | Haiku 5.5 (prompt > 100K) | $0.50 | $2.50 | $0.05 | $0.625 | $1.00 |
133
+ | Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | $2.00 |
134
+ | Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 | $1.60 |
135
+ | Haiku 3 | $0.25 | $1.25 | $0.03 | $0.30 | $0.50 |
136
+
137
+ OpenAI prices (for Codex) are from [OpenAI's pricing page](https://developers.openai.com/api/docs/pricing), Standard tier. OpenAI has a single cache-write price, listed only for some models; elsewhere cache writes bill as ordinary input.
138
+
139
+ | OpenAI model | Input | Cached input | Output | Cache write | Over 272K input |
140
+ |---|---|---|---|---|---|
141
+ | gpt-6-astra | $10.00 | $1.00 | $50.00 | $12.50 | $20 / $2 / $75 / $25 |
142
+ | gpt-6.1-sol | $2.00 | $0.10 | $10.00 | $2.50 | $4 / $0.20 / $15 / $5 |
143
+ | gpt-6-sol | $2.00 | $0.20 | $10.00 | $2.50 | $4 / $0.40 / $15 / $5 |
144
+ | gpt-6-luna | $0.10 | $0.01 | $0.50 | $0.125 | $0.20 / $0.02 / $0.75 / $0.25 |
145
+ | gpt-5.6-sol | $4.00 | $0.40 | $20.00 | $5.00 | $8 / $0.80 / $30 / $10 |
146
+ | gpt-5.6-terra | $2.00 | $0.20 | $12.00 | $2.50 | $4 / $0.40 / $18 / $5 |
147
+ | gpt-5.6-luna | $0.20 | $0.02 | $1.20 | $0.25 | $0.40 / $0.04 / $1.80 / $0.50 |
148
+ | gpt-5.5 | $5.00 | $0.50 | $30.00 | — | $10 / $1 / $45 |
149
+ | gpt-5.5-pro / gpt-5.4-pro | $30.00 | — | $180.00 | — | $60 / — / $270 |
150
+ | gpt-5.4 | $2.50 | $0.25 | $15.00 | — | $5 / $0.50 / $22.50 |
151
+ | gpt-5.4-mini | $0.75 | $0.075 | $4.50 | — | |
152
+ | gpt-5.4-nano | $0.20 | $0.02 | $1.25 | — | |
153
+ | gpt-5.3-codex / gpt-5.2 | $1.75 | $0.175 | $14.00 | — | |
154
+ | gpt-5.2-pro | $21.00 | — | $168.00 | — | |
155
+ | gpt-5.1 / gpt-5 | $1.25 | $0.125 | $10.00 | — | |
156
+ | gpt-5-pro | $15.00 | — | $120.00 | — | |
157
+ | gpt-5-mini | $0.25 | $0.025 | $2.00 | — | |
158
+ | gpt-5-nano | $0.05 | $0.005 | $0.40 | — | |
159
+
160
+ Codex variants without their own price (e.g. `gpt-5.1-codex`) are priced as their base model, consistent with the one variant OpenAI does list (`gpt-5.3-codex` = `gpt-5.2`). Long-context and Haiku 5.5 prompt-length tiers are applied per request.
161
+
162
+ **Every model version has its own row.** A model only matches a row if it is that version or a dated snapshot of it — never a newer minor version. So a newly released model (say `claude-opus-5-7`, or `gpt-5.1-codex-mini`) shows as unknown and is flagged with `?` in the Model column, rather than silently inheriting its predecessor's price. Unknown Claude models are estimated at Sonnet rates, unknown OpenAI models at GPT-5 rates. Pricing is date-aware, so any time-limited introductory price applies only to usage on the days it was in effect.
163
+
164
+ ## Energy estimate
165
+
166
+ `cctrack --energy` adds an estimated inference energy column (Wh/kWh):
167
+
168
+ ```bash
169
+ cctrack --energy
170
+ ```
171
+
172
+ This is a **rough estimate, not a measurement** — Anthropic does not publish per-token energy figures or model sizes. It is modelled from logged tokens using the published full-stack per-prompt figures the major labs have disclosed (Google: 0.24 Wh median Gemini text prompt; OpenAI: ~0.34 Wh average ChatGPT query — both include accelerator, host, cooling and idle capacity), apportioned across token classes by relative compute cost and scaled by model tier:
173
+
174
+ - **output tokens dominate** — decode is sequential and memory-bandwidth bound
175
+ - **input and cache writes** are prefill: parallel, far cheaper per token
176
+ - **cache reads** skip prefill entirely (~0.1× input)
177
+ - both cache-write TTLs use the same figure — a 1-hour write costs more money but the same compute
178
+
179
+ Treat results as ±1 order of magnitude. It covers datacenter inference only: not model training, and not your own machine. Scale the whole model with `CCTRACK_ENERGY_SCALE=1.5` if you have better numbers for your workload.
180
+
181
+ ## Origins
182
+
183
+ This is a Python rewrite of the Go-based [cctrack dashboard](https://github.com/ksred/cctrack). The Go version provides a full web dashboard with real-time updates, session explorer, and project breakdown. This Python version strips it down to the essentials: a single command that parses logs and prints a cost report. The JSONL parsing logic, deduplication strategy, and rate card are ported directly from the Go implementation.
184
+
185
+ ## License
186
+
187
+ [MIT](LICENSE)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cctrack"
7
- version = "0.6.0"
7
+ version = "0.8.0"
8
8
  description = "Claude Code cost tracker — scans JSONL logs and reports token usage + estimated cost"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """Diagnose a cctrack --explore day and show what each session really spent.
3
+
4
+ The hook's `total_cost_usd` is a session-lifetime cumulative that (a) can be
5
+ large the moment cctrack first sees a long-running session, and (b) drops on
6
+ resume/compaction. cctrack 0.6.0 mis-attributed both: it booked a session's
7
+ whole pre-existing cumulative onto the first day it saw the session, and it
8
+ re-added post-drop levels as fresh spend — so a session whose counter never
9
+ passed $1,086 could be reported at $2,083, and a day's total could balloon far
10
+ above real spend.
11
+
12
+ This script reads the raw statusline data for a date and, per session, prints:
13
+
14
+ * the cost_usd trajectory,
15
+ * BUGGY: what cctrack ≤0.6.0 booked (first level + every post-drop level),
16
+ * FIXED: what cctrack now books — only observed increments, never the
17
+ standing level (== the honest spend cctrack can see on that day).
18
+
19
+ If FIXED is far below BUGGY, the old report was inflated. FIXED is what the
20
+ corrected cctrack reports; any spend from earlier days the hook didn't capture
21
+ is genuinely unattributable and isn't booked on this day.
22
+
23
+ Usage:
24
+ python3 scripts/diagnose_session_cost.py <YYYY-MM-DD> [session_prefix ...]
25
+
26
+ # every session that has data that day:
27
+ python3 scripts/diagnose_session_cost.py 2026-08-03
28
+
29
+ # just specific ones (any unique prefix of the session id works):
30
+ python3 scripts/diagnose_session_cost.py 2026-08-03 34dfdf14 63fd9dae 7c112e0a
31
+
32
+ Reads ~/.claude/cctrack and any ~/.sandy/sandboxes/*/claude/cctrack. Stdlib only.
33
+ """
34
+ import glob
35
+ import json
36
+ import sys
37
+ from pathlib import Path
38
+
39
+
40
+ def buggy_increment(cur: float, prev: float) -> float:
41
+ """What cctrack <= 0.6.0 did: a meaningful drop counted the new value in
42
+ full (and prev started at 0, so the first reading counted in full too)."""
43
+ if cur < prev - max(1e-6, abs(prev) * 0.001):
44
+ return max(0.0, cur)
45
+ return max(0.0, cur - prev)
46
+
47
+
48
+ def hook_dirs() -> list[Path]:
49
+ dirs = [Path.home() / ".claude" / "cctrack"]
50
+ dirs += [Path(p) for p in glob.glob(
51
+ str(Path.home() / ".sandy" / "sandboxes" / "*" / "claude" / "cctrack"))]
52
+ return [d for d in dirs if d.exists()]
53
+
54
+
55
+ def load_rows(date_str: str) -> dict[str, list[tuple[str, float]]]:
56
+ """Return {session_id: [(ts, cost_usd), ...]} for the given date."""
57
+ rows: dict[str, list[tuple[str, float]]] = {}
58
+ for d in hook_dirs():
59
+ for f in glob.glob(str(d / f"statusline-{date_str}.jsonl")):
60
+ try:
61
+ lines = Path(f).read_text(errors="replace").splitlines()
62
+ except OSError:
63
+ continue
64
+ for line in lines:
65
+ line = line.strip()
66
+ if not line:
67
+ continue
68
+ try:
69
+ e = json.loads(line)
70
+ except (json.JSONDecodeError, ValueError):
71
+ continue
72
+ sid = e.get("session_id", "")
73
+ ts = e.get("_ts", "")
74
+ if not sid or not ts:
75
+ continue
76
+ rows.setdefault(sid, []).append((ts, float(e.get("cost_usd", 0) or 0)))
77
+ return rows
78
+
79
+
80
+ def analyze(sid: str, seq: list[tuple[str, float]]) -> dict:
81
+ seq.sort()
82
+ costs = [c for _, c in seq]
83
+
84
+ # BUGGY (<= 0.6.0): prev starts at 0; drops re-add the full new level.
85
+ buggy = prev = 0.0
86
+ drops = 0
87
+ for c in costs:
88
+ if c < prev - max(1e-6, abs(prev) * 0.001):
89
+ drops += 1
90
+ buggy += buggy_increment(c, prev)
91
+ prev = c
92
+
93
+ # FIXED: first reading is a baseline (not counted); a drop rebaselines
94
+ # (not counted); only genuine upward increments are booked.
95
+ fixed = 0.0
96
+ prev = None
97
+ for c in costs:
98
+ if prev is None:
99
+ prev = c
100
+ elif c < prev - max(1e-6, abs(prev) * 0.001):
101
+ prev = c
102
+ else:
103
+ fixed += c - prev
104
+ prev = c
105
+
106
+ return {
107
+ "sid": sid,
108
+ "renders": len(costs),
109
+ "min": min(costs),
110
+ "max": max(costs),
111
+ "first": costs[0],
112
+ "last": costs[-1],
113
+ "drops": drops,
114
+ "buggy": buggy,
115
+ "fixed": fixed,
116
+ "costs": costs,
117
+ }
118
+
119
+
120
+ def main() -> int:
121
+ if len(sys.argv) < 2:
122
+ print(__doc__)
123
+ return 2
124
+
125
+ date_str = sys.argv[1]
126
+ prefixes = sys.argv[2:]
127
+
128
+ rows = load_rows(date_str)
129
+ if not rows:
130
+ print(f"No statusline data found for {date_str} in:")
131
+ for d in hook_dirs() or ["(no hook directories exist)"]:
132
+ print(f" {d}")
133
+ return 1
134
+
135
+ selected = {sid: seq for sid, seq in rows.items()
136
+ if not prefixes or any(sid.startswith(p) for p in prefixes)}
137
+ if not selected:
138
+ print(f"No sessions matching {prefixes} on {date_str}. "
139
+ f"Sessions present: {', '.join(s[:12] for s in rows)}")
140
+ return 1
141
+
142
+ results = [analyze(sid, seq) for sid, seq in selected.items()]
143
+ results.sort(key=lambda r: -r["buggy"])
144
+
145
+ grand_buggy = grand_fixed = 0.0
146
+ for r in results:
147
+ grand_buggy += r["buggy"]
148
+ grand_fixed += r["fixed"]
149
+ inflation = (r["buggy"] / r["fixed"]) if r["fixed"] > 0.005 else float("inf")
150
+ tag = "" if inflation < 1.15 else f" ← inflated {inflation:.0f}×" if inflation != float("inf") else " ← booked from nothing"
151
+ print(f"\n=== {r['sid'][:12]} ({r['renders']} renders) ===")
152
+ print(f" cost_usd first={r['first']:.2f} min={r['min']:.2f} "
153
+ f"max={r['max']:.2f} last={r['last']:.2f} drops={r['drops']}")
154
+ print(f" BUGGY (cctrack ≤0.6.0 booked on this day): ${r['buggy']:.2f}{tag}")
155
+ print(f" FIXED (real observed spend this day): ${r['fixed']:.2f}")
156
+ shown = " ".join(f"{c:.1f}" for c in r["costs"][:40])
157
+ print(f" trajectory: {shown}" + (" …" if len(r["costs"]) > 40 else ""))
158
+
159
+ print(f"\n{'─' * 60}")
160
+ print(f"selected sessions — buggy total: ${grand_buggy:.2f} fixed total: ${grand_fixed:.2f}")
161
+ if grand_fixed > 0.005 and grand_buggy / grand_fixed > 1.15:
162
+ print(f"The old report over-stated these sessions by "
163
+ f"{grand_buggy / grand_fixed:.1f}×. The FIXED column is the honest")
164
+ print("spend cctrack can see for this day. Any earlier-day spend the hook")
165
+ print("never captured is unattributable and is not booked here.")
166
+ else:
167
+ print("Buggy and fixed agree — these sessions were attributed correctly.")
168
+ return 0
169
+
170
+
171
+ if __name__ == "__main__":
172
+ sys.exit(main())