From 48cb0d912f024b1da17e0fe82544a07edf93914a Mon Sep 17 00:00:00 2001 From: Hasnaat Hussain Date: Wed, 12 Aug 2026 22:27:35 +0500 Subject: [PATCH] fix(anthropic): preserve immutable drop params Signed-off-by: Hasnaat Hussain --- .../pass_through/messages/handler.py | 30 ++++++--- ...erimental_pass_through_messages_handler.py | 67 ++++++++++++------- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/litellm/llms/anthropic/pass_through/messages/handler.py b/litellm/llms/anthropic/pass_through/messages/handler.py index 68786e79d77..4f7cc091c81 100644 --- a/litellm/llms/anthropic/pass_through/messages/handler.py +++ b/litellm/llms/anthropic/pass_through/messages/handler.py @@ -680,20 +680,32 @@ def anthropic_messages_handler( or kwargs.get("drop_params") is True ) - if should_drop_params: - anthropic_messages_optional_request_params = _drop_unsupported_anthropic_messages_params( + filtered_anthropic_messages_params: Final = ( + _drop_unsupported_anthropic_messages_params( anthropic_messages_optional_request_params=anthropic_messages_optional_request_params, model=model, custom_llm_provider=custom_llm_provider, additional_drop_params=kwargs.get("additional_drop_params"), ) - if is_reasoning_auto_summary_enabled(): - thinking_param: Final = anthropic_messages_optional_request_params.get("thinking") - if isinstance(thinking_param, dict) and thinking_param.get("type") != "disabled": - anthropic_messages_optional_request_params["thinking"] = { - **thinking_param, - "display": "summarized", + if should_drop_params + else anthropic_messages_optional_request_params + ) + thinking_param: Final = filtered_anthropic_messages_params.get("thinking") + final_anthropic_messages_params: Final = ( + MappingProxyType( + { + **filtered_anthropic_messages_params, + "thinking": { + **thinking_param, + "display": "summarized", + }, } + ) + if is_reasoning_auto_summary_enabled() + and isinstance(thinking_param, dict) + and thinking_param.get("type") != "disabled" + else filtered_anthropic_messages_params + ) resolved_api_base: Final = ( dynamic_api_base @@ -704,7 +716,7 @@ def anthropic_messages_handler( model=model, messages=strip_provider_specific_fields_from_anthropic_messages(messages), anthropic_messages_provider_config=anthropic_messages_provider_config, - anthropic_messages_optional_request_params=dict(anthropic_messages_optional_request_params), + anthropic_messages_optional_request_params=dict(final_anthropic_messages_params), _is_async=is_async, client=client, custom_llm_provider=custom_llm_provider, diff --git a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index 4b8cf805641..05d0257ecec 100644 --- a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -299,11 +299,15 @@ async def test_bedrock_converse_budget_tokens_preserved(): # Verify thinking parameter is passed through with budget_tokens preserved thinking_param = call_kwargs.get("thinking") - assert thinking_param is not None, "thinking parameter should be passed to acompletion" - assert thinking_param.get("type") == "enabled", "thinking.type should be 'enabled'" - assert thinking_param.get("budget_tokens") == 1024, ( - f"thinking.budget_tokens should be 1024, but got {thinking_param.get('budget_tokens')}" - ) + assert ( + thinking_param is not None + ), "thinking parameter should be passed to acompletion" + assert ( + thinking_param.get("type") == "enabled" + ), "thinking.type should be 'enabled'" + assert ( + thinking_param.get("budget_tokens") == 1024 + ), f"thinking.budget_tokens should be 1024, but got {thinking_param.get('budget_tokens')}" def test_openai_model_with_thinking_converts_to_reasoning(): @@ -335,18 +339,23 @@ def test_openai_model_with_thinking_converts_to_reasoning(): call_kwargs = mock_responses.call_args.kwargs # Verify reasoning is set (converted from thinking) - assert "reasoning" in call_kwargs, "reasoning should be passed to litellm.responses" + assert ( + "reasoning" in call_kwargs + ), "reasoning should be passed to litellm.responses" # budget_tokens=1024 -> effort="low" (at the LOW budget threshold) # reasoning_auto_summary is False by default, so no summary key expected_reasoning = {"effort": "low"} assert call_kwargs["reasoning"] == expected_reasoning, ( - f"reasoning should be {expected_reasoning} for budget_tokens=1024, got {call_kwargs.get('reasoning')}" + f"reasoning should be {expected_reasoning} for budget_tokens=1024, " + f"got {call_kwargs.get('reasoning')}" ) assert "summary" not in call_kwargs["reasoning"] # Verify thinking is NOT passed directly to the Responses API - assert "thinking" not in call_kwargs, "thinking should NOT be passed directly to litellm.responses" + assert ( + "thinking" not in call_kwargs + ), "thinking should NOT be passed directly to litellm.responses" class TestThinkingParameterTransformation: @@ -399,7 +408,9 @@ class TestThinkingParameterTransformation: thinking=thinking, model="openai/gpt-5.2", ) - assert result == {"reasoning_effort": {"effort": "high", "summary": "detailed"}} + assert result == { + "reasoning_effort": {"effort": "high", "summary": "detailed"} + } finally: litellm.reasoning_auto_summary = original @@ -597,9 +608,9 @@ class TestThinkingSummaryPreservation: mock_responses.assert_called_once() call_kwargs = mock_responses.call_args.kwargs reasoning = call_kwargs["reasoning"] - assert reasoning["summary"] == "concise", ( - f"Expected summary='concise', got summary='{reasoning.get('summary')}'" - ) + assert ( + reasoning["summary"] == "concise" + ), f"Expected summary='concise', got summary='{reasoning.get('summary')}'" def test_responses_adapter_preserves_summary(self): """translate_thinking_to_reasoning should include summary when user provides it.""" @@ -608,7 +619,9 @@ class TestThinkingSummaryPreservation: ) thinking = {"type": "enabled", "budget_tokens": 5000, "summary": "concise"} - result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking) + result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning( + thinking + ) assert result == {"effort": "high", "summary": "concise"} def test_responses_adapter_no_summary_by_default(self): @@ -622,7 +635,11 @@ class TestThinkingSummaryPreservation: try: litellm.reasoning_auto_summary = False thinking = {"type": "enabled", "budget_tokens": 5000} - result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking) + result = ( + LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning( + thinking + ) + ) assert result == {"effort": "high"} assert result is not None and "summary" not in result finally: @@ -639,7 +656,9 @@ class TestThinkingSummaryPreservation: thinking=thinking, model="openai/gpt-5.2", ) - assert result == {"reasoning_effort": {"effort": "high", "summary": "concise"}} + assert result == { + "reasoning_effort": {"effort": "high", "summary": "concise"} + } def test_translate_thinking_for_model_disabled_stays_plain_string_when_auto_summary_enabled(self): """Disabled thinking must stay a plain string even when reasoning_auto_summary is on.""" @@ -785,7 +804,9 @@ def test_presanitized_flag_not_leaked_to_provider_params(): def fake_base_handler(*args, **kwargs): captured.update(kwargs) - captured["optional"] = kwargs.get("anthropic_messages_optional_request_params", {}) + captured["optional"] = kwargs.get( + "anthropic_messages_optional_request_params", {} + ) return "stub" with patch.object( @@ -1686,18 +1707,12 @@ async def test_anthropic_messages_forwards_safeguards_and_dangerous_tool_use_bet assert response["safeguard_results"] == safeguard_results @pytest.mark.asyncio async def test_anthropic_pass_through_drop_params(monkeypatch): - from unittest.mock import AsyncMock - from litellm.llms.anthropic.experimental_pass_through.messages.handler import ( - anthropic_messages_handler, - ) - - mock_handler = AsyncMock() from litellm.llms.anthropic.experimental_pass_through.messages import handler + mock_handler = AsyncMock() monkeypatch.setattr(handler.base_llm_http_handler, "anthropic_messages_handler", mock_handler) - # Test automatic dropping of context_management for haiku on vertex_ai - await anthropic_messages_handler( + await handler.anthropic_messages_handler( model="vertex_ai/claude-3-haiku-20240307", messages=[{"role": "user", "content": "hello"}], max_tokens=64, @@ -1706,6 +1721,6 @@ async def test_anthropic_pass_through_drop_params(monkeypatch): custom_llm_provider="vertex_ai", is_async=True, ) - called_kwargs = mock_handler.call_args.kwargs - optional_params = called_kwargs.get("anthropic_messages_optional_request_params", {}) + + optional_params = mock_handler.call_args.kwargs.get("anthropic_messages_optional_request_params", {}) assert "context_management" not in optional_params