mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(exceptions): keep a refused connection an APIConnectionError
#38318 taught exception_type to map upstream status codes for providers with no branch of their own. It reads the status code off the exception, but _handle_error stamps 500 onto every failure that never carried one, so a refused connection reached the mapper wearing a status code nothing upstream had sent, and came back as InternalServerError instead of APIConnectionError. The two are not interchangeable to a caller: a 5xx says the provider answered and failed, which the router treats as a reason to cool the deployment down, while a connection error says the request never landed. BaseLLMException now records whether its status code was received or synthesized, _handle_error sets that when it invents the 500, and the status mapper declines to act on a code litellm made up, so those failures fall through to the APIConnectionError the branch was always meant to produce. Genuine upstream 5xx responses are untouched, which the second test pins. The search transformation assertion #38318 had loosened to InternalServerError goes back to APIConnectionError for the same reason.
This commit is contained in:
parent
3300fc3a96
commit
a4049b730c
5 changed files with 63 additions and 4 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue