diff --git a/calibre_toolkit/models.py b/calibre_toolkit/models.py index 605c4c1..484059e 100644 --- a/calibre_toolkit/models.py +++ b/calibre_toolkit/models.py @@ -35,7 +35,7 @@ # the latest model is a one-line edit when a new generation ships. _ALIAS_MAP: dict[str, str] = { "fast": "claude-haiku-4-5-20251001", - "latest": "claude-opus-4-7", + "latest": "claude-opus-4-8", "legacy": "claude-sonnet-4-6", } @@ -50,8 +50,12 @@ # (so anyone who pinned a specific ID for reproducibility keeps working) # but logs a one-time warning so the user knows it's time to switch. _DEPRECATED_MODELS: dict[str, str] = { - # Add entries here as Anthropic retires model IDs. Example shape: - # "claude-sonnet-3-5": "claude-sonnet-4-6", + # Per Anthropic's deprecation table as of 2026-06-10. + "claude-opus-4-1": "claude-opus-4-8", # retires 2026-08-05 + "claude-opus-4-1-20250805": "claude-opus-4-8", # retires 2026-08-05 + "claude-opus-4-20250514": "claude-opus-4-8", # retires 2026-06-15 + "claude-sonnet-4-20250514": "claude-sonnet-4-6", # retires 2026-06-15 + "claude-3-haiku-20240307": "claude-haiku-4-5", # retired 2026-04-19 } diff --git a/calibre_toolkit/usage.py b/calibre_toolkit/usage.py index 9da868c..20056f4 100644 --- a/calibre_toolkit/usage.py +++ b/calibre_toolkit/usage.py @@ -117,10 +117,11 @@ def _get(name: str) -> int: # ── Pricing ────────────────────────────────────────────────────────────────── # -# USD per million tokens. Anthropic's public list prices as of January 2026 -# for the Claude 4.x family. Sources: anthropic.com/pricing. Keyed by model -# family prefix so a new minor version (claude-sonnet-4-7, 4-8) inherits -# pricing until a deliberate update. +# USD per million tokens. Anthropic's public list prices, verified against +# platform.claude.com/docs/en/about-claude/pricing on 2026-06-10. Keyed by +# model family prefix, longest match wins — so the per-version Opus 4.5+ +# entries take precedence over the bare "claude-opus-4" fallback, which +# keeps the old $15/$75 rates for Opus 4.0/4.1-era IDs. # # When the model is unknown we return None and the summary omits the # dollar estimate — token counts are still shown so the user can plug @@ -134,7 +135,20 @@ class _PriceTable: cache_write_per_million: float +_OPUS_45_PLUS = _PriceTable( + input_per_million=5.00, + output_per_million=25.00, + cache_read_per_million=0.50, + cache_write_per_million=6.25, +) + _PRICES: dict[str, _PriceTable] = { + "claude-opus-4-8": _OPUS_45_PLUS, + "claude-opus-4-7": _OPUS_45_PLUS, + "claude-opus-4-6": _OPUS_45_PLUS, + "claude-opus-4-5": _OPUS_45_PLUS, + # Opus 4.0/4.1 (deprecated) — dated IDs like claude-opus-4-20250514 + # fall through to this prefix. "claude-opus-4": _PriceTable( input_per_million=15.00, output_per_million=75.00, @@ -148,12 +162,13 @@ class _PriceTable: cache_write_per_million=3.75, ), "claude-haiku-4": _PriceTable( - input_per_million=0.80, - output_per_million=4.00, - cache_read_per_million=0.08, - cache_write_per_million=1.00, + input_per_million=1.00, + output_per_million=5.00, + cache_read_per_million=0.10, + cache_write_per_million=1.25, ), - # 3.x legacy support — older but still callable. + # 3.x legacy — retired models, kept so replaying old usage logs + # still produces a dollar estimate. "claude-3-5-sonnet": _PriceTable( input_per_million=3.00, output_per_million=15.00, diff --git a/tests/test_usage.py b/tests/test_usage.py index f47764d..c569fc7 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -138,14 +138,19 @@ def test_resolves_sonnet_4_6_prefix(self): assert table.output_per_million == 15.00 def test_resolves_opus_prefix(self): - table = _resolve_price_table("claude-opus-4-7") + # Opus 4.5+ bills at $5/MTok; the bare claude-opus-4 prefix keeps + # the old $15 rate for 4.0/4.1-era IDs. + table = _resolve_price_table("claude-opus-4-8") assert table is not None - assert table.input_per_million == 15.00 + assert table.input_per_million == 5.00 + legacy = _resolve_price_table("claude-opus-4-20250514") + assert legacy is not None + assert legacy.input_per_million == 15.00 def test_resolves_haiku_prefix(self): table = _resolve_price_table("claude-haiku-4-5-20251001") assert table is not None - assert table.input_per_million == 0.80 + assert table.input_per_million == 1.00 def test_unknown_model_returns_none(self): assert _resolve_price_table("gpt-4") is None @@ -205,9 +210,9 @@ def test_cost_estimate_combines_per_model(self): agg = UsageAggregate() # 1M Sonnet input → $3.00 agg.record(TokenUsage(input_tokens=1_000_000), "claude-sonnet-4-6") - # 1M Haiku input → $0.80 + # 1M Haiku input → $1.00 agg.record(TokenUsage(input_tokens=1_000_000), "claude-haiku-4-5") - assert agg.cost_estimate_usd() == pytest.approx(3.80) + assert agg.cost_estimate_usd() == pytest.approx(4.00) def test_cost_estimate_none_when_every_model_unpriced(self): agg = UsageAggregate()