diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 5b1b663e85..86378b97d2 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -934,8 +934,15 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) return tools - def _ensure_context_management_beta_header(self, headers: dict) -> None: - beta_value = ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value + def _ensure_beta_header(self, headers: dict, beta_value: str) -> None: + """ + Ensure a beta header value is present in the anthropic-beta header. + Merges with existing values instead of overriding them. + + Args: + headers: Dictionary of headers to update + beta_value: The beta header value to add + """ existing_beta = headers.get("anthropic-beta") if existing_beta is None: headers["anthropic-beta"] = beta_value @@ -944,6 +951,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if beta_value not in existing_values: headers["anthropic-beta"] = f"{existing_beta}, {beta_value}" + def _ensure_context_management_beta_header(self, headers: dict) -> None: + beta_value = ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value + self._ensure_beta_header(headers, beta_value) + def update_headers_with_optional_anthropic_beta( self, headers: dict, optional_params: dict ) -> dict: @@ -960,20 +971,20 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if tool.get("type", None) and tool.get("type").startswith( ANTHROPIC_HOSTED_TOOLS.WEB_FETCH.value ): - headers["anthropic-beta"] = ( - ANTHROPIC_BETA_HEADER_VALUES.WEB_FETCH_2025_09_10.value + self._ensure_beta_header( + headers, ANTHROPIC_BETA_HEADER_VALUES.WEB_FETCH_2025_09_10.value ) elif tool.get("type", None) and tool.get("type").startswith( ANTHROPIC_HOSTED_TOOLS.MEMORY.value ): - headers["anthropic-beta"] = ( - ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value + self._ensure_beta_header( + headers, ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value ) if optional_params.get("context_management") is not None: self._ensure_context_management_beta_header(headers) if optional_params.get("output_format") is not None: - headers["anthropic-beta"] = ( - ANTHROPIC_BETA_HEADER_VALUES.STRUCTURED_OUTPUT_2025_09_25.value + self._ensure_beta_header( + headers, ANTHROPIC_BETA_HEADER_VALUES.STRUCTURED_OUTPUT_2025_09_25.value ) return headers diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index e7a123aa8c..7e96c4634f 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -681,6 +681,73 @@ def test_anthropic_chat_headers_add_context_management_beta(): assert headers["anthropic-beta"] == "context-management-2025-06-27" +def test_anthropic_beta_header_merging_with_output_format(): + """ + Test that anthropic-beta headers from extra_headers are merged with + output_format beta headers instead of being overridden. + + This is a regression test for: https://github.com/BerriAI/litellm/issues/... + When using response_format with a Pydantic model AND extra_headers with + anthropic-beta (e.g., for context-1m extension), both beta headers should + be present in the final request. + """ + config = AnthropicConfig() + + # Simulate headers that already have the context-1m beta header from extra_headers + headers = {"anthropic-beta": "context-1m-2025-08-07"} + + # Simulate output_format being set (happens when using response_format with Sonnet 4.5) + optional_params = { + "output_format": { + "type": "json_schema", + "schema": {"type": "object", "properties": {}} + } + } + + result_headers = config.update_headers_with_optional_anthropic_beta( + headers, optional_params + ) + + # Both beta headers should be present + beta_value = result_headers["anthropic-beta"] + assert "context-1m-2025-08-07" in beta_value, \ + f"User's context-1m beta header missing from: {beta_value}" + assert "structured-outputs-2025-11-13" in beta_value, \ + f"Structured output beta header missing from: {beta_value}" + + +def test_anthropic_beta_header_merging_with_multiple_features(): + """ + Test that multiple beta headers can be merged when using multiple features. + """ + config = AnthropicConfig() + + # Start with a user-provided beta header + headers = {"anthropic-beta": "context-1m-2025-08-07"} + + # Use multiple features that require beta headers + optional_params = { + "output_format": { + "type": "json_schema", + "schema": {"type": "object", "properties": {}} + }, + "context_management": _sample_context_management_payload(), + "tools": [{"type": "web_fetch_20250910", "name": "web_fetch"}] + } + + result_headers = config.update_headers_with_optional_anthropic_beta( + headers, optional_params + ) + + beta_value = result_headers["anthropic-beta"] + + # All beta headers should be present + assert "context-1m-2025-08-07" in beta_value + assert "structured-outputs-2025-11-13" in beta_value + assert "context-management-2025-06-27" in beta_value + assert "web-fetch-2025-09-10" in beta_value + + def test_anthropic_chat_transform_request_includes_context_management(): config = AnthropicConfig() headers = {}