diff --git a/litellm-rust/crates/llms-types/src/providers/anthropic/beta.rs b/litellm-rust/crates/llms-types/src/providers/anthropic/beta.rs index 8429c98a88a..55d04be4974 100644 --- a/litellm-rust/crates/llms-types/src/providers/anthropic/beta.rs +++ b/litellm-rust/crates/llms-types/src/providers/anthropic/beta.rs @@ -98,6 +98,8 @@ pub enum AnthropicBeta { McpServers20251204, #[strum(serialize = "mid-conversation-output-config-2026-07-01")] MidConversationOutputConfig20260701, + #[strum(serialize = "mid-conversation-system-clear-at-2026-08-21")] + MidConversationSystemClearAt20260821, #[strum(serialize = "mid-conversation-tool-changes-2026-07-01")] MidConversationToolChanges20260701, #[strum(serialize = "output-128k-2025-02-19")] @@ -129,7 +131,7 @@ pub enum AnthropicBeta { } impl AnthropicBeta { - pub const KNOWN: [Self; 40] = [ + pub const KNOWN: [Self; 41] = [ Self::Oauth20250420, Self::WebFetch20250910, Self::WebSearch20250305, @@ -157,6 +159,7 @@ impl AnthropicBeta { Self::McpClient20251120, Self::McpServers20251204, Self::MidConversationOutputConfig20260701, + Self::MidConversationSystemClearAt20260821, Self::MidConversationToolChanges20260701, Self::Output128k20250219, Self::PromptCachingScope20260105, @@ -257,10 +260,14 @@ impl AnthropicBeta { } Self::MidConversationOutputConfig20260701 | Self::MidConversationToolChanges20260701 => &[ - BetaProvider::AzureAi, BetaProvider::BedrockConverse, BetaProvider::BedrockMantle, - BetaProvider::VertexAi, + BetaProvider::Databricks, + ], + Self::MidConversationSystemClearAt20260821 => &[ + BetaProvider::BedrockConverse, + BetaProvider::Bedrock, + BetaProvider::BedrockMantle, BetaProvider::Databricks, ], Self::Output128k20250219 | Self::TokenEfficientTools20250219 => &[ @@ -429,6 +436,7 @@ mod tests { AnthropicBeta::McpClient20251120, AnthropicBeta::McpServers20251204, AnthropicBeta::MidConversationOutputConfig20260701, + AnthropicBeta::MidConversationSystemClearAt20260821, AnthropicBeta::MidConversationToolChanges20260701, AnthropicBeta::Output128k20250219, AnthropicBeta::PromptCachingScope20260105, diff --git a/tests/unit/llms/custom_httpx/test_llm_http_handler.py b/tests/unit/llms/custom_httpx/test_llm_http_handler.py index cb3f1edc929..b03be3f9f98 100644 --- a/tests/unit/llms/custom_httpx/test_llm_http_handler.py +++ b/tests/unit/llms/custom_httpx/test_llm_http_handler.py @@ -5726,8 +5726,8 @@ async def test_async_anthropic_messages_handler_filters_betas_by_the_azure_ai_ro monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True") anthropic_beta_headers_manager._BETA_HEADERS_CONFIG = None - foundry_api_base = "https://my-resource.services.ai.azure.com/anthropic" - upstream_response = httpx.Response( + foundry_api_base: Final = "https://my-resource.services.ai.azure.com/anthropic" + upstream_response: Final = httpx.Response( 200, json={ "id": "msg_123", @@ -5740,9 +5740,9 @@ async def test_async_anthropic_messages_handler_filters_betas_by_the_azure_ai_ro }, request=httpx.Request("POST", f"{foundry_api_base}/v1/messages"), ) - mock_client = AsyncMock(spec=AsyncHTTPHandler) + mock_client: Final = AsyncMock(spec=AsyncHTTPHandler) mock_client.post = AsyncMock(return_value=upstream_response) - mock_logging_obj = Mock() + mock_logging_obj: Final = Mock() mock_logging_obj.model_call_details = {} mock_logging_obj.stream = False mock_logging_obj.dynamic_success_callbacks = None @@ -5761,5 +5761,5 @@ async def test_async_anthropic_messages_handler_filters_betas_by_the_azure_ai_ro kwargs={"extra_headers": {"anthropic-beta": "advisor-tool-2026-03-01,thinking-binding-controls-2026-08-01"}}, ) - sent_headers = mock_client.post.call_args.kwargs["headers"] + sent_headers: Final = mock_client.post.call_args.kwargs["headers"] assert sent_headers["anthropic-beta"] == "thinking-binding-controls-2026-08-01" diff --git a/tests/unit/test_anthropic_beta_headers_deployment_rows.py b/tests/unit/test_anthropic_beta_headers_deployment_rows.py deleted file mode 100644 index dd123b686e1..00000000000 --- a/tests/unit/test_anthropic_beta_headers_deployment_rows.py +++ /dev/null @@ -1,115 +0,0 @@ -"""The beta allowlist row is picked per deployment: an ``anthropic`` deployment on a Foundry host gets ``azure_ai``.""" - -import json -import os -from typing import Final - -import pytest - -import litellm -from litellm import anthropic_beta_headers_manager -from litellm.anthropic_beta_headers_manager import ( - is_foundry_api_base, - resolve_beta_provider, - update_headers_with_filtered_beta, - update_request_with_filtered_beta, -) - -FOUNDRY_API_BASE: Final = "https://my-resource.services.ai.azure.com/anthropic" -DIRECT_API_BASE: Final = "https://api.anthropic.com" -DIRECT_ONLY_BETA: Final = "advisor-tool-2026-03-01" -PRESERVED_THINKING_BETAS: Final = ( - "thinking-binding-controls-2026-08-01", - "mid-conversation-system-clear-at-2026-08-21", - "mid-conversation-tool-changes-2026-07-01", - "mid-conversation-output-config-2026-07-01", - "thinking-display-updates-2026-08-18", -) - - -@pytest.fixture(autouse=True) -def _bundled_config(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True") - anthropic_beta_headers_manager._BETA_HEADERS_CONFIG = None - - -def _bundled_rows() -> dict[str, dict[str, str | None]]: - with open(os.path.join(os.path.dirname(litellm.__file__), "anthropic_beta_headers_config.json")) as f: - rows: dict[str, dict[str, str | None]] = json.load(f) - return rows - - -@pytest.mark.parametrize( - ("api_base", "expected"), - ( - (FOUNDRY_API_BASE, True), - ("https://my-resource.openai.azure.com", True), - ("my-resource.services.ai.azure.com/anthropic", True), - (DIRECT_API_BASE, False), - ("https://services.ai.azure.com.example.com", False), - ("", False), - (None, False), - ), -) -def test_is_foundry_api_base_matches_only_foundry_hosts(api_base: str | None, expected: bool) -> None: - assert is_foundry_api_base(api_base) is expected - - -@pytest.mark.parametrize( - ("provider", "api_base", "expected"), - ( - ("anthropic", FOUNDRY_API_BASE, "azure_ai"), - ("anthropic", DIRECT_API_BASE, "anthropic"), - ("anthropic", None, "anthropic"), - ("vertex_ai", FOUNDRY_API_BASE, "vertex_ai"), - ("bedrock_converse", None, "bedrock_converse"), - ), -) -def test_resolve_beta_provider_picks_the_row_per_deployment(provider: str, api_base: str | None, expected: str) -> None: - assert resolve_beta_provider(provider, api_base) == expected - - -def test_foundry_deployment_headers_follow_the_azure_ai_row() -> None: - headers: Final = {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"} - result: dict[str, str] = update_headers_with_filtered_beta( - headers=headers, provider="anthropic", api_base=FOUNDRY_API_BASE - ) - assert result == {"anthropic-beta": "thinking-binding-controls-2026-08-01"} - - -def test_direct_deployment_headers_keep_every_anthropic_row_beta() -> None: - headers: Final = {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"} - result: dict[str, str] = update_headers_with_filtered_beta( - headers=headers, provider="anthropic", api_base=DIRECT_API_BASE - ) - assert result == {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"} - - -def test_foundry_deployment_request_filters_header_and_body_betas_together() -> None: - headers: dict[str, str] - data: dict[str, object] - headers, data = update_request_with_filtered_beta( - headers={"anthropic-beta": DIRECT_ONLY_BETA}, - request_data={"anthropic_beta": [DIRECT_ONLY_BETA, "thinking-binding-controls-2026-08-01"], "max_tokens": 8}, - provider="anthropic", - api_base=FOUNDRY_API_BASE, - ) - assert headers == {} - assert data == {"anthropic_beta": ["thinking-binding-controls-2026-08-01"], "max_tokens": 8} - - -@pytest.mark.parametrize("provider", ("anthropic", "azure_ai", "vertex_ai")) -@pytest.mark.parametrize("beta", PRESERVED_THINKING_BETAS) -def test_live_verified_platforms_forward_every_preserved_thinking_beta(provider: str, beta: str) -> None: - assert _bundled_rows()[provider][beta] == beta - - -@pytest.mark.parametrize("provider", ("bedrock", "bedrock_converse", "bedrock_mantle", "databricks")) -def test_unverified_platforms_hold_back_the_system_clear_beta(provider: str) -> None: - assert _bundled_rows()[provider]["mid-conversation-system-clear-at-2026-08-21"] is None - assert ( - update_headers_with_filtered_beta( - headers={"anthropic-beta": "mid-conversation-system-clear-at-2026-08-21"}, provider=provider - ) - == {} - ) diff --git a/tests/unit/test_anthropic_beta_headers_filtering.py b/tests/unit/test_anthropic_beta_headers_filtering.py index deba7fe9587..ec15f0a8dd5 100644 --- a/tests/unit/test_anthropic_beta_headers_filtering.py +++ b/tests/unit/test_anthropic_beta_headers_filtering.py @@ -16,7 +16,10 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest import litellm +from litellm import anthropic_beta_headers_manager from litellm.anthropic_beta_headers_manager import ( + resolve_beta_provider, + is_foundry_api_base, GetAnthropicBetaHeadersConfig, filter_and_transform_beta_headers, get_beta_headers_config, @@ -53,9 +56,7 @@ class TestAnthropicBetaHeadersFiltering: def get_supported_headers(self, provider: str) -> List[str]: """Get headers with non-null values for a provider.""" provider_config = self.config.get(provider, {}) - return [ - header for header, value in provider_config.items() if value is not None - ] + return [header for header, value in provider_config.items() if value is not None] def get_unsupported_headers(self, provider: str) -> List[str]: """Get headers with null values for a provider.""" @@ -65,11 +66,7 @@ class TestAnthropicBetaHeadersFiltering: def get_mapped_headers(self, provider: str) -> Dict[str, str]: """Get mapping of input headers to provider-specific headers.""" provider_config = self.config.get(provider, {}) - return { - header: value - for header, value in provider_config.items() - if value is not None - } + return {header: value for header, value in provider_config.items() if value is not None} @pytest.mark.parametrize( "provider", @@ -82,24 +79,20 @@ class TestAnthropicBetaHeadersFiltering: unsupported_headers = self.get_unsupported_headers(provider) mapped_headers = self.get_mapped_headers(provider) - filtered = filter_and_transform_beta_headers( - beta_headers=all_headers, provider=provider - ) + filtered = filter_and_transform_beta_headers(beta_headers=all_headers, provider=provider) assert ("compact-2026-09-04" in filtered) is (provider == "anthropic") for header in unsupported_headers: - assert ( - header not in filtered - ), f"Unsupported header '{header}' should be filtered out for {provider}" - assert ( - mapped_headers.get(header) not in filtered - ), f"Mapped value of unsupported header '{header}' should not appear for {provider}" + assert header not in filtered, f"Unsupported header '{header}' should be filtered out for {provider}" + assert mapped_headers.get(header) not in filtered, ( + f"Mapped value of unsupported header '{header}' should not appear for {provider}" + ) for header in supported_headers: expected_mapped = mapped_headers[header] - assert ( - expected_mapped in filtered - ), f"Supported header '{header}' should be mapped to '{expected_mapped}' for {provider}" + assert expected_mapped in filtered, ( + f"Supported header '{header}' should be mapped to '{expected_mapped}' for {provider}" + ) @pytest.mark.parametrize( "provider", @@ -114,20 +107,14 @@ class TestAnthropicBetaHeadersFiltering: ] all_headers = self.get_all_beta_headers() + unknown_headers - filtered = filter_and_transform_beta_headers( - beta_headers=all_headers, provider=provider - ) + filtered = filter_and_transform_beta_headers(beta_headers=all_headers, provider=provider) for unknown in unknown_headers: - assert ( - unknown not in filtered - ), f"Unknown header '{unknown}' should be filtered out for {provider}" + assert unknown not in filtered, f"Unknown header '{unknown}' should be filtered out for {provider}" def test_update_request_with_filtered_beta_vertex_ai(self): """Test combined filtering for both HTTP headers and request body betas.""" - headers = { - "anthropic-beta": "files-api-2025-04-14,context-management-2025-06-27,code-execution-2025-05-22" - } + headers = {"anthropic-beta": "files-api-2025-04-14,context-management-2025-06-27,code-execution-2025-05-22"} request_data = { "anthropic_beta": [ "files-api-2025-04-14", @@ -143,9 +130,7 @@ class TestAnthropicBetaHeadersFiltering: ) assert filtered_headers.get("anthropic-beta") == "context-management-2025-06-27" - assert filtered_request_data.get("anthropic_beta") == [ - "context-management-2025-06-27" - ] + assert filtered_request_data.get("anthropic_beta") == ["context-management-2025-06-27"] @pytest.mark.asyncio async def test_anthropic_messages_http_headers_filtering(self): @@ -153,9 +138,7 @@ class TestAnthropicBetaHeadersFiltering: all_headers = self.get_all_beta_headers() unsupported = self.get_unsupported_headers("anthropic") - with patch( - "litellm.llms.custom_httpx.http_handler.get_async_httpx_client" - ) as mock_client_factory: + with patch("litellm.llms.custom_httpx.http_handler.get_async_httpx_client") as mock_client_factory: mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { @@ -191,9 +174,9 @@ class TestAnthropicBetaHeadersFiltering: if beta_header: beta_values = [b.strip() for b in beta_header.split(",")] for unsupported_header in unsupported: - assert ( - unsupported_header not in beta_values - ), f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Anthropic" + assert unsupported_header not in beta_values, ( + f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Anthropic" + ) @pytest.mark.asyncio async def test_azure_ai_messages_http_headers_filtering(self): @@ -201,9 +184,7 @@ class TestAnthropicBetaHeadersFiltering: all_headers = self.get_all_beta_headers() unsupported = self.get_unsupported_headers("azure_ai") - with patch( - "litellm.llms.custom_httpx.http_handler.get_async_httpx_client" - ) as mock_client_factory: + with patch("litellm.llms.custom_httpx.http_handler.get_async_httpx_client") as mock_client_factory: mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { @@ -241,9 +222,9 @@ class TestAnthropicBetaHeadersFiltering: if beta_header: beta_values = [b.strip() for b in beta_header.split(",")] for unsupported_header in unsupported: - assert ( - unsupported_header not in beta_values - ), f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Azure AI" + assert unsupported_header not in beta_values, ( + f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Azure AI" + ) @pytest.mark.asyncio async def test_bedrock_converse_headers_and_body_filtering(self): @@ -256,9 +237,7 @@ class TestAnthropicBetaHeadersFiltering: mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { - "output": { - "message": {"role": "assistant", "content": [{"text": "Hello"}]} - }, + "output": {"message": {"role": "assistant", "content": [{"text": "Hello"}]}}, "stopReason": "end_turn", "usage": {"inputTokens": 10, "outputTokens": 20}, } @@ -290,27 +269,25 @@ class TestAnthropicBetaHeadersFiltering: if beta_header: beta_values = [b.strip() for b in beta_header.split(",")] for unsupported_header in unsupported: - assert ( - unsupported_header not in beta_values - ), f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Bedrock Converse" + assert unsupported_header not in beta_values, ( + f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Bedrock Converse" + ) data = call_kwargs.get("data") if data: body = json.loads(data) - body_beta = body.get("additionalModelRequestFields", {}).get( - "anthropic_beta", [] - ) + body_beta = body.get("additionalModelRequestFields", {}).get("anthropic_beta", []) for unsupported_header in unsupported: - assert ( - unsupported_header not in body_beta - ), f"Unsupported header '{unsupported_header}' should not be in request body for Bedrock Converse" + assert unsupported_header not in body_beta, ( + f"Unsupported header '{unsupported_header}' should not be in request body for Bedrock Converse" + ) for header, mapped_value in mapped_headers.items(): if header in all_headers and mapped_value in body_beta: - assert ( - mapped_value in body_beta - ), f"Supported header '{header}' should be mapped to '{mapped_value}' in request body for Bedrock Converse" + assert mapped_value in body_beta, ( + f"Supported header '{header}' should be mapped to '{mapped_value}' in request body for Bedrock Converse" + ) @pytest.mark.asyncio async def test_vertex_ai_messages_http_headers_filtering(self): @@ -318,9 +295,7 @@ class TestAnthropicBetaHeadersFiltering: all_headers = self.get_all_beta_headers() unsupported = self.get_unsupported_headers("vertex_ai") - with patch( - "litellm.llms.custom_httpx.http_handler.get_async_httpx_client" - ) as mock_client_factory: + with patch("litellm.llms.custom_httpx.http_handler.get_async_httpx_client") as mock_client_factory: mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { @@ -338,9 +313,7 @@ class TestAnthropicBetaHeadersFiltering: mock_client.post = AsyncMock(return_value=mock_response) mock_client_factory.return_value = mock_client - with patch( - "litellm.llms.vertex_ai.vertex_llm_base.VertexBase._ensure_access_token" - ) as mock_token: + with patch("litellm.llms.vertex_ai.vertex_llm_base.VertexBase._ensure_access_token") as mock_token: mock_token.return_value = ("test-token", "test-project") try: @@ -363,9 +336,9 @@ class TestAnthropicBetaHeadersFiltering: if beta_header: beta_values = [b.strip() for b in beta_header.split(",")] for unsupported_header in unsupported: - assert ( - unsupported_header not in beta_values - ), f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Vertex AI" + assert unsupported_header not in beta_values, ( + f"Unsupported header '{unsupported_header}' should not be in HTTP headers for Vertex AI" + ) def test_header_mapping_correctness(self): """Test that headers are mapped correctly for providers with transformations.""" @@ -402,17 +375,15 @@ class TestAnthropicBetaHeadersFiltering: beta_headers=[test_case["input"]], provider=test_case["provider"] ) - assert ( - test_case["expected"] in filtered - ), f"Header '{test_case['input']}' should be mapped to '{test_case['expected']}' for {test_case['provider']}, but got: {filtered}" + assert test_case["expected"] in filtered, ( + f"Header '{test_case['input']}' should be mapped to '{test_case['expected']}' for {test_case['provider']}, but got: {filtered}" + ) def test_filter_and_transform_beta_headers_vertex_ai_keeps_compact(self): """Vertex AI supports compact context edits, so the compact beta header must be forwarded instead of stripped (it was previously mapped to null, which broke compact_20260112 context edits over /v1/messages).""" - filtered = filter_and_transform_beta_headers( - beta_headers=["compact-2026-01-12"], provider="vertex_ai" - ) + filtered = filter_and_transform_beta_headers(beta_headers=["compact-2026-01-12"], provider="vertex_ai") assert filtered == ["compact-2026-01-12"] @@ -486,13 +457,11 @@ class TestAnthropicBetaHeadersFiltering: unsupported = self.get_unsupported_headers(provider) if unsupported: - filtered = filter_and_transform_beta_headers( - beta_headers=unsupported, provider=provider - ) + filtered = filter_and_transform_beta_headers(beta_headers=unsupported, provider=provider) - assert ( - len(filtered) == 0 - ), f"All null-value headers should be filtered out for {provider}, but got: {filtered}" + assert len(filtered) == 0, ( + f"All null-value headers should be filtered out for {provider}, but got: {filtered}" + ) def test_empty_headers_list(self): """Test that empty headers list returns empty result.""" @@ -503,13 +472,9 @@ class TestAnthropicBetaHeadersFiltering: "bedrock", "vertex_ai", ]: - filtered = filter_and_transform_beta_headers( - beta_headers=[], provider=provider - ) + filtered = filter_and_transform_beta_headers(beta_headers=[], provider=provider) - assert ( - len(filtered) == 0 - ), f"Empty headers list should return empty result for {provider}" + assert len(filtered) == 0, f"Empty headers list should return empty result for {provider}" def test_mixed_supported_and_unsupported_headers(self): """Test filtering with a mix of supported, unsupported, and unknown headers.""" @@ -529,20 +494,12 @@ class TestAnthropicBetaHeadersFiltering: test_headers = [supported[0]] + [unsupported[0]] + ["unknown-header-123"] - filtered = filter_and_transform_beta_headers( - beta_headers=test_headers, provider=provider - ) + filtered = filter_and_transform_beta_headers(beta_headers=test_headers, provider=provider) expected_mapped = mapped_headers[supported[0]] - assert ( - expected_mapped in filtered - ), f"Supported header should be in result for {provider}" - assert ( - unsupported[0] not in filtered - ), f"Unsupported header should not be in result for {provider}" - assert ( - "unknown-header-123" not in filtered - ), f"Unknown header should not be in result for {provider}" + assert expected_mapped in filtered, f"Supported header should be in result for {provider}" + assert unsupported[0] not in filtered, f"Unsupported header should not be in result for {provider}" + assert "unknown-header-123" not in filtered, f"Unknown header should not be in result for {provider}" @pytest.mark.parametrize("provider", ["anthropic", "bedrock", "bedrock_mantle", "vertex_ai"]) def test_blank_anthropic_beta_header_is_removed(self, provider): @@ -570,3 +527,77 @@ def test_local_flags_load_bundled_beta_headers_without_fetching(monkeypatch, fla config = get_beta_headers_config(url="https://example.test/anthropic_beta_headers_config.json") remote_get.assert_not_called() assert config == GetAnthropicBetaHeadersConfig.load_local_beta_headers_config() + + +FOUNDRY_API_BASE: Final = "https://my-resource.services.ai.azure.com/anthropic" +DIRECT_API_BASE: Final = "https://api.anthropic.com" +DIRECT_ONLY_BETA: Final = "advisor-tool-2026-03-01" +PRESERVED_THINKING_BETAS: Final = ( + "thinking-binding-controls-2026-08-01", + "mid-conversation-system-clear-at-2026-08-21", + "mid-conversation-tool-changes-2026-07-01", + "mid-conversation-output-config-2026-07-01", + "thinking-display-updates-2026-08-18", +) + + +@pytest.fixture +def bundled_config(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True") + anthropic_beta_headers_manager._BETA_HEADERS_CONFIG = None + + +def test_foundry_deployment_headers_follow_the_azure_ai_row(bundled_config: None) -> None: + headers: Final = {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"} + result: dict[str, str] = update_headers_with_filtered_beta( + headers=headers, provider="anthropic", api_base=FOUNDRY_API_BASE + ) + assert result == {"anthropic-beta": "thinking-binding-controls-2026-08-01"} + + +def test_direct_deployment_headers_keep_every_anthropic_row_beta(bundled_config: None) -> None: + headers: Final = {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"} + result: dict[str, str] = update_headers_with_filtered_beta( + headers=headers, provider="anthropic", api_base=DIRECT_API_BASE + ) + assert result == {"anthropic-beta": f"{DIRECT_ONLY_BETA},thinking-binding-controls-2026-08-01"} + + +def test_foundry_deployment_request_filters_header_and_body_betas_together(bundled_config: None) -> None: + headers: dict[str, str] + data: dict[str, object] + headers, data = update_request_with_filtered_beta( + headers={"anthropic-beta": DIRECT_ONLY_BETA}, + request_data={"anthropic_beta": [DIRECT_ONLY_BETA, "thinking-binding-controls-2026-08-01"], "max_tokens": 8}, + provider="anthropic", + api_base=FOUNDRY_API_BASE, + ) + assert headers == {} + assert data == {"anthropic_beta": ["thinking-binding-controls-2026-08-01"], "max_tokens": 8} + + +PRESERVED_THINKING_BETAS: Final = ( + "thinking-binding-controls-2026-08-01", + "mid-conversation-system-clear-at-2026-08-21", + "mid-conversation-tool-changes-2026-07-01", + "mid-conversation-output-config-2026-07-01", + "thinking-display-updates-2026-08-18", +) + + +@pytest.mark.parametrize("provider", ["anthropic", "azure_ai", "vertex_ai"]) +def test_live_verified_platforms_forward_every_preserved_thinking_beta(bundled_config: None, provider: str) -> None: + result: dict[str, str] = update_headers_with_filtered_beta( + headers={"anthropic-beta": ",".join(PRESERVED_THINKING_BETAS)}, provider=provider + ) + assert sorted(result["anthropic-beta"].split(",")) == sorted(PRESERVED_THINKING_BETAS), result + + +@pytest.mark.parametrize("provider", ["anthropic", "azure_ai", "vertex_ai", "bedrock", "bedrock_converse"]) +def test_each_row_forwards_exactly_its_verified_preserved_thinking_cells(bundled_config: None, provider: str) -> None: + row: Final[dict[str, str | None]] = GetAnthropicBetaHeadersConfig.load_local_beta_headers_config()[provider] + forwarded: Final = tuple(beta for beta in PRESERVED_THINKING_BETAS if row.get(beta) is not None) + result: dict[str, str] = update_headers_with_filtered_beta( + headers={"anthropic-beta": ",".join(PRESERVED_THINKING_BETAS)}, provider=provider + ) + assert sorted(result["anthropic-beta"].split(",")) == sorted(forwarded), (provider, result)