coding-agent-cost 0.3.0__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.3.0/coding_agent_cost.egg-info → coding_agent_cost-0.4.0}/PKG-INFO +49 -3
  2. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/README.md +48 -2
  3. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/__init__.py +1 -1
  4. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/aggregate.py +15 -1
  5. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/cli.py +6 -0
  6. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/facts.py +12 -0
  7. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/rates.json +46 -3
  8. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/rates.py +73 -5
  9. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/readers/claude.py +47 -0
  10. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0/coding_agent_cost.egg-info}/PKG-INFO +49 -3
  11. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/SOURCES.txt +1 -0
  12. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/pyproject.toml +1 -1
  13. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_aggregate.py +157 -0
  14. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_astra_pricing.py +2 -2
  15. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_cli.py +52 -0
  16. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_e0a_review3_hardening.py +3 -3
  17. {coding_agent_cost-0.3.0 → 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.3.0 → coding_agent_cost-0.4.0}/tests/test_rates.py +200 -2
  20. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_reader_claude.py +191 -0
  21. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_reader_codex.py +20 -0
  22. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_sonnet_5_5_rates.py +1 -1
  23. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_version_consistency.py +8 -7
  24. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/LICENSE +0 -0
  25. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/config.py +0 -0
  26. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/readers/__init__.py +0 -0
  27. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/readers/codex.py +0 -0
  28. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/agent_cost/renderers.py +0 -0
  29. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/dependency_links.txt +0 -0
  30. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/entry_points.txt +0 -0
  31. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/coding_agent_cost.egg-info/top_level.txt +0 -0
  32. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/setup.cfg +0 -0
  33. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_e0a_fable_5_1_and_sonnet_5_correction.py +0 -0
  34. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_measure_v1_contract.py +0 -0
  35. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_opus_5_5_rates.py +0 -0
  36. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_reader_dedup.py +0 -0
  37. {coding_agent_cost-0.3.0 → coding_agent_cost-0.4.0}/tests/test_reader_output_lower_bound.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-agent-cost
3
- Version: 0.3.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
@@ -244,6 +244,16 @@ network, never calls `gh`, and never resolves branches or PRs.
244
244
  present in the log, it's used; otherwise the cache-write tokens are priced
245
245
  at the 5-minute rate as an explicit **lower bound** and flagged
246
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.
247
257
  - **Codex CLI**: rollout files record a *cumulative* token count after each
248
258
  turn; agent-cost turns that into per-turn deltas. Under the Codex token-rate
249
259
  tariff used here, cache writes are not added as a separate charge. The current
@@ -303,6 +313,37 @@ carries a `catalog_version`, a list of `sources` (the pricing page a rate
303
313
  came from), and is validated on load (no duplicate model keys or aliases,
304
314
  no negative rates, no overlapping periods for the same model).
305
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
+
306
347
  To use your own catalog instead of the one bundled with the package, pass
307
348
  `--rates path/to/rates.json` to `report` or `export` -- this fully replaces
308
349
  the bundled catalog, it does not merge with it. Inspect any catalog with:
@@ -402,10 +443,11 @@ agent-cost makes zero network calls. `agent-cost export`'s JSONL never
402
443
  includes absolute file paths, rollout paths, prompt/message content, or git
403
444
  branch names -- only the fields needed to reproduce a cost estimate:
404
445
  `occurred_at_utc`, `agent`, `session_id`, `model_raw`, `model_key`,
405
- `token_kind`, `tokens`, `mode`, and `source_quality` (a fixed-vocabulary
446
+ `token_kind`, `tokens`, `mode`, `source_quality` (a fixed-vocabulary
406
447
  caveat about how that one fact was derived: `"ok"`, Codex's
407
448
  `"first_event_delta"`, or Claude's `"identity_missing"` /
408
- `"output_lower_bound"` -- never null).
449
+ `"output_lower_bound"` -- never null), and `prompt_tokens` (the request's
450
+ prompt length as an integer, or null when unknown).
409
451
 
410
452
  ## License
411
453
 
@@ -429,6 +471,10 @@ MIT. See [LICENSE](LICENSE).
429
471
  未知のモデル・単価表にない token 種別は `unpriced` として扱い、憶測の価格を出しません。
430
472
  - 単価表 (`agent_cost/rates.json`) は履歴型カタログで、値上げは新しい期間として追加します。
431
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`)になります。
432
478
  - `agent-cost measure --session-id ID [--session-id ID ...] --format json` は他プログラムから
433
479
  subprocess で叩くための機械可読な契約です(`protocol_version: "measure/v1"`)。指定した
434
480
  session_id が1件も見つからなくても終了コードは0(空集計として表現)、`--session-id` 未指定など
@@ -218,6 +218,16 @@ network, never calls `gh`, and never resolves branches or PRs.
218
218
  present in the log, it's used; otherwise the cache-write tokens are priced
219
219
  at the 5-minute rate as an explicit **lower bound** and flagged
220
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.
221
231
  - **Codex CLI**: rollout files record a *cumulative* token count after each
222
232
  turn; agent-cost turns that into per-turn deltas. Under the Codex token-rate
223
233
  tariff used here, cache writes are not added as a separate charge. The current
@@ -277,6 +287,37 @@ carries a `catalog_version`, a list of `sources` (the pricing page a rate
277
287
  came from), and is validated on load (no duplicate model keys or aliases,
278
288
  no negative rates, no overlapping periods for the same model).
279
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
+
280
321
  To use your own catalog instead of the one bundled with the package, pass
281
322
  `--rates path/to/rates.json` to `report` or `export` -- this fully replaces
282
323
  the bundled catalog, it does not merge with it. Inspect any catalog with:
@@ -376,10 +417,11 @@ agent-cost makes zero network calls. `agent-cost export`'s JSONL never
376
417
  includes absolute file paths, rollout paths, prompt/message content, or git
377
418
  branch names -- only the fields needed to reproduce a cost estimate:
378
419
  `occurred_at_utc`, `agent`, `session_id`, `model_raw`, `model_key`,
379
- `token_kind`, `tokens`, `mode`, and `source_quality` (a fixed-vocabulary
420
+ `token_kind`, `tokens`, `mode`, `source_quality` (a fixed-vocabulary
380
421
  caveat about how that one fact was derived: `"ok"`, Codex's
381
422
  `"first_event_delta"`, or Claude's `"identity_missing"` /
382
- `"output_lower_bound"` -- never null).
423
+ `"output_lower_bound"` -- never null), and `prompt_tokens` (the request's
424
+ prompt length as an integer, or null when unknown).
383
425
 
384
426
  ## License
385
427
 
@@ -403,6 +445,10 @@ MIT. See [LICENSE](LICENSE).
403
445
  未知のモデル・単価表にない token 種別は `unpriced` として扱い、憶測の価格を出しません。
404
446
  - 単価表 (`agent_cost/rates.json`) は履歴型カタログで、値上げは新しい期間として追加します。
405
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`)になります。
406
452
  - `agent-cost measure --session-id ID [--session-id ID ...] --format json` は他プログラムから
407
453
  subprocess で叩くための機械可読な契約です(`protocol_version: "measure/v1"`)。指定した
408
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.3.0"
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}")
@@ -84,6 +84,12 @@ class Fact:
84
84
  tokens: int
85
85
  mode: str = "unknown"
86
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
87
93
 
88
94
  def __post_init__(self) -> None:
89
95
  if self.agent not in AGENTS:
@@ -96,5 +102,11 @@ class Fact:
96
102
  raise ValueError(f"invalid source_quality: {self.source_quality!r}")
97
103
  if self.tokens < 0:
98
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}")
99
111
  if self.occurred_at_utc.tzinfo is None:
100
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
@@ -127,6 +127,33 @@ def _extract_tokens(usage: dict) -> Optional[dict]:
127
127
  return tokens
128
128
 
129
129
 
130
+ def _prompt_tokens(usage: dict) -> Optional[int]:
131
+ """The request's prompt length -- ``input_tokens`` +
132
+ ``cache_read_input_tokens`` + ``cache_creation_input_tokens`` (missing
133
+ counts as 0), the definition the pricing page's long-context tiers use.
134
+
135
+ Returns ``None`` when the usage is inconsistent: a ``cache_creation`` TTL
136
+ breakdown larger than ``cache_creation_input_tokens``, or a negative
137
+ field (including either TTL breakdown field). Raises
138
+ ``TypeError``/``ValueError`` on a field that fails int conversion, which
139
+ the caller counts as malformed (as for ``_extract_tokens``).
140
+ """
141
+ input_tokens = int(usage.get("input_tokens") or 0)
142
+ cache_read = int(usage.get("cache_read_input_tokens") or 0)
143
+ cache_creation_total = int(usage.get("cache_creation_input_tokens") or 0)
144
+ cache_creation = usage.get("cache_creation")
145
+ if isinstance(cache_creation, dict):
146
+ ephemeral_5m = int(cache_creation.get("ephemeral_5m_input_tokens") or 0)
147
+ ephemeral_1h = int(cache_creation.get("ephemeral_1h_input_tokens") or 0)
148
+ if ephemeral_5m < 0 or ephemeral_1h < 0:
149
+ return None
150
+ if ephemeral_5m + ephemeral_1h > cache_creation_total:
151
+ return None
152
+ if input_tokens < 0 or cache_read < 0 or cache_creation_total < 0:
153
+ return None
154
+ return input_tokens + cache_read + cache_creation_total
155
+
156
+
130
157
  def _valid_id(value) -> Optional[str]:
131
158
  """Accept only a non-empty ``str`` as a dedup identifier.
132
159
 
@@ -476,6 +503,11 @@ def parse_session_detailed(jsonl_path: Path) -> ClaudeParseResult:
476
503
  if tokens is None:
477
504
  malformed += 1
478
505
  continue
506
+ try:
507
+ prompt_tokens = _prompt_tokens(usage)
508
+ except (TypeError, ValueError):
509
+ malformed += 1
510
+ continue
479
511
 
480
512
  model_raw = message.get("model") or "(unknown)"
481
513
  mode = _detect_mode(usage)
@@ -491,6 +523,7 @@ def parse_session_detailed(jsonl_path: Path) -> ClaudeParseResult:
491
523
  "sid": sid,
492
524
  "stop_reason": stop_reason,
493
525
  "tokens": tokens,
526
+ "prompt_tokens": prompt_tokens,
494
527
  }
495
528
 
496
529
  key = (msg_id, req_id) if (msg_id is not None and req_id is not None) else None
@@ -546,6 +579,8 @@ def parse_session_detailed(jsonl_path: Path) -> ClaudeParseResult:
546
579
  group_conflicting = not is_streaming_progression
547
580
  if group_conflicting:
548
581
  conflicting_duplicate_groups += 1
582
+ prompt_lengths = {r["prompt_tokens"] for r in rows}
583
+ prompt_lengths_agree = len(prompt_lengths) == 1 and None not in prompt_lengths
549
584
 
550
585
  if group_skipped > 0 or group_conflicting:
551
586
  dedup_units.append(
@@ -563,6 +598,17 @@ def parse_session_detailed(jsonl_path: Path) -> ClaudeParseResult:
563
598
  "mode": _resolve_mode(rows, adopted),
564
599
  "sid": adopted["sid"],
565
600
  "tokens": adopted["tokens"],
601
+ # A conflicting group's rows disagree (possibly on the input
602
+ # side), and a non-conflicting group's rows can still
603
+ # disagree on prompt length (one row's usage inconsistent,
604
+ # so None). Either way the adopted row's prompt length isn't
605
+ # a settled fact: leave it unknown (priced as a lower bound)
606
+ # rather than pick a tier from it.
607
+ "prompt_tokens": (
608
+ adopted["prompt_tokens"]
609
+ if not group_conflicting and prompt_lengths_agree
610
+ else None
611
+ ),
566
612
  }
567
613
  source_quality = "ok"
568
614
  output_source_quality = "ok" if _is_final(adopted) else "output_lower_bound"
@@ -580,6 +626,7 @@ def parse_session_detailed(jsonl_path: Path) -> ClaudeParseResult:
580
626
  tokens=amount,
581
627
  mode=row["mode"],
582
628
  source_quality=output_source_quality if kind == "output" else source_quality,
629
+ prompt_tokens=row["prompt_tokens"],
583
630
  )
584
631
  )
585
632
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-agent-cost
3
- Version: 0.3.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
@@ -244,6 +244,16 @@ network, never calls `gh`, and never resolves branches or PRs.
244
244
  present in the log, it's used; otherwise the cache-write tokens are priced
245
245
  at the 5-minute rate as an explicit **lower bound** and flagged
246
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.
247
257
  - **Codex CLI**: rollout files record a *cumulative* token count after each
248
258
  turn; agent-cost turns that into per-turn deltas. Under the Codex token-rate
249
259
  tariff used here, cache writes are not added as a separate charge. The current
@@ -303,6 +313,37 @@ carries a `catalog_version`, a list of `sources` (the pricing page a rate
303
313
  came from), and is validated on load (no duplicate model keys or aliases,
304
314
  no negative rates, no overlapping periods for the same model).
305
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
+
306
347
  To use your own catalog instead of the one bundled with the package, pass
307
348
  `--rates path/to/rates.json` to `report` or `export` -- this fully replaces
308
349
  the bundled catalog, it does not merge with it. Inspect any catalog with:
@@ -402,10 +443,11 @@ agent-cost makes zero network calls. `agent-cost export`'s JSONL never
402
443
  includes absolute file paths, rollout paths, prompt/message content, or git
403
444
  branch names -- only the fields needed to reproduce a cost estimate:
404
445
  `occurred_at_utc`, `agent`, `session_id`, `model_raw`, `model_key`,
405
- `token_kind`, `tokens`, `mode`, and `source_quality` (a fixed-vocabulary
446
+ `token_kind`, `tokens`, `mode`, `source_quality` (a fixed-vocabulary
406
447
  caveat about how that one fact was derived: `"ok"`, Codex's
407
448
  `"first_event_delta"`, or Claude's `"identity_missing"` /
408
- `"output_lower_bound"` -- never null).
449
+ `"output_lower_bound"` -- never null), and `prompt_tokens` (the request's
450
+ prompt length as an integer, or null when unknown).
409
451
 
410
452
  ## License
411
453
 
@@ -429,6 +471,10 @@ MIT. See [LICENSE](LICENSE).
429
471
  未知のモデル・単価表にない token 種別は `unpriced` として扱い、憶測の価格を出しません。
430
472
  - 単価表 (`agent_cost/rates.json`) は履歴型カタログで、値上げは新しい期間として追加します。
431
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`)になります。
432
478
  - `agent-cost measure --session-id ID [--session-id ID ...] --format json` は他プログラムから
433
479
  subprocess で叩くための機械可読な契約です(`protocol_version: "measure/v1"`)。指定した
434
480
  session_id が1件も見つからなくても終了コードは0(空集計として表現)、`--session-id` 未指定など
@@ -23,6 +23,7 @@ tests/test_cli.py
23
23
  tests/test_e0a_fable_5_1_and_sonnet_5_correction.py
24
24
  tests/test_e0a_review3_hardening.py
25
25
  tests/test_facts.py
26
+ tests/test_haiku_5_5_rates.py
26
27
  tests/test_measure_v1_contract.py
27
28
  tests/test_opus_5_5_rates.py
28
29
  tests/test_rates.py