From c0fd8f6012f9ff2a8fe6681770439d54ec915bf8 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:03:10 -0700 Subject: [PATCH] fix(anthropic): merge a case-variant Anthropic-Beta client header instead of clobbering it --- .../messages/transformation.py | 23 +++++++++++-------- ...est_anthropic_messages_per_turn_control.py | 12 ++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 67cda1984cb..27cdac34116 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -689,16 +689,20 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): """ beta_values: Final[set] = set() - # Get existing beta headers if any - existing_beta: Final = headers.get("anthropic-beta") - if existing_beta: - beta_values.update(b.strip() for b in existing_beta.split(",")) + existing_beta: Final = tuple( + piece.strip() + for key, value in headers.items() + if key.lower() == "anthropic-beta" + for piece in value.split(",") + if piece.strip() + ) + beta_values.update(existing_beta) # Check for context management context_management_param: Final = optional_params.get("context_management") if context_management_param is not None: # Check edits array for compact_20260112 type - edits: Final = context_management_param.get("edits", []) + edits: Final = context_management_param.get("edits", ()) has_compact = False has_other = False @@ -740,7 +744,8 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): if AnthropicModelInfo().is_tool_search_used(tools): beta_values.add(get_tool_search_beta_header(custom_llm_provider)) - if beta_values: - headers["anthropic-beta"] = ",".join(sorted(beta_values)) - - return headers + if not beta_values: + return headers + merged: Final = {key: value for key, value in headers.items() if key.lower() != "anthropic-beta"} + merged["anthropic-beta"] = ",".join(sorted(beta_values)) + return merged diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_per_turn_control.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_per_turn_control.py index 7ed3526c4b6..d444258d02f 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_per_turn_control.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_per_turn_control.py @@ -83,6 +83,18 @@ def test_forwarded_client_betas_survive_alongside_the_added_one(): assert PER_TURN_CONTROL in _betas(headers) +def test_case_variant_client_beta_header_is_merged(): + """A client or config can spell the header ``Anthropic-Beta``; the proxy forwards it as + is, so the merge must read it whatever the casing and write one canonical header instead + of a lowercase one that clobbers it.""" + headers = _validate( + _claude_code_turn({"effort": "low"}), headers={"Anthropic-Beta": "interleaved-thinking-2025-05-14"} + ) + + assert [key for key in headers if key.lower() == "anthropic-beta"] == ["anthropic-beta"] + assert _betas(headers) == {"interleaved-thinking-2025-05-14", PER_TURN_CONTROL} + + def test_added_per_turn_control_beta_survives_the_anthropic_allowlist(): """The proxy filters ``anthropic-beta`` against the bundled allowlist right after the headers are built. A name missing from it is dropped silently, which would turn