coding-agent-cost 0.2.2__tar.gz → 0.4.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.
- {coding_agent_cost-0.2.2/coding_agent_cost.egg-info → coding_agent_cost-0.4.0}/PKG-INFO +61 -6
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/README.md +60 -5
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/__init__.py +1 -1
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/aggregate.py +15 -1
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/cli.py +6 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/facts.py +18 -2
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/rates.json +46 -3
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/rates.py +73 -5
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/readers/claude.py +72 -3
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0/coding_agent_cost.egg-info}/PKG-INFO +61 -6
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/SOURCES.txt +2 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/pyproject.toml +1 -1
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_aggregate.py +157 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_astra_pricing.py +2 -2
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_cli.py +53 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_e0a_review3_hardening.py +3 -3
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_facts.py +41 -0
- coding_agent_cost-0.4.0/tests/test_haiku_5_5_rates.py +87 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_rates.py +200 -2
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_reader_claude.py +191 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_reader_codex.py +20 -0
- coding_agent_cost-0.4.0/tests/test_reader_output_lower_bound.py +265 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_sonnet_5_5_rates.py +1 -1
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_version_consistency.py +8 -7
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/LICENSE +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/config.py +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/readers/__init__.py +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/readers/codex.py +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/renderers.py +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/dependency_links.txt +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/entry_points.txt +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/top_level.txt +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/setup.cfg +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_e0a_fable_5_1_and_sonnet_5_correction.py +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_measure_v1_contract.py +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_opus_5_5_rates.py +0 -0
- {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_reader_dedup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-agent-cost
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Estimate AI coding agent (Claude Code / Codex CLI) token usage and cost from local logs
|
|
5
5
|
Author: shiki-yusuke
|
|
6
6
|
License: MIT
|
|
@@ -232,10 +232,28 @@ network, never calls `gh`, and never resolves branches or PRs.
|
|
|
232
232
|
flagged, since the reader's own cross-check of real transcripts found
|
|
233
233
|
exactly that pattern in every observed duplicated group. See
|
|
234
234
|
`CHANGELOG.md`'s 0.2.0 entry.
|
|
235
|
+
If the row a group adopts has no `stop_reason` (common in subagent
|
|
236
|
+
transcripts, where a message ending in `tool_use` may never get its final
|
|
237
|
+
line), its `output_tokens` is only the streaming head's value: the
|
|
238
|
+
group's `output` fact is flagged `source_quality: "output_lower_bound"`
|
|
239
|
+
-- the observed lower bound of that message's output tokens, not
|
|
240
|
+
guaranteed to be >= the true count. The group's input-side facts stay
|
|
241
|
+
`"ok"`, and the token amount itself is unchanged (still priced and
|
|
242
|
+
included in rows/totals); the flag is a warning only.
|
|
235
243
|
When Anthropic's prompt-cache TTL breakdown (5-minute vs 1-hour writes) is
|
|
236
244
|
present in the log, it's used; otherwise the cache-write tokens are priced
|
|
237
245
|
at the 5-minute rate as an explicit **lower bound** and flagged
|
|
238
246
|
`lower_bound` rather than guessed at the (more expensive) 1-hour rate.
|
|
247
|
+
Each Claude fact also carries its request's prompt length
|
|
248
|
+
(`prompt_tokens` = `input_tokens` + `cache_read_input_tokens` +
|
|
249
|
+
`cache_creation_input_tokens`), used only to pick the tier of a model
|
|
250
|
+
whose price depends on prompt length (`claude-haiku-5-5`).
|
|
251
|
+
- **Prompt-length tiers need a known prompt length.** A fact whose prompt
|
|
252
|
+
length is unknown -- every Codex fact (a delta of cumulative totals, not
|
|
253
|
+
one request), a Claude fact from a conflicting dedup group, or a Claude
|
|
254
|
+
row whose cache-write TTL breakdown exceeds its `cache_creation_input_tokens`
|
|
255
|
+
-- is priced at the model's base (cheapest) tier and flagged
|
|
256
|
+
`lower_bound`. Models without tiers are unaffected.
|
|
239
257
|
- **Codex CLI**: rollout files record a *cumulative* token count after each
|
|
240
258
|
turn; agent-cost turns that into per-turn deltas. Under the Codex token-rate
|
|
241
259
|
tariff used here, cache writes are not added as a separate charge. The current
|
|
@@ -295,6 +313,37 @@ carries a `catalog_version`, a list of `sources` (the pricing page a rate
|
|
|
295
313
|
came from), and is validated on load (no duplicate model keys or aliases,
|
|
296
314
|
no negative rates, no overlapping periods for the same model).
|
|
297
315
|
|
|
316
|
+
A rate period can price by prompt length (catalog `schema_version` `"2"`).
|
|
317
|
+
The period's own five values are the base tier, for prompts up to the first
|
|
318
|
+
threshold; each entry in `prompt_tiers` applies to prompts *strictly over*
|
|
319
|
+
its `prompt_tokens_over`, and a request uses the largest threshold it
|
|
320
|
+
exceeds. Prompt length is the request's input + cache-read + cache-write
|
|
321
|
+
tokens, and the chosen tier prices every token kind of that request,
|
|
322
|
+
output included:
|
|
323
|
+
|
|
324
|
+
```json
|
|
325
|
+
{
|
|
326
|
+
"rate_id": "claude-haiku-5-5-launch-2026-10-07",
|
|
327
|
+
"effective_from": "2026-10-07T00:00:00+00:00",
|
|
328
|
+
"effective_until": null,
|
|
329
|
+
"input_nocache": "0.10", "cache_read": "0.01", "cache_write_5m": "0.125", "cache_write_1h": "0.20", "output": "0.50",
|
|
330
|
+
"prompt_tiers": [
|
|
331
|
+
{ "prompt_tokens_over": 100000,
|
|
332
|
+
"input_nocache": "0.50", "cache_read": "0.05", "cache_write_5m": "0.625", "cache_write_1h": "1.0", "output": "2.50" }
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
In a period with `prompt_tiers`, all five values of the base and of every
|
|
338
|
+
tier must be present and non-null, thresholds must be positive integers in
|
|
339
|
+
ascending order without duplicates, and every value must be greater than or
|
|
340
|
+
equal to the same value in the previous tier (the base first). That
|
|
341
|
+
monotonicity is what makes the base tier a true lower bound for a fact whose
|
|
342
|
+
prompt length is unknown. A `schema_version` `"1"` catalog may not contain
|
|
343
|
+
`prompt_tiers`; agent-cost 0.3.0 and earlier reject a `"2"` catalog as
|
|
344
|
+
unsupported rather than mispricing it, so a custom `--rates` file that uses
|
|
345
|
+
tiers must declare `"2"`.
|
|
346
|
+
|
|
298
347
|
To use your own catalog instead of the one bundled with the package, pass
|
|
299
348
|
`--rates path/to/rates.json` to `report` or `export` -- this fully replaces
|
|
300
349
|
the bundled catalog, it does not merge with it. Inspect any catalog with:
|
|
@@ -342,7 +391,7 @@ agent-cost measure --session-id <id> [--session-id <id> ...] \
|
|
|
342
391
|
```json
|
|
343
392
|
{
|
|
344
393
|
"protocol_version": "measure/v1",
|
|
345
|
-
"producer_version": "0.
|
|
394
|
+
"producer_version": "0.3.0",
|
|
346
395
|
"accounting_basis": "agent-cost-raw-total/v2",
|
|
347
396
|
"generated_at": "...",
|
|
348
397
|
"window": { "since": "...", "until": null },
|
|
@@ -363,7 +412,7 @@ agent-cost measure --session-id <id> [--session-id <id> ...] \
|
|
|
363
412
|
"duplicate_rows_skipped": 0,
|
|
364
413
|
"conflicting_duplicate_groups": 0,
|
|
365
414
|
"missing_dedup_identity_rows": 0,
|
|
366
|
-
"source_quality": { "ok": 41, "first_event_delta": 2, "identity_missing": 0 }
|
|
415
|
+
"source_quality": { "ok": 41, "first_event_delta": 2, "identity_missing": 0, "output_lower_bound": 1 }
|
|
367
416
|
}
|
|
368
417
|
}
|
|
369
418
|
```
|
|
@@ -394,9 +443,11 @@ agent-cost makes zero network calls. `agent-cost export`'s JSONL never
|
|
|
394
443
|
includes absolute file paths, rollout paths, prompt/message content, or git
|
|
395
444
|
branch names -- only the fields needed to reproduce a cost estimate:
|
|
396
445
|
`occurred_at_utc`, `agent`, `session_id`, `model_raw`, `model_key`,
|
|
397
|
-
`token_kind`, `tokens`, `mode`,
|
|
398
|
-
caveat about how that one fact was derived
|
|
399
|
-
`"first_event_delta"
|
|
446
|
+
`token_kind`, `tokens`, `mode`, `source_quality` (a fixed-vocabulary
|
|
447
|
+
caveat about how that one fact was derived: `"ok"`, Codex's
|
|
448
|
+
`"first_event_delta"`, or Claude's `"identity_missing"` /
|
|
449
|
+
`"output_lower_bound"` -- never null), and `prompt_tokens` (the request's
|
|
450
|
+
prompt length as an integer, or null when unknown).
|
|
400
451
|
|
|
401
452
|
## License
|
|
402
453
|
|
|
@@ -420,6 +471,10 @@ MIT. See [LICENSE](LICENSE).
|
|
|
420
471
|
未知のモデル・単価表にない token 種別は `unpriced` として扱い、憶測の価格を出しません。
|
|
421
472
|
- 単価表 (`agent_cost/rates.json`) は履歴型カタログで、値上げは新しい期間として追加します。
|
|
422
473
|
`--rates PATH` で別カタログに完全差し替えできます。
|
|
474
|
+
- prompt 長で単価が変わるモデル(`claude-haiku-5-5`、100,000 tokens 超で高い単価)は期間の
|
|
475
|
+
`prompt_tiers`(schema_version `"2"`)で表し、リクエストごとの prompt 長(input + cache read +
|
|
476
|
+
cache write)で tier を選びます。prompt 長が不明な fact(Codex 由来、conflicting group、usage
|
|
477
|
+
不整合)は基底 tier(最安)で **下限推計**(`lower_bound`)になります。
|
|
423
478
|
- `agent-cost measure --session-id ID [--session-id ID ...] --format json` は他プログラムから
|
|
424
479
|
subprocess で叩くための機械可読な契約です(`protocol_version: "measure/v1"`)。指定した
|
|
425
480
|
session_id が1件も見つからなくても終了コードは0(空集計として表現)、`--session-id` 未指定など
|
|
@@ -206,10 +206,28 @@ network, never calls `gh`, and never resolves branches or PRs.
|
|
|
206
206
|
flagged, since the reader's own cross-check of real transcripts found
|
|
207
207
|
exactly that pattern in every observed duplicated group. See
|
|
208
208
|
`CHANGELOG.md`'s 0.2.0 entry.
|
|
209
|
+
If the row a group adopts has no `stop_reason` (common in subagent
|
|
210
|
+
transcripts, where a message ending in `tool_use` may never get its final
|
|
211
|
+
line), its `output_tokens` is only the streaming head's value: the
|
|
212
|
+
group's `output` fact is flagged `source_quality: "output_lower_bound"`
|
|
213
|
+
-- the observed lower bound of that message's output tokens, not
|
|
214
|
+
guaranteed to be >= the true count. The group's input-side facts stay
|
|
215
|
+
`"ok"`, and the token amount itself is unchanged (still priced and
|
|
216
|
+
included in rows/totals); the flag is a warning only.
|
|
209
217
|
When Anthropic's prompt-cache TTL breakdown (5-minute vs 1-hour writes) is
|
|
210
218
|
present in the log, it's used; otherwise the cache-write tokens are priced
|
|
211
219
|
at the 5-minute rate as an explicit **lower bound** and flagged
|
|
212
220
|
`lower_bound` rather than guessed at the (more expensive) 1-hour rate.
|
|
221
|
+
Each Claude fact also carries its request's prompt length
|
|
222
|
+
(`prompt_tokens` = `input_tokens` + `cache_read_input_tokens` +
|
|
223
|
+
`cache_creation_input_tokens`), used only to pick the tier of a model
|
|
224
|
+
whose price depends on prompt length (`claude-haiku-5-5`).
|
|
225
|
+
- **Prompt-length tiers need a known prompt length.** A fact whose prompt
|
|
226
|
+
length is unknown -- every Codex fact (a delta of cumulative totals, not
|
|
227
|
+
one request), a Claude fact from a conflicting dedup group, or a Claude
|
|
228
|
+
row whose cache-write TTL breakdown exceeds its `cache_creation_input_tokens`
|
|
229
|
+
-- is priced at the model's base (cheapest) tier and flagged
|
|
230
|
+
`lower_bound`. Models without tiers are unaffected.
|
|
213
231
|
- **Codex CLI**: rollout files record a *cumulative* token count after each
|
|
214
232
|
turn; agent-cost turns that into per-turn deltas. Under the Codex token-rate
|
|
215
233
|
tariff used here, cache writes are not added as a separate charge. The current
|
|
@@ -269,6 +287,37 @@ carries a `catalog_version`, a list of `sources` (the pricing page a rate
|
|
|
269
287
|
came from), and is validated on load (no duplicate model keys or aliases,
|
|
270
288
|
no negative rates, no overlapping periods for the same model).
|
|
271
289
|
|
|
290
|
+
A rate period can price by prompt length (catalog `schema_version` `"2"`).
|
|
291
|
+
The period's own five values are the base tier, for prompts up to the first
|
|
292
|
+
threshold; each entry in `prompt_tiers` applies to prompts *strictly over*
|
|
293
|
+
its `prompt_tokens_over`, and a request uses the largest threshold it
|
|
294
|
+
exceeds. Prompt length is the request's input + cache-read + cache-write
|
|
295
|
+
tokens, and the chosen tier prices every token kind of that request,
|
|
296
|
+
output included:
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"rate_id": "claude-haiku-5-5-launch-2026-10-07",
|
|
301
|
+
"effective_from": "2026-10-07T00:00:00+00:00",
|
|
302
|
+
"effective_until": null,
|
|
303
|
+
"input_nocache": "0.10", "cache_read": "0.01", "cache_write_5m": "0.125", "cache_write_1h": "0.20", "output": "0.50",
|
|
304
|
+
"prompt_tiers": [
|
|
305
|
+
{ "prompt_tokens_over": 100000,
|
|
306
|
+
"input_nocache": "0.50", "cache_read": "0.05", "cache_write_5m": "0.625", "cache_write_1h": "1.0", "output": "2.50" }
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
In a period with `prompt_tiers`, all five values of the base and of every
|
|
312
|
+
tier must be present and non-null, thresholds must be positive integers in
|
|
313
|
+
ascending order without duplicates, and every value must be greater than or
|
|
314
|
+
equal to the same value in the previous tier (the base first). That
|
|
315
|
+
monotonicity is what makes the base tier a true lower bound for a fact whose
|
|
316
|
+
prompt length is unknown. A `schema_version` `"1"` catalog may not contain
|
|
317
|
+
`prompt_tiers`; agent-cost 0.3.0 and earlier reject a `"2"` catalog as
|
|
318
|
+
unsupported rather than mispricing it, so a custom `--rates` file that uses
|
|
319
|
+
tiers must declare `"2"`.
|
|
320
|
+
|
|
272
321
|
To use your own catalog instead of the one bundled with the package, pass
|
|
273
322
|
`--rates path/to/rates.json` to `report` or `export` -- this fully replaces
|
|
274
323
|
the bundled catalog, it does not merge with it. Inspect any catalog with:
|
|
@@ -316,7 +365,7 @@ agent-cost measure --session-id <id> [--session-id <id> ...] \
|
|
|
316
365
|
```json
|
|
317
366
|
{
|
|
318
367
|
"protocol_version": "measure/v1",
|
|
319
|
-
"producer_version": "0.
|
|
368
|
+
"producer_version": "0.3.0",
|
|
320
369
|
"accounting_basis": "agent-cost-raw-total/v2",
|
|
321
370
|
"generated_at": "...",
|
|
322
371
|
"window": { "since": "...", "until": null },
|
|
@@ -337,7 +386,7 @@ agent-cost measure --session-id <id> [--session-id <id> ...] \
|
|
|
337
386
|
"duplicate_rows_skipped": 0,
|
|
338
387
|
"conflicting_duplicate_groups": 0,
|
|
339
388
|
"missing_dedup_identity_rows": 0,
|
|
340
|
-
"source_quality": { "ok": 41, "first_event_delta": 2, "identity_missing": 0 }
|
|
389
|
+
"source_quality": { "ok": 41, "first_event_delta": 2, "identity_missing": 0, "output_lower_bound": 1 }
|
|
341
390
|
}
|
|
342
391
|
}
|
|
343
392
|
```
|
|
@@ -368,9 +417,11 @@ agent-cost makes zero network calls. `agent-cost export`'s JSONL never
|
|
|
368
417
|
includes absolute file paths, rollout paths, prompt/message content, or git
|
|
369
418
|
branch names -- only the fields needed to reproduce a cost estimate:
|
|
370
419
|
`occurred_at_utc`, `agent`, `session_id`, `model_raw`, `model_key`,
|
|
371
|
-
`token_kind`, `tokens`, `mode`,
|
|
372
|
-
caveat about how that one fact was derived
|
|
373
|
-
`"first_event_delta"
|
|
420
|
+
`token_kind`, `tokens`, `mode`, `source_quality` (a fixed-vocabulary
|
|
421
|
+
caveat about how that one fact was derived: `"ok"`, Codex's
|
|
422
|
+
`"first_event_delta"`, or Claude's `"identity_missing"` /
|
|
423
|
+
`"output_lower_bound"` -- never null), and `prompt_tokens` (the request's
|
|
424
|
+
prompt length as an integer, or null when unknown).
|
|
374
425
|
|
|
375
426
|
## License
|
|
376
427
|
|
|
@@ -394,6 +445,10 @@ MIT. See [LICENSE](LICENSE).
|
|
|
394
445
|
未知のモデル・単価表にない token 種別は `unpriced` として扱い、憶測の価格を出しません。
|
|
395
446
|
- 単価表 (`agent_cost/rates.json`) は履歴型カタログで、値上げは新しい期間として追加します。
|
|
396
447
|
`--rates PATH` で別カタログに完全差し替えできます。
|
|
448
|
+
- prompt 長で単価が変わるモデル(`claude-haiku-5-5`、100,000 tokens 超で高い単価)は期間の
|
|
449
|
+
`prompt_tiers`(schema_version `"2"`)で表し、リクエストごとの prompt 長(input + cache read +
|
|
450
|
+
cache write)で tier を選びます。prompt 長が不明な fact(Codex 由来、conflicting group、usage
|
|
451
|
+
不整合)は基底 tier(最安)で **下限推計**(`lower_bound`)になります。
|
|
397
452
|
- `agent-cost measure --session-id ID [--session-id ID ...] --format json` は他プログラムから
|
|
398
453
|
subprocess で叩くための機械可読な契約です(`protocol_version: "measure/v1"`)。指定した
|
|
399
454
|
session_id が1件も見つからなくても終了コードは0(空集計として表現)、`--session-id` 未指定など
|
|
@@ -28,6 +28,11 @@ def price_fact(catalog: RateCatalog, fact: Fact) -> Tuple[Optional[Decimal], str
|
|
|
28
28
|
never at the more expensive 1-hour rate -- and flagged accordingly.
|
|
29
29
|
An unrecognized model or an unpriced token kind for an otherwise-known
|
|
30
30
|
model both return ``(None, "unpriced", None)`` rather than a guess.
|
|
31
|
+
|
|
32
|
+
For a period with ``prompt_tiers``, the fact is priced at the tier with
|
|
33
|
+
the largest ``prompt_tokens_over`` its ``prompt_tokens`` exceeds (the
|
|
34
|
+
base values when none). A fact whose ``prompt_tokens`` is unknown is
|
|
35
|
+
priced at the base values -- the cheapest tier -- as a lower bound.
|
|
31
36
|
"""
|
|
32
37
|
resolved_key, period = catalog.rate_for(fact.model_key, fact.occurred_at_utc)
|
|
33
38
|
if resolved_key is None or period is None:
|
|
@@ -47,7 +52,16 @@ def price_fact(catalog: RateCatalog, fact: Fact) -> Tuple[Optional[Decimal], str
|
|
|
47
52
|
rate_field = "cache_write_5m"
|
|
48
53
|
lower_bound = True
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
values = period.values
|
|
56
|
+
if period.prompt_tiers:
|
|
57
|
+
if fact.prompt_tokens is None:
|
|
58
|
+
lower_bound = True
|
|
59
|
+
else:
|
|
60
|
+
for tier in period.prompt_tiers:
|
|
61
|
+
if fact.prompt_tokens > tier.prompt_tokens_over:
|
|
62
|
+
values = tier.values
|
|
63
|
+
|
|
64
|
+
rate = values.get(rate_field)
|
|
51
65
|
if rate is None:
|
|
52
66
|
return None, "unpriced", None
|
|
53
67
|
|
|
@@ -168,6 +168,7 @@ def cmd_export(args) -> int:
|
|
|
168
168
|
"tokens": f.tokens,
|
|
169
169
|
"mode": f.mode,
|
|
170
170
|
"source_quality": f.source_quality,
|
|
171
|
+
"prompt_tokens": f.prompt_tokens,
|
|
171
172
|
}
|
|
172
173
|
out.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
173
174
|
finally:
|
|
@@ -305,6 +306,11 @@ def cmd_rates_show(args) -> int:
|
|
|
305
306
|
print(f" [{period.rate_id}] {period.effective_from.isoformat()} .. {until}")
|
|
306
307
|
for field_name, value in period.values.items():
|
|
307
308
|
print(f" {field_name}: {value if value is not None else '(unpriced)'}")
|
|
309
|
+
if period.prompt_tiers:
|
|
310
|
+
print(" prompt_tiers:")
|
|
311
|
+
for tier in period.prompt_tiers:
|
|
312
|
+
values = " ".join(f"{name}={value}" for name, value in tier.values.items())
|
|
313
|
+
print(f" > {tier.prompt_tokens_over}: {values}")
|
|
308
314
|
return 0
|
|
309
315
|
|
|
310
316
|
print(f"catalog_version: {catalog.catalog_version}")
|
|
@@ -32,10 +32,14 @@ MODES = ("fast", "normal", "unknown")
|
|
|
32
32
|
# (e.g. Codex's first delta in a rollout is measured against an assumed
|
|
33
33
|
# zero baseline; Claude's reader can't dedup a row that lacks a full
|
|
34
34
|
# (message.id, requestId) pair, so it emits that row individually and
|
|
35
|
-
# flags it "identity_missing" rather than silently treating it as "ok"
|
|
35
|
+
# flags it "identity_missing" rather than silently treating it as "ok";
|
|
36
|
+
# "output_lower_bound" marks a Claude output fact whose tokens are only the
|
|
37
|
+
# observed lower bound of that message's output_tokens -- the reader's
|
|
38
|
+
# adopted row had no stop_reason, so the value is not guaranteed to be
|
|
39
|
+
# >= the true count).
|
|
36
40
|
# This is never left unset -- every Fact defaults to "ok" so export never
|
|
37
41
|
# emits a null source_quality.
|
|
38
|
-
SOURCE_QUALITY_VALUES = ("ok", "first_event_delta", "identity_missing")
|
|
42
|
+
SOURCE_QUALITY_VALUES = ("ok", "first_event_delta", "identity_missing", "output_lower_bound")
|
|
39
43
|
|
|
40
44
|
_BRACKET_SUFFIX = re.compile(r"\[[^\]]*\]$")
|
|
41
45
|
_DATE_SUFFIX = re.compile(r"@\d{6,8}$")
|
|
@@ -80,6 +84,12 @@ class Fact:
|
|
|
80
84
|
tokens: int
|
|
81
85
|
mode: str = "unknown"
|
|
82
86
|
source_quality: str = "ok"
|
|
87
|
+
# The prompt length of the one request that produced this fact (usage's
|
|
88
|
+
# input_tokens + cache_read_input_tokens + cache_creation_input_tokens).
|
|
89
|
+
# Used only to pick the tier of a prompt-length-tiered rate. None means
|
|
90
|
+
# unknown (a Codex fact, or a Claude fact whose usage was inconsistent or
|
|
91
|
+
# came from a conflicting dedup group).
|
|
92
|
+
prompt_tokens: Optional[int] = None
|
|
83
93
|
|
|
84
94
|
def __post_init__(self) -> None:
|
|
85
95
|
if self.agent not in AGENTS:
|
|
@@ -92,5 +102,11 @@ class Fact:
|
|
|
92
102
|
raise ValueError(f"invalid source_quality: {self.source_quality!r}")
|
|
93
103
|
if self.tokens < 0:
|
|
94
104
|
raise ValueError(f"tokens must be >= 0, got {self.tokens}")
|
|
105
|
+
if self.prompt_tokens is not None and (
|
|
106
|
+
isinstance(self.prompt_tokens, bool)
|
|
107
|
+
or not isinstance(self.prompt_tokens, int)
|
|
108
|
+
or self.prompt_tokens < 0
|
|
109
|
+
):
|
|
110
|
+
raise ValueError(f"prompt_tokens must be None or an int >= 0, got {self.prompt_tokens!r}")
|
|
95
111
|
if self.occurred_at_utc.tzinfo is None:
|
|
96
112
|
raise ValueError("occurred_at_utc must be timezone-aware")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schema_version": "
|
|
3
|
-
"catalog_version": "2026-09
|
|
2
|
+
"schema_version": "2",
|
|
3
|
+
"catalog_version": "2026-10-09",
|
|
4
4
|
"currency": "USD",
|
|
5
5
|
"unit": "per_mtok",
|
|
6
6
|
"usd_per_credit": "0.04",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"claude-fable-5-1 added 2026-09-09: rate_id claude-fable-5-1-local-first-observed-2026-09-02. effective_from (2026-09-02T01:17:55Z) is the timestamp the raw model ID was first observed across all 54 local transcript files (none found in August), not a confirmed official launch date -- if an authoritative launch date is confirmed later, add it as a separate rate_id with effective_until on this one rather than editing this period in place. Values (cache_read $0.25/MTok = 0.025x base input) differ from claude-fable-5's cache_read ($1.00/MTok = 0.1x); this is a genuine per-model price difference confirmed against the primary source (see sources below), not an inconsistency to reconcile.",
|
|
13
13
|
"claude-sonnet-5 corrected 2026-09-09: a prior period (rate_id claude-sonnet-5-standard, effective_from 2026-09-01, values 3.0/0.30/3.75/6.0/15.0) priced a previously-announced rate increase that the pricing page's claude-sonnet-5-introductory-pricing note states did not occur (\"The previously scheduled increase to $3/$15 ... on September 1, 2026 will not occur\"). That period has been replaced by claude-sonnet-5-standard-2026-09-01, which carries the same $2/$0.20/$2.50/$4/$10 values as the preceding claude-sonnet-5-launch-promo period, since the launch price is confirmed to be the ongoing price with no change at the 2026-09-01 boundary. Any measure/v1 or report output computed against the pre-correction catalog for claude-sonnet-5 sessions occurring on or after 2026-09-01 overstated cost by 1.5x for that model. To detect this class of error going forward, cross-check each priced model's rate periods against its primary pricing page's Note text on a recurring (e.g. monthly) basis for mentions of a rate-change announcement being withdrawn or not taking effect -- a period can be internally valid (schema-wise) and still price a rate that was announced but never actually charged.",
|
|
14
14
|
"claude-opus-5-5 added 2026-09-23: rate_id claude-opus-5-5-launch-2026-09-22. effective_from 2026-09-22T00:00:00Z is 00:00:00Z of the official public launch date (anthropic.com/claude-opus-5-5, 'September 22, 2026'); the launch time of day is not published, but a transcript row whose raw model ID is claude-opus-5-5 cannot predate the launch, so pricing from the start of that UTC day cannot price a pre-launch event. Values 4.0/0.20/5.0/8.0/20.0 per the pricing page: cache_read is 0.05x base input (pricing-page footnote 2), a genuine per-model difference from the standard 0.1x and from claude-fable-5-1's 0.025x. fast_multiplier 2.0 (fast mode $8/$40 per MTok on the pricing page). Claude Code's CHANGELOG entry for 2.1.280 states Opus 5.5 became the default model (see sources); rows whose raw model ID is claude-opus-5-5 would be unpriced without this entry. Claude Sonnet 5.5 and Claude Haiku 5.5 are announced to follow 'in the coming weeks' -- add them as separate entries when their model IDs and prices are published, never as aliases of the 5.x entries.",
|
|
15
|
-
"claude-sonnet-5-5 added 2026-09-29: rate_id claude-sonnet-5-5-launch-2026-09-28. effective_from 2026-09-28T00:00:00Z is 00:00:00Z of the official public launch date (anthropic.com/claude-sonnet-5-5, 'September 28, 2026'); as with claude-opus-5-5, the launch time of day is not published, but a transcript row whose raw model ID is claude-sonnet-5-5 cannot predate the launch, so pricing from the start of that UTC day cannot price a pre-launch event. Values 2.0/0.20/2.50/4.0/10.0 per the pricing page (Sonnet 5.5 row, no footnote: cache_read is the standard 0.1x, unlike claude-opus-5-5's 0.05x and claude-fable-5-1's 0.025x). Identical to claude-sonnet-5's ongoing values, but kept as a separate entry, never an alias: the pricing page lists the two rows independently and a future change to one need not apply to the other. fast_multiplier 1.0: the pricing page's fast-mode table lists only the Opus models. Claude Code's CHANGELOG entry for 2.1.284 states Sonnet 5.5 is now the default Sonnet model, so the `sonnet` alias in agent frontmatter resolves to it and rows whose raw model ID is claude-sonnet-5-5 are expected from that version onward; no such row existed in the local transcripts on 2026-09-29. Claude Haiku 5.5 is announced to follow 'in the coming weeks' -- add it as a separate entry when its model ID and prices are published."
|
|
15
|
+
"claude-sonnet-5-5 added 2026-09-29: rate_id claude-sonnet-5-5-launch-2026-09-28. effective_from 2026-09-28T00:00:00Z is 00:00:00Z of the official public launch date (anthropic.com/claude-sonnet-5-5, 'September 28, 2026'); as with claude-opus-5-5, the launch time of day is not published, but a transcript row whose raw model ID is claude-sonnet-5-5 cannot predate the launch, so pricing from the start of that UTC day cannot price a pre-launch event. Values 2.0/0.20/2.50/4.0/10.0 per the pricing page (Sonnet 5.5 row, no footnote: cache_read is the standard 0.1x, unlike claude-opus-5-5's 0.05x and claude-fable-5-1's 0.025x). Identical to claude-sonnet-5's ongoing values, but kept as a separate entry, never an alias: the pricing page lists the two rows independently and a future change to one need not apply to the other. fast_multiplier 1.0: the pricing page's fast-mode table lists only the Opus models. Claude Code's CHANGELOG entry for 2.1.284 states Sonnet 5.5 is now the default Sonnet model, so the `sonnet` alias in agent frontmatter resolves to it and rows whose raw model ID is claude-sonnet-5-5 are expected from that version onward; no such row existed in the local transcripts on 2026-09-29. Claude Haiku 5.5 is announced to follow 'in the coming weeks' -- add it as a separate entry when its model ID and prices are published.",
|
|
16
|
+
"claude-haiku-5-5 added 2026-10-09: rate_id claude-haiku-5-5-launch-2026-10-07. effective_from 2026-10-07T00:00:00Z is 00:00:00Z of the official release date (platform.claude.com/docs/en/models/haiku-5-5/overview, 'Released October 7, 2026'); as with claude-sonnet-5-5, the release time of day is not published, but a transcript row whose raw model ID is claude-haiku-5-5 cannot predate the release. Its prices are tiered by prompt length: the period's top-level values (0.10/0.01/0.125/0.20/0.50) apply to prompts up to 100,000 tokens, and prompt_tiers[0] (prompt_tokens_over 100000: 0.50/0.05/0.625/1.0/2.50) to prompts over 100,000 tokens, per the pricing page's two Haiku 5.5 rows (no footnote: cache_read is the standard 0.1x). Prompt length follows the pricing page's Long context pricing section: it counts all of a request's input tokens including cache reads and cache writes, and each request is priced on its own, so a request over the threshold pays the higher prices for every token kind, output included. A fact whose prompt length is unknown (Codex, a conflicting Claude dedup group, inconsistent usage) is priced at the top-level values as a lower_bound. A separate entry, never an alias of claude-haiku-4-5. fast_multiplier 1.0: the pricing page's fast-mode table lists only the Opus models. Claude Code's CHANGELOG entry for 2.1.293 states Haiku 5.5 is now the default Haiku model, so the `haiku` alias resolves to it from that version onward. schema_version is 2 because prompt_tiers is new: agent-cost 0.3.0 and earlier reject schema_version 2 as unsupported instead of silently pricing every claude-haiku-5-5 request at the cheaper tier, and a schema_version 1 catalog may not carry prompt_tiers."
|
|
16
17
|
],
|
|
17
18
|
"sources": [
|
|
18
19
|
{
|
|
@@ -97,6 +98,21 @@
|
|
|
97
98
|
"url": "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md",
|
|
98
99
|
"retrieved_at": "2026-09-29",
|
|
99
100
|
"note": "Claude Code 2.1.284 entry: added Claude Sonnet 5.5 (claude-sonnet-5-5), now the default Sonnet model; the reason claude-sonnet-5-5 rows are expected in transcripts from that version"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"url": "https://platform.claude.com/docs/en/about-claude/pricing",
|
|
104
|
+
"retrieved_at": "2026-10-09",
|
|
105
|
+
"note": "used for claude-haiku-5-5 (two Haiku 5.5 rows: prompts up to 100K tokens $0.10 / $0.125 / $0.20 / $0.01 / $0.50, prompts over 100K tokens $0.50 / $0.625 / $1 / $0.05 / $2.50; no footnote so cache_read is the standard 0.1x; fast-mode table lists Opus models only); Long context pricing section: a request's prompt length counts all of its input tokens including cache reads and cache writes, and each request is priced on its own"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"url": "https://platform.claude.com/docs/en/models/haiku-5-5/overview",
|
|
109
|
+
"retrieved_at": "2026-10-09",
|
|
110
|
+
"note": "Haiku 5.5 overview: Released October 7, 2026, model ID claude-haiku-5-5, Retirement not sooner than October 7, 2027"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"url": "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md",
|
|
114
|
+
"retrieved_at": "2026-10-09",
|
|
115
|
+
"note": "Claude Code 2.1.293 entry: Added Claude Haiku 5.5 (claude-haiku-5-5), now the default Haiku model; 1M context, $0.10/$0.50 per MTok ($0.50/$2.50 for prompts over 100K); the reason claude-haiku-5-5 rows are expected in transcripts from that version"
|
|
100
116
|
}
|
|
101
117
|
],
|
|
102
118
|
"models": [
|
|
@@ -314,6 +330,33 @@
|
|
|
314
330
|
}
|
|
315
331
|
]
|
|
316
332
|
},
|
|
333
|
+
{
|
|
334
|
+
"model_key": "claude-haiku-5-5",
|
|
335
|
+
"aliases": [],
|
|
336
|
+
"fast_multiplier": "1.0",
|
|
337
|
+
"rates": [
|
|
338
|
+
{
|
|
339
|
+
"rate_id": "claude-haiku-5-5-launch-2026-10-07",
|
|
340
|
+
"effective_from": "2026-10-07T00:00:00+00:00",
|
|
341
|
+
"effective_until": null,
|
|
342
|
+
"input_nocache": "0.10",
|
|
343
|
+
"cache_read": "0.01",
|
|
344
|
+
"cache_write_5m": "0.125",
|
|
345
|
+
"cache_write_1h": "0.20",
|
|
346
|
+
"output": "0.50",
|
|
347
|
+
"prompt_tiers": [
|
|
348
|
+
{
|
|
349
|
+
"prompt_tokens_over": 100000,
|
|
350
|
+
"input_nocache": "0.50",
|
|
351
|
+
"cache_read": "0.05",
|
|
352
|
+
"cache_write_5m": "0.625",
|
|
353
|
+
"cache_write_1h": "1.0",
|
|
354
|
+
"output": "2.50"
|
|
355
|
+
}
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
]
|
|
359
|
+
},
|
|
317
360
|
{
|
|
318
361
|
"model_key": "gpt-5.6-sol",
|
|
319
362
|
"aliases": [],
|
|
@@ -16,7 +16,7 @@ from datetime import datetime, timezone
|
|
|
16
16
|
from decimal import Decimal, InvalidOperation
|
|
17
17
|
from importlib import resources
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import Optional
|
|
19
|
+
from typing import Optional, Tuple
|
|
20
20
|
|
|
21
21
|
RATE_FIELDS = (
|
|
22
22
|
"input_nocache",
|
|
@@ -30,8 +30,11 @@ RATE_FIELDS = (
|
|
|
30
30
|
# knows how to interpret. A catalog claiming anything else is rejected
|
|
31
31
|
# outright rather than silently treated as USD-per-mtok -- a catalog in a
|
|
32
32
|
# different currency or unit priced as if it were USD-per-mtok would be
|
|
33
|
-
# wrong by a fixed, silent factor.
|
|
34
|
-
|
|
33
|
+
# wrong by a fixed, silent factor. Schema "2" adds optional per-period
|
|
34
|
+
# ``prompt_tiers``; it is a separate version (and "1" may not carry tiers)
|
|
35
|
+
# so that a reader that predates tiers rejects a tiered catalog as
|
|
36
|
+
# unsupported instead of silently pricing every request at the base tier.
|
|
37
|
+
SUPPORTED_SCHEMA_VERSIONS = ("1", "2")
|
|
35
38
|
SUPPORTED_CURRENCIES = ("USD",)
|
|
36
39
|
SUPPORTED_UNITS = ("per_mtok",)
|
|
37
40
|
|
|
@@ -40,12 +43,28 @@ class RatesValidationError(ValueError):
|
|
|
40
43
|
"""The rates catalog failed structural or business-rule validation."""
|
|
41
44
|
|
|
42
45
|
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class PromptTier:
|
|
48
|
+
"""Rate values for requests whose prompt length is strictly greater
|
|
49
|
+
than ``prompt_tokens_over``."""
|
|
50
|
+
|
|
51
|
+
prompt_tokens_over: int
|
|
52
|
+
values: dict
|
|
53
|
+
|
|
54
|
+
|
|
43
55
|
@dataclass(frozen=True)
|
|
44
56
|
class RatePeriod:
|
|
45
57
|
rate_id: str
|
|
46
58
|
effective_from: datetime
|
|
47
59
|
effective_until: Optional[datetime]
|
|
60
|
+
# Base tier: prompt length <= the first tier's threshold (or every
|
|
61
|
+
# request, when there are no tiers).
|
|
48
62
|
values: dict
|
|
63
|
+
# Ascending by threshold; a request uses the tier with the largest
|
|
64
|
+
# threshold its prompt length exceeds. Every value in a tiered period
|
|
65
|
+
# is non-null and non-decreasing tier to tier, so ``values`` is the
|
|
66
|
+
# cheapest (a lower bound) when the prompt length is unknown.
|
|
67
|
+
prompt_tiers: Tuple[PromptTier, ...] = ()
|
|
49
68
|
|
|
50
69
|
def covers(self, moment: datetime) -> bool:
|
|
51
70
|
if moment < self.effective_from:
|
|
@@ -125,7 +144,46 @@ def _parse_datetime(value, *, field_name: str) -> datetime:
|
|
|
125
144
|
return dt.astimezone(timezone.utc)
|
|
126
145
|
|
|
127
146
|
|
|
128
|
-
def
|
|
147
|
+
def _build_prompt_tiers(tiers_raw, base_values: dict, *, where: str) -> Tuple[PromptTier, ...]:
|
|
148
|
+
if not isinstance(tiers_raw, list):
|
|
149
|
+
raise RatesValidationError(f"{where}: prompt_tiers must be a list")
|
|
150
|
+
if not tiers_raw:
|
|
151
|
+
return ()
|
|
152
|
+
|
|
153
|
+
for f in RATE_FIELDS:
|
|
154
|
+
if base_values[f] is None:
|
|
155
|
+
raise RatesValidationError(f"{where}: {f} is required when prompt_tiers is present")
|
|
156
|
+
|
|
157
|
+
tiers = []
|
|
158
|
+
previous_values = base_values
|
|
159
|
+
previous_over = None
|
|
160
|
+
for idx, tier_raw in enumerate(tiers_raw):
|
|
161
|
+
tier_where = f"{where}.prompt_tiers[{idx}]"
|
|
162
|
+
if not isinstance(tier_raw, dict):
|
|
163
|
+
raise RatesValidationError(f"{tier_where}: must be an object")
|
|
164
|
+
over = tier_raw.get("prompt_tokens_over")
|
|
165
|
+
if isinstance(over, bool) or not isinstance(over, int) or over <= 0:
|
|
166
|
+
raise RatesValidationError(f"{tier_where}: prompt_tokens_over must be a positive int, got {over!r}")
|
|
167
|
+
if previous_over is not None and over <= previous_over:
|
|
168
|
+
raise RatesValidationError(
|
|
169
|
+
f"{tier_where}: prompt_tokens_over must be ascending without duplicates "
|
|
170
|
+
f"({over} after {previous_over})"
|
|
171
|
+
)
|
|
172
|
+
values = {f: _parse_decimal(tier_raw.get(f), field_name=f"{tier_where}.{f}") for f in RATE_FIELDS}
|
|
173
|
+
for f in RATE_FIELDS:
|
|
174
|
+
if values[f] is None:
|
|
175
|
+
raise RatesValidationError(f"{tier_where}: {f} is required")
|
|
176
|
+
if values[f] < previous_values[f]:
|
|
177
|
+
raise RatesValidationError(
|
|
178
|
+
f"{tier_where}: {f} {values[f]} is lower than the previous tier's {previous_values[f]}"
|
|
179
|
+
)
|
|
180
|
+
tiers.append(PromptTier(prompt_tokens_over=over, values=values))
|
|
181
|
+
previous_values = values
|
|
182
|
+
previous_over = over
|
|
183
|
+
return tuple(tiers)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _build_model_entry(raw: dict, *, schema_version: str) -> ModelEntry:
|
|
129
187
|
model_key = raw.get("model_key")
|
|
130
188
|
if not model_key or not isinstance(model_key, str):
|
|
131
189
|
raise RatesValidationError(f"model entry missing model_key: {raw!r}")
|
|
@@ -169,12 +227,22 @@ def _build_model_entry(raw: dict) -> ModelEntry:
|
|
|
169
227
|
f: _parse_decimal(rate_raw.get(f), field_name=f"{model_key}.{rate_id}.{f}")
|
|
170
228
|
for f in RATE_FIELDS
|
|
171
229
|
}
|
|
230
|
+
prompt_tiers: Tuple[PromptTier, ...] = ()
|
|
231
|
+
if "prompt_tiers" in rate_raw:
|
|
232
|
+
if schema_version == "1":
|
|
233
|
+
raise RatesValidationError(
|
|
234
|
+
f"{model_key}.{rate_id}: prompt_tiers requires schema_version \"2\""
|
|
235
|
+
)
|
|
236
|
+
prompt_tiers = _build_prompt_tiers(
|
|
237
|
+
rate_raw["prompt_tiers"], values, where=f"{model_key}.{rate_id}"
|
|
238
|
+
)
|
|
172
239
|
periods.append(
|
|
173
240
|
RatePeriod(
|
|
174
241
|
rate_id=rate_id,
|
|
175
242
|
effective_from=effective_from,
|
|
176
243
|
effective_until=effective_until,
|
|
177
244
|
values=values,
|
|
245
|
+
prompt_tiers=prompt_tiers,
|
|
178
246
|
)
|
|
179
247
|
)
|
|
180
248
|
|
|
@@ -227,7 +295,7 @@ def _validate_and_build(data: dict) -> RateCatalog:
|
|
|
227
295
|
|
|
228
296
|
models: dict = {}
|
|
229
297
|
for raw in data.get("models") or []:
|
|
230
|
-
entry = _build_model_entry(raw)
|
|
298
|
+
entry = _build_model_entry(raw, schema_version=schema_version)
|
|
231
299
|
if entry.model_key in models:
|
|
232
300
|
raise RatesValidationError(f"duplicate model_key: {entry.model_key}")
|
|
233
301
|
models[entry.model_key] = entry
|