coding-agent-cost 0.5.1__tar.gz → 0.5.3__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 (42) hide show
  1. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/PKG-INFO +3 -1
  2. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/__init__.py +1 -1
  3. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/rates.json +146 -3
  4. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/PKG-INFO +3 -1
  5. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/SOURCES.txt +2 -0
  6. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/pyproject.toml +3 -1
  7. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_astra_pricing.py +2 -2
  8. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_billing_plan.py +27 -4
  9. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_cli.py +1 -1
  10. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_e0a_review3_hardening.py +3 -3
  11. coding_agent_cost-0.5.3/tests/test_fable_5_2_rates.py +83 -0
  12. coding_agent_cost-0.5.3/tests/test_gpt_6_1_rates.py +117 -0
  13. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_haiku_5_5_rates.py +1 -1
  14. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_rates.py +6 -3
  15. coding_agent_cost-0.5.3/tests/test_sonnet_5_5_rates.py +120 -0
  16. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_version_consistency.py +7 -7
  17. coding_agent_cost-0.5.1/tests/test_sonnet_5_5_rates.py +0 -69
  18. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/LICENSE +0 -0
  19. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/README.md +0 -0
  20. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/aggregate.py +0 -0
  21. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/billing_plan.py +0 -0
  22. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/cli.py +0 -0
  23. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/config.py +0 -0
  24. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/facts.py +0 -0
  25. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/rates.py +0 -0
  26. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/readers/__init__.py +0 -0
  27. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/readers/claude.py +0 -0
  28. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/readers/codex.py +0 -0
  29. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/renderers.py +0 -0
  30. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/dependency_links.txt +0 -0
  31. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/entry_points.txt +0 -0
  32. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/top_level.txt +0 -0
  33. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/setup.cfg +0 -0
  34. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_aggregate.py +0 -0
  35. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_e0a_fable_5_1_and_sonnet_5_correction.py +0 -0
  36. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_facts.py +0 -0
  37. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_measure_v1_contract.py +0 -0
  38. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_opus_5_5_rates.py +0 -0
  39. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_reader_claude.py +0 -0
  40. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_reader_codex.py +0 -0
  41. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_reader_dedup.py +0 -0
  42. {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/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.5.1
3
+ Version: 0.5.3
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
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3.9
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
20
22
  Classifier: Topic :: Software Development :: Build Tools
21
23
  Classifier: Topic :: Utilities
22
24
  Requires-Python: >=3.9
@@ -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.5.1"
9
+ __version__ = "0.5.3"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": "2",
3
- "catalog_version": "2026-10-09",
3
+ "catalog_version": "2026-11-10",
4
4
  "currency": "USD",
5
5
  "unit": "per_mtok",
6
6
  "usd_per_credit": "0.04",
@@ -13,7 +13,10 @@
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
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
+ "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.",
17
+ "claude-sonnet-5-5 cache_read cut 2026-10-13: the launch period (rate_id claude-sonnet-5-5-launch-2026-09-28) now ends at 2026-10-07T00:00:00Z and a new period (rate_id claude-sonnet-5-5-cache-read-cut-2026-10-07) prices cache_read at 0.10 instead of 0.20 from that instant; input_nocache, cache_write_5m, cache_write_1h and output are unchanged (2.0/2.50/4.0/10.0). anthropic.com/claude-haiku-5-5 (dated October 7, 2026) states 'starting today, we're lowering the price of cache reads on Claude Sonnet 5.5' and 'Cache reads now cost 50% less: $0.10 per million tokens rather than $0.20'; the source gives a calendar date only, so effective_from is 00:00:00Z of that day, following the claude-sonnet-5-standard-2026-09-01 precedent. The pricing page (retrieved 2026-10-13) lists the Sonnet 5.5 row as $2 / $2.50 / $4 / $0.10 / $10 with footnote 2: cache hits and refreshes on Claude Opus 5.5 and Claude Sonnet 5.5 are 0.05x the base input price, so cache_read is now 0.05x like claude-opus-5-5. Claude Code's CHANGELOG entry for 2.1.296 (2026-10-09) updated /cost, the status line, --max-budget-usd and the SDK's cost figures to $0.10 per million Sonnet 5.5 cache-read tokens; that is the date Claude Code's own figures changed, not the API price's effective date. catalog 2026-10-09 priced claude-sonnet-5-5 cache_read at 0.20 for events from 2026-10-07 through 2026-10-13, overstating that component 2x; reports produced with it for windows touching 2026-10-07 or later should be regenerated. claude-sonnet-5 is unchanged (cache_read stays 0.20): the pricing page's footnote 3 covers its input/output prices only, and footnote 2 names Opus 5.5 and Sonnet 5.5, not Sonnet 5.",
18
+ "gpt-6.1-terra, gpt-6.1-sol and codex-auto-review added 2026-11-09: standard tier, short context USD per MTok from the human-approved price table pricing-2026-11-v4 (developers.openai.com/api/docs/pricing, retrieved 2026-11-05); effective_from 2026-10-01T00:00:00Z is a placeholder before the first observed use (official launch date unconfirmed; rate_id *-launch-date-unconfirmed, following the claude-opus-5 placeholder precedent); usage from 2026-11-04 must be priced, so the retrieval date 2026-11-05 cannot be used. codex-auto-review is the model name of the Codex automatic review (guardian subagent) and has no published price; a human decision on 2026-11-06 prices it the same as gpt-6.1-terra. credits_per_mtok follow usd_per_credit=0.04. Existing entries (including gpt-5.6-terra) are unchanged.",
19
+ "claude-fable-5-2 and claude-mythos-5-2 added 2026-11-10: rate_ids claude-fable-5-2-launch-2026-11-09 / claude-mythos-5-2-launch-2026-11-09. effective_from 2026-11-09T00:00:00Z is 00:00:00Z of the official announcement date (anthropic.com/claude-fable-and-mythos-5-2, published November 9, 2026); the launch time of day is not published, but a transcript row whose raw model ID is claude-fable-5-2 or claude-mythos-5-2 cannot predate the launch. Values 8.0/0.20/10.0/16.0/40.0 per the pricing page (retrieved 2026-11-10): cache_read is 0.025x base input (footnote 1), like claude-fable-5-1. claude-mythos-5-2 (limited availability, verified organizations only) has the same prices; it is kept as a separate entry, never an alias of claude-fable-5-2. fast_multiplier 2.0 (fast mode $16/$80 per MTok on the pricing page). No prompt-length tiers (1M context at standard prices). Claude Code's CHANGELOG entry for 2.1.321 adds claude-fable-5-2 at $8/$40 with $0.20 cache reads. claude-fable-5-1 is unchanged (10.0/0.25/12.50/20.0/50.0). claude-mythos-5-1 stays absent (official release date unconfirmed), so its rows remain unpriced."
17
20
  ],
18
21
  "sources": [
19
22
  {
@@ -113,6 +116,30 @@
113
116
  "url": "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md",
114
117
  "retrieved_at": "2026-10-09",
115
118
  "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"
119
+ },
120
+ {
121
+ "url": "https://platform.claude.com/docs/en/about-claude/pricing",
122
+ "retrieved_at": "2026-10-13",
123
+ "note": "used for the claude-sonnet-5-5 cache_read cut (Sonnet 5.5 row: $2 / $2.50 / $4 / $0.10 / $10; footnote 2: cache hits and refreshes on Claude Opus 5.5 and Claude Sonnet 5.5 are priced at 0.05x the base input price)"
124
+ },
125
+ {
126
+ "url": "https://www.anthropic.com/claude-haiku-5-5",
127
+ "retrieved_at": "2026-10-13",
128
+ "note": "Haiku 5.5 announcement dated October 7, 2026, 'Further updates': starting that day, Claude Sonnet 5.5 cache reads cost $0.10 per million tokens rather than $0.20; the effective date of the claude-sonnet-5-5 cache_read cut"
129
+ },
130
+ {
131
+ "url": "https://developers.openai.com/api/docs/pricing",
132
+ "retrieved_at": "2026-11-05"
133
+ },
134
+ {
135
+ "url": "https://www.anthropic.com/claude-fable-and-mythos-5-2",
136
+ "retrieved_at": "2026-11-10",
137
+ "note": "Fable 5.2 / Mythos 5.2 announcement published November 9, 2026; the effective date of claude-fable-5-2 and claude-mythos-5-2"
138
+ },
139
+ {
140
+ "url": "https://platform.claude.com/docs/en/about-claude/pricing",
141
+ "retrieved_at": "2026-11-10",
142
+ "note": "Fable 5.2 / Mythos 5.2 rows: $8 input, $10 5m cache write, $16 1h cache write, $0.20 cache hits (footnote 1: 0.025x), $40 output; fast mode $16/$80"
116
143
  }
117
144
  ],
118
145
  "models": [
@@ -150,6 +177,23 @@
150
177
  }
151
178
  ]
152
179
  },
180
+ {
181
+ "model_key": "claude-fable-5-2",
182
+ "aliases": [],
183
+ "fast_multiplier": "2.0",
184
+ "rates": [
185
+ {
186
+ "rate_id": "claude-fable-5-2-launch-2026-11-09",
187
+ "effective_from": "2026-11-09T00:00:00+00:00",
188
+ "effective_until": null,
189
+ "input_nocache": "8.0",
190
+ "cache_read": "0.20",
191
+ "cache_write_5m": "10.0",
192
+ "cache_write_1h": "16.0",
193
+ "output": "40.0"
194
+ }
195
+ ]
196
+ },
153
197
  {
154
198
  "model_key": "claude-mythos-5",
155
199
  "aliases": [],
@@ -167,6 +211,23 @@
167
211
  }
168
212
  ]
169
213
  },
214
+ {
215
+ "model_key": "claude-mythos-5-2",
216
+ "aliases": [],
217
+ "fast_multiplier": "2.0",
218
+ "rates": [
219
+ {
220
+ "rate_id": "claude-mythos-5-2-launch-2026-11-09",
221
+ "effective_from": "2026-11-09T00:00:00+00:00",
222
+ "effective_until": null,
223
+ "input_nocache": "8.0",
224
+ "cache_read": "0.20",
225
+ "cache_write_5m": "10.0",
226
+ "cache_write_1h": "16.0",
227
+ "output": "40.0"
228
+ }
229
+ ]
230
+ },
170
231
  {
171
232
  "model_key": "claude-opus-4-8",
172
233
  "aliases": [],
@@ -304,12 +365,22 @@
304
365
  {
305
366
  "rate_id": "claude-sonnet-5-5-launch-2026-09-28",
306
367
  "effective_from": "2026-09-28T00:00:00+00:00",
307
- "effective_until": null,
368
+ "effective_until": "2026-10-07T00:00:00+00:00",
308
369
  "input_nocache": "2.0",
309
370
  "cache_read": "0.20",
310
371
  "cache_write_5m": "2.50",
311
372
  "cache_write_1h": "4.0",
312
373
  "output": "10.0"
374
+ },
375
+ {
376
+ "rate_id": "claude-sonnet-5-5-cache-read-cut-2026-10-07",
377
+ "effective_from": "2026-10-07T00:00:00+00:00",
378
+ "effective_until": null,
379
+ "input_nocache": "2.0",
380
+ "cache_read": "0.10",
381
+ "cache_write_5m": "2.50",
382
+ "cache_write_1h": "4.0",
383
+ "output": "10.0"
313
384
  }
314
385
  ]
315
386
  },
@@ -668,6 +739,78 @@
668
739
  "output": "50.0"
669
740
  }
670
741
  ]
742
+ },
743
+ {
744
+ "model_key": "gpt-6.1-terra",
745
+ "aliases": [],
746
+ "fast_multiplier": "1.0",
747
+ "credits_per_mtok": {
748
+ "input_nocache": "25.0",
749
+ "cache_read": "1.25",
750
+ "cache_write_5m": null,
751
+ "cache_write_1h": null,
752
+ "output": "125.0"
753
+ },
754
+ "rates": [
755
+ {
756
+ "rate_id": "gpt-6.1-terra-launch-date-unconfirmed",
757
+ "effective_from": "2026-10-01T00:00:00+00:00",
758
+ "effective_until": null,
759
+ "input_nocache": "1.00",
760
+ "cache_read": "0.05",
761
+ "cache_write_5m": null,
762
+ "cache_write_1h": null,
763
+ "output": "5.00"
764
+ }
765
+ ]
766
+ },
767
+ {
768
+ "model_key": "gpt-6.1-sol",
769
+ "aliases": [],
770
+ "fast_multiplier": "1.0",
771
+ "credits_per_mtok": {
772
+ "input_nocache": "50.0",
773
+ "cache_read": "2.5",
774
+ "cache_write_5m": null,
775
+ "cache_write_1h": null,
776
+ "output": "250.0"
777
+ },
778
+ "rates": [
779
+ {
780
+ "rate_id": "gpt-6.1-sol-launch-date-unconfirmed",
781
+ "effective_from": "2026-10-01T00:00:00+00:00",
782
+ "effective_until": null,
783
+ "input_nocache": "2.00",
784
+ "cache_read": "0.10",
785
+ "cache_write_5m": null,
786
+ "cache_write_1h": null,
787
+ "output": "10.00"
788
+ }
789
+ ]
790
+ },
791
+ {
792
+ "model_key": "codex-auto-review",
793
+ "aliases": [],
794
+ "fast_multiplier": "1.0",
795
+ "credits_per_mtok": {
796
+ "input_nocache": "25.0",
797
+ "cache_read": "1.25",
798
+ "cache_write_5m": null,
799
+ "cache_write_1h": null,
800
+ "output": "125.0"
801
+ },
802
+ "rates": [
803
+ {
804
+ "rate_id": "codex-auto-review-2026-11-terra-equivalent",
805
+ "effective_from": "2026-10-01T00:00:00+00:00",
806
+ "effective_until": null,
807
+ "input_nocache": "1.00",
808
+ "cache_read": "0.05",
809
+ "cache_write_5m": null,
810
+ "cache_write_1h": null,
811
+ "output": "5.00"
812
+ }
813
+ ]
671
814
  }
672
815
  ]
673
816
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-agent-cost
3
- Version: 0.5.1
3
+ Version: 0.5.3
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
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3.9
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
20
22
  Classifier: Topic :: Software Development :: Build Tools
21
23
  Classifier: Topic :: Utilities
22
24
  Requires-Python: >=3.9
@@ -24,7 +24,9 @@ tests/test_billing_plan.py
24
24
  tests/test_cli.py
25
25
  tests/test_e0a_fable_5_1_and_sonnet_5_correction.py
26
26
  tests/test_e0a_review3_hardening.py
27
+ tests/test_fable_5_2_rates.py
27
28
  tests/test_facts.py
29
+ tests/test_gpt_6_1_rates.py
28
30
  tests/test_haiku_5_5_rates.py
29
31
  tests/test_measure_v1_contract.py
30
32
  tests/test_opus_5_5_rates.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coding-agent-cost"
7
- version = "0.5.1"
7
+ version = "0.5.3"
8
8
  description = "Estimate AI coding agent (Claude Code / Codex CLI) token usage and cost from local logs"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -31,6 +31,8 @@ classifiers = [
31
31
  "Programming Language :: Python :: 3.10",
32
32
  "Programming Language :: Python :: 3.11",
33
33
  "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Programming Language :: Python :: 3.14",
34
36
  "Topic :: Software Development :: Build Tools",
35
37
  "Topic :: Utilities",
36
38
  ]
@@ -45,7 +45,7 @@ def test_standard_fast_and_unknown_mode_prices(kind, usd, credits, mode, multipl
45
45
 
46
46
  def test_catalog_observation_boundary_is_not_backdated():
47
47
  catalog = load_rates()
48
- assert catalog.catalog_version == "2026-10-09"
48
+ assert catalog.catalog_version == "2026-11-10"
49
49
  entry = catalog.models["gpt-6-astra"]
50
50
  assert entry.aliases == ()
51
51
  assert entry.rates[0].effective_from == CUTOFF
@@ -149,7 +149,7 @@ def test_measure_reads_isolated_synthetic_astra_with_existing_contract(tmp_path,
149
149
  assert cli.main(["measure", "--session-id", "synthetic-astra", "--agent", "codex"]) == 0
150
150
  report = json.loads(capsys.readouterr().out)
151
151
  assert report["protocol_version"] == "measure/v1"
152
- assert report["rates"]["catalog_version"] == "2026-10-09"
152
+ assert report["rates"]["catalog_version"] == "2026-11-10"
153
153
  assert len(report["rates"]["sha256"]) == 64
154
154
  assert report["sessions"]["synthetic-astra"]["totals"]["estimated_cost_usd"] == 122
155
155
  assert report["total"]["totals"]["credits"] == 3050
@@ -13,6 +13,8 @@ import copy
13
13
  import hashlib
14
14
  import json
15
15
  import os
16
+
17
+ _REAL_FSTAT = os.fstat
16
18
  import traceback
17
19
  from datetime import datetime, timezone
18
20
  from decimal import Decimal
@@ -583,11 +585,24 @@ def test_realpath_oserror_is_billing_plan_error(tmp_path, monkeypatch):
583
585
  _assert_no_leak(info.value, path)
584
586
 
585
587
 
588
+ def _is_fd_of(fd, path):
589
+ """True when fd refers to path. Keeps fakes scoped to the plan file: pytest's
590
+ capture machinery calls os.fstat/os.read/os.close on its own fds (pytest 9.2+)."""
591
+ try:
592
+ st = _REAL_FSTAT(fd)
593
+ except OSError:
594
+ return False
595
+ target = os.stat(path)
596
+ return (st.st_dev, st.st_ino) == (target.st_dev, target.st_ino)
597
+
598
+
586
599
  def test_fstat_oserror_is_billing_plan_error(tmp_path, monkeypatch):
587
600
  path = _write_plan(tmp_path, _base_plan())
588
601
 
589
602
  def failing_fstat(fd):
590
- raise _oserror_on(path)
603
+ if _is_fd_of(fd, path):
604
+ raise _oserror_on(path)
605
+ return _REAL_FSTAT(fd)
591
606
 
592
607
  monkeypatch.setattr(bp.os, "fstat", failing_fstat)
593
608
  with pytest.raises(bp.BillingPlanError) as info:
@@ -598,8 +613,12 @@ def test_fstat_oserror_is_billing_plan_error(tmp_path, monkeypatch):
598
613
  def test_read_oserror_is_billing_plan_error(tmp_path, monkeypatch):
599
614
  path = _write_plan(tmp_path, _base_plan())
600
615
 
616
+ real_read = os.read
617
+
601
618
  def failing_read(fd, n):
602
- raise _oserror_on(path)
619
+ if _is_fd_of(fd, path):
620
+ raise _oserror_on(path)
621
+ return real_read(fd, n)
603
622
 
604
623
  monkeypatch.setattr(bp.os, "read", failing_read)
605
624
  with pytest.raises(bp.BillingPlanError) as info:
@@ -612,8 +631,10 @@ def test_close_oserror_is_billing_plan_error(tmp_path, monkeypatch):
612
631
  real_close = os.close
613
632
 
614
633
  def failing_close(fd):
634
+ mine = _is_fd_of(fd, path)
615
635
  real_close(fd)
616
- raise _oserror_on(path)
636
+ if mine:
637
+ raise _oserror_on(path)
617
638
 
618
639
  monkeypatch.setattr(bp.os, "close", failing_close)
619
640
  with pytest.raises(bp.BillingPlanError) as info:
@@ -626,8 +647,10 @@ def test_close_oserror_does_not_replace_an_earlier_error(tmp_path, monkeypatch):
626
647
  real_close = os.close
627
648
 
628
649
  def failing_close(fd):
650
+ mine = _is_fd_of(fd, path)
629
651
  real_close(fd)
630
- raise _oserror_on(path)
652
+ if mine:
653
+ raise _oserror_on(path)
631
654
 
632
655
  monkeypatch.setattr(bp.os, "close", failing_close)
633
656
  with pytest.raises(bp.BillingPlanError) as info:
@@ -886,7 +886,7 @@ def test_rates_show_model_without_tiers_prints_no_prompt_tiers(capsys):
886
886
  def test_rates_validate_packaged_catalog_with_prompt_tiers_exits_zero(capsys):
887
887
  rc = cli.main(["rates", "validate"])
888
888
  assert rc == 0
889
- assert "catalog_version=2026-10-09" in capsys.readouterr().out
889
+ assert "catalog_version=2026-11-10" in capsys.readouterr().out
890
890
 
891
891
 
892
892
  # ---------------------------------------------------------------------------
@@ -19,7 +19,7 @@ architect レビュー (3 巡目) と builder レビューで、既存テスト
19
19
  かつ確定金額が計上されない。境界ちょうどは priced になる。
20
20
 
21
21
  条件D (PR レビュー3点目・追加分): catalog は
22
- catalog_version == "2026-10-09"(rates.json の現行値)であり、sources の中に url
22
+ catalog_version == "2026-11-10"(rates.json の現行値)であり、sources の中に url
23
23
  "https://platform.claude.com/docs/en/about-claude/pricing" の
24
24
  entry があって、その note に
25
25
  "sha256=d79ad28567196bd55dd50e2fd89341b9da9774a45c1d02fcf387078507fd15e0"
@@ -224,7 +224,7 @@ def test_condition_c_claude_fable_5_1_at_effective_from_boundary_is_priced_contr
224
224
 
225
225
 
226
226
  def test_condition_d_catalog_version_and_pricing_source_sha256_note():
227
- """条件D: catalog_version == "2026-10-09"(rates.json の現行値)であり、sources に url
227
+ """条件D: catalog_version == "2026-11-10"(rates.json の現行値)であり、sources に url
228
228
  "https://platform.claude.com/docs/en/about-claude/pricing" の entry が
229
229
  あって、その note に指定の sha256 文字列を含むこと。
230
230
 
@@ -233,7 +233,7 @@ def test_condition_d_catalog_version_and_pricing_source_sha256_note():
233
233
  ダイジェストを貼り付けてしまう。
234
234
  """
235
235
  catalog = load_rates()
236
- assert catalog.catalog_version == "2026-10-09"
236
+ assert catalog.catalog_version == "2026-11-10"
237
237
 
238
238
  matches = [
239
239
  s
@@ -0,0 +1,83 @@
1
+ """Acceptance tests for claude-fable-5-2 / claude-mythos-5-2 (catalog_version 2026-11-10).
2
+
3
+ Source: platform.claude.com/docs/en/about-claude/pricing (retrieved 2026-11-10) and the
4
+ announcement anthropic.com/claude-fable-and-mythos-5-2 (published 2026-11-09).
5
+ """
6
+
7
+ from datetime import datetime
8
+ from decimal import Decimal
9
+
10
+ from agent_cost.rates import load_rates
11
+
12
+ USD = {
13
+ "input_nocache": "8.0",
14
+ "cache_read": "0.20",
15
+ "cache_write_5m": "10.0",
16
+ "cache_write_1h": "16.0",
17
+ "output": "40.0",
18
+ }
19
+ MODELS = ("claude-fable-5-2", "claude-mythos-5-2")
20
+
21
+
22
+ def dt(s):
23
+ return datetime.fromisoformat(s)
24
+
25
+
26
+ def test_fable_and_mythos_5_2_resolve_with_pricing_page_values_from_launch():
27
+ catalog = load_rates()
28
+ for model in MODELS:
29
+ for occurred_at in ("2026-11-09T00:00:00+00:00", "2026-11-10T03:00:00+00:00", "2027-06-01T00:00:00+00:00"):
30
+ resolved, period = catalog.rate_for(model, dt(occurred_at))
31
+ assert resolved == model
32
+ assert period is not None, (model, occurred_at)
33
+ assert period.rate_id == f"{model}-launch-2026-11-09"
34
+ assert period.effective_until is None
35
+ assert period.prompt_tiers == ()
36
+ for kind, value in USD.items():
37
+ assert period.values[kind] == Decimal(value), (model, kind)
38
+
39
+
40
+ def test_fable_and_mythos_5_2_are_unpriced_before_launch():
41
+ catalog = load_rates()
42
+ for model in MODELS:
43
+ resolved, period = catalog.rate_for(model, dt("2026-11-08T23:59:59+00:00"))
44
+ assert resolved == model
45
+ assert period is None
46
+
47
+
48
+ def test_cache_read_is_0_025x_and_fast_mode_is_2x():
49
+ catalog = load_rates()
50
+ for model in MODELS:
51
+ entry = catalog.models[model]
52
+ assert entry.aliases == ()
53
+ assert entry.fast_multiplier == Decimal("2.0") # fast mode $16 / $80
54
+ assert len(entry.rates) == 1
55
+ _, period = catalog.rate_for(model, dt("2026-11-10T00:00:00+00:00"))
56
+ assert period.values["cache_read"] == period.values["input_nocache"] * Decimal("0.025")
57
+
58
+
59
+ def test_separate_entries_not_aliases():
60
+ catalog = load_rates()
61
+ for model in MODELS:
62
+ for other in catalog.models.values():
63
+ assert model not in other.aliases
64
+
65
+
66
+ def test_fable_5_1_is_unchanged_and_mythos_5_1_stays_unpriced():
67
+ catalog = load_rates()
68
+ _, period = catalog.rate_for("claude-fable-5-1", dt("2026-11-10T00:00:00+00:00"))
69
+ assert period.rate_id == "claude-fable-5-1-local-first-observed-2026-09-02"
70
+ assert period.values["input_nocache"] == Decimal("10.0")
71
+ assert period.values["cache_read"] == Decimal("0.25")
72
+ assert period.values["output"] == Decimal("50.0")
73
+ assert "claude-mythos-5-1" not in catalog.models
74
+ _, period = catalog.rate_for("claude-mythos-5-1", dt("2026-11-10T00:00:00+00:00"))
75
+ assert period is None
76
+
77
+
78
+ def test_catalog_version_and_sources_record_fable_5_2():
79
+ catalog = load_rates()
80
+ assert catalog.catalog_version == "2026-11-10"
81
+ urls = [(s.get("url"), s.get("retrieved_at")) for s in catalog.sources]
82
+ assert ("https://www.anthropic.com/claude-fable-and-mythos-5-2", "2026-11-10") in urls
83
+ assert ("https://platform.claude.com/docs/en/about-claude/pricing", "2026-11-10") in urls
@@ -0,0 +1,117 @@
1
+ """Acceptance tests for gpt-6.1-terra / gpt-6.1-sol / codex-auto-review (catalog_version 2026-11-09).
2
+
3
+ Source: human-approved price table pricing-2026-11-v4 (developers.openai.com/api/docs/pricing,
4
+ retrieved 2026-11-05; standard tier, short context). codex-auto-review (Codex automatic
5
+ review / guardian subagent) has no published price; priced as gpt-6.1-terra by a human
6
+ decision on 2026-11-06.
7
+ """
8
+
9
+ import json
10
+ from datetime import datetime, timezone
11
+ from decimal import Decimal
12
+ from pathlib import Path
13
+
14
+ import agent_cost
15
+ from agent_cost.rates import load_rates
16
+
17
+ UTC = timezone.utc
18
+
19
+ EXPECTED = {
20
+ "gpt-6.1-terra": (
21
+ "gpt-6.1-terra-launch-date-unconfirmed",
22
+ {"input_nocache": "1.00", "cache_read": "0.05", "output": "5.00"},
23
+ {"input_nocache": "25.0", "cache_read": "1.25", "output": "125.0"},
24
+ ),
25
+ "gpt-6.1-sol": (
26
+ "gpt-6.1-sol-launch-date-unconfirmed",
27
+ {"input_nocache": "2.00", "cache_read": "0.10", "output": "10.00"},
28
+ {"input_nocache": "50.0", "cache_read": "2.5", "output": "250.0"},
29
+ ),
30
+ "codex-auto-review": (
31
+ "codex-auto-review-2026-11-terra-equivalent",
32
+ {"input_nocache": "1.00", "cache_read": "0.05", "output": "5.00"},
33
+ {"input_nocache": "25.0", "cache_read": "1.25", "output": "125.0"},
34
+ ),
35
+ }
36
+
37
+
38
+ def dt(s):
39
+ return datetime.fromisoformat(s)
40
+
41
+
42
+ def test_gpt_6_1_models_resolve_with_pricing_page_values_from_effective_from():
43
+ catalog = load_rates()
44
+ for model, (rate_id, usd, _credits) in EXPECTED.items():
45
+ for occurred_at in ("2026-10-01T00:00:00+00:00", "2026-11-04T07:49:42+00:00", "2027-01-01T00:00:00+00:00"):
46
+ resolved, period = catalog.rate_for(model, dt(occurred_at))
47
+ assert resolved == model
48
+ assert period is not None, (model, occurred_at)
49
+ assert period.rate_id == rate_id
50
+ assert period.effective_until is None
51
+ for kind, value in usd.items():
52
+ assert period.values[kind] == Decimal(value), (model, kind)
53
+ assert period.values["cache_write_5m"] is None
54
+ assert period.values["cache_write_1h"] is None
55
+ assert period.prompt_tiers == ()
56
+
57
+
58
+ def test_gpt_6_1_models_are_unpriced_before_effective_from():
59
+ catalog = load_rates()
60
+ for model in EXPECTED:
61
+ resolved, period = catalog.rate_for(model, dt("2026-09-30T23:59:59+00:00"))
62
+ assert resolved == model
63
+ assert period is None
64
+
65
+
66
+ def test_gpt_6_1_models_credits_follow_usd_per_credit_and_entry_shape():
67
+ catalog = load_rates()
68
+ usd_per_credit = Decimal(catalog.usd_per_credit)
69
+ for model, (_rate_id, usd, credits) in EXPECTED.items():
70
+ entry = catalog.models[model]
71
+ assert entry.aliases == ()
72
+ assert entry.fast_multiplier == Decimal("1.0")
73
+ assert entry.credits_per_mtok["cache_write_5m"] is None
74
+ assert entry.credits_per_mtok["cache_write_1h"] is None
75
+ for kind, value in credits.items():
76
+ assert entry.credits_per_mtok[kind] == Decimal(value), (model, kind)
77
+ assert entry.credits_per_mtok[kind] * usd_per_credit == Decimal(usd[kind])
78
+
79
+
80
+ def test_codex_auto_review_is_priced_like_gpt_6_1_terra_but_is_its_own_entry():
81
+ catalog = load_rates()
82
+ at = dt("2026-11-06T00:00:00+00:00")
83
+ _, terra = catalog.rate_for("gpt-6.1-terra", at)
84
+ _, review = catalog.rate_for("codex-auto-review", at)
85
+ assert review.values == terra.values
86
+ assert review.rate_id != terra.rate_id
87
+ assert "codex-auto-review" not in catalog.models["gpt-6.1-terra"].aliases
88
+
89
+
90
+ def test_unknown_gpt_6_1_luna_stays_unpriced():
91
+ catalog = load_rates()
92
+ assert "gpt-6.1-luna" not in catalog.models
93
+ resolved, period = catalog.rate_for("gpt-6.1-luna", dt("2026-11-06T00:00:00+00:00"))
94
+ assert period is None
95
+
96
+
97
+ def test_existing_gpt_5_6_terra_values_are_unchanged():
98
+ catalog = load_rates()
99
+ entry = catalog.models["gpt-5.6-terra"]
100
+ assert len(entry.rates) == 1
101
+ _, period = catalog.rate_for("gpt-5.6-terra", dt("2026-11-06T00:00:00+00:00"))
102
+ assert period.rate_id == "gpt-5.6-terra-2026-07-secondary-sourced"
103
+ assert period.values["input_nocache"] == Decimal("2.50")
104
+ assert period.values["cache_read"] == Decimal("0.25")
105
+ assert period.values["output"] == Decimal("15.0")
106
+ assert entry.credits_per_mtok["input_nocache"] == Decimal("62.5")
107
+ assert entry.credits_per_mtok["cache_read"] == Decimal("6.25")
108
+ assert entry.credits_per_mtok["output"] == Decimal("375.0")
109
+
110
+
111
+ def test_catalog_version_and_source_record_the_gpt_6_1_pricing_page():
112
+ catalog = load_rates()
113
+ assert catalog.catalog_version >= "2026-11-09"
114
+ urls = [(s.get("url"), s.get("retrieved_at")) for s in catalog.sources]
115
+ assert urls.count(("https://developers.openai.com/api/docs/pricing", "2026-11-05")) == 1
116
+ raw = json.loads((Path(agent_cost.__file__).parent / "rates.json").read_text(encoding="utf-8"))
117
+ assert any("codex-auto-review" in n and "2026-11-06" in n for n in raw["notes"])
@@ -79,7 +79,7 @@ def test_claude_haiku_5_5_cache_multipliers_are_standard_in_every_tier_and_no_fa
79
79
 
80
80
  def test_catalog_version_schema_version_and_sources_record_the_haiku_5_5_primary_sources():
81
81
  catalog = load_rates()
82
- assert catalog.catalog_version == "2026-10-09"
82
+ assert catalog.catalog_version == "2026-11-10"
83
83
  assert catalog.schema_version == "2"
84
84
  urls = {(s.get("url"), s.get("retrieved_at")) for s in catalog.sources}
85
85
  assert ("https://platform.claude.com/docs/en/about-claude/pricing", "2026-10-09") in urls
@@ -514,8 +514,10 @@ def test_packaged_catalog_only_claude_haiku_5_5_has_prompt_tiers():
514
514
 
515
515
 
516
516
  # sha256 of the packaged catalog's models other than claude-haiku-5-5, as
517
- # json.dumps(sort_keys=True, separators=(",", ":")).
518
- _EXISTING_MODELS_SHA256 = "f3bfe615ec748aaa6d443083a6fe87b87c3cfd8fac68b4dfc3e1f1608c2e90d9"
517
+ # json.dumps(sort_keys=True, separators=(",", ":")). Updated deliberately in
518
+ # 0.5.2: claude-sonnet-5-5's launch period gained effective_until 2026-10-07
519
+ # and a cache_read-cut period was appended (was f3bfe615...2e90d9).
520
+ _EXISTING_MODELS_SHA256 = "1f326f1446e013fef49070e22113f78875fb3f8e7e8a65bd7ed4c5ca819e61fa"
519
521
 
520
522
 
521
523
  def test_packaged_catalog_existing_models_are_unchanged():
@@ -525,6 +527,7 @@ def test_packaged_catalog_existing_models_are_unchanged():
525
527
  rows are never changed in place; a price change adds a new period.
526
528
  """
527
529
  raw = json.loads(resources.files("agent_cost").joinpath("rates.json").read_bytes())
528
- existing = [m for m in raw["models"] if m["model_key"] != "claude-haiku-5-5"]
530
+ added_after = {"claude-haiku-5-5", "gpt-6.1-terra", "gpt-6.1-sol", "codex-auto-review", "claude-fable-5-2", "claude-mythos-5-2"}
531
+ existing = [m for m in raw["models"] if m["model_key"] not in added_after]
529
532
  digest = hashlib.sha256(json.dumps(existing, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
530
533
  assert digest == _EXISTING_MODELS_SHA256
@@ -0,0 +1,120 @@
1
+ """Acceptance tests for the claude-sonnet-5-5 rate entry (catalog_version 2026-10-13).
2
+
3
+ Primary sources: platform.claude.com/docs/en/about-claude/pricing (Sonnet 5.5
4
+ row; retrieved 2026-09-29 with no cache_read footnote, retrieved 2026-10-13 at
5
+ $0.10 cache_read with footnote 2 = 0.05x), anthropic.com/claude-sonnet-5-5
6
+ (launch 2026-09-28), anthropic.com/claude-haiku-5-5 (cache_read cut 'starting
7
+ today', 2026-10-07), Claude Code CHANGELOG 2.1.284 (Sonnet 5.5 is the default
8
+ Sonnet) and 2.1.296 (Sonnet 5.5 cache reads priced at $0.10).
9
+ """
10
+
11
+ from datetime import datetime, timezone
12
+ from decimal import Decimal
13
+
14
+ from agent_cost.rates import load_rates
15
+
16
+ UTC = timezone.utc
17
+
18
+
19
+ def dt(s):
20
+ return datetime.fromisoformat(s).replace(tzinfo=UTC)
21
+
22
+
23
+ LAUNCH = "claude-sonnet-5-5-launch-2026-09-28"
24
+ CACHE_READ_CUT = "claude-sonnet-5-5-cache-read-cut-2026-10-07"
25
+
26
+
27
+ def _assert_unchanged_values(period):
28
+ assert period.values["input_nocache"] == Decimal("2.0")
29
+ assert period.values["cache_write_5m"] == Decimal("2.50")
30
+ assert period.values["cache_write_1h"] == Decimal("4.0")
31
+ assert period.values["output"] == Decimal("10.0")
32
+
33
+
34
+ def test_claude_sonnet_5_5_resolves_to_launch_period_before_cache_read_cut():
35
+ catalog = load_rates()
36
+ for occurred_at in ("2026-09-28T00:00:00+00:00", "2026-10-06T23:59:59+00:00"):
37
+ resolved, period = catalog.rate_for("claude-sonnet-5-5", dt(occurred_at))
38
+ assert resolved == "claude-sonnet-5-5"
39
+ assert period is not None
40
+ assert period.rate_id == LAUNCH
41
+ assert period.values["cache_read"] == Decimal("0.20")
42
+ _assert_unchanged_values(period)
43
+
44
+
45
+ def test_claude_sonnet_5_5_resolves_to_cache_read_cut_period_from_2026_10_07():
46
+ catalog = load_rates()
47
+ for occurred_at in ("2026-10-07T00:00:00+00:00", "2026-12-01T00:00:00+00:00"):
48
+ resolved, period = catalog.rate_for("claude-sonnet-5-5", dt(occurred_at))
49
+ assert resolved == "claude-sonnet-5-5"
50
+ assert period is not None
51
+ assert period.rate_id == CACHE_READ_CUT
52
+ assert period.values["cache_read"] == Decimal("0.10")
53
+ _assert_unchanged_values(period)
54
+
55
+
56
+ def test_claude_sonnet_5_5_cache_read_cut_boundary():
57
+ catalog = load_rates()
58
+ _, before = catalog.rate_for("claude-sonnet-5-5", dt("2026-10-06T23:59:59+00:00"))
59
+ _, at = catalog.rate_for("claude-sonnet-5-5", dt("2026-10-07T00:00:00+00:00"))
60
+ assert before.rate_id == LAUNCH
61
+ assert before.values["cache_read"] == Decimal("0.20")
62
+ assert at.rate_id == CACHE_READ_CUT
63
+ assert at.values["cache_read"] == Decimal("0.10")
64
+
65
+
66
+ def test_claude_sonnet_5_5_is_unpriced_before_launch_date():
67
+ catalog = load_rates()
68
+ resolved, before = catalog.rate_for("claude-sonnet-5-5", dt("2026-09-27T23:59:59+00:00"))
69
+ assert resolved == "claude-sonnet-5-5"
70
+ assert before is None
71
+
72
+
73
+ def test_claude_sonnet_5_5_is_its_own_entry_not_an_alias_of_sonnet_5():
74
+ # Same values as claude-sonnet-5 today, but the pricing page lists the rows
75
+ # independently; a future change to one must not silently apply to the other.
76
+ catalog = load_rates()
77
+ assert "claude-sonnet-5-5" in catalog.models
78
+ assert catalog.models["claude-sonnet-5-5"].aliases == ()
79
+ assert "claude-sonnet-5-5" not in catalog.models["claude-sonnet-5"].aliases
80
+ _, s5 = catalog.rate_for("claude-sonnet-5", dt("2026-09-28T00:00:00+00:00"))
81
+ _, s55 = catalog.rate_for("claude-sonnet-5-5", dt("2026-09-28T00:00:00+00:00"))
82
+ assert s5.rate_id != s55.rate_id
83
+ assert s5.values == s55.values
84
+
85
+
86
+ def test_claude_sonnet_5_cache_read_is_not_cut_with_sonnet_5_5():
87
+ # The 2026-10-07 cut applies to Sonnet 5.5 only (pricing-page footnote 2
88
+ # names Opus 5.5 and Sonnet 5.5); claude-sonnet-5 keeps cache_read 0.20.
89
+ catalog = load_rates()
90
+ for occurred_at in ("2026-10-07T00:00:00+00:00", "2026-12-01T00:00:00+00:00"):
91
+ _, s5 = catalog.rate_for("claude-sonnet-5", dt(occurred_at))
92
+ _, s55 = catalog.rate_for("claude-sonnet-5-5", dt(occurred_at))
93
+ assert s5.values["cache_read"] == Decimal("0.20")
94
+ assert s55.values["cache_read"] == Decimal("0.10")
95
+ assert s5.values != s55.values
96
+
97
+
98
+ def test_claude_sonnet_5_5_cache_read_multiplier_per_period_and_no_fast_mode():
99
+ catalog = load_rates()
100
+ entry = catalog.models["claude-sonnet-5-5"]
101
+ assert entry.fast_multiplier == Decimal("1.0")
102
+ periods = {p.rate_id: p for p in entry.rates}
103
+ assert set(periods) == {LAUNCH, CACHE_READ_CUT}
104
+ launch = periods[LAUNCH]
105
+ cut = periods[CACHE_READ_CUT]
106
+ assert launch.values["cache_read"] == launch.values["input_nocache"] * Decimal("0.1")
107
+ assert cut.values["cache_read"] == cut.values["input_nocache"] * Decimal("0.05")
108
+ for period in (launch, cut):
109
+ assert period.values["cache_write_5m"] == period.values["input_nocache"] * Decimal("1.25")
110
+ assert period.values["cache_write_1h"] == period.values["input_nocache"] * Decimal("2")
111
+
112
+
113
+ def test_catalog_version_and_sources_record_the_sonnet_5_5_primary_sources():
114
+ catalog = load_rates()
115
+ assert catalog.catalog_version == "2026-11-10"
116
+ urls = {(s.get("url"), s.get("retrieved_at")) for s in catalog.sources}
117
+ assert ("https://platform.claude.com/docs/en/about-claude/pricing", "2026-09-29") in urls
118
+ assert ("https://www.anthropic.com/claude-sonnet-5-5", "2026-09-29") in urls
119
+ assert ("https://platform.claude.com/docs/en/about-claude/pricing", "2026-10-13") in urls
120
+ assert ("https://www.anthropic.com/claude-haiku-5-5", "2026-10-13") in urls
@@ -1,8 +1,8 @@
1
1
  """Spec: pyproject.toml's [project].version and agent_cost.__version__ must
2
- agree, and both must read "0.5.1" for this change (``--since`` /
3
- ``--until`` values whose UTC normalization overflows are rejected with the
4
- usual ``[error] invalid date/time`` message instead of a traceback -- a bug
5
- fix, so it bumps the patch version). A broken
2
+ agree, and both must read "0.5.3" for this change (the packaged catalog adds
3
+ claude-fable-5-2, claude-mythos-5-2, gpt-6.1-terra, gpt-6.1-sol and
4
+ codex-auto-review -- catalog entries only, with no code or schema change, so
5
+ it bumps the patch version). A broken
6
6
  implementation would bump one file but not the other, or forget the bump
7
7
  entirely.
8
8
 
@@ -30,6 +30,6 @@ def test_pyproject_version_matches_package_version():
30
30
  assert _pyproject_version() == agent_cost.__version__
31
31
 
32
32
 
33
- def test_version_is_0_5_1():
34
- assert agent_cost.__version__ == "0.5.1"
35
- assert _pyproject_version() == "0.5.1"
33
+ def test_version_is_0_5_2():
34
+ assert agent_cost.__version__ == "0.5.3"
35
+ assert _pyproject_version() == "0.5.3"
@@ -1,69 +0,0 @@
1
- """Acceptance tests for the claude-sonnet-5-5 rate entry (catalog_version 2026-09-29).
2
-
3
- Primary sources: platform.claude.com/docs/en/about-claude/pricing (Sonnet 5.5
4
- row, no cache_read footnote), anthropic.com/claude-sonnet-5-5 (launch
5
- 2026-09-28), Claude Code CHANGELOG 2.1.284 (Sonnet 5.5 is the default Sonnet).
6
- """
7
-
8
- from datetime import datetime, timezone
9
- from decimal import Decimal
10
-
11
- from agent_cost.rates import load_rates
12
-
13
- UTC = timezone.utc
14
-
15
-
16
- def dt(s):
17
- return datetime.fromisoformat(s).replace(tzinfo=UTC)
18
-
19
-
20
- def test_claude_sonnet_5_5_resolves_with_pricing_page_values_on_or_after_launch():
21
- catalog = load_rates()
22
- for occurred_at in ("2026-09-28T00:00:00+00:00", "2026-12-01T00:00:00+00:00"):
23
- resolved, period = catalog.rate_for("claude-sonnet-5-5", dt(occurred_at))
24
- assert resolved == "claude-sonnet-5-5"
25
- assert period is not None
26
- assert period.rate_id == "claude-sonnet-5-5-launch-2026-09-28"
27
- assert period.values["input_nocache"] == Decimal("2.0")
28
- assert period.values["cache_read"] == Decimal("0.20")
29
- assert period.values["cache_write_5m"] == Decimal("2.50")
30
- assert period.values["cache_write_1h"] == Decimal("4.0")
31
- assert period.values["output"] == Decimal("10.0")
32
-
33
-
34
- def test_claude_sonnet_5_5_is_unpriced_before_launch_date():
35
- catalog = load_rates()
36
- resolved, before = catalog.rate_for("claude-sonnet-5-5", dt("2026-09-27T23:59:59+00:00"))
37
- assert resolved == "claude-sonnet-5-5"
38
- assert before is None
39
-
40
-
41
- def test_claude_sonnet_5_5_is_its_own_entry_not_an_alias_of_sonnet_5():
42
- # Same values as claude-sonnet-5 today, but the pricing page lists the rows
43
- # independently; a future change to one must not silently apply to the other.
44
- catalog = load_rates()
45
- assert "claude-sonnet-5-5" in catalog.models
46
- assert catalog.models["claude-sonnet-5-5"].aliases == ()
47
- assert "claude-sonnet-5-5" not in catalog.models["claude-sonnet-5"].aliases
48
- _, s5 = catalog.rate_for("claude-sonnet-5", dt("2026-09-28T00:00:00+00:00"))
49
- _, s55 = catalog.rate_for("claude-sonnet-5-5", dt("2026-09-28T00:00:00+00:00"))
50
- assert s5.rate_id != s55.rate_id
51
- assert s5.values == s55.values
52
-
53
-
54
- def test_claude_sonnet_5_5_cache_read_is_standard_multiplier_and_no_fast_mode():
55
- catalog = load_rates()
56
- entry = catalog.models["claude-sonnet-5-5"]
57
- assert entry.fast_multiplier == Decimal("1.0")
58
- period = entry.rates[0]
59
- assert period.values["cache_read"] == period.values["input_nocache"] * Decimal("0.1")
60
- assert period.values["cache_write_5m"] == period.values["input_nocache"] * Decimal("1.25")
61
- assert period.values["cache_write_1h"] == period.values["input_nocache"] * Decimal("2")
62
-
63
-
64
- def test_catalog_version_and_sources_record_the_sonnet_5_5_primary_sources():
65
- catalog = load_rates()
66
- assert catalog.catalog_version == "2026-10-09"
67
- urls = {(s.get("url"), s.get("retrieved_at")) for s in catalog.sources}
68
- assert ("https://platform.claude.com/docs/en/about-claude/pricing", "2026-09-29") in urls
69
- assert ("https://www.anthropic.com/claude-sonnet-5-5", "2026-09-29") in urls