tokenol 0.1.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.
- tokenol-0.1.0/.claude/settings.local.json +35 -0
- tokenol-0.1.0/.claudesignore +12 -0
- tokenol-0.1.0/.github/workflows/ci.yml +24 -0
- tokenol-0.1.0/.gitignore +16 -0
- tokenol-0.1.0/CHANGELOG.md +40 -0
- tokenol-0.1.0/PKG-INFO +253 -0
- tokenol-0.1.0/README.md +220 -0
- tokenol-0.1.0/docs/ASSUMPTIONS.md +34 -0
- tokenol-0.1.0/docs/METRICS.md +148 -0
- tokenol-0.1.0/docs/screenshots/main.jpg +0 -0
- tokenol-0.1.0/docs/screenshots/project.jpg +0 -0
- tokenol-0.1.0/docs/screenshots/session_bottom.jpg +0 -0
- tokenol-0.1.0/docs/screenshots/session_top.jpg +0 -0
- tokenol-0.1.0/pyproject.toml +64 -0
- tokenol-0.1.0/src/tokenol/__init__.py +3 -0
- tokenol-0.1.0/src/tokenol/assumptions.py +51 -0
- tokenol-0.1.0/src/tokenol/cli.py +408 -0
- tokenol-0.1.0/src/tokenol/enums.py +26 -0
- tokenol-0.1.0/src/tokenol/ingest/__init__.py +0 -0
- tokenol-0.1.0/src/tokenol/ingest/builder.py +88 -0
- tokenol-0.1.0/src/tokenol/ingest/discovery.py +43 -0
- tokenol-0.1.0/src/tokenol/ingest/parser.py +162 -0
- tokenol-0.1.0/src/tokenol/ingest/schema.py +10 -0
- tokenol-0.1.0/src/tokenol/metrics/__init__.py +0 -0
- tokenol-0.1.0/src/tokenol/metrics/context.py +99 -0
- tokenol-0.1.0/src/tokenol/metrics/cost.py +145 -0
- tokenol-0.1.0/src/tokenol/metrics/history.py +80 -0
- tokenol-0.1.0/src/tokenol/metrics/patterns.py +246 -0
- tokenol-0.1.0/src/tokenol/metrics/rollups.py +342 -0
- tokenol-0.1.0/src/tokenol/metrics/thresholds.py +87 -0
- tokenol-0.1.0/src/tokenol/metrics/verdicts.py +37 -0
- tokenol-0.1.0/src/tokenol/metrics/windows.py +103 -0
- tokenol-0.1.0/src/tokenol/model/__init__.py +0 -0
- tokenol-0.1.0/src/tokenol/model/events.py +116 -0
- tokenol-0.1.0/src/tokenol/model/pricing.py +112 -0
- tokenol-0.1.0/src/tokenol/model/registry.py +57 -0
- tokenol-0.1.0/src/tokenol/report/__init__.py +0 -0
- tokenol-0.1.0/src/tokenol/report/text.py +352 -0
- tokenol-0.1.0/src/tokenol/serve/__init__.py +0 -0
- tokenol-0.1.0/src/tokenol/serve/app.py +312 -0
- tokenol-0.1.0/src/tokenol/serve/prefs.py +68 -0
- tokenol-0.1.0/src/tokenol/serve/session_detail.py +222 -0
- tokenol-0.1.0/src/tokenol/serve/state.py +1370 -0
- tokenol-0.1.0/src/tokenol/serve/static/app.js +1205 -0
- tokenol-0.1.0/src/tokenol/serve/static/chart.js +230 -0
- tokenol-0.1.0/src/tokenol/serve/static/components.js +159 -0
- tokenol-0.1.0/src/tokenol/serve/static/day.html +102 -0
- tokenol-0.1.0/src/tokenol/serve/static/day.js +108 -0
- tokenol-0.1.0/src/tokenol/serve/static/index.html +332 -0
- tokenol-0.1.0/src/tokenol/serve/static/model.html +51 -0
- tokenol-0.1.0/src/tokenol/serve/static/model.js +57 -0
- tokenol-0.1.0/src/tokenol/serve/static/project.html +137 -0
- tokenol-0.1.0/src/tokenol/serve/static/project.js +240 -0
- tokenol-0.1.0/src/tokenol/serve/static/session.html +297 -0
- tokenol-0.1.0/src/tokenol/serve/static/session.js +848 -0
- tokenol-0.1.0/src/tokenol/serve/static/styles.css +797 -0
- tokenol-0.1.0/src/tokenol/serve/streaming.py +63 -0
- tokenol-0.1.0/tests/conftest.py +3 -0
- tokenol-0.1.0/tests/fixtures/basic.jsonl +2 -0
- tokenol-0.1.0/tests/fixtures/dedup.jsonl +2 -0
- tokenol-0.1.0/tests/fixtures/gemini.jsonl +1 -0
- tokenol-0.1.0/tests/fixtures/interrupted.jsonl +1 -0
- tokenol-0.1.0/tests/fixtures/missing_ids.jsonl +1 -0
- tokenol-0.1.0/tests/fixtures/sidechain.jsonl +1 -0
- tokenol-0.1.0/tests/test_cli.py +161 -0
- tokenol-0.1.0/tests/test_context_metrics.py +139 -0
- tokenol-0.1.0/tests/test_dedup.py +42 -0
- tokenol-0.1.0/tests/test_history_and_thresholds.py +132 -0
- tokenol-0.1.0/tests/test_metrics.py +72 -0
- tokenol-0.1.0/tests/test_parser.py +122 -0
- tokenol-0.1.0/tests/test_patterns.py +255 -0
- tokenol-0.1.0/tests/test_serve_app.py +736 -0
- tokenol-0.1.0/tests/test_serve_prefs.py +63 -0
- tokenol-0.1.0/tests/test_serve_state.py +490 -0
- tokenol-0.1.0/tests/test_serve_streaming.py +188 -0
- tokenol-0.1.0/tests/test_session_detail.py +147 -0
- tokenol-0.1.0/tests/test_verdicts.py +96 -0
- tokenol-0.1.0/tests/test_windows.py +161 -0
- tokenol-0.1.0/uv.lock +1112 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(find /home/ff235/.claude-* -name \".credentials.json\" -exec ls -la {} \\\\;)",
|
|
5
|
+
"Bash(mount)",
|
|
6
|
+
"Bash(xargs -I{} head -c 2000 \"{}\")",
|
|
7
|
+
"Bash(xargs -I{} grep -o '\"thinking\"[^}]*' \"{}\")",
|
|
8
|
+
"Bash(grep -o '\"type\":\"thinking\",\"thinking\":\"[^\"]\\\\{0,200\\\\}' /home/ff235/.claude/projects/-home-ff235-dev-ReviewSense/3f7581b5-b523-4b68-be8f-c570ec2e30cb/subagents/agent-a35dd56.jsonl)",
|
|
9
|
+
"Bash(python3 *)",
|
|
10
|
+
"WebFetch(domain:efficienist.com)",
|
|
11
|
+
"Bash(node -e \" const fs = require\\('fs'\\); const src = fs.readFileSync\\('/home/ff235/.npm-global/lib/node_modules/ccusage/dist/data-loader-9ESMosno.js', 'utf8'\\); // Search for dedup pattern const idx = src.indexOf\\('seenRequestId'\\) > 0 ? src.indexOf\\('seenRequestId'\\) : src.indexOf\\('Set\\('\\) ; console.log\\('idx:', idx\\); console.log\\(src.substring\\(Math.max\\(0,idx-100\\), idx+600\\)\\); \")",
|
|
12
|
+
"Bash(node -e \" const fs = require\\('fs'\\); const src = fs.readFileSync\\('/home/ff235/.npm-global/lib/node_modules/ccusage/dist/data-loader-9ESMosno.js', 'utf8'\\); // Find where requestId dedup happens near token counting let idx = 0; while \\(true\\) { idx = src.indexOf\\('requestId', idx+1\\); if \\(idx < 0\\) break; const chunk = src.substring\\(Math.max\\(0,idx-50\\), idx+200\\); if \\(chunk.includes\\('has\\('\\) || chunk.includes\\('add\\('\\) || chunk.includes\\('seen'\\) || chunk.includes\\('skip'\\) || chunk.includes\\('continue'\\)\\) { console.log\\('=== at', idx, '==='\\); console.log\\(chunk\\); console.log\\(\\); } } \")",
|
|
13
|
+
"Bash(node -e \" const fs = require\\('fs'\\); // Look in the other bundle file const src = fs.readFileSync\\('/home/ff235/.npm-global/lib/node_modules/ccusage/dist/calculate-cost-DWGfKMSD.js', 'utf8'\\); console.log\\('File size:', src.length\\); // Find key patterns for \\(const t of ['requestId', 'seenReq', 'dedup', 'has\\(', 'skip', 'cache_read', 'output_tokens']\\) { const idx = src.indexOf\\(t\\); if \\(idx >= 0\\) { console.log\\('--- found', t, 'at', idx\\); console.log\\(src.substring\\(Math.max\\(0,idx-50\\), idx+200\\)\\); console.log\\(\\); } } \")",
|
|
14
|
+
"Bash(node *)",
|
|
15
|
+
"Bash(mkdir -p /tmp/cc-leak-probe)",
|
|
16
|
+
"Read(//tmp/**)",
|
|
17
|
+
"Bash(npm pack *)",
|
|
18
|
+
"Bash(pip install *)",
|
|
19
|
+
"Bash(python *)",
|
|
20
|
+
"Bash(tokenol daily *)",
|
|
21
|
+
"Bash(ruff check *)",
|
|
22
|
+
"Bash(tokenol sessions *)",
|
|
23
|
+
"Bash(tokenol live *)",
|
|
24
|
+
"Bash(git add *)",
|
|
25
|
+
"Bash(git commit -m ' *)",
|
|
26
|
+
"Bash(uv run *)",
|
|
27
|
+
"Bash(uv build *)",
|
|
28
|
+
"Bash(git commit *)",
|
|
29
|
+
"Bash(git push *)",
|
|
30
|
+
"Bash(env *)",
|
|
31
|
+
"Bash(find ~/.claude* -type d -name \"frontend*\")",
|
|
32
|
+
"Bash(uv sync *)"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- name: Install
|
|
20
|
+
run: pip install -e ".[dev,serve]"
|
|
21
|
+
- name: Lint
|
|
22
|
+
run: ruff check src tests
|
|
23
|
+
- name: Test
|
|
24
|
+
run: pytest --cov=tokenol --cov-report=term-missing
|
tokenol-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to tokenol are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-04-23
|
|
8
|
+
|
|
9
|
+
Initial public release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **CLI** (`tokenol`): `today`, `week`, `window`, `verdicts`, `models`, `projects`,
|
|
13
|
+
`assumptions`, `doctor`, `watch`, `serve`.
|
|
14
|
+
- **Ingestion**: discovery across `~/.claude*` dirs (honours `CLAUDE_CONFIG_DIR`),
|
|
15
|
+
JSONL parsing with per-file mtime cache, compound-key deduplication,
|
|
16
|
+
Windows cwd normalization.
|
|
17
|
+
- **Metrics**: cost rollups with full 4-component billing (input, output,
|
|
18
|
+
cache_read, cache_creation); 5-hour rolling-window cost; context growth,
|
|
19
|
+
cache hit rate, cache reuse, cost-per-kW output; session verdicts
|
|
20
|
+
(`OK`, `CONTEXT_CREEP`, `RUNAWAY_WINDOW`, `TOOL_ERROR_STORM`,
|
|
21
|
+
`SIDECHAIN_HEAVY`, `DUAL_SESSION_CONFLICT`).
|
|
22
|
+
- **Pattern detection** on session drill-down: `idle_expiry`,
|
|
23
|
+
`compaction_reinflation`, `context_ceiling_plateau`, `sidechain_explosion`,
|
|
24
|
+
`tool_error_storm` with severity escalation.
|
|
25
|
+
- **Live dashboard** (`tokenol serve`): SSE-streamed main view with headline
|
|
26
|
+
tiles (+ last-hour trajectory), hourly / daily charts (linear ↔ log
|
|
27
|
+
toggle), model and project rollups, recent-activity table.
|
|
28
|
+
- **Drill-down pages**: `/session/<id>` (patterns, cost-per-turn small
|
|
29
|
+
multiples, per-turn modal), `/project/<cwd>` (cache trend with auto
|
|
30
|
+
hourly/daily bucketing, verdict distribution with tooltips),
|
|
31
|
+
`/day/<date>`, `/model/<name>`.
|
|
32
|
+
- **Preferences**: user-editable thresholds and ranges persisted via
|
|
33
|
+
`XDG_CONFIG_HOME`.
|
|
34
|
+
- **Project grouping**: shortest-proper-ancestor cwd rule generalizes across
|
|
35
|
+
nested repos without per-user configuration.
|
|
36
|
+
|
|
37
|
+
### Tested
|
|
38
|
+
- 184 unit + integration tests on Python 3.10 / 3.11 / 3.12.
|
|
39
|
+
|
|
40
|
+
[0.1.0]: https://github.com/yourorg/tokenol/releases/tag/v0.1.0
|
tokenol-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tokenol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Audit Claude Code JSONL session logs: track cost, cache health, context blow-ups, and 5h-window pressure.
|
|
5
|
+
Project-URL: Homepage, https://github.com/farhanferoz/tokenol
|
|
6
|
+
Project-URL: Issues, https://github.com/farhanferoz/tokenol/issues
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: anthropic,audit,claude,cli,cost,tokens
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: duckdb>=0.10
|
|
19
|
+
Requires-Dist: rich>=13
|
|
20
|
+
Requires-Dist: typer>=0.12
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
23
|
+
Requires-Dist: hypothesis>=6; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
28
|
+
Provides-Extra: serve
|
|
29
|
+
Requires-Dist: fastapi>=0.111; extra == 'serve'
|
|
30
|
+
Requires-Dist: uvicorn[standard]>=0.29; extra == 'serve'
|
|
31
|
+
Requires-Dist: watchfiles>=0.21; extra == 'serve'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# tokenol
|
|
35
|
+
|
|
36
|
+
Audit [Claude Code](https://claude.com/claude-code) JSONL session logs for cost, cache health, context blow-ups, and 5-hour rate-limit pressure.
|
|
37
|
+
|
|
38
|
+
`tokenol` parses the session transcripts that Claude Code writes to `~/.claude*/projects/**/*.jsonl` and produces per-day, per-session, per-project, and per-model rollups — plus a live burn-rate view for the active 5-hour window.
|
|
39
|
+
|
|
40
|
+
## Why tokenol
|
|
41
|
+
|
|
42
|
+
Claude Code bills you for everything the model reads — input, output, **and** cache creation/reads. When the prompt cache is working, 95%+ of your context tokens cost a tenth of full input price. When it isn't — idle gaps past the 5-minute TTL, context compaction, two sessions in the same repo thrashing each other — the same conversation can cost 10× more without looking any different.
|
|
43
|
+
|
|
44
|
+
`tokenol` tells you which sessions, projects, and hours did that, and usually why. You run it locally over the JSONL logs Claude Code already writes; nothing is uploaded anywhere.
|
|
45
|
+
|
|
46
|
+
## Dashboard
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
Session drill-down — pattern detection + cost-per-turn small multiples:
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
Project page — cache efficiency trend, verdict distribution, top turns:
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pipx install tokenol
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Requires Python 3.10+.
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Daily token / cost aggregates over the last 14 days
|
|
71
|
+
tokenol daily
|
|
72
|
+
|
|
73
|
+
# Hourly breakdown for today
|
|
74
|
+
tokenol hourly
|
|
75
|
+
|
|
76
|
+
# Top 10 most expensive sessions in the last 30 days
|
|
77
|
+
tokenol sessions --since 30d --top 10 --sort cost
|
|
78
|
+
|
|
79
|
+
# Per-project rollup
|
|
80
|
+
tokenol projects
|
|
81
|
+
|
|
82
|
+
# Live view: burn rate + projected end-of-window cost
|
|
83
|
+
tokenol live --last 20m
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
All commands scan every JSONL file under `$CLAUDE_CONFIG_DIR` (falling back to the standard `~/.claude*` locations) and deduplicate turns using the same `message.id:requestId` compound key that [ccusage](https://github.com/ryoppippi/ccusage) uses.
|
|
87
|
+
|
|
88
|
+
### Scanning multiple projects
|
|
89
|
+
|
|
90
|
+
If you use workspace isolation (one `~/.claude-<project>` directory per repo, pointed at via `CLAUDE_CONFIG_DIR`), `tokenol` by default only sees the currently-active project. Pass **`--all-projects`** (or `-A`) to any command to scan every `~/.claude*` directory and get a cross-project view:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Total spend across every project in the last 14 days
|
|
94
|
+
tokenol daily --since 14d --all-projects
|
|
95
|
+
|
|
96
|
+
# Which sessions cost the most, globally
|
|
97
|
+
tokenol sessions --since 30d --top 10 -A
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
You can also set `CLAUDE_CONFIG_DIR` to a colon- or comma-separated list of paths to scan a specific subset.
|
|
101
|
+
|
|
102
|
+
## Commands
|
|
103
|
+
|
|
104
|
+
| Command | What it shows |
|
|
105
|
+
| ---------- | --------------------------------------------------------------------------- |
|
|
106
|
+
| `daily` | Per-day tokens (input, output, cache read/creation), cost, turn count |
|
|
107
|
+
| `hourly` | Per-hour breakdown for a single day (defaults to today) |
|
|
108
|
+
| `live` | Active 5-hour window burn rate, recent-activity rate, projected final cost |
|
|
109
|
+
| `sessions` | Per-session detail table with blow-up verdict (RUNAWAY, CONTEXT_CREEP, …) |
|
|
110
|
+
| `projects` | Per-project rollup grouped by `cwd` |
|
|
111
|
+
| `models` | Per-model rollup with tool-use counts and error rates |
|
|
112
|
+
| `verify` | Cross-check tokenol totals against `ccusage --json` (if installed) |
|
|
113
|
+
| `serve` | Launch a local browser dashboard with live burn-rate gauge and all panels |
|
|
114
|
+
|
|
115
|
+
Every command accepts:
|
|
116
|
+
|
|
117
|
+
- `--since 14d` — lookback window (e.g. `7d`, `30d`, or an ISO date)
|
|
118
|
+
- `--all-projects` / `-A` — scan every `~/.claude*` directory (ignores `CLAUDE_CONFIG_DIR`)
|
|
119
|
+
- `--strict` — exit non-zero if any cost-computation assumption fired
|
|
120
|
+
- `--show-assumptions` — always print the assumption footer
|
|
121
|
+
- `--log-level debug|info|warning`
|
|
122
|
+
|
|
123
|
+
`tokenol sessions` additionally takes `--sort` (`cost`, `input`, `output`, `cache_read`, `turns`, `max_input`, `duration`) and `--top`.
|
|
124
|
+
|
|
125
|
+
`tokenol live` takes `--last 20m|2h|30s` and exits non-zero if the projected window cost exceeds the configured reference.
|
|
126
|
+
|
|
127
|
+
## Live dashboard
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Install with dashboard dependencies
|
|
131
|
+
pipx install 'tokenol[serve]'
|
|
132
|
+
|
|
133
|
+
# Start the dashboard (binds to http://127.0.0.1:8787)
|
|
134
|
+
tokenol serve
|
|
135
|
+
|
|
136
|
+
# Cross-project view, faster tick, custom reference threshold
|
|
137
|
+
tokenol serve --all-projects --tick 2s --reference 25
|
|
138
|
+
|
|
139
|
+
# Open browser automatically
|
|
140
|
+
tokenol serve --open
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The dashboard updates via SSE as Claude Code writes events to disk. Main page layout (top to bottom):
|
|
144
|
+
|
|
145
|
+
| Panel | What it shows |
|
|
146
|
+
|---|---|
|
|
147
|
+
| **Topbar** | Today's cost · sessions · output · last-active time; global period selector (Today / 7D / 30D / All) |
|
|
148
|
+
| **Efficiency tiles** | Hit% · $/kW · Ctx · Cache reuse — each with a delta chip vs 7-day median and colour-coded threshold |
|
|
149
|
+
| **Hour By Hour** | Hourly metric timeline with day-picker, metric pills, project/model filters, and click-to-drilldown |
|
|
150
|
+
| **Daily History** | 30-day metric history with 7-day moving average overlay; range pills (7D / 30D / 90D / All) |
|
|
151
|
+
| **Models** | Per-model cost, turns, output, and efficiency metrics; local range override; click row → `/model/<name>` |
|
|
152
|
+
| **Recent Activity** | Active projects in the last 60 min with Ctx used, $/kW, hit%, verdict; sortable; click row → `/project/<cwd>` |
|
|
153
|
+
|
|
154
|
+
Keyboard shortcuts: `?` Glossary · `/` Find · `,` Settings · `Esc` close/back · `g t` scroll to top · `↑↓ Enter` table row navigation · `← →` chart cursor.
|
|
155
|
+
|
|
156
|
+
### Efficiency metric glossary
|
|
157
|
+
|
|
158
|
+
| Metric | Definition | Target |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| **Hit%** | `cache_read / (cache_read + cache_creation + input)` | ≥ 95% |
|
|
161
|
+
| **$/kW** | `cost × 1000 / output_tokens` — dollars per 1k output tokens | < $0.20 |
|
|
162
|
+
| **Ctx** | `cache_read / output` as N:1 — context tokens read per output token | < 400:1 |
|
|
163
|
+
| **Cache reuse** | `cache_read / cache_creation` as N:1 — low = cache thrashing | > 50:1 |
|
|
164
|
+
| **Ctx used** | Latest turn's visible context ÷ model context window | < 85% |
|
|
165
|
+
|
|
166
|
+
### Preferences
|
|
167
|
+
|
|
168
|
+
User preferences (tick cadence and threshold overrides) are saved to:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
$XDG_CONFIG_HOME/tokenol/prefs.json # default: ~/.config/tokenol/prefs.json
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Shape:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"tick_seconds": 300,
|
|
179
|
+
"reference_usd": 50.0,
|
|
180
|
+
"thresholds": {
|
|
181
|
+
"hit_rate_good_pct": 95,
|
|
182
|
+
"hit_rate_red_pct": 85,
|
|
183
|
+
"cost_per_kw_good": 0.20,
|
|
184
|
+
"cost_per_kw_red": 0.40,
|
|
185
|
+
"ctx_ratio_red": 400.0,
|
|
186
|
+
"cache_reuse_good": 50.0,
|
|
187
|
+
"cache_reuse_red": 20.0
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Reset to defaults via the Settings modal (`POST /api/prefs {"thresholds": "reset"}`).
|
|
193
|
+
|
|
194
|
+
### Session drill-down
|
|
195
|
+
|
|
196
|
+
Click any session to open the drill-down page (`/session/<id>`). It shows:
|
|
197
|
+
|
|
198
|
+
- **What likely went wrong** — automated pattern cards at the top of the page, each with a headline, the measurable signal that triggered it, and a suggested fix. Five patterns are detected:
|
|
199
|
+
|
|
200
|
+
| Pattern | Signal |
|
|
201
|
+
|---|---|
|
|
202
|
+
| **Idle expiry** | Gap ≥ 1 h between turns + next turn was ≥ 80% cache_creation — the 5-minute prompt-cache TTL expired |
|
|
203
|
+
| **Compaction re-inflation** | Visible-token count dropped then climbed back to ≥ 80% of the previous peak — compacting but immediately refilling the context |
|
|
204
|
+
| **Context ceiling plateau** | ≥ 20 consecutive turns at ≥ 90% of the model's context window — paying near-full-context input rates throughout |
|
|
205
|
+
| **Sidechain explosion** | Sidechain/task-agent work accounts for > 40% of session cost |
|
|
206
|
+
| **Tool error storm** | > 20% error rate across any 10-turn window |
|
|
207
|
+
|
|
208
|
+
- **Cost per turn** — stacked bar chart (input / output / cache_read / cache_creation). Toggle "All" or "Top 30" to focus on the most expensive turns. Click any bar to open the per-turn detail modal.
|
|
209
|
+
|
|
210
|
+
- **Per-turn modal** — cost component breakdown, token counts, tool call results (✓/✗), first 500 chars of the user prompt and assistant preview. Navigate with ← / → or close with Esc.
|
|
211
|
+
|
|
212
|
+
## What it detects
|
|
213
|
+
|
|
214
|
+
For every session, `tokenol` computes a blow-up verdict against spec-defined thresholds:
|
|
215
|
+
|
|
216
|
+
| Verdict (table label) | Trigger |
|
|
217
|
+
| ----------------------------- | ------------------------------------------------------ |
|
|
218
|
+
| `RUNAWAY_WINDOW` (`runaway`) | Any 5-hour window costs ≥ \$50 |
|
|
219
|
+
| `CONTEXT_CREEP` (`ctx-creep`) | Max single-turn input ≥ 500k **and** growth ≥ 2k/turn |
|
|
220
|
+
| `TOOL_ERROR_STORM` (`tool-errs`) | ≥ 10 tool uses with > 30% error rate |
|
|
221
|
+
| `SIDECHAIN_HEAVY` (`sidechain`) | Sidechain session costing > \$5 |
|
|
222
|
+
| `OK` (`ok`) | Everything else |
|
|
223
|
+
|
|
224
|
+
### Daily efficiency columns
|
|
225
|
+
|
|
226
|
+
The `tokenol daily` report shows these cost/cache efficiency ratios:
|
|
227
|
+
|
|
228
|
+
| Column | Meaning | Target |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| `$/kW` | USD per 1,000 output tokens | `< $0.20` |
|
|
231
|
+
| `Ctx` | Context tokens read per output token (N:1) | lower is better |
|
|
232
|
+
| `Cache reuse` | Cache reads per cache-creation token (N:1) | `> 50:1` |
|
|
233
|
+
| `Hit%` | % of context served from prompt cache | `≥ 95%` |
|
|
234
|
+
|
|
235
|
+
## Pricing
|
|
236
|
+
|
|
237
|
+
Flat per-model rates (no 1M-token tier surcharge — matches ccusage's default behaviour). The current registry lives in `src/tokenol/metrics/cost.py`. When a turn's model isn't in the registry, `tokenol` records an `UNKNOWN_MODEL_FALLBACK` assumption tag and uses a conservative default; run with `--show-assumptions` or `--strict` to surface these.
|
|
238
|
+
|
|
239
|
+
See [`docs/METRICS.md`](docs/METRICS.md) for metric definitions and [`docs/ASSUMPTIONS.md`](docs/ASSUMPTIONS.md) for the full list of assumption tags.
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
git clone https://github.com/farhanferoz/tokenol
|
|
245
|
+
cd tokenol
|
|
246
|
+
uv sync --extra dev
|
|
247
|
+
uv run pytest
|
|
248
|
+
uv run ruff check
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Licence
|
|
252
|
+
|
|
253
|
+
MIT
|
tokenol-0.1.0/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# tokenol
|
|
2
|
+
|
|
3
|
+
Audit [Claude Code](https://claude.com/claude-code) JSONL session logs for cost, cache health, context blow-ups, and 5-hour rate-limit pressure.
|
|
4
|
+
|
|
5
|
+
`tokenol` parses the session transcripts that Claude Code writes to `~/.claude*/projects/**/*.jsonl` and produces per-day, per-session, per-project, and per-model rollups — plus a live burn-rate view for the active 5-hour window.
|
|
6
|
+
|
|
7
|
+
## Why tokenol
|
|
8
|
+
|
|
9
|
+
Claude Code bills you for everything the model reads — input, output, **and** cache creation/reads. When the prompt cache is working, 95%+ of your context tokens cost a tenth of full input price. When it isn't — idle gaps past the 5-minute TTL, context compaction, two sessions in the same repo thrashing each other — the same conversation can cost 10× more without looking any different.
|
|
10
|
+
|
|
11
|
+
`tokenol` tells you which sessions, projects, and hours did that, and usually why. You run it locally over the JSONL logs Claude Code already writes; nothing is uploaded anywhere.
|
|
12
|
+
|
|
13
|
+
## Dashboard
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
Session drill-down — pattern detection + cost-per-turn small multiples:
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
Project page — cache efficiency trend, verdict distribution, top turns:
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pipx install tokenol
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires Python 3.10+.
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Daily token / cost aggregates over the last 14 days
|
|
38
|
+
tokenol daily
|
|
39
|
+
|
|
40
|
+
# Hourly breakdown for today
|
|
41
|
+
tokenol hourly
|
|
42
|
+
|
|
43
|
+
# Top 10 most expensive sessions in the last 30 days
|
|
44
|
+
tokenol sessions --since 30d --top 10 --sort cost
|
|
45
|
+
|
|
46
|
+
# Per-project rollup
|
|
47
|
+
tokenol projects
|
|
48
|
+
|
|
49
|
+
# Live view: burn rate + projected end-of-window cost
|
|
50
|
+
tokenol live --last 20m
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
All commands scan every JSONL file under `$CLAUDE_CONFIG_DIR` (falling back to the standard `~/.claude*` locations) and deduplicate turns using the same `message.id:requestId` compound key that [ccusage](https://github.com/ryoppippi/ccusage) uses.
|
|
54
|
+
|
|
55
|
+
### Scanning multiple projects
|
|
56
|
+
|
|
57
|
+
If you use workspace isolation (one `~/.claude-<project>` directory per repo, pointed at via `CLAUDE_CONFIG_DIR`), `tokenol` by default only sees the currently-active project. Pass **`--all-projects`** (or `-A`) to any command to scan every `~/.claude*` directory and get a cross-project view:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Total spend across every project in the last 14 days
|
|
61
|
+
tokenol daily --since 14d --all-projects
|
|
62
|
+
|
|
63
|
+
# Which sessions cost the most, globally
|
|
64
|
+
tokenol sessions --since 30d --top 10 -A
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can also set `CLAUDE_CONFIG_DIR` to a colon- or comma-separated list of paths to scan a specific subset.
|
|
68
|
+
|
|
69
|
+
## Commands
|
|
70
|
+
|
|
71
|
+
| Command | What it shows |
|
|
72
|
+
| ---------- | --------------------------------------------------------------------------- |
|
|
73
|
+
| `daily` | Per-day tokens (input, output, cache read/creation), cost, turn count |
|
|
74
|
+
| `hourly` | Per-hour breakdown for a single day (defaults to today) |
|
|
75
|
+
| `live` | Active 5-hour window burn rate, recent-activity rate, projected final cost |
|
|
76
|
+
| `sessions` | Per-session detail table with blow-up verdict (RUNAWAY, CONTEXT_CREEP, …) |
|
|
77
|
+
| `projects` | Per-project rollup grouped by `cwd` |
|
|
78
|
+
| `models` | Per-model rollup with tool-use counts and error rates |
|
|
79
|
+
| `verify` | Cross-check tokenol totals against `ccusage --json` (if installed) |
|
|
80
|
+
| `serve` | Launch a local browser dashboard with live burn-rate gauge and all panels |
|
|
81
|
+
|
|
82
|
+
Every command accepts:
|
|
83
|
+
|
|
84
|
+
- `--since 14d` — lookback window (e.g. `7d`, `30d`, or an ISO date)
|
|
85
|
+
- `--all-projects` / `-A` — scan every `~/.claude*` directory (ignores `CLAUDE_CONFIG_DIR`)
|
|
86
|
+
- `--strict` — exit non-zero if any cost-computation assumption fired
|
|
87
|
+
- `--show-assumptions` — always print the assumption footer
|
|
88
|
+
- `--log-level debug|info|warning`
|
|
89
|
+
|
|
90
|
+
`tokenol sessions` additionally takes `--sort` (`cost`, `input`, `output`, `cache_read`, `turns`, `max_input`, `duration`) and `--top`.
|
|
91
|
+
|
|
92
|
+
`tokenol live` takes `--last 20m|2h|30s` and exits non-zero if the projected window cost exceeds the configured reference.
|
|
93
|
+
|
|
94
|
+
## Live dashboard
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Install with dashboard dependencies
|
|
98
|
+
pipx install 'tokenol[serve]'
|
|
99
|
+
|
|
100
|
+
# Start the dashboard (binds to http://127.0.0.1:8787)
|
|
101
|
+
tokenol serve
|
|
102
|
+
|
|
103
|
+
# Cross-project view, faster tick, custom reference threshold
|
|
104
|
+
tokenol serve --all-projects --tick 2s --reference 25
|
|
105
|
+
|
|
106
|
+
# Open browser automatically
|
|
107
|
+
tokenol serve --open
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The dashboard updates via SSE as Claude Code writes events to disk. Main page layout (top to bottom):
|
|
111
|
+
|
|
112
|
+
| Panel | What it shows |
|
|
113
|
+
|---|---|
|
|
114
|
+
| **Topbar** | Today's cost · sessions · output · last-active time; global period selector (Today / 7D / 30D / All) |
|
|
115
|
+
| **Efficiency tiles** | Hit% · $/kW · Ctx · Cache reuse — each with a delta chip vs 7-day median and colour-coded threshold |
|
|
116
|
+
| **Hour By Hour** | Hourly metric timeline with day-picker, metric pills, project/model filters, and click-to-drilldown |
|
|
117
|
+
| **Daily History** | 30-day metric history with 7-day moving average overlay; range pills (7D / 30D / 90D / All) |
|
|
118
|
+
| **Models** | Per-model cost, turns, output, and efficiency metrics; local range override; click row → `/model/<name>` |
|
|
119
|
+
| **Recent Activity** | Active projects in the last 60 min with Ctx used, $/kW, hit%, verdict; sortable; click row → `/project/<cwd>` |
|
|
120
|
+
|
|
121
|
+
Keyboard shortcuts: `?` Glossary · `/` Find · `,` Settings · `Esc` close/back · `g t` scroll to top · `↑↓ Enter` table row navigation · `← →` chart cursor.
|
|
122
|
+
|
|
123
|
+
### Efficiency metric glossary
|
|
124
|
+
|
|
125
|
+
| Metric | Definition | Target |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| **Hit%** | `cache_read / (cache_read + cache_creation + input)` | ≥ 95% |
|
|
128
|
+
| **$/kW** | `cost × 1000 / output_tokens` — dollars per 1k output tokens | < $0.20 |
|
|
129
|
+
| **Ctx** | `cache_read / output` as N:1 — context tokens read per output token | < 400:1 |
|
|
130
|
+
| **Cache reuse** | `cache_read / cache_creation` as N:1 — low = cache thrashing | > 50:1 |
|
|
131
|
+
| **Ctx used** | Latest turn's visible context ÷ model context window | < 85% |
|
|
132
|
+
|
|
133
|
+
### Preferences
|
|
134
|
+
|
|
135
|
+
User preferences (tick cadence and threshold overrides) are saved to:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
$XDG_CONFIG_HOME/tokenol/prefs.json # default: ~/.config/tokenol/prefs.json
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Shape:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"tick_seconds": 300,
|
|
146
|
+
"reference_usd": 50.0,
|
|
147
|
+
"thresholds": {
|
|
148
|
+
"hit_rate_good_pct": 95,
|
|
149
|
+
"hit_rate_red_pct": 85,
|
|
150
|
+
"cost_per_kw_good": 0.20,
|
|
151
|
+
"cost_per_kw_red": 0.40,
|
|
152
|
+
"ctx_ratio_red": 400.0,
|
|
153
|
+
"cache_reuse_good": 50.0,
|
|
154
|
+
"cache_reuse_red": 20.0
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Reset to defaults via the Settings modal (`POST /api/prefs {"thresholds": "reset"}`).
|
|
160
|
+
|
|
161
|
+
### Session drill-down
|
|
162
|
+
|
|
163
|
+
Click any session to open the drill-down page (`/session/<id>`). It shows:
|
|
164
|
+
|
|
165
|
+
- **What likely went wrong** — automated pattern cards at the top of the page, each with a headline, the measurable signal that triggered it, and a suggested fix. Five patterns are detected:
|
|
166
|
+
|
|
167
|
+
| Pattern | Signal |
|
|
168
|
+
|---|---|
|
|
169
|
+
| **Idle expiry** | Gap ≥ 1 h between turns + next turn was ≥ 80% cache_creation — the 5-minute prompt-cache TTL expired |
|
|
170
|
+
| **Compaction re-inflation** | Visible-token count dropped then climbed back to ≥ 80% of the previous peak — compacting but immediately refilling the context |
|
|
171
|
+
| **Context ceiling plateau** | ≥ 20 consecutive turns at ≥ 90% of the model's context window — paying near-full-context input rates throughout |
|
|
172
|
+
| **Sidechain explosion** | Sidechain/task-agent work accounts for > 40% of session cost |
|
|
173
|
+
| **Tool error storm** | > 20% error rate across any 10-turn window |
|
|
174
|
+
|
|
175
|
+
- **Cost per turn** — stacked bar chart (input / output / cache_read / cache_creation). Toggle "All" or "Top 30" to focus on the most expensive turns. Click any bar to open the per-turn detail modal.
|
|
176
|
+
|
|
177
|
+
- **Per-turn modal** — cost component breakdown, token counts, tool call results (✓/✗), first 500 chars of the user prompt and assistant preview. Navigate with ← / → or close with Esc.
|
|
178
|
+
|
|
179
|
+
## What it detects
|
|
180
|
+
|
|
181
|
+
For every session, `tokenol` computes a blow-up verdict against spec-defined thresholds:
|
|
182
|
+
|
|
183
|
+
| Verdict (table label) | Trigger |
|
|
184
|
+
| ----------------------------- | ------------------------------------------------------ |
|
|
185
|
+
| `RUNAWAY_WINDOW` (`runaway`) | Any 5-hour window costs ≥ \$50 |
|
|
186
|
+
| `CONTEXT_CREEP` (`ctx-creep`) | Max single-turn input ≥ 500k **and** growth ≥ 2k/turn |
|
|
187
|
+
| `TOOL_ERROR_STORM` (`tool-errs`) | ≥ 10 tool uses with > 30% error rate |
|
|
188
|
+
| `SIDECHAIN_HEAVY` (`sidechain`) | Sidechain session costing > \$5 |
|
|
189
|
+
| `OK` (`ok`) | Everything else |
|
|
190
|
+
|
|
191
|
+
### Daily efficiency columns
|
|
192
|
+
|
|
193
|
+
The `tokenol daily` report shows these cost/cache efficiency ratios:
|
|
194
|
+
|
|
195
|
+
| Column | Meaning | Target |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `$/kW` | USD per 1,000 output tokens | `< $0.20` |
|
|
198
|
+
| `Ctx` | Context tokens read per output token (N:1) | lower is better |
|
|
199
|
+
| `Cache reuse` | Cache reads per cache-creation token (N:1) | `> 50:1` |
|
|
200
|
+
| `Hit%` | % of context served from prompt cache | `≥ 95%` |
|
|
201
|
+
|
|
202
|
+
## Pricing
|
|
203
|
+
|
|
204
|
+
Flat per-model rates (no 1M-token tier surcharge — matches ccusage's default behaviour). The current registry lives in `src/tokenol/metrics/cost.py`. When a turn's model isn't in the registry, `tokenol` records an `UNKNOWN_MODEL_FALLBACK` assumption tag and uses a conservative default; run with `--show-assumptions` or `--strict` to surface these.
|
|
205
|
+
|
|
206
|
+
See [`docs/METRICS.md`](docs/METRICS.md) for metric definitions and [`docs/ASSUMPTIONS.md`](docs/ASSUMPTIONS.md) for the full list of assumption tags.
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
git clone https://github.com/farhanferoz/tokenol
|
|
212
|
+
cd tokenol
|
|
213
|
+
uv sync --extra dev
|
|
214
|
+
uv run pytest
|
|
215
|
+
uv run ruff check
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Licence
|
|
219
|
+
|
|
220
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# tokenol Assumptions Catalog
|
|
2
|
+
|
|
3
|
+
Every heuristic or fallback that tokenol applies is recorded as an `AssumptionTag`.
|
|
4
|
+
These surface in three places:
|
|
5
|
+
- Report footer summary (printed automatically when any fire)
|
|
6
|
+
- `--show-assumptions` flag forces the footer to print even when empty
|
|
7
|
+
- `--log-level debug` emits stderr JSON lines per decision
|
|
8
|
+
- `--strict` mode treats any fired assumption as a fatal error
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Assumption Tags
|
|
13
|
+
|
|
14
|
+
| Tag | Heuristic | Why needed | Error mode |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| `WINDOW_BOUNDARY_HEURISTIC` | 5h window starts at first billable event; runs 5h wall-clock; next event after expiry starts a new window | Anthropic's exact server-side rule isn't published; matches community reverse-engineering | Drifts if Anthropic uses fixed-UTC or overlapping-rolling windows |
|
|
17
|
+
| `UNKNOWN_MODEL_FALLBACK` | Unknown Claude model inherits pricing + context from nearest-known family sibling | No machine-readable pricing feed; new models appear before we update | Warned per model; slight mispricing until registry updated |
|
|
18
|
+
| `DEDUP_PASSTHROUGH` | Events with `null` `message.id` or `null` `requestId` pass through dedup (matches ccusage behavior) | Cannot form the compound hash key | Rare; logged per event |
|
|
19
|
+
| `INTERRUPTED_TURN_SKIPPED` | Assistant messages with no `usage` fields (stop_reason=NONE) excluded from cost | Request never completed; no billing data | None — correctly excludes |
|
|
20
|
+
| `GEMINI_UNPRICED` | Non-Claude models (gemini-*) parsed but not priced | Multi-provider pricing is post-v1 | Cost rows show `—` for these models |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## New tags added in Phase 2
|
|
25
|
+
|
|
26
|
+
`WINDOW_BOUNDARY_HEURISTIC` was defined in Phase 1 but first fired in Phase 2 when `align_windows()` is called. It fires once per `align_windows()` call, not once per turn.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## CLI Flags
|
|
31
|
+
|
|
32
|
+
- `--strict` — refuse any assumption fallback; error out with non-zero exit code.
|
|
33
|
+
- `--show-assumptions` — force the footer to print even if no assumptions fired. Useful for CI output.
|
|
34
|
+
- `--log-level debug` — set Python's root logging level to DEBUG; enables verbose per-decision output.
|