From 24338f389def5aa0b29e9bcf338ac7502a3b03da Mon Sep 17 00:00:00 2001 From: Osraka <98612432+Osraka@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:38:43 +0300 Subject: [PATCH 1/3] fix(pricing): resolve versioned Bedrock profile IDs --- agent/usage_pricing.py | 34 +++++++++++++++++++++++----- tests/agent/test_usage_pricing.py | 37 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index cd61af18e91c1..3bc44b1abd694 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -913,15 +913,25 @@ def _normalize_bedrock_model_name(model: str) -> str: """Normalize a Bedrock model id to its bare foundation-model form. Bedrock cross-region inference profiles prefix the foundation model id - with a region scope (``us.`` / ``global.`` / ``eu.`` / ``ap.`` / ``jp.``), + with a region scope (``us.`` / ``global.`` / ``eu.`` / ``apac.`` / ...), 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. Mirrors the - prefix list in ``bedrock_adapter.is_anthropic_bedrock_model``. Also - normalizes dot-notation version numbers (``4.7`` → ``4-7``). + lookup or every cross-region session prices as unknown. Also normalizes + dot-notation version numbers (``4.7`` → ``4-7``). """ name = model.lower().strip() - for prefix in ("us.", "global.", "eu.", "ap.", "jp."): + for prefix in ( + "global.", + "us.", + "eu.", + "ap.", + "apac.", + "jp.", + "ca.", + "sa.", + "me.", + "af.", + ): if name.startswith(prefix): name = name[len(prefix):] break @@ -967,6 +977,20 @@ 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 04aa4ec4c9cff..f9c0e318e0124 100644 --- a/tests/agent/test_usage_pricing.py +++ b/tests/agent/test_usage_pricing.py @@ -369,6 +369,43 @@ def test_bedrock_cross_region_profile_prefix_resolves_to_pricing(): assert scoped.cache_read_cost_per_million == bare.cache_read_cost_per_million +def test_bedrock_versioned_inference_profile_resolves_to_bare_pricing(): + """Bedrock profile IDs may include the provider's dated version suffix. + + The pricing table intentionally uses shorter model-family IDs, so the + lookup needs a longest-prefix fallback after stripping the region scope. + """ + bare = get_pricing_entry("anthropic.claude-sonnet-4-6", provider="bedrock") + assert bare is not None + + for model in ( + "us.anthropic.claude-sonnet-4-6-20250514-v1:0", + "global.anthropic.claude-sonnet-4-6-20250514-v1:0", + ): + scoped = get_pricing_entry(model, provider="bedrock") + assert scoped is not None, model + assert scoped.input_cost_per_million == bare.input_cost_per_million + assert scoped.output_cost_per_million == bare.output_cost_per_million + assert scoped.cache_read_cost_per_million == bare.cache_read_cost_per_million + assert scoped.cache_write_cost_per_million == bare.cache_write_cost_per_million + + +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. + """ + entry = get_pricing_entry( + "apac.anthropic.claude-haiku-4-5-20251001-v1:0", + provider="bedrock", + ) + + 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 + + def test_bedrock_claude_cached_session_estimates_cost_not_unknown(): """A Bedrock Claude session with cache hits must produce a dollar estimate, not ``unknown`` — the user-visible symptom in #50295. From 4b10b8257f104240d6585eaf3606154be8478bdc Mon Sep 17 00:00:00 2001 From: Osraka <98612432+Osraka@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:52:41 +0300 Subject: [PATCH 2/3] fix(pricing): restrict Bedrock profile normalization --- agent/usage_pricing.py | 23 ++++++++--------------- tests/agent/test_usage_pricing.py | 26 ++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index 3bc44b1abd694..a8098d5cb70e0 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 f9c0e318e0124..da81d7c589d43 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(): From 18461964851d21a3b8f04494fed02cb87fd0924b Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 20 Jul 2026 02:51:17 -0700 Subject: [PATCH 3/3] feat(pricing): add Bedrock rows for Opus 4.8/4.7, correct Opus 4.6 to $5/$25 Adds current-gen Claude Opus pricing rows on Bedrock keyed to Anthropic's published list price, which commercial Bedrock on-demand mirrors. Also corrects the existing Opus 4.6 row: it carried Claude-3-era Opus pricing ($15/$75); Opus 4.5+ list at $5/$25 with cache write 1.25x / read 0.1x. The AWS Price List API had not published these SKUs machine-readably as of 2026-07, so these are commercial-list snapshots pending an authoritative machine source. Reapplied from PR #62327 (commit authored under a placeholder identity, so cherry-pick was not usable; sonnet-5 row from that PR already landed via #67932). Co-authored-by: pgregg88 <4943027+pgregg88@users.noreply.github.com> --- agent/usage_pricing.py | 40 +++++++++++++++++++++++++++---- tests/agent/test_usage_pricing.py | 37 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index a8098d5cb70e0..2884381bf1677 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -545,17 +545,47 @@ class CostResult: # Bedrock charges the same per-token rates as the model provider but # through AWS billing. These are the on-demand prices (no commitment). # Source: https://aws.amazon.com/bedrock/pricing/ + # Current-gen Claude Opus on Bedrock. Commercial Bedrock on-demand + # mirrors Anthropic's published list price for the Claude line + # ($5/$25 for Opus 4.6/4.7/4.8; cache write = 1.25x input at the + # 5-minute TTL, cache read = 0.1x input). NOTE: the AWS Price List API + # had not published these SKUs machine-readably as of 2026-07 — these + # are commercial-list snapshots pending an authoritative machine source. + ( + "bedrock", + "anthropic.claude-opus-4-8", + ): PricingEntry( + input_cost_per_million=Decimal("5.00"), + output_cost_per_million=Decimal("25.00"), + cache_read_cost_per_million=Decimal("0.50"), + cache_write_cost_per_million=Decimal("6.25"), + source="official_docs_snapshot", + source_url="https://aws.amazon.com/bedrock/pricing/", + pricing_version="anthropic-list-2026-07", + ), + ( + "bedrock", + "anthropic.claude-opus-4-7", + ): PricingEntry( + input_cost_per_million=Decimal("5.00"), + output_cost_per_million=Decimal("25.00"), + cache_read_cost_per_million=Decimal("0.50"), + cache_write_cost_per_million=Decimal("6.25"), + source="official_docs_snapshot", + source_url="https://aws.amazon.com/bedrock/pricing/", + pricing_version="anthropic-list-2026-07", + ), ( "bedrock", "anthropic.claude-opus-4-6", ): PricingEntry( - input_cost_per_million=Decimal("15.00"), - output_cost_per_million=Decimal("75.00"), - cache_read_cost_per_million=Decimal("1.50"), - cache_write_cost_per_million=Decimal("18.75"), + input_cost_per_million=Decimal("5.00"), + output_cost_per_million=Decimal("25.00"), + cache_read_cost_per_million=Decimal("0.50"), + cache_write_cost_per_million=Decimal("6.25"), source="official_docs_snapshot", source_url="https://aws.amazon.com/bedrock/pricing/", - pricing_version="bedrock-pricing-2026-04", + pricing_version="anthropic-list-2026-07", ), ( "bedrock", diff --git a/tests/agent/test_usage_pricing.py b/tests/agent/test_usage_pricing.py index da81d7c589d43..9b82d2e2ad478 100644 --- a/tests/agent/test_usage_pricing.py +++ b/tests/agent/test_usage_pricing.py @@ -348,6 +348,43 @@ def test_bedrock_claude_rows_all_carry_cache_pricing(): assert entry.cache_write_cost_per_million > entry.input_cost_per_million, key +def test_bedrock_current_gen_claude_rows_resolve(): + """Current-gen Claude models (Opus 4.8/4.7, Sonnet 5) must have Bedrock + pricing rows so cached sessions report a dollar cost, not ``unknown``. + Assert each resolves via the bare id and a cross-region inference profile + (us./global. prefix), that every id for a given model resolves to the same + entry, and that the row carries the cache fields a Bedrock Claude session + needs. + + (Version-suffixed IDs like ``...-v1:0`` are covered separately by the + normalizer test in the suffix-strip change; this test intentionally sticks + to id shapes that resolve on ``main`` so it is independent of that PR.) + """ + url = "https://bedrock-runtime.us-east-1.amazonaws.com" + for bare in ( + "anthropic.claude-opus-4-8", + "anthropic.claude-opus-4-7", + "anthropic.claude-sonnet-5", + ): + ref = get_pricing_entry(bare, provider="bedrock", base_url=url) + assert ref is not None, bare + assert ref.input_cost_per_million is not None, bare + assert ref.output_cost_per_million is not None, bare + # Output costs more than input across the Claude line; sanity-check the + # row isn't malformed (input < output). + assert ref.output_cost_per_million > ref.input_cost_per_million, bare + # Cache fields present so cached sessions price correctly (the #50295 + # symptom was unknown cost on cached Bedrock Claude sessions). + assert ref.cache_read_cost_per_million is not None, bare + assert ref.cache_write_cost_per_million is not None, bare + # Cross-region inference profiles resolve to the same entry. + for mid in (f"us.{bare}", f"global.{bare}"): + entry = get_pricing_entry(mid, provider="bedrock", base_url=url) + assert entry is not None, mid + assert entry.input_cost_per_million == ref.input_cost_per_million, mid + assert entry.output_cost_per_million == ref.output_cost_per_million, mid + + def test_bedrock_cross_region_profile_prefix_resolves_to_pricing(): """Cross-region inference profiles (us./global./eu. prefixes) must resolve to the same pricing entry as the bare foundation-model id. Without prefix