Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions headroom/providers/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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},
Expand All @@ -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},
Expand Down
18 changes: 9 additions & 9 deletions tests/test_provider_model_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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."""
Expand All @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions tests/test_providers/test_anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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}
Loading