claude-agent-ledger 0.7.0 → 0.7.2

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.
@@ -1,29 +0,0 @@
1
- ---
2
- name: Model pricing gap
3
- about: A model ID shows $0 in your ledger — we're missing it from the price table
4
- title: 'pricing: add <model-id>'
5
- labels: pricing
6
- ---
7
-
8
- ## Model ID
9
-
10
- <!-- e.g. claude-opus-4-8-20260301 -->
11
-
12
- ## Anthropic's published rates
13
-
14
- - Input: $<X> / 1M tokens
15
- - Output: $<Y> / 1M tokens
16
- - Link to Anthropic pricing page (with date):
17
-
18
- ## Where you saw it
19
-
20
- Sample from your log (redact the content, keep the model field):
21
-
22
- ```json
23
- {"message":{"model":"<model-id>", "usage":{"input_tokens":..., "output_tokens":...}}}
24
- ```
25
-
26
- ## Optional: PR
27
-
28
- If you want to open the PR, the pricing table lives in `src/pricing.ts`. Add a
29
- matching test in `test/pricing.test.ts`.
@@ -1,19 +0,0 @@
1
- ## What this PR does
2
-
3
- <!-- One or two sentences. -->
4
-
5
- ## Checklist
6
-
7
- - [ ] `bun run typecheck` passes
8
- - [ ] `bun test` passes
9
- - [ ] If you touched `src/pricing.ts`, there's a matching test in
10
- `test/pricing.test.ts`
11
- - [ ] If you touched `src/parser.ts`, the change doesn't break the nested
12
- `subagents/` walk (see `test/parser.test.ts`)
13
- - [ ] No runtime dependencies added (this tool intentionally has none)
14
-
15
- ## Log snippet (if relevant)
16
-
17
- <!-- If the change is about handling some real log shape, include a redacted
18
- example of what it looks like. Helps me reason about whether other tools
19
- produce the same shape. -->
@@ -1,19 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v4
14
- - uses: oven-sh/setup-bun@v2
15
- with:
16
- bun-version: latest
17
- - run: bun install --frozen-lockfile
18
- - run: bun run typecheck
19
- - run: bun test
package/CHANGELOG.md DELETED
@@ -1,200 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to `claude-agent-ledger` will be documented here.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [0.7.0] — 2026-04-23
9
-
10
- ### Added — live tail + property tests
11
-
12
- - **`agent-ledger watch [intervalSec]`** — live polling mode. Re-aggregates
13
- today's cost every N seconds (default 10), prints a delta meter:
14
-
15
- ```
16
- [14:46:34] today $3310.71 ↑ +$3310.71 · 9560 turns (Δ9560) · top: (main) $3167.95
17
- [14:46:40] today $3310.71 · +$0.00 · 9560 turns (Δ0) · top: (main) $3167.95
18
- ```
19
-
20
- Answers "what's happening right now" without needing to re-run queries.
21
- Ctrl-C to exit.
22
-
23
- - **Property tests via `fast-check`** — 7 new tests, 1400+ generated cases
24
- covering:
25
- - `costFor()` returns non-negative for any valid input
26
- - `totalCost === sum(components)` exactly (ε < 1e-9)
27
- - `cache_read ≤ fresh input` cost (90% discount holds)
28
- - `cache_1h ≥ cache_5m` cost (1h tier always more expensive)
29
- - `Aggregator.total === sum(rows)` for any random turn stream
30
- - Non-assistant turns contribute 0 cost
31
- - `SubagentGraph.totalCost ≥ selfCost` (children only add)
32
-
33
- Total test count: 22 → 29 (kept all prior tests passing).
34
-
35
- ## [0.6.0] — 2026-04-23
36
-
37
- ### Added — `agent-ledger explain <sessionId>` (the LLM-native move)
38
-
39
- A new subcommand that answers **"why was this single session expensive?"**
40
- This is the question Helicone / Langfuse / OpenLLMetry physically cannot
41
- answer — they observe API calls, not Claude Code's prompts and tool calls.
42
-
43
- Two modes:
44
-
45
- - **Heuristic mode** (default, no API key needed) — analyzes the session
46
- JSONL and prints:
47
- - Subagent + total cost + turn count
48
- - Model breakdown (which model dominated)
49
- - Tool invocation count (Read/Bash/Grep/Edit/MCP — top 8)
50
- - Top 3 most expensive turns with full token + tool breakdown
51
- - Cache reuse ratio + verdict (`<5×` warning, `>20×` good)
52
-
53
- - **LLM mode** (set `ANTHROPIC_API_KEY`) — pipes the heuristic evidence into
54
- Haiku 4.5 with a tightly-scoped prompt: "identify the SPECIFIC root cause
55
- + ONE concrete action to cut this session's cost by 50%+." Costs about
56
- $0.001/run. Cite turn numbers from the breakdown.
57
-
58
- ```bash
59
- agent-ledger explain 63063a38
60
- agent-ledger explain b988bd89-803a-4c78 # full uuid or 8-char prefix both work
61
- ```
62
-
63
- ## [0.5.0] — 2026-04-23
64
-
65
- ### Added — performance + production-readiness foundation
66
-
67
- - **Streaming JSONL parser** — replaces `await file.text()` (which OOM'd on
68
- 10GB+ logs). Now reads via `Bun.file().stream()` + `TextDecoder` line-by-line.
69
- Memory profile is constant regardless of log size.
70
- - **Concurrent file walk** — `findSessionLogs` parallelizes per-project
71
- `readdir` (`Promise.all`), and `parseAll` reads up to 8 JSONL files
72
- concurrently. ~5–10× cold-start speedup on multi-project setups.
73
- - **Zero materialization** — bin no longer collects every turn into an array
74
- before aggregating. Instead one parser stream fans out to N `Aggregator`
75
- instances in parallel. Memory is now O(unique-keys) not O(turns).
76
- - **`--by tool`** — new GroupKey. Attribute spend per tool call (`Read`,
77
- `Bash`, `Grep`, `Write`, MCP tool names). Answers "which tool is eating my
78
- budget" — the most actionable lens for a Claude Code user.
79
- - **`--tree` (the MOAT)** — subagent-graph cost attribution. Walks the
80
- parent→child orchestration tree and shows
81
- `orchestrator → 3×researcher + 6×swift-dev = $312`. Helicone / Langfuse /
82
- OpenLLMetry can't do this — they observe API calls, not Claude Code's
83
- multi-agent runtime.
84
- - **`--budget <USD>`** — exit code `2` if total cost exceeds budget. Wire it
85
- into your CI / shell startup / `pre-commit` to gate spend.
86
- - **`--json` works for `--summary`** — previously summary was text-only.
87
- Now pipe-friendly: `agent-ledger week --summary --json | jq`.
88
- - **`--verbose`** — print parse stats (turns / elapsed) to stderr.
89
-
90
- ### Changed
91
-
92
- - `Aggregator` is now a class (`add()` / `finalize()`) instead of an async
93
- function. Enables single-pass fan-out and live-tail in v0.6.
94
- - `parseAll` and `parseFile` no longer block on full file reads.
95
- - `SessionTurn` adds `parentSessionId` and `toolUses[]` fields.
96
-
97
- ### Internal
98
-
99
- - Backwards-compatible: the old `aggregate(turns, from, to, group)` async
100
- function still exists and wraps the new `Aggregator` class. All v0.4.x
101
- tests pass without modification (22/22).
102
-
103
- ## [0.4.1] — 2026-04-22
104
-
105
- ### Added
106
- - `--anonymize` flag — replaces project paths with `~/repo-A`, `~/repo-B`,
107
- ... and session ids with `sess-A`, `sess-B`, ... ordered by spend descending.
108
- Use this when you want to share your output publicly (HN / Bluesky /
109
- presentation) without leaking employer names or internal repo names.
110
- Subagent / model / day labels stay untouched (those are public info).
111
-
112
- ## [0.4.0] — 2026-04-22
113
-
114
- ### Added
115
- - `--by project` — group spend by Claude Code project directory.
116
- Decodes `~/.claude/projects/-Users-xiangjie-clawbot` → `~/clawbot` so you can
117
- see which project is burning budget.
118
- - `--by session` — group by session ID. Surfaces the single most expensive
119
- conversations (useful for hunting the one that went sideways).
120
- - Summary dashboard now shows:
121
- - `Projects` count
122
- - `Top project` line
123
- - `Project mix` block — top 3 projects with $ and %
124
- - `decodeProjectId()` exported for reuse. Special-cases the `~/` home prefix,
125
- falls back to slash-joined path for other roots.
126
- - Row label column now auto-widens up to 40 chars for project paths.
127
-
128
- ### Changed
129
- - `formatTable` takes an optional `groupName` arg so the header column matches
130
- the group (e.g. "project", "session").
131
-
132
- ## [0.3.0] — 2026-04-22
133
-
134
- ### Added
135
- - `--summary` flag — single-screen dashboard with shadow cost, top subagent /
136
- model / day, cache reuse ratio, and **leverage vs $200/mo Max plan**.
137
- This is the killer view for "why install over /cost?"
138
- - Auto-derived insights footer on the default table:
139
- - Top contributor with %
140
- - 1h cache spend call-out (when ≥5% of total)
141
- - Cache reuse ratio (read/write) with verdict
142
- - Monthly $ equivalent + leverage vs Max plan
143
- - Daily-mode insights: peak day, avg/day, peak/trough variance
144
-
145
- ### Changed
146
- - `--by day` polish:
147
- - Bar fixed to 40-char width so session counts align in their own column
148
- - Brighter cyan (`\x1b[96m`) bar character
149
- - Total row separated by horizontal rule, properly aligned
150
- - Header line now shows period + total + sessions + group count at a glance:
151
- `agent-ledger week 2026-04-15 → 2026-04-22 · $13,356.27 shadow · 217 sessions · 7 agents`
152
-
153
- ## [0.2.0] — 2026-04-21
154
-
155
- ### Added
156
- - `--by <subagent|model|day>` flag — group rows by subagent (default), by
157
- Claude model, or by date. The `day` mode renders an ASCII bar chart of
158
- daily spend, useful for spotting "expensive Tuesdays."
159
- - Server-tool pricing: `web_search_requests` and `web_fetch_requests` from
160
- the `server_tool_use` block are now counted at $0.01/request and surface
161
- in a footer row when present.
162
- - ANSI color output, TTY-aware (NO_COLOR / FORCE_COLOR respected).
163
- - Subagent / model / day labels in cyan
164
- - Cost amounts color-coded by magnitude (red ≥$1000, yellow ≥$100,
165
- green ≥$1, dim <$1)
166
- - Total row bolded
167
- - New tests for server-tool pricing, `--by model`, and `--by day`
168
- (22 tests, up from 17).
169
-
170
- ### Changed
171
- - `aggregate(turns, from, to)` accepts an optional 4th arg `group: GroupKey`
172
- (`'subagent' | 'model' | 'day'`). Default behavior unchanged.
173
- - `AggregatedRow` now carries `webSearchRequests` and `webFetchRequests`
174
- counters; `CostBreakdown` adds `serverToolUseCost`.
175
-
176
- ### Not yet
177
- - Calibration against real Anthropic invoices (need volunteers — see
178
- `.github/ISSUE_TEMPLATE/`)
179
- - Cost forecasting (trailing 7-day burn rate)
180
- - Homebrew tap
181
-
182
- ## [0.1.0] — 2026-04-21
183
-
184
- Initial public release.
185
-
186
- ### Added
187
- - JSONL parser with recursive walk of `~/.claude/projects/**`, including nested
188
- `subagents/` directories
189
- - Per-subagent attribution via sidecar `agent-<hash>.meta.json` files
190
- - Pricing with cache TTL split: `ephemeral_5m_input_tokens` (1.25×) vs
191
- `ephemeral_1h_input_tokens` (2×)
192
- - Terminal table output (default) and Markdown export (`--md`)
193
- - JSON output (`--json`) for piping into other tools
194
- - `--plan pro|max` to suppress dollar columns on fixed-price subscriptions
195
- - Support for current and prior-generation model IDs (Opus 4.6/4.7,
196
- Sonnet 4.5/4.6, Haiku 4.0/4.5)
197
- - Family-prefix fallback for unknown future models (e.g. `claude-opus-5-*`
198
- resolves to Opus pricing)
199
- - `<synthetic>` turns (Claude Code context compaction) correctly priced at $0
200
- - Published to npm as `claude-agent-ledger`
package/CONTRIBUTING.md DELETED
@@ -1,42 +0,0 @@
1
- # Contributing
2
-
3
- Thanks for looking. This project is pre-alpha and in active shape — I'm still learning
4
- what people actually need from a Claude Code ledger.
5
-
6
- ## Quick help
7
-
8
- The most useful contributions right now:
9
-
10
- 1. **Run it on your own logs and tell me what it got wrong.** Open an issue with
11
- the shadow cost number `agent-ledger month` reports, and the actual Anthropic
12
- invoice if you're on pay-as-you-go. I want to calibrate.
13
- 2. **Add a model I missed.** `src/pricing.ts` has the table. Open a PR with the
14
- model ID and the published rates, plus a test in `test/pricing.test.ts`.
15
- 3. **Report a log format surprise.** If `agent-ledger` skips usage records or
16
- attributes them to `(main)` when they should be a subagent, paste a redacted
17
- log snippet into an issue.
18
-
19
- ## Running locally
20
-
21
- ```bash
22
- bun install
23
- bun test
24
- bun run typecheck
25
- bun run bin/agent-ledger.ts today
26
- ```
27
-
28
- ## Style
29
-
30
- - TypeScript strict. `noUncheckedIndexedAccess` is on.
31
- - Short functions. If a function needs a comment to explain what it does, the
32
- function is probably doing too much.
33
- - No runtime dependencies except Bun built-ins (we want a small install surface).
34
- - Tests use `bun:test`. Fixtures live in `test/fixtures/`.
35
-
36
- ## Out of scope (for now)
37
-
38
- - Real-time dashboards / web UIs
39
- - Sending data anywhere (the tool is read-only by design)
40
- - Tracking API keys or authenticating to Anthropic
41
-
42
- If you want those, that's a different tool — and that's fine, just not this one.
package/bun.lock DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "lockfileVersion": 1,
3
- "configVersion": 1,
4
- "workspaces": {
5
- "": {
6
- "name": "agent-ledger",
7
- "devDependencies": {
8
- "@types/bun": "latest",
9
- "fast-check": "^4.7.0",
10
- "typescript": "^5.6.0",
11
- },
12
- },
13
- },
14
- "packages": {
15
- "@types/bun": ["@types/bun@1.3.12", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.12.tgz", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
16
-
17
- "@types/node": ["@types/node@25.6.0", "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
18
-
19
- "bun-types": ["bun-types@1.3.12", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.12.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
20
-
21
- "fast-check": ["fast-check@4.7.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ=="],
22
-
23
- "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="],
24
-
25
- "typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
26
-
27
- "undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
28
- }
29
- }
@@ -1,75 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { join } from 'node:path';
3
- import { aggregate } from '../src/aggregator.ts';
4
- import { parseFile } from '../src/parser.ts';
5
-
6
- const FIXTURE = join(import.meta.dir, 'fixtures', 'sample.jsonl');
7
-
8
- describe('aggregate()', () => {
9
- test('groups assistant turns by subagent', async () => {
10
- const from = new Date('2026-04-21T00:00:00Z');
11
- const to = new Date('2026-04-21T23:59:59Z');
12
- const report = await aggregate(parseFile(FIXTURE), from, to);
13
-
14
- const agents = report.rows.map((r) => r.subagent);
15
- expect(agents).toContain('ios-factory');
16
- expect(agents).toContain('Reality Checker');
17
- expect(agents).toContain('(main)');
18
- });
19
-
20
- test('skips user turns (no usage data)', async () => {
21
- const from = new Date('2026-04-21T00:00:00Z');
22
- const to = new Date('2026-04-21T23:59:59Z');
23
- const report = await aggregate(parseFile(FIXTURE), from, to);
24
-
25
- // 4 assistant turns in fixture (3 subagents, some with >1 turn each).
26
- const totalSessions = report.total.sessionCount;
27
- expect(totalSessions).toBeGreaterThan(0);
28
- });
29
-
30
- test('rows are sorted by total cost descending', async () => {
31
- const from = new Date('2026-04-21T00:00:00Z');
32
- const to = new Date('2026-04-21T23:59:59Z');
33
- const report = await aggregate(parseFile(FIXTURE), from, to);
34
-
35
- for (let i = 1; i < report.rows.length; i++) {
36
- const prev = report.rows[i - 1]!.cost.totalCost;
37
- const curr = report.rows[i]!.cost.totalCost;
38
- expect(prev).toBeGreaterThanOrEqual(curr);
39
- }
40
- });
41
-
42
- test('total row sums all agent rows', async () => {
43
- const from = new Date('2026-04-21T00:00:00Z');
44
- const to = new Date('2026-04-21T23:59:59Z');
45
- const report = await aggregate(parseFile(FIXTURE), from, to);
46
-
47
- const sumCost = report.rows.reduce((s, r) => s + r.cost.totalCost, 0);
48
- expect(report.total.cost.totalCost).toBeCloseTo(sumCost, 6);
49
- });
50
-
51
- test('--by model groups by model id', async () => {
52
- const from = new Date('2026-04-21T00:00:00Z');
53
- const to = new Date('2026-04-21T23:59:59Z');
54
- const report = await aggregate(parseFile(FIXTURE), from, to, 'model');
55
-
56
- // Fixture has assistant turns with claude-opus-4-7 model
57
- const labels = report.rows.map((r) => r.subagent);
58
- expect(labels.some((l) => l.includes('claude-') || l === '(no-model)')).toBe(true);
59
- });
60
-
61
- test('--by day groups by ISO date and sorts chronologically', async () => {
62
- const from = new Date('2026-04-21T00:00:00Z');
63
- const to = new Date('2026-04-21T23:59:59Z');
64
- const report = await aggregate(parseFile(FIXTURE), from, to, 'day');
65
-
66
- // Each row label should look like a date YYYY-MM-DD
67
- for (const row of report.rows) {
68
- expect(row.subagent).toMatch(/^\d{4}-\d{2}-\d{2}$/);
69
- }
70
- // Ascending date order
71
- for (let i = 1; i < report.rows.length; i++) {
72
- expect(report.rows[i - 1]!.subagent <= report.rows[i]!.subagent).toBe(true);
73
- }
74
- });
75
- });
@@ -1,2 +0,0 @@
1
- {"type":"assistant","timestamp":"2026-04-21T10:05:00Z","message":{"role":"assistant","model":"claude-sonnet-4-6","usage":{"input_tokens":4200,"output_tokens":1300,"cache_creation_input_tokens":2000,"cache_read_input_tokens":22000}}}
2
- {"type":"assistant","timestamp":"2026-04-21T10:07:00Z","message":{"role":"assistant","model":"claude-sonnet-4-6","usage":{"input_tokens":800,"output_tokens":220,"cache_read_input_tokens":26000}}}
@@ -1 +0,0 @@
1
- {"agentType":"Reality Checker","description":"Review migration safety"}
@@ -1,2 +0,0 @@
1
- {"type":"user","timestamp":"2026-04-21T10:00:00Z","message":{"role":"user","content":"hello"}}
2
- {"type":"assistant","timestamp":"2026-04-21T10:00:12Z","message":{"role":"assistant","model":"claude-opus-4-7","usage":{"input_tokens":1200,"output_tokens":420,"cache_read_input_tokens":8000}}}
@@ -1,6 +0,0 @@
1
- {"type":"user","timestamp":"2026-04-21T10:00:00Z","message":{"role":"user","content":"Fix the release script"}}
2
- {"type":"assistant","timestamp":"2026-04-21T10:00:12Z","subagentType":"ios-factory","message":{"role":"assistant","model":"claude-opus-4-7","usage":{"input_tokens":4200,"output_tokens":1180,"cache_creation_input_tokens":2400,"cache_read_input_tokens":18000}}}
3
- {"type":"assistant","timestamp":"2026-04-21T10:01:02Z","subagentType":"ios-factory","message":{"role":"assistant","model":"claude-opus-4-7","usage":{"input_tokens":2100,"output_tokens":640,"cache_read_input_tokens":22000}}}
4
- {"type":"user","timestamp":"2026-04-21T11:30:00Z","message":{"role":"user","content":"Review this PR"}}
5
- {"type":"assistant","timestamp":"2026-04-21T11:30:08Z","subagentType":"Reality Checker","message":{"role":"assistant","model":"claude-sonnet-4-6","usage":{"input_tokens":8800,"output_tokens":1520,"cache_creation_input_tokens":1200,"cache_read_input_tokens":44000}}}
6
- {"type":"assistant","timestamp":"2026-04-21T15:10:00Z","message":{"role":"assistant","model":"claude-haiku-4-5-20251001","usage":{"input_tokens":1200,"output_tokens":320}}}
@@ -1,55 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { join } from 'node:path';
3
- import { aggregate } from '../src/aggregator.ts';
4
- import { parseAll } from '../src/parser.ts';
5
-
6
- const PROJECTS_DIR = join(import.meta.dir, 'fixtures', 'projects');
7
-
8
- describe('parseAll()', () => {
9
- test('walks nested subagents/ directories and attributes subagent type', async () => {
10
- const from = new Date('2026-04-21T00:00:00Z');
11
- const to = new Date('2026-04-21T23:59:59Z');
12
-
13
- const report = await aggregate(
14
- parseAll({ from, to, projectsDir: PROJECTS_DIR }),
15
- from,
16
- to,
17
- );
18
-
19
- const agents = report.rows.map((r) => r.subagent).sort();
20
- expect(agents).toContain('(main)');
21
- expect(agents).toContain('Reality Checker');
22
- });
23
-
24
- test('main-session turns are attributed to (main), not to any subagent', async () => {
25
- const from = new Date('2026-04-21T00:00:00Z');
26
- const to = new Date('2026-04-21T23:59:59Z');
27
-
28
- const report = await aggregate(
29
- parseAll({ from, to, projectsDir: PROJECTS_DIR }),
30
- from,
31
- to,
32
- );
33
-
34
- const main = report.rows.find((r) => r.subagent === '(main)');
35
- const realityChecker = report.rows.find((r) => r.subagent === 'Reality Checker');
36
-
37
- expect(main).toBeDefined();
38
- expect(realityChecker).toBeDefined();
39
- expect(main!.sessionCount).toBe(1);
40
- expect(realityChecker!.sessionCount).toBe(1);
41
- });
42
-
43
- test('respects date range filter', async () => {
44
- const from = new Date('2030-01-01T00:00:00Z');
45
- const to = new Date('2030-01-02T00:00:00Z');
46
-
47
- const report = await aggregate(
48
- parseAll({ from, to, projectsDir: PROJECTS_DIR }),
49
- from,
50
- to,
51
- );
52
-
53
- expect(report.rows.length).toBe(0);
54
- });
55
- });
@@ -1,144 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { costFor, isKnownModel } from '../src/pricing.ts';
3
-
4
- describe('costFor()', () => {
5
- test('Opus output is $75/M', () => {
6
- const cost = costFor(
7
- { input_tokens: 0, output_tokens: 1_000_000 },
8
- 'claude-opus-4-7',
9
- );
10
- expect(cost.outputCost).toBeCloseTo(75, 6);
11
- });
12
-
13
- test('Opus input is $15/M', () => {
14
- const cost = costFor(
15
- { input_tokens: 1_000_000, output_tokens: 0 },
16
- 'claude-opus-4-7',
17
- );
18
- expect(cost.inputCost).toBeCloseTo(15, 6);
19
- });
20
-
21
- test('5-minute cache creation is 1.25× base input', () => {
22
- const cost = costFor(
23
- {
24
- input_tokens: 0,
25
- output_tokens: 0,
26
- cache_creation: { ephemeral_5m_input_tokens: 1_000_000 },
27
- },
28
- 'claude-opus-4-7',
29
- );
30
- // Opus input $15 × 1.25 = $18.75 per 1M
31
- expect(cost.cacheCreation5mCost).toBeCloseTo(18.75, 4);
32
- expect(cost.cacheCreation1hCost).toBeCloseTo(0, 6);
33
- });
34
-
35
- test('1-hour cache creation is 2× base input (the TTL trap)', () => {
36
- const cost = costFor(
37
- {
38
- input_tokens: 0,
39
- output_tokens: 0,
40
- cache_creation: { ephemeral_1h_input_tokens: 1_000_000 },
41
- },
42
- 'claude-opus-4-7',
43
- );
44
- // Opus input $15 × 2 = $30 per 1M
45
- expect(cost.cacheCreation1hCost).toBeCloseTo(30, 4);
46
- expect(cost.cacheCreation5mCost).toBeCloseTo(0, 6);
47
- });
48
-
49
- test('cache read is 10% of base input', () => {
50
- const cost = costFor(
51
- {
52
- input_tokens: 0,
53
- output_tokens: 0,
54
- cache_read_input_tokens: 1_000_000,
55
- },
56
- 'claude-opus-4-7',
57
- );
58
- // Opus input $15 × 0.1 = $1.50 per 1M
59
- expect(cost.cacheReadCost).toBeCloseTo(1.5, 4);
60
- });
61
-
62
- test('legacy flat cache_creation_input_tokens falls back to 5m TTL pricing', () => {
63
- const cost = costFor(
64
- {
65
- input_tokens: 0,
66
- output_tokens: 0,
67
- cache_creation_input_tokens: 1_000_000,
68
- },
69
- 'claude-opus-4-7',
70
- );
71
- expect(cost.cacheCreation5mCost).toBeCloseTo(18.75, 4);
72
- });
73
-
74
- test('prior-generation model IDs (opus-4-6) resolve to current pricing', () => {
75
- const cost = costFor(
76
- { input_tokens: 1_000_000, output_tokens: 0 },
77
- 'claude-opus-4-6',
78
- );
79
- expect(cost.inputCost).toBeCloseTo(15, 6);
80
- });
81
-
82
- test('unknown future opus variant falls back to opus family pricing', () => {
83
- const cost = costFor(
84
- { input_tokens: 1_000_000, output_tokens: 0 },
85
- 'claude-opus-9-9',
86
- );
87
- expect(cost.inputCost).toBeCloseTo(15, 6);
88
- });
89
-
90
- test('<synthetic> model is free', () => {
91
- const cost = costFor(
92
- {
93
- input_tokens: 1_000_000,
94
- output_tokens: 1_000_000,
95
- cache_read_input_tokens: 1_000_000,
96
- },
97
- '<synthetic>',
98
- );
99
- expect(cost.totalCost).toBe(0);
100
- });
101
-
102
- test('isKnownModel recognizes current, prior, and family matches', () => {
103
- expect(isKnownModel('claude-opus-4-7')).toBe(true);
104
- expect(isKnownModel('claude-opus-4-6')).toBe(true);
105
- expect(isKnownModel('claude-sonnet-4-6')).toBe(true);
106
- expect(isKnownModel('<synthetic>')).toBe(true);
107
- expect(isKnownModel('something-entirely-unknown')).toBe(false);
108
- expect(isKnownModel(undefined)).toBe(false);
109
- });
110
-
111
- test('server_tool_use: web_search at $0.01/req', () => {
112
- const cost = costFor(
113
- {
114
- input_tokens: 0,
115
- output_tokens: 0,
116
- server_tool_use: { web_search_requests: 100 },
117
- },
118
- 'claude-opus-4-7',
119
- );
120
- // 100 × $0.01 = $1.00
121
- expect(cost.serverToolUseCost).toBeCloseTo(1.0, 6);
122
- expect(cost.totalCost).toBeCloseTo(1.0, 6);
123
- });
124
-
125
- test('server_tool_use: web_fetch counted, summed with web_search', () => {
126
- const cost = costFor(
127
- {
128
- input_tokens: 0,
129
- output_tokens: 0,
130
- server_tool_use: { web_search_requests: 50, web_fetch_requests: 50 },
131
- },
132
- 'claude-opus-4-7',
133
- );
134
- expect(cost.serverToolUseCost).toBeCloseTo(1.0, 6);
135
- });
136
-
137
- test('server_tool_use absent → no cost', () => {
138
- const cost = costFor(
139
- { input_tokens: 1000, output_tokens: 1000 },
140
- 'claude-opus-4-7',
141
- );
142
- expect(cost.serverToolUseCost).toBe(0);
143
- });
144
- });