diff --git a/headroom/providers/anthropic.py b/headroom/providers/anthropic.py index 93d3d1bd6..20efd0fda 100644 --- a/headroom/providers/anthropic.py +++ b/headroom/providers/anthropic.py @@ -83,12 +83,18 @@ def sanitize_anthropic_model_metadata(value: Any) -> Any: # Anthropic model context limits # All Claude 3+ models have 200K context ANTHROPIC_CONTEXT_LIMITS: dict[str, int] = { + # Claude 4.8 (Opus 4.8) - 1M context + "claude-opus-4-8": 1000000, # Claude 4.7 (Opus 4.7) - 1M context "claude-opus-4-7": 1000000, # Claude 4.6 (Opus 4.6) - 1M context "claude-opus-4-6": 1000000, # Claude 4.5 (Opus 4.5) "claude-opus-4-5-20251101": 200000, + # Claude Sonnet 4.6 — 1M context window (long-context pricing tier) + "claude-sonnet-4-6": 1000000, + # Claude Sonnet 4.5 + "claude-sonnet-4-5": 200000, # Claude 4 (Sonnet 4, Haiku 4) "claude-sonnet-4-20250514": 200000, "claude-haiku-4-5-20251001": 200000, @@ -110,17 +116,23 @@ def sanitize_anthropic_model_metadata(value: Any) -> Any: # Fallback pricing - LiteLLM is preferred source # NOTE: These are ESTIMATES. Always verify against actual Anthropic billing. -# Last updated: 2025-01-14 +# Last updated: 2026-06-27 ANTHROPIC_PRICING: dict[str, dict[str, float]] = { - # Claude 4.7 (Opus tier pricing) - "claude-opus-4-7": {"input": 15.00, "output": 75.00, "cached_input": 1.50}, - # Claude 4.6 (Opus tier pricing) - "claude-opus-4-6": {"input": 15.00, "output": 75.00, "cached_input": 1.50}, - # Claude 4.5 (Opus tier pricing) - "claude-opus-4-5-20251101": {"input": 15.00, "output": 75.00, "cached_input": 1.50}, + # Claude 4.8 — current Opus tier (anthropic.com/pricing): $5 in / $25 out, + # prompt-cache read = 0.1x input ($0.50). + "claude-opus-4-8": {"input": 5.00, "output": 25.00, "cached_input": 0.50}, + # Claude 4.7 (current Opus tier) + "claude-opus-4-7": {"input": 5.00, "output": 25.00, "cached_input": 0.50}, + # Claude 4.6 (current Opus tier) + "claude-opus-4-6": {"input": 5.00, "output": 25.00, "cached_input": 0.50}, + # Claude 4.5 (current Opus tier — same rates as 4.6–4.8) + "claude-opus-4-5-20251101": {"input": 5.00, "output": 25.00, "cached_input": 0.50}, + # Claude Sonnet 4.6 / 4.5 (current Sonnet tier): $3 in / $15 out, cache read $0.30 + "claude-sonnet-4-6": {"input": 3.00, "output": 15.00, "cached_input": 0.30}, + "claude-sonnet-4-5": {"input": 3.00, "output": 15.00, "cached_input": 0.30}, # Claude 4 (Sonnet/Haiku tier pricing) "claude-sonnet-4-20250514": {"input": 3.00, "output": 15.00, "cached_input": 0.30}, - "claude-haiku-4-5-20251001": {"input": 0.80, "output": 4.00, "cached_input": 0.08}, + "claude-haiku-4-5-20251001": {"input": 1.00, "output": 5.00, "cached_input": 0.10}, # Claude 3.5 "claude-3-5-sonnet-20241022": {"input": 3.00, "output": 15.00, "cached_input": 0.30}, "claude-3-5-sonnet-latest": {"input": 3.00, "output": 15.00, "cached_input": 0.30}, diff --git a/tests/test_providers/test_anthropic.py b/tests/test_providers/test_anthropic.py index cdf503627..2402048bc 100644 --- a/tests/test_providers/test_anthropic.py +++ b/tests/test_providers/test_anthropic.py @@ -77,6 +77,13 @@ def test_get_context_limit_claude_opus(self, anthropic_provider): def test_get_context_limit_strips_ansi_model_suffix(self, anthropic_provider): assert anthropic_provider.get_context_limit("claude-opus-4-7[1m]") == 1000000 + def test_get_context_limit_claude_opus_4_8(self, anthropic_provider): + assert anthropic_provider.get_context_limit("claude-opus-4-8") == 1000000 + + def test_get_context_limit_claude_sonnet_4_6(self, anthropic_provider): + # Sonnet 4.6 ships with the 1M context window (long-context pricing tier). + assert anthropic_provider.get_context_limit("claude-sonnet-4-6") == 1000000 + def test_supports_model_known(self, anthropic_provider): assert anthropic_provider.supports_model("claude-3-5-sonnet-20241022") @@ -110,3 +117,38 @@ def test_pricing_lookup_strips_ansi_model_suffix(self, anthropic_provider): assert anthropic_provider._get_pricing("claude-opus-4-7[1m]") == ( anthropic_provider._get_pricing("claude-opus-4-7") ) + + @pytest.mark.parametrize( + "model", + ["claude-opus-4-5-20251101", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8"], + ) + def test_current_opus_tier_pricing(self, anthropic_provider, model): + # Opus 4.5–4.8 share the same current-tier rates per anthropic.com/pricing + # (verified 2026-06-27): $5/MTok input, $25/MTok output, cache read = 0.1x input ($0.50). + assert anthropic_provider._get_pricing(model) == { + "input": 5.00, + "output": 25.00, + "cached_input": 0.50, + } + + @pytest.mark.parametrize( + "model", ["claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4-20250514"] + ) + def test_current_sonnet_tier_pricing(self, anthropic_provider, model): + # Sonnet 4 / 4.5 / 4.6 all sit at the current Sonnet tier per anthropic.com/pricing + # (verified 2026-06-27): $3/MTok input, $15/MTok output, cache read = 0.1x input ($0.30). + assert anthropic_provider._get_pricing(model) == { + "input": 3.00, + "output": 15.00, + "cached_input": 0.30, + } + + def test_current_haiku_tier_pricing(self, anthropic_provider): + # Haiku 4.5 (current Haiku tier, verified 2026-06-27): $1/MTok input, + # $5/MTok output, cache read = 0.1x input ($0.10). Was previously stored + # with Haiku 3.5 rates ($0.80/$4/$0.08). + assert anthropic_provider._get_pricing("claude-haiku-4-5-20251001") == { + "input": 1.00, + "output": 5.00, + "cached_input": 0.10, + }