From 7d0bb9853637975effb2b4f83e04e9a7195be8de Mon Sep 17 00:00:00 2001 From: Cayman Roden Date: Wed, 3 Jun 2026 05:10:59 -0700 Subject: [PATCH] test: cover typed-exception mapping branches for #24551 Co-Authored-By: Claude Sonnet 4.6 --- .../test_handle_error_typed_exceptions.py | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/tests/litellm/test_handle_error_typed_exceptions.py b/tests/litellm/test_handle_error_typed_exceptions.py index 1910fe9e9a7..05a3404a1bf 100644 --- a/tests/litellm/test_handle_error_typed_exceptions.py +++ b/tests/litellm/test_handle_error_typed_exceptions.py @@ -6,6 +6,8 @@ exceptions (RateLimitError, ContextWindowExceededError, etc.), breaking Router retry/fallback logic. """ +from unittest.mock import AsyncMock, MagicMock, patch + import httpx import pytest @@ -138,3 +140,204 @@ class TestHandleErrorTypedExceptions: model="claude-sonnet-4-6", custom_llm_provider="anthropic", ) + + +class TestHandleErrorWithProviderConfig: + """Exercise the provider_config (non-None) branch of _handle_error.""" + + def setup_method(self): + self.handler = BaseLLMHTTPHandler() + + def _make_status_error(self, status_code=429, text="rate limit exceeded"): + mock_response = httpx.Response( + status_code=status_code, + request=httpx.Request("POST", "https://api.anthropic.com/v1/messages"), + text=text, + ) + return httpx.HTTPStatusError( + message=f"{status_code}", + request=mock_response.request, + response=mock_response, + ) + + def test_provider_config_without_model_raises_raw_exception(self): + """provider_config set, no model/provider: raise the raw exception from + get_error_class unchanged (covers the else branch and the final raise).""" + raw = BaseLLMException( + status_code=429, + message="rate limit exceeded", + headers={}, + ) + provider_config = MagicMock() + provider_config.get_error_class.return_value = raw + + with pytest.raises(BaseLLMException) as exc_info: + self.handler._handle_error( + e=self._make_status_error(), + provider_config=provider_config, + ) + + # The exact object from get_error_class is re-raised (no typed mapping). + assert exc_info.value is raw + provider_config.get_error_class.assert_called_once() + call_kwargs = provider_config.get_error_class.call_args.kwargs + assert call_kwargs["status_code"] == 429 + assert "error_message" in call_kwargs + assert "headers" in call_kwargs + + def test_provider_config_with_model_maps_to_typed_exception(self): + """provider_config set plus model/provider: the raw exception is mapped to + a typed litellm exception via exception_type.""" + raw = BaseLLMException( + status_code=429, + message="rate_limit_error: too many requests", + headers={}, + ) + provider_config = MagicMock() + provider_config.get_error_class.return_value = raw + + with pytest.raises(litellm.RateLimitError): + self.handler._handle_error( + e=self._make_status_error(), + provider_config=provider_config, + model="claude-sonnet-4-6", + custom_llm_provider="anthropic", + ) + + +class TestAnthropicMessagesRetryErrorMapping: + """Drive both except branches of the anthropic messages HTTP retry helper.""" + + def setup_method(self): + self.handler = BaseLLMHTTPHandler() + + def _make_provider_config(self): + provider_config = MagicMock() + # max_attempts = max(1, 1) -> first attempt is the last, so the retry + # short-circuit is False and execution falls through to the raise. + provider_config.max_retry_on_anthropic_messages_http_error = 1 + provider_config.should_retry_anthropic_messages_on_http_error.return_value = ( + False + ) + provider_config.get_error_class.side_effect = ( + lambda error_message, status_code, headers: BaseLLMException( + status_code=status_code, + message=error_message, + headers=headers, + ) + ) + return provider_config + + async def _call_retry_helper(self, async_httpx_client, provider_config): + return await self.handler._async_post_anthropic_messages_with_http_error_retry( + async_httpx_client=async_httpx_client, + request_url="https://api.anthropic.com/v1/messages", + headers={}, + signed_json_body=None, + request_body={"model": "claude-sonnet-4-6"}, + stream=False, + logging_obj=MagicMock(), + provider_config=provider_config, + litellm_params=litellm.types.router.GenericLiteLLMParams(), + api_key="sk-test", + model="claude-sonnet-4-6", + custom_llm_provider="anthropic", + ) + + async def test_http_status_error_maps_to_typed_exception(self): + """The except httpx.HTTPStatusError branch re-raises through _handle_error + with model/custom_llm_provider, producing a typed exception.""" + mock_response = httpx.Response( + status_code=429, + request=httpx.Request("POST", "https://api.anthropic.com/v1/messages"), + text="rate_limit_error: too many requests", + ) + status_error = httpx.HTTPStatusError( + message="429", + request=mock_response.request, + response=mock_response, + ) + async_httpx_client = MagicMock() + async_httpx_client.post = AsyncMock(side_effect=status_error) + + with pytest.raises(litellm.RateLimitError): + await self._call_retry_helper( + async_httpx_client, self._make_provider_config() + ) + + async def test_generic_exception_maps_to_typed_exception(self): + """The bare except Exception branch re-raises through _handle_error with + model/custom_llm_provider, producing a mapped litellm exception.""" + async_httpx_client = MagicMock() + async_httpx_client.post = AsyncMock(side_effect=ValueError("boom")) + + provider_config = self._make_provider_config() + + with patch.object( + self.handler, "_handle_error", wraps=self.handler._handle_error + ) as spy: + # A generic exception defaults to status 500, mapping to InternalServerError. + with pytest.raises(litellm.InternalServerError): + await self._call_retry_helper(async_httpx_client, provider_config) + + # model and custom_llm_provider were threaded through to _handle_error. + spy.assert_called_once() + kwargs = spy.call_args.kwargs + assert kwargs["model"] == "claude-sonnet-4-6" + assert kwargs["custom_llm_provider"] == "anthropic" + assert isinstance(kwargs["e"], ValueError) + + +class TestAnthropicMessagesHandlerThreadsProvider: + """Confirm async_anthropic_messages_handler threads custom_llm_provider into + the retry helper (the call-site kwarg).""" + + def setup_method(self): + self.handler = BaseLLMHTTPHandler() + + async def test_custom_llm_provider_passed_to_retry_helper(self): + provider_config = MagicMock() + provider_config.validate_anthropic_messages_environment.return_value = ( + {}, + "https://api.anthropic.com", + ) + provider_config.sign_request.return_value = ({}, None) + provider_config.transform_anthropic_messages_request.return_value = { + "model": "claude-sonnet-4-6" + } + provider_config.get_complete_url.return_value = ( + "https://api.anthropic.com/v1/messages" + ) + + logging_obj = MagicMock() + + with patch.object( + self.handler, + "_async_post_anthropic_messages_with_http_error_retry", + new=AsyncMock(return_value=MagicMock()), + ) as retry_mock: + with patch( + "litellm.llms.custom_httpx.llm_http_handler.update_headers_with_filtered_beta", + return_value={}, + ): + try: + await self.handler.async_anthropic_messages_handler( + model="claude-sonnet-4-6", + messages=[{"role": "user", "content": "hi"}], + anthropic_messages_provider_config=provider_config, + anthropic_messages_optional_request_params={}, + custom_llm_provider="anthropic", + litellm_params=litellm.types.router.GenericLiteLLMParams(), + logging_obj=logging_obj, + client=None, + api_key="sk-test", + api_base="https://api.anthropic.com", + stream=False, + ) + except Exception: + # Post-response transform/logging may fail against mocks; we only + # assert the retry helper was reached with the threaded kwarg. + pass + + retry_mock.assert_called_once() + assert retry_mock.call_args.kwargs["custom_llm_provider"] == "anthropic"