diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index 70374f87b99..8f8c955d971 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -2222,6 +2222,8 @@ def _map_exception_by_status( status_code: Final = original_exception.status_code if hasattr(original_exception, "status_code") else None if not isinstance(status_code, int) or status_code < 400: return + if getattr(original_exception, "status_code_is_synthesized", False): + return message: Final = f"{exception_provider} - {error_str}" response: Final = original_exception.response if hasattr(original_exception, "response") else None match status_code: diff --git a/litellm/llms/base_llm/chat/transformation.py b/litellm/llms/base_llm/chat/transformation.py index d147063df73..11c763ceb9a 100644 --- a/litellm/llms/base_llm/chat/transformation.py +++ b/litellm/llms/base_llm/chat/transformation.py @@ -46,8 +46,10 @@ class BaseLLMException(Exception): request: httpx.Request | None = None, response: httpx.Response | None = None, body: dict | None = None, + status_code_is_synthesized: bool = False, ): self.status_code = status_code + self.status_code_is_synthesized = status_code_is_synthesized self.message: str = message self.headers = headers if request: diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index df5365017ac..417ecd80be2 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -5947,11 +5947,13 @@ class BaseLLMHTTPHandler: BaseEvalsAPIConfig, ], ): - status_code = getattr(e, "status_code", 500) + received_status_code: Final = ( + e.response.status_code if isinstance(e, httpx.HTTPStatusError) else getattr(e, "status_code", None) + ) + status_code = received_status_code if isinstance(received_status_code, int) else 500 error_headers = getattr(e, "headers", None) if isinstance(e, httpx.HTTPStatusError): error_text = e.response.text - status_code = e.response.status_code else: error_text = getattr(e, "text", str(e)) error_response: Final = getattr(e, "response", None) @@ -5971,13 +5973,17 @@ class BaseLLMHTTPHandler: status_code=status_code, message=error_text, headers=error_headers, + status_code_is_synthesized=not isinstance(received_status_code, int), ) - raise provider_config.get_error_class( + provider_error: Final = provider_config.get_error_class( error_message=error_text, status_code=status_code, headers=error_headers, ) + if not isinstance(received_status_code, int): + provider_error.status_code_is_synthesized = True + raise provider_error @staticmethod def _append_query_params(url: str, query_params: RealtimeQueryParams | None) -> str: diff --git a/tests/test_litellm/litellm_core_utils/test_exception_mapping_utils.py b/tests/test_litellm/litellm_core_utils/test_exception_mapping_utils.py index 8e89180a9e4..15b7ae9d07a 100644 --- a/tests/test_litellm/litellm_core_utils/test_exception_mapping_utils.py +++ b/tests/test_litellm/litellm_core_utils/test_exception_mapping_utils.py @@ -1163,3 +1163,52 @@ def test_bedrock_mantle_context_overflow_maps_to_context_window_exceeded(): assert excinfo.value.status_code == 400 assert "prompt is too long: 1055489 tokens > 1050000 maximum" in excinfo.value.message + + +def test_branchless_provider_transport_error_maps_to_api_connection_error(): + from litellm.llms.base_llm.chat.transformation import BaseLLMException + + original_exception = BaseLLMException(status_code=500, message="[Errno 111] Connection refused") + original_exception.status_code_is_synthesized = True + + with pytest.raises(litellm.APIConnectionError): + exception_type( + model="test-agent", + original_exception=original_exception, + custom_llm_provider="a2a", + ) + + +def test_branchless_provider_upstream_500_still_maps_to_internal_server_error(): + from litellm.llms.base_llm.chat.transformation import BaseLLMException + + original_exception = BaseLLMException(status_code=500, message="upstream exploded") + + with pytest.raises(litellm.InternalServerError): + exception_type( + model="test-agent", + original_exception=original_exception, + custom_llm_provider="a2a", + ) + + +def test_handle_error_marks_only_a_status_code_it_never_received(): + from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler + + handler = BaseLLMHTTPHandler() + + with pytest.raises(litellm.llms.base_llm.chat.transformation.BaseLLMException) as transport: + raise handler._handle_error(e=httpx.ConnectError("Connection refused"), provider_config=None) + assert transport.value.status_code == 500 + assert transport.value.status_code_is_synthesized is True + + request = httpx.Request(method="POST", url="https://example.invalid") + upstream = httpx.HTTPStatusError( + "server error", + request=request, + response=httpx.Response(status_code=500, request=request, text="upstream exploded"), + ) + with pytest.raises(litellm.llms.base_llm.chat.transformation.BaseLLMException) as received: + raise handler._handle_error(e=upstream, provider_config=None) + assert received.value.status_code == 500 + assert received.value.status_code_is_synthesized is False diff --git a/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py b/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py index 35a54332f66..e6aad7688d1 100644 --- a/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py +++ b/tests/test_litellm/llms/base_llm/search/test_base_search_transformation.py @@ -322,7 +322,7 @@ async def test_query_param_key_not_leaked_with_dummy_caller_key( "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.get", fake_get, ): - with pytest.raises(litellm.InternalServerError): + with pytest.raises(litellm.APIConnectionError): await litellm.asearch( query="secrets", search_provider=provider,