fix(bedrock): add Fable + Claude 4.6/4.7/4.8 1M entries to context table, drop stale cached values

BEDROCK_CONTEXT_LENGTHS was missing entries for current 1M-context Claude
models, and the resolution path in get_model_context_length() short-circuits
to that table (step 1b) before DEFAULT_CONTEXT_LENGTHS is ever consulted, so
the catalog's correct values could never apply on Bedrock:

- claude-fable-5 (no entry at all) fell through to
  BEDROCK_DEFAULT_CONTEXT_LENGTH and reported 128K for a 1M model.
- opus-4-7 / opus-4-8 substring-matched the generic 'anthropic.claude-opus-4'
  key and reported 200K.
- opus-4-6 / sonnet-4-6 had explicit 200K entries predating their 1M windows.

The practical symptom: the agent compresses context prematurely (at ~128K or
~200K of a 1M window) on every Bedrock-hosted current Claude model.

Fixing the table alone is not enough for existing installs: a previously
persisted 128K/200K value in the context-length cache wins at step 1 and
masks the corrected table forever. Step 1 now reconciles Bedrock-context
cache hits against the static table (the table is authoritative for Bedrock
— there is no live probe to reconcile against), invalidating stale entries
so existing users converge to the right window without manual cache surgery.

Tests cover the new table entries (incl. inference-profile and versioned ID
forms), the 128K-default regression for Fable, the stale-cache invalidation
path, and that pre-4.6 models keep their 200K entries.
This commit is contained in:
Avi Fenesh 2026-06-12 14:21:25 +03:00 • committed by Teknium
parent 3b86af90ed
commit 77ba81f75f
4 changed files with 93 additions and 8 deletions

View file

@ -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,

View file

@ -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.<region>.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)

View file

@ -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).

View file

@ -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."""