claude-agent-ledger 0.1.0 → 0.2.0
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.
- package/CHANGELOG.md +34 -12
- package/README.md +104 -58
- package/bin/agent-ledger.ts +29 -9
- package/package.json +1 -1
- package/src/aggregator.ts +37 -12
- package/src/format.ts +111 -22
- package/src/pricing.ts +20 -1
- package/src/types.ts +10 -0
- package/test/aggregator.test.ts +25 -0
- package/test/pricing.test.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to `agent-ledger` will be documented here.
|
|
3
|
+
All notable changes to `claude-agent-ledger` will be documented here.
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.2.0] — 2026-04-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `--by <subagent|model|day>` flag — group rows by subagent (default), by
|
|
12
|
+
Claude model, or by date. The `day` mode renders an ASCII bar chart of
|
|
13
|
+
daily spend, useful for spotting "expensive Tuesdays."
|
|
14
|
+
- Server-tool pricing: `web_search_requests` and `web_fetch_requests` from
|
|
15
|
+
the `server_tool_use` block are now counted at $0.01/request and surface
|
|
16
|
+
in a footer row when present.
|
|
17
|
+
- ANSI color output, TTY-aware (NO_COLOR / FORCE_COLOR respected).
|
|
18
|
+
- Subagent / model / day labels in cyan
|
|
19
|
+
- Cost amounts color-coded by magnitude (red ≥$1000, yellow ≥$100,
|
|
20
|
+
green ≥$1, dim <$1)
|
|
21
|
+
- Total row bolded
|
|
22
|
+
- New tests for server-tool pricing, `--by model`, and `--by day`
|
|
23
|
+
(22 tests, up from 17).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- `aggregate(turns, from, to)` accepts an optional 4th arg `group: GroupKey`
|
|
27
|
+
(`'subagent' | 'model' | 'day'`). Default behavior unchanged.
|
|
28
|
+
- `AggregatedRow` now carries `webSearchRequests` and `webFetchRequests`
|
|
29
|
+
counters; `CostBreakdown` adds `serverToolUseCost`.
|
|
30
|
+
|
|
31
|
+
### Not yet
|
|
32
|
+
- Calibration against real Anthropic invoices (need volunteers — see
|
|
33
|
+
`.github/ISSUE_TEMPLATE/`)
|
|
34
|
+
- Cost forecasting (trailing 7-day burn rate)
|
|
35
|
+
- Homebrew tap
|
|
36
|
+
|
|
37
|
+
## [0.1.0] — 2026-04-21
|
|
38
|
+
|
|
39
|
+
Initial public release.
|
|
9
40
|
|
|
10
41
|
### Added
|
|
11
42
|
- JSONL parser with recursive walk of `~/.claude/projects/**`, including nested
|
|
@@ -21,13 +52,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
52
|
- Family-prefix fallback for unknown future models (e.g. `claude-opus-5-*`
|
|
22
53
|
resolves to Opus pricing)
|
|
23
54
|
- `<synthetic>` turns (Claude Code context compaction) correctly priced at $0
|
|
24
|
-
|
|
25
|
-
### Not yet
|
|
26
|
-
- `--by model` breakdown
|
|
27
|
-
- Cost forecasting (trailing 7-day burn rate)
|
|
28
|
-
- `server_tool_use` pricing (web_search_requests, web_fetch_requests)
|
|
29
|
-
- Homebrew tap
|
|
30
|
-
|
|
31
|
-
## [0.0.1] — 2026-04-21
|
|
32
|
-
|
|
33
|
-
Initial skeleton, not yet published.
|
|
55
|
+
- Published to npm as `claude-agent-ledger`
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# agent-ledger
|
|
1
|
+
# claude-agent-ledger
|
|
2
2
|
|
|
3
|
-
> See exactly where your Claude Code budget goes — per subagent, per
|
|
3
|
+
> See exactly where your Claude Code budget goes — per subagent, per model, per day.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/claude-agent-ledger)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -12,50 +12,70 @@
|
|
|
12
12
|
Claude Code's invoice at month-end tells you one number. It doesn't tell you:
|
|
13
13
|
|
|
14
14
|
- Which of your subagents burned most of it
|
|
15
|
-
- Which
|
|
16
|
-
-
|
|
15
|
+
- Which model (Opus / Sonnet / Haiku) ate the budget
|
|
16
|
+
- Which day of last week was the expensive one
|
|
17
17
|
- Whether your "cheap" Haiku agent is actually quietly expensive
|
|
18
18
|
|
|
19
19
|
If you run **one** Claude Code session, this is fine — you already know.
|
|
20
20
|
If you run **seven** subagents daily (reliability, release, review, iOS factory, docs, ...), you're flying blind.
|
|
21
21
|
|
|
22
|
-
`agent-ledger` reads Claude Code's local session logs and gives you a per-subagent ledger. That's it.
|
|
22
|
+
`claude-agent-ledger` reads Claude Code's local session logs and gives you a per-subagent ledger. That's it.
|
|
23
23
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
A single-file CLI. No server. No account. No data leaves your machine.
|
|
24
|
+
## Install
|
|
27
25
|
|
|
28
26
|
```bash
|
|
29
|
-
# Install
|
|
30
27
|
npm install -g claude-agent-ledger
|
|
31
28
|
# or: bun install -g claude-agent-ledger
|
|
32
29
|
|
|
33
|
-
#
|
|
34
|
-
agent-ledger
|
|
30
|
+
agent-ledger today # today's spend, by subagent
|
|
31
|
+
agent-ledger week --by model # last 7 days, by Claude model
|
|
32
|
+
agent-ledger week --by day # daily bar chart
|
|
33
|
+
agent-ledger month --md > report.md
|
|
34
|
+
```
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
agent-ledger week
|
|
36
|
+
No server. No account. No data leaves your machine.
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
agent-ledger month --md > month-report.md
|
|
41
|
-
```
|
|
38
|
+
## Example output (real)
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
### Default — by subagent
|
|
44
41
|
|
|
45
42
|
```
|
|
46
43
|
agent-ledger week · 2026-04-14 → 2026-04-21
|
|
47
44
|
|
|
48
|
-
subagent sess tokens(in/out)
|
|
49
|
-
|
|
50
|
-
(main)
|
|
51
|
-
general-purpose
|
|
52
|
-
Swift Developer 13 4K / 574K
|
|
53
|
-
Market Researcher 10 107K / 74K
|
|
54
|
-
Explore 13 176K / 77K
|
|
55
|
-
zstackws 3 232 / 20K
|
|
56
|
-
claude-code-guide 1 2K / 2K
|
|
57
|
-
|
|
58
|
-
total
|
|
45
|
+
subagent sess tokens(in/out) cache tokens (1h/5m/read) cost
|
|
46
|
+
───────────────────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
(main) 28 514K / 15.87M 1h:116.10M 5m: 0 r:5229.48M $12524.97
|
|
48
|
+
general-purpose 148 113K / 955K 1h: 0 5m: 20.67M r: 183.94M $599.97
|
|
49
|
+
Swift Developer 13 4K / 574K 1h: 0 5m: 1.61M r: 47.55M $28.93
|
|
50
|
+
Market Researcher 10 107K / 74K 1h: 0 5m: 863K r: 8.61M $7.26
|
|
51
|
+
Explore 13 176K / 77K 1h: 0 5m: 2.60M r: 22.38M $6.47
|
|
52
|
+
zstackws 3 232 / 20K 1h: 0 5m: 829K r: 3.52M $4.47
|
|
53
|
+
claude-code-guide 1 2K / 2K 1h: 0 5m: 56K r: 223K $0.08
|
|
54
|
+
───────────────────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
total 216 916K / 17.57M 1h:116.10M 5m: 26.62M r:5495.71M $13172.14
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### `--by model`
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
subagent sess tokens(in/out) cache tokens (1h/5m/read) cost
|
|
62
|
+
───────────────────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
claude-opus-4-7 16 59K / 8.35M 1h: 61.15M 5m: 2.32M r:2974.59M $6967.29
|
|
64
|
+
claude-opus-4-6 127 544K / 8.25M 1h: 55.02M 5m: 13.30M r:2422.18M $6160.46
|
|
65
|
+
claude-sonnet-4-6 65 135K / 946K 1h: 0 5m: 8.81M r: 98.12M $77.07
|
|
66
|
+
claude-haiku-4-5 13 178K / 67K 1h: 0 5m: 2.19M r: 21.57M $4.33
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `--by day` (ASCII bar chart)
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
2026-04-15 $3749.84 ████████████████████████████████████ (18 sess)
|
|
73
|
+
2026-04-16 $2492.99 ████████████████████████ (175 sess)
|
|
74
|
+
2026-04-17 $2111.63 ████████████████████ (9 sess)
|
|
75
|
+
2026-04-18 $1840.02 ██████████████████ (18 sess)
|
|
76
|
+
2026-04-19 $644.97 ██████ (5 sess)
|
|
77
|
+
2026-04-20 $1095.58 ███████████ (6 sess)
|
|
78
|
+
2026-04-21 $1273.55 ████████████ (9 sess)
|
|
59
79
|
```
|
|
60
80
|
|
|
61
81
|
Yes those are real numbers. Yes it's a lot. See [Shadow cost vs actual bill](#shadow-cost-vs-actual-bill).
|
|
@@ -64,24 +84,27 @@ Yes those are real numbers. Yes it's a lot. See [Shadow cost vs actual bill](#sh
|
|
|
64
84
|
|
|
65
85
|
The `cost` column shows what you **would** pay at Anthropic's pay-as-you-go API rates.
|
|
66
86
|
|
|
67
|
-
If you're on a fixed-price subscription (Claude Pro / Max / Claude Code plan), your actual bill is capped — the shadow cost is what the same usage **would** have cost through the raw API.
|
|
87
|
+
If you're on a fixed-price subscription (Claude Pro / Max / Claude Code plan), your actual bill is capped — the shadow cost is what the same usage **would** have cost through the raw API. Useful for:
|
|
68
88
|
|
|
69
89
|
- Understanding where time-and-token budget goes, regardless of how you pay
|
|
70
90
|
- Spotting runaway agents (a 10× spike is still a 10× spike, capped or not)
|
|
71
91
|
- Deciding when to downshift to Haiku or up to Opus per task
|
|
92
|
+
- Justifying a Max subscription internally ("our shadow cost is $13K/wk, Max is $200/mo — that's a 65× discount")
|
|
72
93
|
|
|
73
|
-
If you're on pay-as-you-go, the `cost` column is roughly your bill (±5% depending on tier / geography).
|
|
94
|
+
If you're on pay-as-you-go, the `cost` column is roughly your bill (±5–10% depending on tier / geography / `server_tool_use` calibration).
|
|
74
95
|
|
|
75
96
|
## How it works
|
|
76
97
|
|
|
77
98
|
Claude Code writes every conversation turn to `~/.claude/projects/<encoded-path>/*.jsonl`.
|
|
78
|
-
Each turn records input/output tokens, cache tokens (with TTL split),
|
|
79
|
-
|
|
80
|
-
`.meta.json`
|
|
99
|
+
Each turn records input/output tokens, cache tokens (with TTL split), the model used,
|
|
100
|
+
and any `server_tool_use` (web_search / web_fetch) requests. Subagent invocations live
|
|
101
|
+
under `<session>/subagents/agent-*.jsonl`, with a sidecar `.meta.json` carrying the real
|
|
102
|
+
subagent name (e.g. `Reality Checker`, `ios-factory`).
|
|
81
103
|
|
|
82
|
-
`agent-ledger` walks the tree, resolves subagent types from the sidecar metadata,
|
|
83
|
-
aggregates by
|
|
84
|
-
anywhere — the tool never touches the network
|
|
104
|
+
`claude-agent-ledger` walks the tree, resolves subagent types from the sidecar metadata,
|
|
105
|
+
and aggregates by your chosen key (subagent, model, or day). Pricing is applied locally
|
|
106
|
+
from `src/pricing.ts`. Nothing is sent anywhere — the tool never touches the network
|
|
107
|
+
after install.
|
|
85
108
|
|
|
86
109
|
### Cache accounting
|
|
87
110
|
|
|
@@ -93,47 +116,71 @@ Anthropic's prompt caching has two TTLs in current logs:
|
|
|
93
116
|
| `ephemeral_1h_input_tokens` | 2.00× base input | 1-hour TTL (long-lived sessions) |
|
|
94
117
|
| `cache_read_input_tokens` | 0.10× base input | Read price (90% discount, TTL-agnostic) |
|
|
95
118
|
|
|
96
|
-
Naive trackers lump these together as "input" and either overstate (by counting all creation as 1.25×) or understate (by ignoring the 1-hour writes entirely). `agent-ledger` breaks them out explicitly — see `src/pricing.ts`.
|
|
119
|
+
Naive trackers lump these together as "input" and either overstate (by counting all creation as 1.25×) or understate (by ignoring the 1-hour writes entirely). `claude-agent-ledger` breaks them out explicitly — see `src/pricing.ts`.
|
|
120
|
+
|
|
121
|
+
### Server tool pricing
|
|
122
|
+
|
|
123
|
+
`web_search_requests` and `web_fetch_requests` are counted at $0.01/request and shown in a footer when present:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
server tools: web_search ×7, web_fetch ×0 ($0.07)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If Anthropic's actual rate differs (or web_fetch has its own price), open an issue.
|
|
130
|
+
|
|
131
|
+
## Flags
|
|
132
|
+
|
|
133
|
+
| Flag | What it does |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `--by <subagent\|model\|day>` | Group rows. Default: `subagent`. `day` renders as ASCII bar chart. |
|
|
136
|
+
| `--md` | Markdown table output (good for committing or sharing) |
|
|
137
|
+
| `--json` | Raw JSON output (pipe into jq) |
|
|
138
|
+
| `--plan pro\|max` | Hide dollar columns; show token utilization only |
|
|
139
|
+
|
|
140
|
+
| Env | What it does |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `NO_COLOR=1` | Disable ANSI colors |
|
|
143
|
+
| `FORCE_COLOR=1` | Force ANSI colors when piping |
|
|
97
144
|
|
|
98
145
|
## What it's not
|
|
99
146
|
|
|
100
147
|
- Not a real-time dashboard (post-hoc log reader)
|
|
101
|
-
- Not a tracker for direct Anthropic API calls (those logs live
|
|
148
|
+
- Not a tracker for direct Anthropic API calls (those logs live server-side)
|
|
102
149
|
- Not a cost-limiter or kill-switch (read-only, by design)
|
|
103
150
|
- Not affiliated with Anthropic
|
|
104
151
|
|
|
105
152
|
## Who this is for
|
|
106
153
|
|
|
107
154
|
- You run ≥ 2 Claude Code subagents and genuinely don't know the per-agent split
|
|
108
|
-
- You're
|
|
155
|
+
- You're on Claude Max and want to know what your "free" usage is worth
|
|
109
156
|
- You're building AI Agent systems and want observability primitives, not vendor dashboards
|
|
110
157
|
|
|
111
158
|
## Status
|
|
112
159
|
|
|
113
|
-
|
|
114
|
-
Pricing
|
|
115
|
-
may drift ±5-10% from Anthropic's actual invoice in edge cases (retries, tier
|
|
116
|
-
server_tool_use
|
|
160
|
+
`v0.2.0` shipped 2026-04-21. Validated on ~200K turns of real Claude Code logs.
|
|
161
|
+
Pricing matches Anthropic published rates as of 2026-04-21. Shadow cost values
|
|
162
|
+
may drift ±5-10% from Anthropic's actual invoice in edge cases (retries, tier
|
|
163
|
+
bumps, `server_tool_use` calibration). Calibration PRs welcome — see issue
|
|
164
|
+
templates.
|
|
117
165
|
|
|
118
166
|
- [x] Log parser (JSONL streaming, recursive subagent discovery)
|
|
119
|
-
- [x] Per-subagent aggregation
|
|
120
|
-
- [x]
|
|
121
|
-
- [x]
|
|
167
|
+
- [x] Per-subagent aggregation
|
|
168
|
+
- [x] Per-model aggregation (`--by model`)
|
|
169
|
+
- [x] Per-day aggregation with bar chart (`--by day`)
|
|
170
|
+
- [x] Terminal table + Markdown + JSON output
|
|
171
|
+
- [x] ANSI color, TTY-aware
|
|
122
172
|
- [x] Cache 1h/5m TTL split pricing
|
|
123
|
-
- [
|
|
124
|
-
- [
|
|
125
|
-
- [ ] Per-model breakdown (`agent-ledger week --by model`)
|
|
173
|
+
- [x] `--plan pro|max` to mask shadow cost
|
|
174
|
+
- [x] `server_tool_use` pricing (web_search, web_fetch)
|
|
126
175
|
- [ ] Cost forecasting (trailing 7-day burn rate)
|
|
127
|
-
- [ ]
|
|
176
|
+
- [ ] Calibration against real Anthropic invoices
|
|
128
177
|
- [ ] Homebrew tap
|
|
129
178
|
|
|
130
|
-
First tagged release: **2026-05-05**.
|
|
131
|
-
|
|
132
179
|
## Install from source
|
|
133
180
|
|
|
134
181
|
```bash
|
|
135
|
-
git clone https://github.com/XJM-free/agent-ledger.git
|
|
136
|
-
cd agent-ledger
|
|
182
|
+
git clone https://github.com/XJM-free/claude-agent-ledger.git
|
|
183
|
+
cd claude-agent-ledger
|
|
137
184
|
bun install
|
|
138
185
|
bun link
|
|
139
186
|
agent-ledger today
|
|
@@ -142,15 +189,14 @@ agent-ledger today
|
|
|
142
189
|
## Development
|
|
143
190
|
|
|
144
191
|
```bash
|
|
145
|
-
bun test
|
|
192
|
+
bun test # 22 pass, 0 fail
|
|
146
193
|
bun run typecheck
|
|
147
194
|
```
|
|
148
195
|
|
|
149
196
|
## Author
|
|
150
197
|
|
|
151
198
|
Built by [@XJM-free](https://jiexiang.dev) while running seven production Claude Code
|
|
152
|
-
subagents for infrastructure work. More context
|
|
153
|
-
Claude Code](https://jiexiang.dev).
|
|
199
|
+
subagents for infrastructure work. More context coming on the blog.
|
|
154
200
|
|
|
155
201
|
## License
|
|
156
202
|
|
package/bin/agent-ledger.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { aggregate } from '../src/aggregator.ts';
|
|
3
|
-
import { formatMarkdown, formatTable } from '../src/format.ts';
|
|
2
|
+
import { aggregate, type GroupKey } from '../src/aggregator.ts';
|
|
3
|
+
import { formatMarkdown, formatTable, formatTrend } from '../src/format.ts';
|
|
4
4
|
import { parseAll } from '../src/parser.ts';
|
|
5
5
|
import type { LedgerReport } from '../src/types.ts';
|
|
6
6
|
|
|
@@ -12,6 +12,7 @@ interface Args {
|
|
|
12
12
|
markdown: boolean;
|
|
13
13
|
json: boolean;
|
|
14
14
|
plan: Plan;
|
|
15
|
+
by: GroupKey;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
function periodRange(period: Period): { from: Date; to: Date } {
|
|
@@ -33,13 +34,21 @@ function usage(): never {
|
|
|
33
34
|
console.error(`Usage: agent-ledger <today|week|month> [flags]
|
|
34
35
|
|
|
35
36
|
Flags:
|
|
36
|
-
--
|
|
37
|
-
|
|
38
|
-
--
|
|
39
|
-
|
|
37
|
+
--by <subagent|model|day> Group rows by subagent (default), model, or day.
|
|
38
|
+
"day" renders as a daily trend bar chart.
|
|
39
|
+
--md Emit Markdown table (good for sharing / commits)
|
|
40
|
+
--json Emit raw JSON (for piping into other tools)
|
|
41
|
+
--plan pro|max Suppress dollar columns (token utilization only)
|
|
42
|
+
Default is pay-as-you-go: shows shadow cost in $.
|
|
43
|
+
|
|
44
|
+
Environment:
|
|
45
|
+
NO_COLOR=1 Disable ANSI colors
|
|
46
|
+
FORCE_COLOR=1 Force colors even when piping
|
|
40
47
|
|
|
41
48
|
Examples:
|
|
42
49
|
agent-ledger today
|
|
50
|
+
agent-ledger week --by model
|
|
51
|
+
agent-ledger week --by day # daily bar chart
|
|
43
52
|
agent-ledger week --md > week.md
|
|
44
53
|
agent-ledger month --json | jq '.total.cost.totalCost'
|
|
45
54
|
agent-ledger week --plan max
|
|
@@ -52,6 +61,7 @@ function parseArgs(argv: string[]): Args {
|
|
|
52
61
|
if (!period || !['today', 'week', 'month'].includes(period)) usage();
|
|
53
62
|
|
|
54
63
|
let plan: Plan = 'payg';
|
|
64
|
+
let by: GroupKey = 'subagent';
|
|
55
65
|
for (let i = 0; i < rest.length; i++) {
|
|
56
66
|
const flag = rest[i];
|
|
57
67
|
if (flag === '--plan') {
|
|
@@ -62,6 +72,14 @@ function parseArgs(argv: string[]): Args {
|
|
|
62
72
|
} else {
|
|
63
73
|
usage();
|
|
64
74
|
}
|
|
75
|
+
} else if (flag === '--by') {
|
|
76
|
+
const next = rest[i + 1];
|
|
77
|
+
if (next === 'subagent' || next === 'model' || next === 'day') {
|
|
78
|
+
by = next;
|
|
79
|
+
i++;
|
|
80
|
+
} else {
|
|
81
|
+
usage();
|
|
82
|
+
}
|
|
65
83
|
}
|
|
66
84
|
}
|
|
67
85
|
return {
|
|
@@ -69,19 +87,19 @@ function parseArgs(argv: string[]): Args {
|
|
|
69
87
|
markdown: rest.includes('--md'),
|
|
70
88
|
json: rest.includes('--json'),
|
|
71
89
|
plan,
|
|
90
|
+
by,
|
|
72
91
|
};
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
function planMask(report: LedgerReport, plan: Plan): LedgerReport {
|
|
76
95
|
if (plan === 'payg') return report;
|
|
77
|
-
// On Pro/Max the per-call dollar number isn't what you pay, so hide it.
|
|
78
|
-
// Keep tokens visible — those are the real utilization signal.
|
|
79
96
|
const zero = () => ({
|
|
80
97
|
inputCost: 0,
|
|
81
98
|
outputCost: 0,
|
|
82
99
|
cacheCreation5mCost: 0,
|
|
83
100
|
cacheCreation1hCost: 0,
|
|
84
101
|
cacheReadCost: 0,
|
|
102
|
+
serverToolUseCost: 0,
|
|
85
103
|
totalCost: 0,
|
|
86
104
|
});
|
|
87
105
|
return {
|
|
@@ -94,7 +112,7 @@ function planMask(report: LedgerReport, plan: Plan): LedgerReport {
|
|
|
94
112
|
async function main(): Promise<void> {
|
|
95
113
|
const args = parseArgs(process.argv.slice(2));
|
|
96
114
|
const { from, to } = periodRange(args.period);
|
|
97
|
-
let report = await aggregate(parseAll({ from, to }), from, to);
|
|
115
|
+
let report = await aggregate(parseAll({ from, to }), from, to, args.by);
|
|
98
116
|
report = planMask(report, args.plan);
|
|
99
117
|
|
|
100
118
|
let output: string;
|
|
@@ -102,6 +120,8 @@ async function main(): Promise<void> {
|
|
|
102
120
|
output = JSON.stringify(report, null, 2);
|
|
103
121
|
} else if (args.markdown) {
|
|
104
122
|
output = formatMarkdown(report, args.period);
|
|
123
|
+
} else if (args.by === 'day') {
|
|
124
|
+
output = formatTrend(report, args.period);
|
|
105
125
|
} else {
|
|
106
126
|
output = formatTable(report, args.period);
|
|
107
127
|
}
|
package/package.json
CHANGED
package/src/aggregator.ts
CHANGED
|
@@ -2,22 +2,28 @@ import { costFor } from './pricing.ts';
|
|
|
2
2
|
import type { AggregatedRow, LedgerReport, SessionTurn } from './types.ts';
|
|
3
3
|
|
|
4
4
|
const MAIN_AGENT_LABEL = '(main)';
|
|
5
|
+
const UNKNOWN_MODEL_LABEL = '(no-model)';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
export type GroupKey = 'subagent' | 'model' | 'day';
|
|
8
|
+
|
|
9
|
+
function emptyRow(label: string): AggregatedRow {
|
|
7
10
|
return {
|
|
8
|
-
subagent,
|
|
11
|
+
subagent: label,
|
|
9
12
|
sessionCount: 0,
|
|
10
13
|
inputTokens: 0,
|
|
11
14
|
outputTokens: 0,
|
|
12
15
|
cacheCreation5mTokens: 0,
|
|
13
16
|
cacheCreation1hTokens: 0,
|
|
14
17
|
cacheReadTokens: 0,
|
|
18
|
+
webSearchRequests: 0,
|
|
19
|
+
webFetchRequests: 0,
|
|
15
20
|
cost: {
|
|
16
21
|
inputCost: 0,
|
|
17
22
|
outputCost: 0,
|
|
18
23
|
cacheCreation5mCost: 0,
|
|
19
24
|
cacheCreation1hCost: 0,
|
|
20
25
|
cacheReadCost: 0,
|
|
26
|
+
serverToolUseCost: 0,
|
|
21
27
|
totalCost: 0,
|
|
22
28
|
},
|
|
23
29
|
};
|
|
@@ -35,19 +41,32 @@ function splitCache(turn: SessionTurn): { fiveMin: number; oneHour: number } {
|
|
|
35
41
|
return { fiveMin: usage.cache_creation_input_tokens ?? 0, oneHour: 0 };
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
function keyOf(turn: SessionTurn, group: GroupKey): string {
|
|
45
|
+
if (group === 'model') return turn.model ?? UNKNOWN_MODEL_LABEL;
|
|
46
|
+
if (group === 'day') return turn.timestamp.slice(0, 10);
|
|
47
|
+
return turn.subagentType ?? MAIN_AGENT_LABEL;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sortKey(group: GroupKey): (a: AggregatedRow, b: AggregatedRow) => number {
|
|
51
|
+
// For day groupings users want chronological; otherwise descend by cost.
|
|
52
|
+
if (group === 'day') return (a, b) => (a.subagent < b.subagent ? -1 : a.subagent > b.subagent ? 1 : 0);
|
|
53
|
+
return (a, b) => b.cost.totalCost - a.cost.totalCost;
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
export async function aggregate(
|
|
39
57
|
turns: AsyncIterable<SessionTurn>,
|
|
40
58
|
from: Date,
|
|
41
59
|
to: Date,
|
|
60
|
+
group: GroupKey = 'subagent',
|
|
42
61
|
): Promise<LedgerReport> {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
62
|
+
const rowsByKey = new Map<string, AggregatedRow>();
|
|
63
|
+
const sessionsByKey = new Map<string, Set<string>>();
|
|
45
64
|
|
|
46
65
|
for await (const turn of turns) {
|
|
47
66
|
if (turn.type !== 'assistant' || !turn.usage) continue;
|
|
48
67
|
|
|
49
|
-
const
|
|
50
|
-
const row =
|
|
68
|
+
const key = keyOf(turn, group);
|
|
69
|
+
const row = rowsByKey.get(key) ?? emptyRow(key);
|
|
51
70
|
|
|
52
71
|
const cost = costFor(turn.usage, turn.model);
|
|
53
72
|
const { fiveMin, oneHour } = splitCache(turn);
|
|
@@ -57,25 +76,28 @@ export async function aggregate(
|
|
|
57
76
|
row.cacheCreation5mTokens += fiveMin;
|
|
58
77
|
row.cacheCreation1hTokens += oneHour;
|
|
59
78
|
row.cacheReadTokens += turn.usage.cache_read_input_tokens ?? 0;
|
|
79
|
+
row.webSearchRequests += turn.usage.server_tool_use?.web_search_requests ?? 0;
|
|
80
|
+
row.webFetchRequests += turn.usage.server_tool_use?.web_fetch_requests ?? 0;
|
|
60
81
|
row.cost.inputCost += cost.inputCost;
|
|
61
82
|
row.cost.outputCost += cost.outputCost;
|
|
62
83
|
row.cost.cacheCreation5mCost += cost.cacheCreation5mCost;
|
|
63
84
|
row.cost.cacheCreation1hCost += cost.cacheCreation1hCost;
|
|
64
85
|
row.cost.cacheReadCost += cost.cacheReadCost;
|
|
86
|
+
row.cost.serverToolUseCost += cost.serverToolUseCost;
|
|
65
87
|
row.cost.totalCost += cost.totalCost;
|
|
66
88
|
|
|
67
|
-
const sessions =
|
|
89
|
+
const sessions = sessionsByKey.get(key) ?? new Set<string>();
|
|
68
90
|
sessions.add(turn.sessionId);
|
|
69
|
-
|
|
91
|
+
sessionsByKey.set(key, sessions);
|
|
70
92
|
|
|
71
|
-
|
|
93
|
+
rowsByKey.set(key, row);
|
|
72
94
|
}
|
|
73
95
|
|
|
74
|
-
for (const [
|
|
75
|
-
row.sessionCount =
|
|
96
|
+
for (const [key, row] of rowsByKey) {
|
|
97
|
+
row.sessionCount = sessionsByKey.get(key)?.size ?? 0;
|
|
76
98
|
}
|
|
77
99
|
|
|
78
|
-
const rows = [...
|
|
100
|
+
const rows = [...rowsByKey.values()].sort(sortKey(group));
|
|
79
101
|
const total = rows.reduce<AggregatedRow>((acc, row) => {
|
|
80
102
|
acc.sessionCount += row.sessionCount;
|
|
81
103
|
acc.inputTokens += row.inputTokens;
|
|
@@ -83,11 +105,14 @@ export async function aggregate(
|
|
|
83
105
|
acc.cacheCreation5mTokens += row.cacheCreation5mTokens;
|
|
84
106
|
acc.cacheCreation1hTokens += row.cacheCreation1hTokens;
|
|
85
107
|
acc.cacheReadTokens += row.cacheReadTokens;
|
|
108
|
+
acc.webSearchRequests += row.webSearchRequests;
|
|
109
|
+
acc.webFetchRequests += row.webFetchRequests;
|
|
86
110
|
acc.cost.inputCost += row.cost.inputCost;
|
|
87
111
|
acc.cost.outputCost += row.cost.outputCost;
|
|
88
112
|
acc.cost.cacheCreation5mCost += row.cost.cacheCreation5mCost;
|
|
89
113
|
acc.cost.cacheCreation1hCost += row.cost.cacheCreation1hCost;
|
|
90
114
|
acc.cost.cacheReadCost += row.cost.cacheReadCost;
|
|
115
|
+
acc.cost.serverToolUseCost += row.cost.serverToolUseCost;
|
|
91
116
|
acc.cost.totalCost += row.cost.totalCost;
|
|
92
117
|
return acc;
|
|
93
118
|
}, emptyRow('total'));
|
package/src/format.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import type { AggregatedRow, LedgerReport } from './types.ts';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const PAD_LABEL = 22;
|
|
4
|
+
|
|
5
|
+
// Color helpers — TTY-aware, NO_COLOR-respecting (https://no-color.org/).
|
|
6
|
+
const USE_COLOR = (() => {
|
|
7
|
+
if (process.env.NO_COLOR) return false;
|
|
8
|
+
if (process.env.FORCE_COLOR) return true;
|
|
9
|
+
return Boolean(process.stdout.isTTY);
|
|
10
|
+
})();
|
|
11
|
+
|
|
12
|
+
const c = {
|
|
13
|
+
dim: USE_COLOR ? '\x1b[2m' : '',
|
|
14
|
+
bold: USE_COLOR ? '\x1b[1m' : '',
|
|
15
|
+
green: USE_COLOR ? '\x1b[32m' : '',
|
|
16
|
+
cyan: USE_COLOR ? '\x1b[36m' : '',
|
|
17
|
+
yellow: USE_COLOR ? '\x1b[33m' : '',
|
|
18
|
+
red: USE_COLOR ? '\x1b[31m' : '',
|
|
19
|
+
magenta: USE_COLOR ? '\x1b[35m' : '',
|
|
20
|
+
gray: USE_COLOR ? '\x1b[90m' : '',
|
|
21
|
+
reset: USE_COLOR ? '\x1b[0m' : '',
|
|
22
|
+
};
|
|
4
23
|
|
|
5
24
|
function fmtTokens(n: number): string {
|
|
6
25
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
@@ -12,43 +31,104 @@ function fmtUsd(n: number): string {
|
|
|
12
31
|
return `$${n.toFixed(2)}`;
|
|
13
32
|
}
|
|
14
33
|
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
function colorCost(n: number, padded: string): string {
|
|
35
|
+
if (!USE_COLOR) return padded;
|
|
36
|
+
if (n >= 1000) return `${c.bold}${c.red}${padded}${c.reset}`;
|
|
37
|
+
if (n >= 100) return `${c.yellow}${padded}${c.reset}`;
|
|
38
|
+
if (n >= 1) return `${c.green}${padded}${c.reset}`;
|
|
39
|
+
return `${c.dim}${padded}${c.reset}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatRow(row: AggregatedRow, isTotal = false): string {
|
|
43
|
+
const labelText = row.subagent.padEnd(PAD_LABEL);
|
|
44
|
+
const label = isTotal
|
|
45
|
+
? `${c.bold}${labelText}${c.reset}`
|
|
46
|
+
: `${c.cyan}${labelText}${c.reset}`;
|
|
47
|
+
const sessions = `${c.gray}${String(row.sessionCount).padStart(4)}${c.reset}`;
|
|
18
48
|
const io = `${fmtTokens(row.inputTokens)} / ${fmtTokens(row.outputTokens)}`.padStart(18);
|
|
19
49
|
const cached1h = fmtTokens(row.cacheCreation1hTokens).padStart(7);
|
|
20
50
|
const cached5m = fmtTokens(row.cacheCreation5mTokens).padStart(7);
|
|
21
51
|
const cacheRead = fmtTokens(row.cacheReadTokens).padStart(8);
|
|
22
|
-
const
|
|
23
|
-
|
|
52
|
+
const costStr = fmtUsd(row.cost.totalCost).padStart(9);
|
|
53
|
+
const cost = colorCost(row.cost.totalCost, costStr);
|
|
54
|
+
return ` ${label}${sessions} ${io} ${c.dim}1h:${c.reset}${cached1h} ${c.dim}5m:${c.reset}${cached5m} ${c.dim}r:${c.reset}${cacheRead} ${cost}`;
|
|
24
55
|
}
|
|
25
56
|
|
|
26
57
|
export function formatTable(report: LedgerReport, label: string): string {
|
|
27
|
-
const header =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
58
|
+
const header =
|
|
59
|
+
`${c.bold}agent-ledger ${label}${c.reset} ${c.dim}·${c.reset} ` +
|
|
60
|
+
`${c.gray}${report.period.from.toISOString().slice(0, 10)} → ${report.period.to.toISOString().slice(0, 10)}${c.reset}`;
|
|
61
|
+
|
|
62
|
+
const colsRaw =
|
|
63
|
+
' ' +
|
|
64
|
+
'subagent'.padEnd(PAD_LABEL) +
|
|
65
|
+
'sess'.padStart(4) +
|
|
66
|
+
' ' +
|
|
67
|
+
'tokens(in/out)'.padStart(18) +
|
|
68
|
+
' ' +
|
|
69
|
+
'cache tokens (1h/5m/read)'.padStart(32) +
|
|
70
|
+
' ' +
|
|
35
71
|
'cost'.padStart(9);
|
|
72
|
+
const cols = `${c.gray}${colsRaw}${c.reset}`;
|
|
36
73
|
|
|
37
|
-
const
|
|
74
|
+
const sepRaw = ' ' + '─'.repeat(PAD_LABEL + 4 + 2 + 18 + 2 + 32 + 2 + 9);
|
|
75
|
+
const sep = `${c.gray}${sepRaw}${c.reset}`;
|
|
38
76
|
|
|
39
|
-
|
|
77
|
+
const lines = [
|
|
40
78
|
header,
|
|
41
79
|
'',
|
|
42
80
|
cols,
|
|
43
81
|
sep,
|
|
44
|
-
...report.rows.map(formatRow),
|
|
82
|
+
...report.rows.map((r) => formatRow(r)),
|
|
45
83
|
sep,
|
|
46
|
-
formatRow({ ...report.total, subagent: 'total' }),
|
|
47
|
-
]
|
|
84
|
+
formatRow({ ...report.total, subagent: 'total' }, true),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// Server tool footer (only when present, since most rows have 0)
|
|
88
|
+
if (report.total.webSearchRequests + report.total.webFetchRequests > 0) {
|
|
89
|
+
lines.push('');
|
|
90
|
+
lines.push(
|
|
91
|
+
` ${c.gray}server tools:${c.reset} ` +
|
|
92
|
+
`web_search ×${report.total.webSearchRequests}, ` +
|
|
93
|
+
`web_fetch ×${report.total.webFetchRequests} ` +
|
|
94
|
+
`(${fmtUsd(report.total.cost.serverToolUseCost)})`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Daily trend chart: vertical-ish ASCII bars per day.
|
|
102
|
+
export function formatTrend(report: LedgerReport, label: string): string {
|
|
103
|
+
const max = report.rows.reduce((m, r) => Math.max(m, r.cost.totalCost), 0);
|
|
104
|
+
const barWidth = 36;
|
|
105
|
+
|
|
106
|
+
const header =
|
|
107
|
+
`${c.bold}agent-ledger ${label} (daily)${c.reset} ${c.dim}·${c.reset} ` +
|
|
108
|
+
`${c.gray}${report.period.from.toISOString().slice(0, 10)} → ${report.period.to.toISOString().slice(0, 10)}${c.reset}`;
|
|
109
|
+
|
|
110
|
+
const lines = [header, ''];
|
|
111
|
+
for (const row of report.rows) {
|
|
112
|
+
const bars = Math.round((row.cost.totalCost / Math.max(max, 0.01)) * barWidth);
|
|
113
|
+
const bar =
|
|
114
|
+
(USE_COLOR ? c.cyan : '') + '█'.repeat(bars) + (USE_COLOR ? c.reset : '');
|
|
115
|
+
const dateLabel = `${c.gray}${row.subagent}${c.reset}`;
|
|
116
|
+
const cost = colorCost(row.cost.totalCost, fmtUsd(row.cost.totalCost).padStart(9));
|
|
117
|
+
const sessions = `${c.gray}(${row.sessionCount} sess)${c.reset}`;
|
|
118
|
+
lines.push(` ${dateLabel} ${cost} ${bar} ${sessions}`);
|
|
119
|
+
}
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push(
|
|
122
|
+
` ${c.bold}total${c.reset.padEnd(PAD_LABEL - 5)}${' '.repeat(2)}${colorCost(
|
|
123
|
+
report.total.cost.totalCost,
|
|
124
|
+
fmtUsd(report.total.cost.totalCost).padStart(9),
|
|
125
|
+
)}`,
|
|
126
|
+
);
|
|
127
|
+
return lines.join('\n');
|
|
48
128
|
}
|
|
49
129
|
|
|
50
130
|
export function formatMarkdown(report: LedgerReport, label: string): string {
|
|
51
|
-
const header = '|
|
|
131
|
+
const header = '| group | sessions | input | output | cache 1h | cache 5m | cache read | cost |';
|
|
52
132
|
const sep = '|---|---:|---:|---:|---:|---:|---:|---:|';
|
|
53
133
|
const rows = report.rows.map(
|
|
54
134
|
(r) =>
|
|
@@ -66,7 +146,7 @@ export function formatMarkdown(report: LedgerReport, label: string): string {
|
|
|
66
146
|
report.total.cacheReadTokens,
|
|
67
147
|
)} | **${fmtUsd(report.total.cost.totalCost)}** |`;
|
|
68
148
|
|
|
69
|
-
|
|
149
|
+
const lines = [
|
|
70
150
|
`# agent-ledger · ${label}`,
|
|
71
151
|
`_${report.period.from.toISOString().slice(0, 10)} → ${report.period.to.toISOString().slice(0, 10)}_`,
|
|
72
152
|
'',
|
|
@@ -74,5 +154,14 @@ export function formatMarkdown(report: LedgerReport, label: string): string {
|
|
|
74
154
|
sep,
|
|
75
155
|
...rows,
|
|
76
156
|
totalRow,
|
|
77
|
-
]
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
if (report.total.webSearchRequests + report.total.webFetchRequests > 0) {
|
|
160
|
+
lines.push('');
|
|
161
|
+
lines.push(
|
|
162
|
+
`_Server tools: web_search ×${report.total.webSearchRequests}, web_fetch ×${report.total.webFetchRequests} (${fmtUsd(report.total.cost.serverToolUseCost)})_`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return lines.join('\n');
|
|
78
167
|
}
|
package/src/pricing.ts
CHANGED
|
@@ -30,6 +30,12 @@ const CACHE_5M_MULTIPLIER = 1.25;
|
|
|
30
30
|
const CACHE_1H_MULTIPLIER = 2.0;
|
|
31
31
|
const CACHE_READ_MULTIPLIER = 0.1;
|
|
32
32
|
|
|
33
|
+
// Anthropic server-tool pricing (https://www.anthropic.com/pricing as of 2026-04):
|
|
34
|
+
// Web search: $10 per 1,000 requests = $0.01 / request
|
|
35
|
+
// Web fetch: treated identically here pending Anthropic split (file an issue if wrong)
|
|
36
|
+
const WEB_SEARCH_PRICE_PER_REQUEST = 0.01;
|
|
37
|
+
const WEB_FETCH_PRICE_PER_REQUEST = 0.01;
|
|
38
|
+
|
|
33
39
|
function resolveModel(model: ModelId | undefined): { input: number; output: number } {
|
|
34
40
|
if (!model) return { input: 0, output: 0 };
|
|
35
41
|
if (FREE_MODELS.has(model)) return { input: 0, output: 0 };
|
|
@@ -77,13 +83,26 @@ export function costFor(usage: TokenUsage, model: ModelId | undefined): CostBrea
|
|
|
77
83
|
const cacheCreation1hCost = perMillion(oneHour, rate.input * CACHE_1H_MULTIPLIER);
|
|
78
84
|
const cacheReadCost = perMillion(cacheRead, rate.input * CACHE_READ_MULTIPLIER);
|
|
79
85
|
|
|
86
|
+
const webSearchReqs = usage.server_tool_use?.web_search_requests ?? 0;
|
|
87
|
+
const webFetchReqs = usage.server_tool_use?.web_fetch_requests ?? 0;
|
|
88
|
+
const serverToolUseCost =
|
|
89
|
+
webSearchReqs * WEB_SEARCH_PRICE_PER_REQUEST +
|
|
90
|
+
webFetchReqs * WEB_FETCH_PRICE_PER_REQUEST;
|
|
91
|
+
|
|
80
92
|
return {
|
|
81
93
|
inputCost,
|
|
82
94
|
outputCost,
|
|
83
95
|
cacheCreation5mCost,
|
|
84
96
|
cacheCreation1hCost,
|
|
85
97
|
cacheReadCost,
|
|
86
|
-
|
|
98
|
+
serverToolUseCost,
|
|
99
|
+
totalCost:
|
|
100
|
+
inputCost +
|
|
101
|
+
outputCost +
|
|
102
|
+
cacheCreation5mCost +
|
|
103
|
+
cacheCreation1hCost +
|
|
104
|
+
cacheReadCost +
|
|
105
|
+
serverToolUseCost,
|
|
87
106
|
};
|
|
88
107
|
}
|
|
89
108
|
|
package/src/types.ts
CHANGED
|
@@ -11,6 +11,11 @@ export interface CacheCreationDetail {
|
|
|
11
11
|
ephemeral_1h_input_tokens?: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface ServerToolUse {
|
|
15
|
+
web_search_requests?: number;
|
|
16
|
+
web_fetch_requests?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
export interface TokenUsage {
|
|
15
20
|
input_tokens: number;
|
|
16
21
|
output_tokens: number;
|
|
@@ -19,6 +24,8 @@ export interface TokenUsage {
|
|
|
19
24
|
cache_creation_input_tokens?: number;
|
|
20
25
|
cache_read_input_tokens?: number;
|
|
21
26
|
cache_creation?: CacheCreationDetail;
|
|
27
|
+
// Anthropic server-side tool use (web_search, web_fetch). Priced per request, not per token.
|
|
28
|
+
server_tool_use?: ServerToolUse;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
export interface SessionTurn {
|
|
@@ -37,6 +44,7 @@ export interface CostBreakdown {
|
|
|
37
44
|
cacheCreation5mCost: number;
|
|
38
45
|
cacheCreation1hCost: number;
|
|
39
46
|
cacheReadCost: number;
|
|
47
|
+
serverToolUseCost: number;
|
|
40
48
|
totalCost: number;
|
|
41
49
|
}
|
|
42
50
|
|
|
@@ -48,6 +56,8 @@ export interface AggregatedRow {
|
|
|
48
56
|
cacheCreation5mTokens: number;
|
|
49
57
|
cacheCreation1hTokens: number;
|
|
50
58
|
cacheReadTokens: number;
|
|
59
|
+
webSearchRequests: number;
|
|
60
|
+
webFetchRequests: number;
|
|
51
61
|
cost: CostBreakdown;
|
|
52
62
|
}
|
|
53
63
|
|
package/test/aggregator.test.ts
CHANGED
|
@@ -47,4 +47,29 @@ describe('aggregate()', () => {
|
|
|
47
47
|
const sumCost = report.rows.reduce((s, r) => s + r.cost.totalCost, 0);
|
|
48
48
|
expect(report.total.cost.totalCost).toBeCloseTo(sumCost, 6);
|
|
49
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
|
+
});
|
|
50
75
|
});
|
package/test/pricing.test.ts
CHANGED
|
@@ -107,4 +107,38 @@ describe('costFor()', () => {
|
|
|
107
107
|
expect(isKnownModel('something-entirely-unknown')).toBe(false);
|
|
108
108
|
expect(isKnownModel(undefined)).toBe(false);
|
|
109
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
|
+
});
|
|
110
144
|
});
|