diff --git a/amplifier_module_provider_anthropic/__init__.py b/amplifier_module_provider_anthropic/__init__.py index 235c088..5f2c83f 100644 --- a/amplifier_module_provider_anthropic/__init__.py +++ b/amplifier_module_provider_anthropic/__init__.py @@ -538,6 +538,17 @@ def __init__( self._enable_1m_context = self._config_bool( self.config.get("enable_1m_context", True) ) + # Operator assertion that this Anthropic account is entitled to the + # beta-gated 1M context window (a usage-tier requirement on top of the + # context-1m-2025-08-07 beta header). Sending the beta header is + # necessary but NOT sufficient: a non-entitled account is still capped + # at 200K. We therefore only *advertise* a beta-gated 1M window when + # the operator confirms entitlement here, so downstream clients do not + # budget against a window the API will reject. GA 1M windows (e.g. + # Opus 4.8+) are unaffected and always advertised. Default False. + self._context_1m_entitled = self._config_bool( + self.config.get("context_1m_entitled", False) + ) self._fallback_sonnet_model = str( self.config.get("fallback_sonnet_model", "claude-sonnet-4-6") ) @@ -670,9 +681,9 @@ def get_info(self) -> ProviderInfo: "max_tokens": 4096, "temperature": 0.7, "timeout": 600.0, - "context_window": 1000000 - if self._enable_1m_context and self._default_caps.supports_1m - else self._default_caps.base_context_window, + "context_window": self._advertised_context_window( + self.default_model, self._default_caps + ), "max_output_tokens": self._default_caps.max_output_tokens, }, config_fields=[ @@ -704,6 +715,22 @@ def get_info(self) -> ProviderInfo: "default_model": "not_contains:haiku" }, # Hide for Haiku (doesn't support 1M) ), + ConfigField( + id="context_1m_entitled", + display_name="1M Context Entitlement", + field_type="boolean", + prompt=( + "Is this account entitled to the beta 1M context window? " + "Only enable if Anthropic has granted 1M access; otherwise " + "the advertised window stays at 200K to avoid overflow." + ), + required=False, + default="false", + requires_model=True, + show_when={ + "default_model": "not_contains:haiku" + }, + ), ConfigField( id="enable_prompt_caching", display_name="Prompt Caching", @@ -859,12 +886,7 @@ async def list_models(self) -> list[ModelInfo]: self._extract_runtime_model_info(raw_model), ) - has_1m = self._enable_1m_context and caps.supports_1m - context_window = ( - max(caps.base_context_window, 1000000) - if has_1m - else caps.base_context_window - ) + context_window = self._advertised_context_window(model_id, caps) result.append( ModelInfo( @@ -1160,6 +1182,32 @@ def _dedupe_headers(headers: list[str]) -> list[str]: deduped.append(header) return deduped + def _advertised_context_window( + self, model_id: str, caps: ModelCapabilities + ) -> int: + """Return the context window to *advertise* for ``model_id``. + + The 1M window exists in two forms: + + * **GA** (e.g. Opus 4.8+) -- always honored; safe to advertise. + * **Beta-gated** (Sonnet 4.6/4.7, Opus 4.6/4.7) -- requires the + ``context-1m-2025-08-07`` beta header AND a usage-tier + entitlement. A non-entitled account is still capped at 200K, so + advertising 1M to it makes downstream clients skip context + compaction and overflow the real cap with a hard + ``prompt is too long: N > 200000 maximum`` 400. + + We advertise beta-gated 1M only when the operator asserts + entitlement via ``context_1m_entitled``; GA 1M is always advertised. + Falls back to ``base_context_window`` (200K) otherwise. + """ + if not (self._enable_1m_context and caps.supports_1m): + return caps.base_context_window + beta_gated = self._should_add_context_1m_beta(model_id, caps) + if beta_gated and not self._context_1m_entitled: + return caps.base_context_window + return max(caps.base_context_window, 1000000) + def _should_add_context_1m_beta( self, model_id: str, request_caps: ModelCapabilities ) -> bool: diff --git a/tests/test_advertised_context_window.py b/tests/test_advertised_context_window.py new file mode 100644 index 0000000..a38ac24 --- /dev/null +++ b/tests/test_advertised_context_window.py @@ -0,0 +1,95 @@ +"""Tests for advertised context-window gating (_advertised_context_window). + +A beta-gated 1M context window (Sonnet 4.6/4.7, Opus 4.6/4.7) must only be +advertised when the operator asserts entitlement via ``context_1m_entitled``; +otherwise the advertised window stays at the 200K base so downstream clients +don't skip compaction and overflow the real cap. GA 1M windows (Opus 4.8+) +are always advertised. +""" + +from typing import cast + +from amplifier_core import ModuleCoordinator + +from amplifier_module_provider_anthropic import AnthropicProvider + +from tests._helpers import FakeCoordinator + +_BASE = 200000 +_ONE_M = 1000000 + + +def _make_provider(default_model: str, **config_overrides) -> AnthropicProvider: + provider = AnthropicProvider( + api_key="test-key", + config={ + "use_streaming": False, + "default_model": default_model, + **config_overrides, + }, + ) + provider.coordinator = cast(ModuleCoordinator, FakeCoordinator()) + return provider + + +def _advertised(model_id: str, **config_overrides) -> int: + provider = _make_provider(model_id, **config_overrides) + caps = AnthropicProvider._get_capabilities(model_id) + return provider._advertised_context_window(model_id, caps) + + +class TestBetaGatedNotEntitled: + """Default (no entitlement): beta-gated 1M is NOT advertised.""" + + def test_sonnet_46_advertises_base(self): + assert _advertised("claude-sonnet-4-6") == _BASE + + def test_opus_47_advertises_base(self): + assert _advertised("claude-opus-4-7-20260416") == _BASE + + +class TestBetaGatedEntitled: + """With context_1m_entitled=True: beta-gated 1M IS advertised.""" + + def test_sonnet_46_advertises_1m(self): + assert _advertised("claude-sonnet-4-6", context_1m_entitled=True) == _ONE_M + + def test_opus_47_advertises_1m(self): + assert ( + _advertised("claude-opus-4-7-20260416", context_1m_entitled=True) == _ONE_M + ) + + +class TestGaOneMillionAlwaysAdvertised: + """GA 1M (Opus 4.8+) is advertised regardless of the entitlement flag.""" + + def test_opus_48_advertises_1m_without_flag(self): + assert _advertised("claude-opus-4-8") == _ONE_M + + def test_opus_48_advertises_1m_with_flag(self): + assert _advertised("claude-opus-4-8", context_1m_entitled=True) == _ONE_M + + +class TestNoOneMillionSupport: + """Models without 1M support always advertise the base window.""" + + def test_haiku_advertises_base(self): + assert _advertised("claude-haiku-4-5", context_1m_entitled=True) == _BASE + + def test_sonnet_45_advertises_base(self): + # Sonnet 4.5 has supports_1m=False + assert _advertised("claude-sonnet-4-5", context_1m_entitled=True) == _BASE + + +class TestEnable1mContextDisabled: + """enable_1m_context=False forces the base window even when entitled.""" + + def test_sonnet_46_disabled(self): + assert ( + _advertised( + "claude-sonnet-4-6", + enable_1m_context=False, + context_1m_entitled=True, + ) + == _BASE + )