diff --git a/litellm/anthropic_beta_headers_manager.py b/litellm/anthropic_beta_headers_manager.py index e5ced61d0c4..07730a0fbf8 100644 --- a/litellm/anthropic_beta_headers_manager.py +++ b/litellm/anthropic_beta_headers_manager.py @@ -242,7 +242,10 @@ def get_provider_name(provider: str) -> str: def has_beta_headers_config(provider: str) -> bool: - return get_provider_name(provider) in _load_beta_headers_config() + config: Final = _bundled_beta_headers_config() + aliases: Final = config.get("provider_aliases", {}) + canonical_provider: Final = (aliases.get(provider) if isinstance(aliases, Mapping) else None) or provider + return canonical_provider in config def filter_and_transform_beta_headers( diff --git a/litellm/llms/azure_ai/anthropic/transformation.py b/litellm/llms/azure_ai/anthropic/transformation.py index dc944ce6cbe..6516827359d 100644 --- a/litellm/llms/azure_ai/anthropic/transformation.py +++ b/litellm/llms/azure_ai/anthropic/transformation.py @@ -110,7 +110,8 @@ class AzureAnthropicConfig(AnthropicConfig): thinking=optional_params.get("thinking"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # optional_params is the base config's bare dict ) # Merge headers - Azure auth (api-key or Authorization) takes precedence - headers = {**anthropic_headers, **headers} + merged_beta_header: Final = {k: v for k, v in anthropic_headers.items() if k == "anthropic-beta"} + headers = {**anthropic_headers, **headers, **merged_beta_header} # Ensure anthropic-version header is set if "anthropic-version" not in headers: diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index da723f20ee7..4522d4172d2 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -18,6 +18,7 @@ from typing_extensions import ReadOnly, TypedDict import litellm from litellm._logging import verbose_logger from litellm.anthropic_beta_gated_fields import active_beta_values, log_beta_gated_strips, strip_beta_gated_thinking +from litellm.anthropic_beta_headers_manager import filter_and_transform_beta_headers from litellm.constants import ( BEDROCK_MIN_THINKING_BUDGET_TOKENS, RESPONSE_FORMAT_TOOL_NAME, @@ -1830,11 +1831,14 @@ class AmazonConverseConfig(BaseConfig): base_model: Final = BedrockModelInfo.get_base_model(model) header_beta_list: Final = get_anthropic_beta_from_headers(headers or {}) - client_beta_list: Final = header_beta_list or sorted( - active_beta_values( - {}, - additional_request_params.get("anthropic_beta"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # additional_request_params is a bare dict - ) + client_beta_list: Final = header_beta_list or filter_and_transform_beta_headers( + sorted( + active_beta_values( + {}, + additional_request_params.get("anthropic_beta"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # additional_request_params is a bare dict + ) + ), + provider="bedrock_converse", ) eager_beta: Final = ( (ANTHROPIC_FINE_GRAINED_TOOL_STREAMING_BETA_HEADER,) diff --git a/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_transformation.py b/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_transformation.py index 90d0df2b113..8927e5aea2d 100644 --- a/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_transformation.py +++ b/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_transformation.py @@ -7,9 +7,11 @@ from unittest.mock import patch import httpx import pytest +import respx from pydantic import JsonValue import litellm +from litellm import anthropic_beta_headers_manager from litellm.litellm_core_utils.litellm_logging import Logging from litellm.llms.anthropic.messages.native.transformation import ( AnthropicMessagesConfig, @@ -521,3 +523,29 @@ def test_a_provider_without_a_beta_headers_row_forwards_beta_gated_fields_under_ ) assert request["thinking"] == thinking assert request["messages"][-1]["output_config"] == {"effort": "low"} + + +def test_a_first_party_messages_request_never_fetches_the_remote_beta_headers_config( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", raising=False) + monkeypatch.setattr(anthropic_beta_headers_manager, "_BETA_HEADERS_CONFIG", None) + + with respx.mock(assert_all_called=False) as router: + remote_config: Final = router.get(litellm.anthropic_beta_headers_url).mock( + return_value=httpx.Response(200, json={}) + ) + request: Final = AnthropicMessagesConfig().transform_anthropic_messages_request( + model="claude-opus-4-8", + messages=[ + {"role": "user", "content": "hi"}, + {"role": "system", "content": "be terse", "clear_at": "end_turn"}, + {"role": "user", "content": "go"}, + ], + anthropic_messages_optional_request_params={"max_tokens": 64}, + litellm_params=GenericLiteLLMParams(drop_params=True), + headers={}, + ) + + assert request["messages"][1] == {"role": "system", "content": "be terse"} + assert not remote_config.called diff --git a/tests/unit/llms/azure_ai/claude/test_azure_anthropic_handler.py b/tests/unit/llms/azure_ai/claude/test_azure_anthropic_handler.py index 994ef2a5ca2..d886303da8f 100644 --- a/tests/unit/llms/azure_ai/claude/test_azure_anthropic_handler.py +++ b/tests/unit/llms/azure_ai/claude/test_azure_anthropic_handler.py @@ -310,3 +310,34 @@ def test_completion_adds_the_binding_beta_so_block_binding_is_kept( sent: Final = json.loads(route.calls.last.request.content) assert sent["thinking"] == {"type": "enabled", "budget_tokens": 1024, "block_binding": block_binding} assert "thinking-binding-controls-2026-08-01" in route.calls.last.request.headers["anthropic-beta"].split(",") + + +@respx.mock +def test_completion_sends_the_binding_beta_next_to_the_callers_own_betas(local_beta_headers_config: None) -> None: + route: Final = respx.post("https://example-resource.services.ai.azure.com/anthropic/v1/messages").mock( + return_value=Response( + 200, + json={ + "id": "msg_bound", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-6", + "content": [{"type": "text", "text": "Hello"}], + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 1, "output_tokens": 1}, + }, + ) + ) + + litellm.completion( + model="azure_ai/claude-sonnet-4-6", + messages=[{"role": "user", "content": "hi"}], + api_base="https://example-resource.services.ai.azure.com/anthropic", + api_key="azure-test-key", + thinking={"type": "enabled", "budget_tokens": 1024, "block_binding": {"prefix_mismatch_behavior": "error"}}, + extra_headers={"anthropic-beta": "context-1m-2025-08-07"}, + ) + + sent_betas: Final = set(route.calls.last.request.headers["anthropic-beta"].split(",")) + assert {"context-1m-2025-08-07", "thinking-binding-controls-2026-08-01"} <= sent_betas diff --git a/tests/unit/llms/bedrock/chat/test_converse_transformation.py b/tests/unit/llms/bedrock/chat/test_converse_transformation.py index 403417709f0..2d42d0bf4e0 100644 --- a/tests/unit/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/unit/llms/bedrock/chat/test_converse_transformation.py @@ -10884,6 +10884,19 @@ def test_converse_keeps_beta_gated_thinking_fields_the_body_anthropic_beta_carri } +def test_converse_drops_body_betas_the_bedrock_converse_row_rejects_when_adding_thinking_betas( + local_beta_headers_config: None, +) -> None: + fields: Final = _converse_bound_thinking_request( + None, body_betas=("per-turn-control-2026-07-01", "context-1m-2025-08-07") + ) + assert sorted(fields["anthropic_beta"]) == [ + "context-1m-2025-08-07", + "thinking-binding-controls-2026-08-01", + "thinking-display-updates-2026-08-18", + ] + + def test_converse_sends_only_the_header_betas_when_header_and_body_both_carry_betas( monkeypatch: pytest.MonkeyPatch, ) -> None: