diff --git a/agent/bedrock_adapter.py b/agent/bedrock_adapter.py index d3e6dfcd101e..50f05e3aa29f 100644 --- a/agent/bedrock_adapter.py +++ b/agent/bedrock_adapter.py @@ -1352,12 +1352,16 @@ BEDROCK_CONTEXT_LENGTHS: Dict[str, int] = { # Anthropic Claude models on Bedrock. # Context windows per Anthropic's official models comparison # (https://platform.claude.com/docs/en/about-claude/models/overview). - # Sonnet 5 / Opus 4.8 / 4.7 / 4.6 / Sonnet 4.6 have 1M generally + # Fable / Sonnet 5 / Opus 4.8 / 4.7 / 4.6 / Sonnet 4.6 have 1M generally # available (no beta header required as of April 2026). Sonnet 4.5 and # Sonnet 4 had their `context-1m-2025-08-07` beta retired on # April 30, 2026, so they are standard 200K; Haiku 4.5 is 200K. + # These 1M entries must match agent/model_metadata.py + # DEFAULT_CONTEXT_LENGTHS or the agent compresses context prematurely. # Keys are matched by longest-substring, so the versioned 4-6/4-7/4-8 # entries win over the generic "anthropic.claude-opus-4" fallback. + "anthropic.claude-fable-5": 1_000_000, + "anthropic.claude-fable": 1_000_000, "anthropic.claude-sonnet-5": 1_000_000, "anthropic.claude-opus-4-8": 1_000_000, "anthropic.claude-opus-4-7": 1_000_000, diff --git a/agent/model_metadata.py b/agent/model_metadata.py index 19e58e3d738f..7e193aab09a6 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -2175,6 +2175,12 @@ def get_model_context_length( if endpoint_context is not None: return endpoint_context + is_bedrock_context = provider == "bedrock" or ( + base_url + and base_url_hostname(base_url).startswith("bedrock-runtime.") + and base_url_host_matches(base_url, "amazonaws.com") + ) + # 1. Check persistent cache (model+provider) # LM Studio is excluded — its loaded context length is transient (the # user can reload the model with a different context_length at any time @@ -2243,6 +2249,26 @@ def get_model_context_length( model, base_url, ) # Fall through; step 5b reconciles and overwrites if portal responds. + # Invalidate stale Bedrock entries seeded before the Claude 4.6+ + # long-context table was corrected to 1M. + elif is_bedrock_context: + try: + from agent.bedrock_adapter import get_bedrock_context_length + bedrock_ctx = get_bedrock_context_length(model) + if cached != bedrock_ctx: + logger.info( + "Dropping stale Bedrock cache entry %s@%s -> %s; " + "using static Bedrock table value %s", + model, + base_url, + f"{cached:,}", + f"{bedrock_ctx:,}", + ) + _invalidate_cached_context_length(model, base_url) + return bedrock_ctx + except ImportError: + pass + return cached else: if is_local_endpoint(base_url): return _reconcile_local_cached_context_length( @@ -2253,17 +2279,13 @@ def get_model_context_length( # 1b. AWS Bedrock — use static context length table. # Bedrock's ListFoundationModels API doesn't expose context window sizes, # so we maintain a curated table in bedrock_adapter.py that reflects - # AWS-imposed limits (e.g. 200K for Claude models vs 1M on the native - # Anthropic API). This must run BEFORE the custom-endpoint probe at + # Bedrock-hosted model limits (e.g. older Claude 4 at 200K; Claude + # Opus/Sonnet 4.6+ at 1M). This must run BEFORE the custom-endpoint probe at # step 2 — bedrock-runtime..amazonaws.com is not in # _URL_TO_PROVIDER, so it would otherwise be treated as a custom endpoint, # fail the /models probe (Bedrock doesn't expose that shape), and fall # back to the 128K default before reaching the original step 4b branch. - if provider == "bedrock" or ( - base_url - and base_url_hostname(base_url).startswith("bedrock-runtime.") - and base_url_host_matches(base_url, "amazonaws.com") - ): + if is_bedrock_context: try: from agent.bedrock_adapter import get_bedrock_context_length return get_bedrock_context_length(model) diff --git a/tests/agent/test_bedrock_adapter.py b/tests/agent/test_bedrock_adapter.py index 16d771d8806b..2f8a6dc652fa 100644 --- a/tests/agent/test_bedrock_adapter.py +++ b/tests/agent/test_bedrock_adapter.py @@ -1181,6 +1181,22 @@ class TestBedrockContextLength: # Opus 4.6 has 1M context generally available (no beta header required). assert get_bedrock_context_length("anthropic.claude-opus-4-6-20250514-v1:0") == 1_000_000 + def test_claude_fable_5(self): + from agent.bedrock_adapter import get_bedrock_context_length + # Fable is a 1M-context model. DEFAULT_CONTEXT_LENGTHS already maps + # claude-fable-5 -> 1M, but the Bedrock resolution path short-circuits + # to this table before consulting it, so without entries here every + # Fable inference profile fell through to + # BEDROCK_DEFAULT_CONTEXT_LENGTH (128K). + assert get_bedrock_context_length("us.anthropic.claude-fable-5") == 1_000_000 + assert get_bedrock_context_length("global.anthropic.claude-fable-5") == 1_000_000 + assert get_bedrock_context_length("anthropic.claude-fable-5-v1:0") == 1_000_000 + + def test_claude_opus_4_base_stays_200k(self): + from agent.bedrock_adapter import get_bedrock_context_length + # The original Opus 4 (no minor version) keeps the 200K window. + assert get_bedrock_context_length("anthropic.claude-opus-4-20250514-v1:0") == 200_000 + def test_claude_sonnet_versioned(self): from agent.bedrock_adapter import get_bedrock_context_length # Sonnet 4.6 has 1M context generally available (no beta header required). diff --git a/tests/agent/test_model_metadata.py b/tests/agent/test_model_metadata.py index 64f7573afcc2..166e0c1f97fa 100644 --- a/tests/agent/test_model_metadata.py +++ b/tests/agent/test_model_metadata.py @@ -1117,6 +1117,49 @@ class TestBedrockContextResolution: assert ctx == 200000 mock_fetch.assert_not_called() + @patch("agent.model_metadata.fetch_endpoint_model_metadata") + def test_bedrock_claude_4_6_resolves_to_1m_before_probe(self, mock_fetch): + """Claude 4.6 Bedrock IDs resolve to the 1M table entry.""" + ctx = get_model_context_length( + "us.anthropic.claude-sonnet-4-6", + provider="bedrock", + base_url="https://bedrock-runtime.us-east-2.amazonaws.com", + ) + assert ctx == 1_000_000 + mock_fetch.assert_not_called() + + @patch("agent.model_metadata.fetch_endpoint_model_metadata") + def test_bedrock_claude_fable_resolves_to_1m_not_128k_default(self, mock_fetch): + """Fable on Bedrock must hit its own table entry, not the 128K default. + + DEFAULT_CONTEXT_LENGTHS maps claude-fable-5 -> 1M, but the Bedrock + branch at step 1b returns get_bedrock_context_length() before that + catalog is ever consulted — so a missing BEDROCK_CONTEXT_LENGTHS + entry silently reported 128K for a 1M model. + """ + ctx = get_model_context_length( + "global.anthropic.claude-fable-5", + provider="bedrock", + base_url="https://bedrock-runtime.us-east-2.amazonaws.com", + ) + assert ctx == 1_000_000 + mock_fetch.assert_not_called() + + @patch("agent.model_metadata.fetch_endpoint_model_metadata") + def test_bedrock_claude_4_6_ignores_stale_200k_cache(self, mock_fetch, tmp_path): + """Old 200K Bedrock cache entries must not mask the 1M table entry.""" + cache_file = tmp_path / "context_length_cache.yaml" + base_url = "https://bedrock-runtime.us-east-2.amazonaws.com" + with patch("agent.model_metadata._get_context_cache_path", return_value=cache_file): + save_context_length("us.anthropic.claude-sonnet-4-6", base_url, 200_000) + ctx = get_model_context_length( + "us.anthropic.claude-sonnet-4-6", + provider="bedrock", + base_url=base_url, + ) + assert ctx == 1_000_000 + mock_fetch.assert_not_called() + @patch("agent.model_metadata.fetch_endpoint_model_metadata") def test_bedrock_url_without_provider_hint(self, mock_fetch): """bedrock-runtime host infers Bedrock even when provider is omitted."""