diff --git a/headroom/providers/anthropic.py b/headroom/providers/anthropic.py index e1ff0126c..56a8d0790 100644 --- a/headroom/providers/anthropic.py +++ b/headroom/providers/anthropic.py @@ -83,12 +83,22 @@ 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 Fable 5 - 1M context + "claude-fable-5": 1000000, + # Claude 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 5 - 1M context + "claude-sonnet-5": 1000000, + # Claude Sonnet 4.6 - 1M context window + "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 +120,25 @@ 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-07-04 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 Fable 5 (anthropic.com/pricing): $10 in / $50 out, cache read $1. + "claude-fable-5": {"input": 10.00, "output": 50.00, "cached_input": 1.00}, + # Claude Opus 4.8 — current Opus tier: $5 in / $25 out, cache read $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 5 / 4.6 / 4.5 (current Sonnet tier): $3 in / $15 out, cache read $0.30 + "claude-sonnet-5": {"input": 3.00, "output": 15.00, "cached_input": 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}, @@ -136,7 +154,7 @@ def sanitize_anthropic_model_metadata(value: Any) -> Any: # Default limits for pattern-based inference # Used when a model isn't in the explicit list but matches a known pattern _PATTERN_DEFAULTS = { - "opus": {"context": 200000, "pricing": {"input": 15.00, "output": 75.00, "cached_input": 1.50}}, + "opus": {"context": 200000, "pricing": {"input": 5.00, "output": 25.00, "cached_input": 0.50}}, "sonnet": { "context": 200000, "pricing": {"input": 3.00, "output": 15.00, "cached_input": 0.30}, diff --git a/tests/test_provider_model_fallback.py b/tests/test_provider_model_fallback.py index 5911e12ee..9de7bc1e1 100644 --- a/tests/test_provider_model_fallback.py +++ b/tests/test_provider_model_fallback.py @@ -94,17 +94,17 @@ def test_pattern_based_inference_opus(self): assert limit == 200000 pricing = provider._get_pricing("claude-opus-5-20260101") - assert pricing["input"] == 15.00 - assert pricing["output"] == 75.00 + assert pricing["input"] == 5.00 + assert pricing["output"] == 25.00 def test_pattern_based_inference_sonnet(self): """Test pattern-based inference for sonnet models.""" provider = AnthropicProvider() - limit = provider.get_context_limit("claude-sonnet-5-20260101") + limit = provider.get_context_limit("claude-sonnet-6-20260101") assert limit == 200000 - pricing = provider._get_pricing("claude-sonnet-5-20260101") + pricing = provider._get_pricing("claude-sonnet-6-20260101") assert pricing["input"] == 3.00 assert pricing["output"] == 15.00 @@ -159,9 +159,9 @@ def test_pricing_for_known_models(self): # Claude Opus 4.5 pricing = provider._get_pricing("claude-opus-4-5-20251101") - assert pricing["input"] == 15.00 - assert pricing["output"] == 75.00 - assert pricing["cached_input"] == 1.50 + assert pricing["input"] == 5.00 + assert pricing["output"] == 25.00 + assert pricing["cached_input"] == 0.50 def test_cost_estimation_for_new_models(self): """Test cost estimation works for new models.""" @@ -174,8 +174,8 @@ def test_cost_estimation_for_new_models(self): cached_tokens=0, ) - # $15/1M input + $75/1M * 0.1M output = $15 + $7.5 = $22.5 - assert cost == pytest.approx(22.5, rel=0.01) + # $5/1M input + $25/1M * 0.1M output = $5 + $2.5 = $7.5 + assert cost == pytest.approx(7.5, rel=0.01) class TestAnthropicConfigLoading: diff --git a/tests/test_providers/test_anthropic.py b/tests/test_providers/test_anthropic.py index cdf503627..cbaf2accc 100644 --- a/tests/test_providers/test_anthropic.py +++ b/tests/test_providers/test_anthropic.py @@ -77,6 +77,11 @@ 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_5_family(self, anthropic_provider): + assert anthropic_provider.get_context_limit("claude-fable-5") == 1000000 + assert anthropic_provider.get_context_limit("claude-opus-4-8") == 1000000 + assert anthropic_provider.get_context_limit("claude-sonnet-5") == 1000000 + def test_supports_model_known(self, anthropic_provider): assert anthropic_provider.supports_model("claude-3-5-sonnet-20241022") @@ -110,3 +115,13 @@ 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") ) + + def test_pricing_claude_5_family(self, anthropic_provider): + fable = anthropic_provider._get_pricing("claude-fable-5") + assert fable == {"input": 10.00, "output": 50.00, "cached_input": 1.00} + + opus = anthropic_provider._get_pricing("claude-opus-4-8") + assert opus == {"input": 5.00, "output": 25.00, "cached_input": 0.50} + + sonnet = anthropic_provider._get_pricing("claude-sonnet-5") + assert sonnet == {"input": 3.00, "output": 15.00, "cached_input": 0.30}