From 2ab8203fae70742be780092f27e40cf214c6f2f0 Mon Sep 17 00:00:00 2001 From: Xiaohan Fu Date: Tue, 27 Jan 2026 02:43:08 +0000 Subject: [PATCH] anthropic request body got mutated fix --- .../chat/guardrail_translation/handler.py | 3 +- .../test_anthropic_guardrail_handler.py | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/chat/guardrail_translation/handler.py b/litellm/llms/anthropic/chat/guardrail_translation/handler.py index f1f278e75ac..a3a3d56a136 100644 --- a/litellm/llms/anthropic/chat/guardrail_translation/handler.py +++ b/litellm/llms/anthropic/chat/guardrail_translation/handler.py @@ -76,7 +76,8 @@ class AnthropicMessagesHandler(BaseTranslation): chat_completion_compatible_request = ( LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai( - anthropic_message_request=cast(AnthropicMessagesRequest, data) + # Use a shallow copy to avoid mutating request data (pop on litellm_metadata). + anthropic_message_request=cast(AnthropicMessagesRequest, data.copy()) ) ) diff --git a/tests/test_litellm/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py b/tests/test_litellm/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py index f8632105da5..82517b7af9e 100644 --- a/tests/test_litellm/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py @@ -37,6 +37,26 @@ class MockPassThroughGuardrail(CustomGuardrail): return inputs +class MockDynamicGuardrail(CustomGuardrail): + """Mock guardrail that records dynamic params from request metadata.""" + + def __init__(self, guardrail_name: str): + super().__init__(guardrail_name=guardrail_name) + self.dynamic_params: Optional[dict] = None + + async def apply_guardrail( + self, + inputs: GenericGuardrailAPIInputs, + request_data: dict, + input_type: Literal["request", "response"], + logging_obj: Optional[Any] = None, + ) -> GenericGuardrailAPIInputs: + self.dynamic_params = self.get_guardrail_dynamic_request_body_params( + request_data + ) + return inputs + + class TestAnthropicMessagesHandlerStreamingOutputProcessing: """Test streaming output processing functionality""" @@ -70,6 +90,35 @@ class TestAnthropicMessagesHandlerStreamingOutputProcessing: # Should return the responses unchanged assert result == responses_so_far + +class TestAnthropicMessagesHandlerInputProcessing: + """Test input processing preserves litellm_metadata for dynamic guardrails.""" + + @pytest.mark.asyncio + async def test_process_input_messages_preserves_litellm_metadata_guardrails(self): + handler = AnthropicMessagesHandler() + guardrail = MockDynamicGuardrail(guardrail_name="cygnal-monitor") + + data = { + "model": "claude-3-5-sonnet-20241022", + "messages": [{"role": "user", "content": "hello"}], + "litellm_metadata": { + "guardrails": [ + { + "cygnal-monitor": { + "extra_body": {"policy_id": "policy-123"} + } + } + ] + }, + } + + with patch("litellm.proxy.proxy_server.premium_user", True): + await handler.process_input_messages(data=data, guardrail_to_apply=guardrail) + + assert data.get("litellm_metadata", {}).get("guardrails") + assert guardrail.dynamic_params == {"policy_id": "policy-123"} + @pytest.mark.asyncio async def test_process_output_streaming_response_empty_choices(self): """Test that streaming response with empty choices doesn't raise IndexError