Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions agent/bedrock_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1276,9 +1276,19 @@ def classify_bedrock_error(error_message: str) -> str:
# detection is unavailable.

BEDROCK_CONTEXT_LENGTHS: Dict[str, int] = {
# Anthropic Claude models on Bedrock
"anthropic.claude-opus-4-6": 200_000,
"anthropic.claude-sonnet-4-6": 200_000,
# Anthropic Claude models on Bedrock.
# AWS and Anthropic list Opus/Sonnet 4.6+ as 1M-context Bedrock models;
# this fallback must not assume the older Claude-wide 200K window.
# These 1M entries must match agent/model_metadata.py DEFAULT_CONTEXT_LENGTHS
# or the agent compresses context prematurely (~200K) on a 1M-capable model.
# Longest-substring matching means more specific keys (…opus-4-8) win over
# the generic …opus-4 fallback, so list the generic 200K entry last.
"anthropic.claude-fable-5": 1_000_000,
"anthropic.claude-fable": 1_000_000,
"anthropic.claude-opus-4-8": 1_000_000,
"anthropic.claude-opus-4-7": 1_000_000,
"anthropic.claude-opus-4-6": 1_000_000,
"anthropic.claude-sonnet-4-6": 1_000_000,
"anthropic.claude-sonnet-4-5": 200_000,
"anthropic.claude-haiku-4-5": 200_000,
"anthropic.claude-opus-4": 200_000,
Expand Down
36 changes: 29 additions & 7 deletions agent/model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,12 @@ def get_model_context_length(
# local servers actually know about. Ollama "model:tag" colons are preserved.
model = _strip_provider_prefix(model)

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
Expand Down Expand Up @@ -1652,23 +1658,39 @@ 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:
return cached

# 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)
Expand Down
31 changes: 28 additions & 3 deletions tests/agent/test_bedrock_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1099,11 +1099,36 @@ class TestBedrockContextLength:

def test_claude_opus_4_6(self):
from agent.bedrock_adapter import get_bedrock_context_length
assert get_bedrock_context_length("anthropic.claude-opus-4-6-20250514-v1:0") == 200_000
# Opus 4.6 is a 1M-context model on Bedrock.
assert get_bedrock_context_length("anthropic.claude-opus-4-6-20250514-v1:0") == 1_000_000

def test_claude_opus_4_7_and_4_8(self):
from agent.bedrock_adapter import get_bedrock_context_length
# Opus 4.7/4.8 inference profiles must not fall back to the generic
# …opus-4 (200K) key.
assert get_bedrock_context_length("us.anthropic.claude-opus-4-7") == 1_000_000
assert get_bedrock_context_length("us.anthropic.claude-opus-4-8") == 1_000_000
assert get_bedrock_context_length("global.anthropic.claude-opus-4-8") == 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
assert get_bedrock_context_length("anthropic.claude-sonnet-4-6-20250514-v1:0") == 200_000
assert get_bedrock_context_length("anthropic.claude-sonnet-4-6-20250514-v1:0") == 1_000_000

def test_nova_pro(self):
from agent.bedrock_adapter import get_bedrock_context_length
Expand All @@ -1120,7 +1145,7 @@ def test_unknown_model_gets_default(self):
def test_inference_profile_resolves(self):
from agent.bedrock_adapter import get_bedrock_context_length
# Cross-region inference profiles contain the base model ID
assert get_bedrock_context_length("us.anthropic.claude-sonnet-4-6") == 200_000
assert get_bedrock_context_length("us.anthropic.claude-sonnet-4-6") == 1_000_000

def test_longest_prefix_wins(self):
from agent.bedrock_adapter import get_bedrock_context_length
Expand Down
43 changes: 43 additions & 0 deletions tests/agent/test_model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,49 @@ def test_bedrock_provider_returns_static_table_before_probe(self, mock_fetch):
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."""
Expand Down
Loading