Skip to content
Closed
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
28 changes: 20 additions & 8 deletions headroom/providers/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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},
Expand Down
42 changes: 42 additions & 0 deletions tests/test_providers/test_anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

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