coding-agent-cost 0.5.0__tar.gz → 0.5.2__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 (40) hide show
  1. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/PKG-INFO +1 -1
  2. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/__init__.py +1 -1
  3. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/cli.py +9 -4
  4. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/rates.json +24 -3
  5. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/coding_agent_cost.egg-info/PKG-INFO +1 -1
  6. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/pyproject.toml +1 -1
  7. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_astra_pricing.py +2 -2
  8. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_cli.py +52 -1
  9. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_e0a_review3_hardening.py +3 -3
  10. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_haiku_5_5_rates.py +1 -1
  11. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_rates.py +4 -2
  12. coding_agent_cost-0.5.2/tests/test_sonnet_5_5_rates.py +120 -0
  13. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_version_consistency.py +7 -6
  14. coding_agent_cost-0.5.0/tests/test_sonnet_5_5_rates.py +0 -69
  15. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/LICENSE +0 -0
  16. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/README.md +0 -0
  17. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/aggregate.py +0 -0
  18. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/billing_plan.py +0 -0
  19. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/config.py +0 -0
  20. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/facts.py +0 -0
  21. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/rates.py +0 -0
  22. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/readers/__init__.py +0 -0
  23. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/readers/claude.py +0 -0
  24. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/readers/codex.py +0 -0
  25. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/agent_cost/renderers.py +0 -0
  26. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/coding_agent_cost.egg-info/SOURCES.txt +0 -0
  27. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/coding_agent_cost.egg-info/dependency_links.txt +0 -0
  28. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/coding_agent_cost.egg-info/entry_points.txt +0 -0
  29. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/coding_agent_cost.egg-info/top_level.txt +0 -0
  30. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/setup.cfg +0 -0
  31. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_aggregate.py +0 -0
  32. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_billing_plan.py +0 -0
  33. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_e0a_fable_5_1_and_sonnet_5_correction.py +0 -0
  34. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_facts.py +0 -0
  35. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_measure_v1_contract.py +0 -0
  36. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_opus_5_5_rates.py +0 -0
  37. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_reader_claude.py +0 -0
  38. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_reader_codex.py +0 -0
  39. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/tests/test_reader_dedup.py +0 -0
  40. {coding_agent_cost-0.5.0 → coding_agent_cost-0.5.2}/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.0
3
+ Version: 0.5.2
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
@@ -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.0"
9
+ __version__ = "0.5.2"
@@ -50,10 +50,15 @@ def _parse_window_bound(value: Optional[str], tz: ZoneInfo) -> Optional[datetime
50
50
  dt = datetime.fromisoformat(text)
51
51
  except ValueError as exc:
52
52
  raise SystemExit(f"[error] invalid date/time: {value!r} ({exc})")
53
- if dt.tzinfo is not None:
54
- return dt.astimezone(timezone.utc)
55
- # Date-only (or naive datetime) input is interpreted in --timezone.
56
- return dt.replace(tzinfo=tz).astimezone(timezone.utc)
53
+ # Normalizing to UTC can overflow datetime's year range near
54
+ # 0001-01-01 / 9999-12-31 (e.g. "0001-01-01T00:00:00+23:59").
55
+ try:
56
+ if dt.tzinfo is not None:
57
+ return dt.astimezone(timezone.utc)
58
+ # Date-only (or naive datetime) input is interpreted in --timezone.
59
+ return dt.replace(tzinfo=tz).astimezone(timezone.utc)
60
+ except OverflowError:
61
+ raise SystemExit(f"[error] invalid date/time: {value!r} (out of range)") from None
57
62
 
58
63
 
59
64
  def _now_utc() -> datetime:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema_version": "2",
3
- "catalog_version": "2026-10-09",
3
+ "catalog_version": "2026-10-13",
4
4
  "currency": "USD",
5
5
  "unit": "per_mtok",
6
6
  "usd_per_credit": "0.04",
@@ -13,7 +13,8 @@
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."
17
18
  ],
18
19
  "sources": [
19
20
  {
@@ -113,6 +114,16 @@
113
114
  "url": "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md",
114
115
  "retrieved_at": "2026-10-09",
115
116
  "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"
117
+ },
118
+ {
119
+ "url": "https://platform.claude.com/docs/en/about-claude/pricing",
120
+ "retrieved_at": "2026-10-13",
121
+ "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)"
122
+ },
123
+ {
124
+ "url": "https://www.anthropic.com/claude-haiku-5-5",
125
+ "retrieved_at": "2026-10-13",
126
+ "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"
116
127
  }
117
128
  ],
118
129
  "models": [
@@ -304,12 +315,22 @@
304
315
  {
305
316
  "rate_id": "claude-sonnet-5-5-launch-2026-09-28",
306
317
  "effective_from": "2026-09-28T00:00:00+00:00",
307
- "effective_until": null,
318
+ "effective_until": "2026-10-07T00:00:00+00:00",
308
319
  "input_nocache": "2.0",
309
320
  "cache_read": "0.20",
310
321
  "cache_write_5m": "2.50",
311
322
  "cache_write_1h": "4.0",
312
323
  "output": "10.0"
324
+ },
325
+ {
326
+ "rate_id": "claude-sonnet-5-5-cache-read-cut-2026-10-07",
327
+ "effective_from": "2026-10-07T00:00:00+00:00",
328
+ "effective_until": null,
329
+ "input_nocache": "2.0",
330
+ "cache_read": "0.10",
331
+ "cache_write_5m": "2.50",
332
+ "cache_write_1h": "4.0",
333
+ "output": "10.0"
313
334
  }
314
335
  ]
315
336
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-agent-cost
3
- Version: 0.5.0
3
+ Version: 0.5.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coding-agent-cost"
7
- version = "0.5.0"
7
+ version = "0.5.2"
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" }
@@ -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-10-13"
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-10-13"
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
@@ -4,6 +4,8 @@ import json
4
4
  import os
5
5
  import re
6
6
  import sqlite3
7
+ import subprocess
8
+ import sys
7
9
  from datetime import datetime, timezone
8
10
  from pathlib import Path
9
11
 
@@ -259,6 +261,55 @@ def test_invalid_since_still_rejected_after_z_suffix_fix(tmp_path, monkeypatch):
259
261
  assert raised # unchanged pre-existing behavior for report/export
260
262
 
261
263
 
264
+ def _run_cli_process(tmp_path, *argv):
265
+ """Run the CLI in a real interpreter so the process exit code, stdout and
266
+ stderr (including any traceback) are observed exactly as a user sees them."""
267
+ claude_home = tmp_path / "claude_home"
268
+ codex_home = tmp_path / "codex_home"
269
+ claude_home.mkdir(exist_ok=True)
270
+ codex_home.mkdir(exist_ok=True)
271
+ env = dict(os.environ)
272
+ env["CLAUDE_HOME"] = str(claude_home)
273
+ env["CODEX_HOME"] = str(codex_home)
274
+ env.pop("AGENT_COST_CONFIG", None)
275
+ repo_root = Path(__file__).resolve().parent.parent
276
+ env["PYTHONPATH"] = os.pathsep.join(filter(None, [str(repo_root), env.get("PYTHONPATH")]))
277
+ return subprocess.run(
278
+ [sys.executable, "-c", "import sys; from agent_cost import cli; sys.exit(cli.main(sys.argv[1:]))", *argv],
279
+ cwd=str(repo_root),
280
+ env=env,
281
+ stdin=subprocess.DEVNULL,
282
+ capture_output=True,
283
+ text=True,
284
+ timeout=60,
285
+ )
286
+
287
+
288
+ @pytest.mark.parametrize(
289
+ "argv, bad_value",
290
+ [
291
+ (["--since", "0001-01-01T00:00:00+23:59"], "0001-01-01T00:00:00+23:59"),
292
+ # Date-only input in a zone whose year-1 offset is positive (Asia/Tokyo
293
+ # LMT +09:18:59). Pacific/Kiritimati does not overflow here: its year-1
294
+ # offset is LMT -10:29:20.
295
+ (["--since", "0001-01-01", "--timezone", "Asia/Tokyo"], "0001-01-01"),
296
+ (["--until", "9999-12-31T23:59:59-12:00"], "9999-12-31T23:59:59-12:00"),
297
+ ],
298
+ )
299
+ def test_report_window_bound_utc_overflow_is_invalid_date_error(tmp_path, argv, bad_value):
300
+ baseline = _run_cli_process(tmp_path, "report", "--since", "not-a-date")
301
+ assert baseline.returncode != 0
302
+ assert "invalid date/time" in baseline.stderr
303
+
304
+ result = _run_cli_process(tmp_path, "report", *argv)
305
+ assert result.returncode == baseline.returncode
306
+ assert result.stdout == ""
307
+ assert "invalid date/time" in result.stderr
308
+ assert repr(bad_value) in result.stderr
309
+ assert "Traceback" not in result.stderr
310
+ assert "OverflowError" not in result.stderr
311
+
312
+
262
313
  def test_report_cli_includes_codex_thread_created_before_window(tmp_path, monkeypatch, capsys):
263
314
  # Regression (real report/export path, not just the reader unit test):
264
315
  # a Codex thread created well before the window must still contribute
@@ -835,7 +886,7 @@ def test_rates_show_model_without_tiers_prints_no_prompt_tiers(capsys):
835
886
  def test_rates_validate_packaged_catalog_with_prompt_tiers_exits_zero(capsys):
836
887
  rc = cli.main(["rates", "validate"])
837
888
  assert rc == 0
838
- assert "catalog_version=2026-10-09" in capsys.readouterr().out
889
+ assert "catalog_version=2026-10-13" in capsys.readouterr().out
839
890
 
840
891
 
841
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-10-13"(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-10-13"(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-10-13"
237
237
 
238
238
  matches = [
239
239
  s
@@ -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-10-13"
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():
@@ -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-10-13"
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,7 +1,8 @@
1
1
  """Spec: pyproject.toml's [project].version and agent_cost.__version__ must
2
- agree, and both must read "0.5.0" for this change (``report --billing-plan
3
- PATH`` adds an opt-in internal_billing block; output without the flag is
4
- unchanged -- an additive change, so it bumps the minor version). A broken
2
+ agree, and both must read "0.5.2" for this change (the packaged catalog
3
+ lowers claude-sonnet-5-5 cache_read to $0.10 from 2026-10-07 -- a price
4
+ correction with no code or schema change, so it bumps the patch version). A
5
+ broken
5
6
  implementation would bump one file but not the other, or forget the bump
6
7
  entirely.
7
8
 
@@ -29,6 +30,6 @@ def test_pyproject_version_matches_package_version():
29
30
  assert _pyproject_version() == agent_cost.__version__
30
31
 
31
32
 
32
- def test_version_is_0_5_0():
33
- assert agent_cost.__version__ == "0.5.0"
34
- assert _pyproject_version() == "0.5.0"
33
+ def test_version_is_0_5_2():
34
+ assert agent_cost.__version__ == "0.5.2"
35
+ assert _pyproject_version() == "0.5.2"
@@ -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