test: cover typed-exception mapping branches for #24551

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Cayman Roden 2026-06-03 05:10:59 -07:00
parent fbf09ec127
commit 7d0bb98536

View file

@ -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"