Bedrock Claude pricing and cache cost fixes
Cluster 50295 · agent. Data as of
Nothing open on GitHub. Fix merged in #50307. Check it covers your case.
The short answer
- Known?
- Yes. 5 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #50307. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #50307. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and PRs address missing or incorrect pricing for Bedrock Claude models, specifically around prompt-cache token costs, versioned/dated profile ID resolution, and cross-region prefix normalization in usage reporting.
Open issues0
Open PRs0
Fixes on record
-
#50307 fix(bedrock): price Claude prompt-cache tokens in /usage
files:
agent/usage_pricing.py,tests/agent/test_usage_pricing.pyShow the diff
-
agent/usage_pricing.py@@ -451,6 +451,8 @@ class CostResult: ): 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"), source="official_docs_snapshot", source_url="https://aws.amazon.com/bedrock/pricing/", pricing_version="bedrock-pricing-2026-04", @@ -461,6 +463,8 @@ class CostResult: ): PricingEntry( input_cost_per_million=Decimal("3.00"), output_cost_per_million=Decimal("15.00"), + cache_read_cost_per_million=Decimal("0.30"), + cache_write_cost_per_million=Decimal("3.75"), source="official_docs_snapshot", source_url="https://aws.amazon.com/bedrock/pricing/", pricing_version="bedrock-pricing-2026-04", @@ -471,6 +475,8 @@ class CostResult: ): PricingEntry( input_cost_per_million=Decimal("3.00"), output_cost_per_million=Decimal("15.00"), + cache_read_cost_per_million=Decimal("0.30"), + cache_write_cost_per_million=Decimal("3.75"), source="official_docs_snapshot", source_url="https://aws.amazon.com/bedrock/pricing/", pricing_version="bedrock-pricing-2026-04", @@ -481,6 +487,8 @@ class CostResult: ): PricingEntry( input_cost_per_million=Decimal("0.80"), output_cost_per_million=Decimal("4.00"), + cache_read_cost_per_million=Decimal("0.08"), + cache_write_cost_per_million=Decimal("1.00"), source="official_docs_snapshot", source_url="https://aws.amazon.com/bedrock/pricing/", pricing_version="bedrock-pricing-2026-04", @@ -584,6 +592,26 @@ def resolve_billing_route( return BillingRoute(provider=provider_name or "unknown", model=model.split("/")[-1] if model else "", base_url=base_url or "", billing_mode="unknown") +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.``), + 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``). + """ + name = model.lower().strip() + for prefix in ("us.", "global.", "eu.", "ap.", "jp."): + if name.startswith(prefix): + name = name[len(prefix):] + break + name = re.sub(r"(\d+)\.(\d+)", r"\1-\2", name) + return name + + def _normalize_anthropic_model_name(model: str) -> str: """Normalize Anthropic model name variants to canonical form. @@ -614,6 +642,14 @@ def _lookup_official_docs_pricing(route: BillingRoute) -> Optional[PricingEntry] entry = _OFFICIAL_DOCS_PRICING.get((route.provider, normalized)) if entry: return entry + # Bedrock cross-region inference profiles carry a region prefix + # (us./global./eu./...) that the bare pricing keys don't have. + if route.provider == "bedrock": + normalized = _normalize_bedrock_model_name(model) + if normalized != model: + entry = _OFFICIAL_DOCS_PRICING.get((route.provider, normalized)) + if entry: + return entry return None -
tests/agent/test_usage_pricing.py@@ -250,3 +250,75 @@ def test_deepseek_v4_pro_estimate_usage_cost(): assert result.amount_usd is not None # 1M input × $1.74/M + 500K output × $3.48/M = $1.74 + $1.74 = $3.48 assert float(result.amount_usd) == 3.48 + + +def test_bedrock_claude_rows_all_carry_cache_pricing(): + """Invariant: every Bedrock Claude pricing row must carry cache-read AND + cache-write rates, otherwise a cached session prices as ``unknown``. + + Bedrock Claude routes through the AnthropicBedrock SDK and injects + cache_control, so cached tokens are always reported — the pricing layer + must be able to value them. See #50295. + """ + from agent.usage_pricing import _OFFICIAL_DOCS_PRICING + + claude_rows = [ + (prov, model) + for (prov, model) in _OFFICIAL_DOCS_PRICING + if prov == "bedrock" and "claude" in model + ] + assert claude_rows, "expected at least one bedrock Claude pricing row" + for key in claude_rows: + entry = _OFFICIAL_DOCS_PRICING[key] + assert entry.input_cost_per_million is not None, key + assert entry.cache_read_cost_per_million is not None, key + assert entry.cache_write_cost_per_million is not None, key + # Cache reads are cheaper than fresh input; cache writes cost more. + assert entry.cache_read_cost_per_million < entry.input_cost_per_million, key + assert entry.cache_write_cost_per_million > entry.input_cost_per_million, key + + +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 + normalization, ``us.anthropic.claude-*`` sessions price as unknown. + """ + bedrock_url = "https://bedrock-runtime.us-east-1.amazonaws.com" + bare = get_pricing_entry( + "anthropic.claude-sonnet-4-5", provider="bedrock", base_url=bedrock_url + ) + assert bare is not None + for prefix in ("us.", "global.", "eu."): + scoped = get_pricing_entry( + f"{prefix}anthropic.claude-sonnet-4-5", + provider="bedrock", + base_url=bedrock_url, + ) + assert scoped is not None, prefix + assert scoped.input_cost_per_million == bare.input_cost_per_million + assert scoped.cache_read_cost_per_million == bare.cache_read_cost_per_million + + +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. + """ + bedrock_url = "https://bedrock-runtime.us-east-1.amazonaws.com" + usage = SimpleNamespace( + input_tokens=55, + output_tokens=7113, + cache_read_input_tokens=[redacted:secret-assignment] + cache_creation_input_tokens=[redacted:secret-assignment] + ) + canonical = normalize_usage(usage, provider="bedrock", api_mode="anthropic_messages") + assert canonical.cache_read_tokens == 1369379 + assert canonical.cache_write_tokens == 42135 + + result = estimate_usage_cost( + "us.anthropic.claude-opus-4-6", + canonical, + provider="bedrock", + base_url=bedrock_url, + ) + assert result.status == "estimated" + assert result.amount_usd is not None
-
-
#67976 fix(pricing): resolve versioned Bedrock profile IDs + current-gen Opus rows (salvage #50437/#62327/#62320)
files:
agent/usage_pricing.py,tests/agent/test_usage_pricing.py
2 PRs closed without merging
From the close comments: duplicate 1 · merged via another change 1
Closed issues · 1
- Closed#50295Bedrock Claude /usage shows cost unknown — pricing table lacks cache cost fields + cross-region prefix normalization
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.40: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab