{"repo":"NousResearch/hermes-agent","public_id":"50295","display_label":"Bedrock Claude pricing and cache cost fixes","summary":"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.","subsystem":"agent","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":50307},"resolution":{"state":"resolved","by":"pr-50307"},"freshness":{"state":"verified","last_verified_at":"2026-07-20T10:27:17+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.4},"fix_edges":[{"issue_number":50295,"pr_number":50307,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":50295,"pr_number":67976,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":50295,"title":"Bedrock Claude /usage shows cost unknown — pricing table lacks cache cost fields + cross-region prefix normalization","url":null,"state":"closed"},{"kind":"pr","number":50307,"title":"fix(bedrock): price Claude prompt-cache tokens in /usage","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/usage_pricing.py","tests/agent/test_usage_pricing.py"],"fix_hunks":[{"file":"agent/usage_pricing.py","hunks":["@@ -451,6 +451,8 @@ class CostResult:\n     ): PricingEntry(\n         input_cost_per_million=Decimal(\"15.00\"),\n         output_cost_per_million=Decimal(\"75.00\"),\n+        cache_read_cost_per_million=Decimal(\"1.50\"),\n+        cache_write_cost_per_million=Decimal(\"18.75\"),\n         source=\"official_docs_snapshot\",\n         source_url=\"https://aws.amazon.com/bedrock/pricing/\",\n         pricing_version=\"bedrock-pricing-2026-04\",","@@ -461,6 +463,8 @@ class CostResult:\n     ): PricingEntry(\n         input_cost_per_million=Decimal(\"3.00\"),\n         output_cost_per_million=Decimal(\"15.00\"),\n+        cache_read_cost_per_million=Decimal(\"0.30\"),\n+        cache_write_cost_per_million=Decimal(\"3.75\"),\n         source=\"official_docs_snapshot\",\n         source_url=\"https://aws.amazon.com/bedrock/pricing/\",\n         pricing_version=\"bedrock-pricing-2026-04\",","@@ -471,6 +475,8 @@ class CostResult:\n     ): PricingEntry(\n         input_cost_per_million=Decimal(\"3.00\"),\n         output_cost_per_million=Decimal(\"15.00\"),\n+        cache_read_cost_per_million=Decimal(\"0.30\"),\n+        cache_write_cost_per_million=Decimal(\"3.75\"),\n         source=\"official_docs_snapshot\",\n         source_url=\"https://aws.amazon.com/bedrock/pricing/\",\n         pricing_version=\"bedrock-pricing-2026-04\",","@@ -481,6 +487,8 @@ class CostResult:\n     ): PricingEntry(\n         input_cost_per_million=Decimal(\"0.80\"),\n         output_cost_per_million=Decimal(\"4.00\"),\n+        cache_read_cost_per_million=Decimal(\"0.08\"),\n+        cache_write_cost_per_million=Decimal(\"1.00\"),\n         source=\"official_docs_snapshot\",\n         source_url=\"https://aws.amazon.com/bedrock/pricing/\",\n         pricing_version=\"bedrock-pricing-2026-04\",","@@ -584,6 +592,26 @@ def resolve_billing_route(\n     return BillingRoute(provider=provider_name or \"unknown\", model=model.split(\"/\")[-1] if model else \"\", base_url=base_url or \"\", billing_mode=\"unknown\")\n \n \n+def _normalize_bedrock_model_name(model: str) -> str:\n+    \"\"\"Normalize a Bedrock model id to its bare foundation-model form.\n+\n+    Bedrock cross-region inference profiles prefix the foundation model id\n+    with a region scope (``us.`` / ``global.`` / ``eu.`` / ``ap.`` / ``jp.``),\n+    e.g. ``us.anthropic.claude-opus-4-7``.  The pricing table is keyed on the\n+    bare ``anthropic.claude-*`` id, so the prefix must be stripped before the\n+    lookup or every cross-region session prices as unknown.  Mirrors the\n+    prefix list in ``bedrock_adapter.is_anthropic_bedrock_model``.  Also\n+    normalizes dot-notation version numbers (``4.7`` → ``4-7``).\n+    \"\"\"\n+    name = model.lower().strip()\n+    for prefix in (\"us.\", \"global.\", \"eu.\", \"ap.\", \"jp.\"):\n+        if name.startswith(prefix):\n+            name = name[len(prefix):]\n+            break\n+    name = re.sub(r\"(\\d+)\\.(\\d+)\", r\"\\1-\\2\", name)\n+    return name\n+\n+\n def _normalize_anthropic_model_name(model: str) -> str:\n     \"\"\"Normalize Anthropic model name variants to canonical form.\n ","@@ -614,6 +642,14 @@ def _lookup_official_docs_pricing(route: BillingRoute) -> Optional[PricingEntry]\n             entry = _OFFICIAL_DOCS_PRICING.get((route.provider, normalized))\n             if entry:\n                 return entry\n+    # Bedrock cross-region inference profiles carry a region prefix\n+    # (us./global./eu./...) that the bare pricing keys don't have.\n+    if route.provider == \"bedrock\":\n+        normalized = _normalize_bedrock_model_name(model)\n+        if normalized != model:\n+            entry = _OFFICIAL_DOCS_PRICING.get((route.provider, normalized))\n+            if entry:\n+                return entry\n     return None\n \n "]},{"file":"tests/agent/test_usage_pricing.py","hunks":["@@ -250,3 +250,75 @@ def test_deepseek_v4_pro_estimate_usage_cost():\n     assert result.amount_usd is not None\n     # 1M input × $1.74/M + 500K output × $3.48/M = $1.74 + $1.74 = $3.48\n     assert float(result.amount_usd) == 3.48\n+\n+\n+def test_bedrock_claude_rows_all_carry_cache_pricing():\n+    \"\"\"Invariant: every Bedrock Claude pricing row must carry cache-read AND\n+    cache-write rates, otherwise a cached session prices as ``unknown``.\n+\n+    Bedrock Claude routes through the AnthropicBedrock SDK and injects\n+    cache_control, so cached tokens are always reported — the pricing layer\n+    must be able to value them.  See #50295.\n+    \"\"\"\n+    from agent.usage_pricing import _OFFICIAL_DOCS_PRICING\n+\n+    claude_rows = [\n+        (prov, model)\n+        for (prov, model) in _OFFICIAL_DOCS_PRICING\n+        if prov == \"bedrock\" and \"claude\" in model\n+    ]\n+    assert claude_rows, \"expected at least one bedrock Claude pricing row\"\n+    for key in claude_rows:\n+        entry = _OFFICIAL_DOCS_PRICING[key]\n+        assert entry.input_cost_per_million is not None, key\n+        assert entry.cache_read_cost_per_million is not None, key\n+        assert entry.cache_write_cost_per_million is not None, key\n+        # Cache reads are cheaper than fresh input; cache writes cost more.\n+        assert entry.cache_read_cost_per_million < entry.input_cost_per_million, key\n+        assert entry.cache_write_cost_per_million > entry.input_cost_per_million, key\n+\n+\n+def test_bedrock_cross_region_profile_prefix_resolves_to_pricing():\n+    \"\"\"Cross-region inference profiles (us./global./eu. prefixes) must resolve\n+    to the same pricing entry as the bare foundation-model id.  Without prefix\n+    normalization, ``us.anthropic.claude-*`` sessions price as unknown.\n+    \"\"\"\n+    bedrock_url = \"https://bedrock-runtime.us-east-1.amazonaws.com\"\n+    bare = get_pricing_entry(\n+        \"anthropic.claude-sonnet-4-5\", provider=\"bedrock\", base_url=bedrock_url\n+    )\n+    assert bare is not None\n+    for prefix in (\"us.\", \"global.\", \"eu.\"):\n+        scoped = get_pricing_entry(\n+            f\"{prefix}anthropic.claude-sonnet-4-5\",\n+            provider=\"bedrock\",\n+            base_url=bedrock_url,\n+        )\n+        assert scoped is not None, prefix\n+        assert scoped.input_cost_per_million == bare.input_cost_per_million\n+        assert scoped.cache_read_cost_per_million == bare.cache_read_cost_per_million\n+\n+\n+def test_bedrock_claude_cached_session_estimates_cost_not_unknown():\n+    \"\"\"A Bedrock Claude session with cache hits must produce a dollar estimate,\n+    not ``unknown`` — the user-visible symptom in #50295.\n+    \"\"\"\n+    bedrock_url = \"https://bedrock-runtime.us-east-1.amazonaws.com\"\n+    usage = SimpleNamespace(\n+        input_tokens=55,\n+        output_tokens=7113,\n+        cache_read_input_tokens=[redacted:secret-assignment]\n+        cache_creation_input_tokens=[redacted:secret-assignment]\n+    )\n+    canonical = normalize_usage(usage, provider=\"bedrock\", api_mode=\"anthropic_messages\")\n+    assert canonical.cache_read_tokens == 1369379\n+    assert canonical.cache_write_tokens == 42135\n+\n+    result = estimate_usage_cost(\n+        \"us.anthropic.claude-opus-4-6\",\n+        canonical,\n+        provider=\"bedrock\",\n+        base_url=bedrock_url,\n+    )\n+    assert result.status == \"estimated\"\n+    assert result.amount_usd is not None\n"]}]},{"kind":"pr","number":50437,"title":"fix(pricing): resolve versioned Bedrock profile IDs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#67976","number":67976}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":67976,"title":"fix(pricing): resolve versioned Bedrock profile IDs + current-gen Opus rows (salvage #50437/#62327/#62320)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/usage_pricing.py","tests/agent/test_usage_pricing.py"]},{"kind":"pr","number":92749,"title":"fix(agent): price dated Anthropic snapshot ids as their family alias","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#71441","number":71441},{"kind":"pr","value":"#50295","number":50295},{"kind":"pr","value":"#71441","number":71441}],"category":"duplicate_of","implication":"duplicate"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}