diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index 3bc44b1abd69..a8098d5cb70e 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -917,7 +917,8 @@ def _normalize_bedrock_model_name(model: str) -> str: e.g. ``us.anthropic.claude-opus-4-7``. The pricing table is keyed on the bare ``anthropic.claude-*`` id, so the prefix must be stripped before the lookup or every cross-region session prices as unknown. Also normalizes - dot-notation version numbers (``4.7`` → ``4-7``). + dot-notation version numbers (``4.7`` → ``4-7``) and the documented + trailing date, revision, and profile components (``-20250514-v1:0``). """ name = model.lower().strip() for prefix in ( @@ -936,6 +937,12 @@ def _normalize_bedrock_model_name(model: str) -> str: name = name[len(prefix):] break name = re.sub(r"(\d+)\.(\d+)", r"\1-\2", name) + # Bedrock inference profile IDs append these documented components to the + # foundation model ID. Strip only the trailing forms, not arbitrary model + # name continuations that could be a distinct SKU. + name = re.sub(r":\d+$", "", name) + name = re.sub(r"-v\d+$", "", name) + name = re.sub(r"-\d{8}$", "", name) return name @@ -977,20 +984,6 @@ def _lookup_official_docs_pricing(route: BillingRoute) -> Optional[PricingEntry] entry = _OFFICIAL_DOCS_PRICING.get((route.provider, normalized)) if entry: return entry - bedrock_entries = ( - (known_model, known_entry) - for (provider, known_model), known_entry in _OFFICIAL_DOCS_PRICING.items() - if provider == route.provider - ) - for known_model, known_entry in sorted( - bedrock_entries, - key=lambda item: len(item[0]), - reverse=True, - ): - if normalized == known_model or normalized.startswith( - (f"{known_model}-", f"{known_model}:") - ): - return known_entry return None diff --git a/tests/agent/test_usage_pricing.py b/tests/agent/test_usage_pricing.py index f9c0e318e012..da81d7c589d4 100644 --- a/tests/agent/test_usage_pricing.py +++ b/tests/agent/test_usage_pricing.py @@ -394,16 +394,34 @@ def test_bedrock_pricing_supports_less_common_inference_profile_prefixes(): """AWS also exposes profile scopes beyond us./global./eu.; those should not silently fall through to unknown pricing. """ + bare = get_pricing_entry("anthropic.claude-haiku-4-5", provider="bedrock") entry = get_pricing_entry( "apac.anthropic.claude-haiku-4-5-20251001-v1:0", provider="bedrock", ) + assert bare is not None assert entry is not None - assert float(entry.input_cost_per_million) == 0.8 - assert float(entry.output_cost_per_million) == 4.0 - assert float(entry.cache_read_cost_per_million) == 0.08 - assert float(entry.cache_write_cost_per_million) == 1.0 + for field in ( + "input_cost_per_million", + "output_cost_per_million", + "cache_read_cost_per_million", + "cache_write_cost_per_million", + ): + assert getattr(entry, field) == getattr(bare, field) + + +def test_bedrock_unknown_model_continuation_does_not_use_base_pricing(): + """Unrecognized Bedrock SKUs must remain unknown rather than inheriting a + similarly named model family's price. + """ + assert ( + get_pricing_entry( + "anthropic.claude-sonnet-4-6-experimental", + provider="bedrock", + ) + is None + ) def test_bedrock_claude_cached_session_estimates_cost_not_unknown():