diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py index effd7dda6a0..24c82986fca 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py @@ -362,6 +362,18 @@ async def anthropic_messages( return response +def _get_mock_response_content( + mock_response: Optional[Union[str, litellm.ModelResponse, Exception, Any]], +) -> Optional[str]: + if isinstance(mock_response, str): + return mock_response + if isinstance(mock_response, litellm.ModelResponse): + first_choice = mock_response.choices[0] + if isinstance(first_choice, litellm.Choices): + return first_choice.message.content + return None + + def validate_anthropic_api_metadata(metadata: Optional[Dict] = None) -> Optional[Dict]: """ Validate Anthropic API metadata - This is done to ensure only allowed `metadata` fields are passed to Anthropic API @@ -466,12 +478,13 @@ def anthropic_messages_handler( if kwargs.get("_websearch_interception_converted_stream", False): litellm_logging_obj.model_call_details["websearch_interception_converted_stream"] = True - if litellm_params.mock_response and isinstance(litellm_params.mock_response, str): + mock_response_content = _get_mock_response_content(litellm_params.mock_response) + if mock_response_content is not None: return mock_response( model=model, messages=messages, max_tokens=max_tokens, - mock_response=litellm_params.mock_response, + mock_response=mock_response_content, ) anthropic_messages_provider_config: Optional[BaseAnthropicMessagesConfig] = None diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index 3327fc39f73..2e8fd6d18c0 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -11,7 +11,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from litellm.anthropic_interface import messages from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler -from litellm.types.utils import Delta, ModelResponse, StreamingChoices +from litellm.types.utils import Choices, Delta, Message, ModelResponse, StreamingChoices def test_anthropic_experimental_pass_through_messages_handler(): @@ -648,6 +648,34 @@ def test_handler_skips_strip_when_presanitized(): assert result is not None +def test_handler_short_circuits_on_model_response_mock_response(): + """Regression test for #31976. BedrockGuardrail with + disable_exception_on_block=True sets a ModelResponse (not a str) as + litellm_params.mock_response to block a request without raising. The + handler must honor it instead of silently calling the real model.""" + from litellm.llms.anthropic.experimental_pass_through.messages import handler + + blocked_response = ModelResponse( + choices=[Choices(message=Message(content="Blocked by guardrail"))], + model="bedrock-guardrail", + ) + + with patch.object( + handler.base_llm_http_handler, + "anthropic_messages_handler", + ) as mock_base_handler: + result = handler.anthropic_messages_handler( + max_tokens=10, + messages=[{"role": "user", "content": "some blocked content"}], + model="anthropic/claude-3-5-sonnet-20241022", + custom_llm_provider="anthropic", + mock_response=blocked_response, + ) + + mock_base_handler.assert_not_called() + assert result["content"][0]["text"] == "Blocked by guardrail" + + def test_presanitized_flag_not_leaked_to_provider_params(): """The private sentinel must be popped, never forwarded as a request param.""" from litellm.llms.anthropic.experimental_pass_through.messages import handler