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.
Files changed (37) hide show
  1. {coding_agent_cost-0.2.2/coding_agent_cost.egg-info → coding_agent_cost-0.4.0}/PKG-INFO +61 -6
  2. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/README.md +60 -5
  3. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/__init__.py +1 -1
  4. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/aggregate.py +15 -1
  5. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/cli.py +6 -0
  6. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/facts.py +18 -2
  7. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/rates.json +46 -3
  8. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/rates.py +73 -5
  9. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/readers/claude.py +72 -3
  10. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0/coding_agent_cost.egg-info}/PKG-INFO +61 -6
  11. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/SOURCES.txt +2 -0
  12. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/pyproject.toml +1 -1
  13. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_aggregate.py +157 -0
  14. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_astra_pricing.py +2 -2
  15. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_cli.py +53 -0
  16. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_e0a_review3_hardening.py +3 -3
  17. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_facts.py +41 -0
  18. coding_agent_cost-0.4.0/tests/test_haiku_5_5_rates.py +87 -0
  19. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_rates.py +200 -2
  20. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_reader_claude.py +191 -0
  21. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_reader_codex.py +20 -0
  22. coding_agent_cost-0.4.0/tests/test_reader_output_lower_bound.py +265 -0
  23. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_sonnet_5_5_rates.py +1 -1
  24. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_version_consistency.py +8 -7
  25. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/LICENSE +0 -0
  26. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/config.py +0 -0
  27. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/readers/__init__.py +0 -0
  28. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/readers/codex.py +0 -0
  29. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/agent_cost/renderers.py +0 -0
  30. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/dependency_links.txt +0 -0
  31. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/entry_points.txt +0 -0
  32. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/top_level.txt +0 -0
  33. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/setup.cfg +0 -0
  34. {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
  35. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_measure_v1_contract.py +0 -0
  36. {coding_agent_cost-0.2.2 → coding_agent_cost-0.4.0}/tests/test_opus_5_5_rates.py +0 -0
  37. {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.2.2
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.2.2",
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`, and `source_quality` (a fixed-vocabulary
398
- caveat about how that one fact was derived, e.g. `"ok"` or Codex's
399
- `"first_event_delta"` -- never null).
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.2.2",
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`, and `source_quality` (a fixed-vocabulary
372
- caveat about how that one fact was derived, e.g. `"ok"` or Codex's
373
- `"first_event_delta"` -- never null).
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` 未指定など
@@ -6,4 +6,4 @@ against a versioned rate catalog. Everything happens locally; there are no
6
6
  network calls.
7
7
  """
8
8
 
9
- __version__ = "0.2.2"
9
+ __version__ = "0.4.0"
@@ -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
- rate = period.values.get(rate_field)
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": "1",
3
- "catalog_version": "2026-09-29",
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
- SUPPORTED_SCHEMA_VERSIONS = ("1",)
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 _build_model_entry(raw: dict) -> ModelEntry:
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