claude-agent-ledger 0.4.1 → 0.7.1
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/README.md +77 -89
- package/bin/agent-ledger.ts +243 -56
- package/package.json +15 -4
- package/src/aggregator.ts +173 -71
- package/src/explain.ts +209 -0
- package/src/parser.ts +110 -32
- package/src/pricing.ts +45 -20
- package/src/tree.ts +107 -0
- package/src/types.ts +26 -8
- package/.github/ISSUE_TEMPLATE/bug.md +0 -35
- package/.github/ISSUE_TEMPLATE/model-pricing.md +0 -29
- package/.github/pull_request_template.md +0 -19
- package/.github/workflows/ci.yml +0 -19
- package/CHANGELOG.md +0 -105
- package/CONTRIBUTING.md +0 -42
- package/bun.lock +0 -24
- package/test/aggregator.test.ts +0 -75
- package/test/fixtures/projects/proj-a/session-abc/subagents/agent-deadbeef.jsonl +0 -2
- package/test/fixtures/projects/proj-a/session-abc/subagents/agent-deadbeef.meta.json +0 -1
- package/test/fixtures/projects/proj-a/session-abc.jsonl +0 -2
- package/test/fixtures/sample.jsonl +0 -6
- package/test/parser.test.ts +0 -55
- package/test/pricing.test.ts +0 -144
- package/tsconfig.json +0 -20
package/CHANGELOG.md
DELETED
|
@@ -1,105 +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.4.1] — 2026-04-22
|
|
9
|
-
|
|
10
|
-
### Added
|
|
11
|
-
- `--anonymize` flag — replaces project paths with `~/repo-A`, `~/repo-B`,
|
|
12
|
-
... and session ids with `sess-A`, `sess-B`, ... ordered by spend descending.
|
|
13
|
-
Use this when you want to share your output publicly (HN / Bluesky /
|
|
14
|
-
presentation) without leaking employer names or internal repo names.
|
|
15
|
-
Subagent / model / day labels stay untouched (those are public info).
|
|
16
|
-
|
|
17
|
-
## [0.4.0] — 2026-04-22
|
|
18
|
-
|
|
19
|
-
### Added
|
|
20
|
-
- `--by project` — group spend by Claude Code project directory.
|
|
21
|
-
Decodes `~/.claude/projects/-Users-xiangjie-clawbot` → `~/clawbot` so you can
|
|
22
|
-
see which project is burning budget.
|
|
23
|
-
- `--by session` — group by session ID. Surfaces the single most expensive
|
|
24
|
-
conversations (useful for hunting the one that went sideways).
|
|
25
|
-
- Summary dashboard now shows:
|
|
26
|
-
- `Projects` count
|
|
27
|
-
- `Top project` line
|
|
28
|
-
- `Project mix` block — top 3 projects with $ and %
|
|
29
|
-
- `decodeProjectId()` exported for reuse. Special-cases the `~/` home prefix,
|
|
30
|
-
falls back to slash-joined path for other roots.
|
|
31
|
-
- Row label column now auto-widens up to 40 chars for project paths.
|
|
32
|
-
|
|
33
|
-
### Changed
|
|
34
|
-
- `formatTable` takes an optional `groupName` arg so the header column matches
|
|
35
|
-
the group (e.g. "project", "session").
|
|
36
|
-
|
|
37
|
-
## [0.3.0] — 2026-04-22
|
|
38
|
-
|
|
39
|
-
### Added
|
|
40
|
-
- `--summary` flag — single-screen dashboard with shadow cost, top subagent /
|
|
41
|
-
model / day, cache reuse ratio, and **leverage vs $200/mo Max plan**.
|
|
42
|
-
This is the killer view for "why install over /cost?"
|
|
43
|
-
- Auto-derived insights footer on the default table:
|
|
44
|
-
- Top contributor with %
|
|
45
|
-
- 1h cache spend call-out (when ≥5% of total)
|
|
46
|
-
- Cache reuse ratio (read/write) with verdict
|
|
47
|
-
- Monthly $ equivalent + leverage vs Max plan
|
|
48
|
-
- Daily-mode insights: peak day, avg/day, peak/trough variance
|
|
49
|
-
|
|
50
|
-
### Changed
|
|
51
|
-
- `--by day` polish:
|
|
52
|
-
- Bar fixed to 40-char width so session counts align in their own column
|
|
53
|
-
- Brighter cyan (`\x1b[96m`) bar character
|
|
54
|
-
- Total row separated by horizontal rule, properly aligned
|
|
55
|
-
- Header line now shows period + total + sessions + group count at a glance:
|
|
56
|
-
`agent-ledger week 2026-04-15 → 2026-04-22 · $13,356.27 shadow · 217 sessions · 7 agents`
|
|
57
|
-
|
|
58
|
-
## [0.2.0] — 2026-04-21
|
|
59
|
-
|
|
60
|
-
### Added
|
|
61
|
-
- `--by <subagent|model|day>` flag — group rows by subagent (default), by
|
|
62
|
-
Claude model, or by date. The `day` mode renders an ASCII bar chart of
|
|
63
|
-
daily spend, useful for spotting "expensive Tuesdays."
|
|
64
|
-
- Server-tool pricing: `web_search_requests` and `web_fetch_requests` from
|
|
65
|
-
the `server_tool_use` block are now counted at $0.01/request and surface
|
|
66
|
-
in a footer row when present.
|
|
67
|
-
- ANSI color output, TTY-aware (NO_COLOR / FORCE_COLOR respected).
|
|
68
|
-
- Subagent / model / day labels in cyan
|
|
69
|
-
- Cost amounts color-coded by magnitude (red ≥$1000, yellow ≥$100,
|
|
70
|
-
green ≥$1, dim <$1)
|
|
71
|
-
- Total row bolded
|
|
72
|
-
- New tests for server-tool pricing, `--by model`, and `--by day`
|
|
73
|
-
(22 tests, up from 17).
|
|
74
|
-
|
|
75
|
-
### Changed
|
|
76
|
-
- `aggregate(turns, from, to)` accepts an optional 4th arg `group: GroupKey`
|
|
77
|
-
(`'subagent' | 'model' | 'day'`). Default behavior unchanged.
|
|
78
|
-
- `AggregatedRow` now carries `webSearchRequests` and `webFetchRequests`
|
|
79
|
-
counters; `CostBreakdown` adds `serverToolUseCost`.
|
|
80
|
-
|
|
81
|
-
### Not yet
|
|
82
|
-
- Calibration against real Anthropic invoices (need volunteers — see
|
|
83
|
-
`.github/ISSUE_TEMPLATE/`)
|
|
84
|
-
- Cost forecasting (trailing 7-day burn rate)
|
|
85
|
-
- Homebrew tap
|
|
86
|
-
|
|
87
|
-
## [0.1.0] — 2026-04-21
|
|
88
|
-
|
|
89
|
-
Initial public release.
|
|
90
|
-
|
|
91
|
-
### Added
|
|
92
|
-
- JSONL parser with recursive walk of `~/.claude/projects/**`, including nested
|
|
93
|
-
`subagents/` directories
|
|
94
|
-
- Per-subagent attribution via sidecar `agent-<hash>.meta.json` files
|
|
95
|
-
- Pricing with cache TTL split: `ephemeral_5m_input_tokens` (1.25×) vs
|
|
96
|
-
`ephemeral_1h_input_tokens` (2×)
|
|
97
|
-
- Terminal table output (default) and Markdown export (`--md`)
|
|
98
|
-
- JSON output (`--json`) for piping into other tools
|
|
99
|
-
- `--plan pro|max` to suppress dollar columns on fixed-price subscriptions
|
|
100
|
-
- Support for current and prior-generation model IDs (Opus 4.6/4.7,
|
|
101
|
-
Sonnet 4.5/4.6, Haiku 4.0/4.5)
|
|
102
|
-
- Family-prefix fallback for unknown future models (e.g. `claude-opus-5-*`
|
|
103
|
-
resolves to Opus pricing)
|
|
104
|
-
- `<synthetic>` turns (Claude Code context compaction) correctly priced at $0
|
|
105
|
-
- 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,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lockfileVersion": 1,
|
|
3
|
-
"configVersion": 1,
|
|
4
|
-
"workspaces": {
|
|
5
|
-
"": {
|
|
6
|
-
"name": "agent-ledger",
|
|
7
|
-
"devDependencies": {
|
|
8
|
-
"@types/bun": "latest",
|
|
9
|
-
"typescript": "^5.6.0",
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
"packages": {
|
|
14
|
-
"@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=="],
|
|
15
|
-
|
|
16
|
-
"@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=="],
|
|
17
|
-
|
|
18
|
-
"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=="],
|
|
19
|
-
|
|
20
|
-
"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=="],
|
|
21
|
-
|
|
22
|
-
"undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
|
23
|
-
}
|
|
24
|
-
}
|
package/test/aggregator.test.ts
DELETED
|
@@ -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}}}
|
package/test/parser.test.ts
DELETED
|
@@ -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
|
-
});
|
package/test/pricing.test.ts
DELETED
|
@@ -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
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"types": ["bun-types"],
|
|
8
|
-
"strict": true,
|
|
9
|
-
"noUncheckedIndexedAccess": true,
|
|
10
|
-
"noImplicitOverride": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"allowImportingTsExtensions": true,
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"isolatedModules": true,
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
"forceConsistentCasingInFileNames": true,
|
|
17
|
-
"verbatimModuleSyntax": true
|
|
18
|
-
},
|
|
19
|
-
"include": ["src/**/*", "bin/**/*", "test/**/*"]
|
|
20
|
-
}
|