From 2845677822f9d1c3aa92e991430663fdb5d2c432 Mon Sep 17 00:00:00 2001 From: Nate Armstrong Date: Thu, 12 Nov 2026 09:29:37 -0800 Subject: [PATCH] fix(messages): name the deployment on a rust stream's transport error (#51358) When the upstream connection drops or stalls mid-stream on the Rust Messages path, the caller gets the raw httpx error without litellm_debug_info, so the llm_exceptions alert names no API base, model group or deployment. Python's stream iterator attaches that info to the same error, and the Rust host now does too. --- litellm/rust_bridge/host/routes/messages.py | 23 ++++++++++-- .../rust_bridge/host/routes/test_messages.py | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/litellm/rust_bridge/host/routes/messages.py b/litellm/rust_bridge/host/routes/messages.py index 48de9b0ac03..36cad1cf93e 100644 --- a/litellm/rust_bridge/host/routes/messages.py +++ b/litellm/rust_bridge/host/routes/messages.py @@ -28,6 +28,7 @@ from litellm.litellm_core_utils.core_helpers import ( proxy_request_headers, stream_cost_injection_requested, ) +from litellm.litellm_core_utils.exception_mapping_utils import with_deployment_debug_info from litellm.litellm_core_utils.get_llm_provider_logic import get_provider_affinity_header from litellm.litellm_core_utils.get_provider_specific_headers import ProviderSpecificHeaderUtils from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj @@ -768,6 +769,20 @@ def _log_resolved_call(request: Mapping[str, object], request_provider: str) -> ) +def _with_deployment_debug_info(error: Exception, request: Mapping[str, object], request_provider: str) -> Exception: + logger: Final = request.get("litellm_logging_obj") + if not isinstance(logger, LiteLLMLoggingObj): + return error + details: Final = logger.model_call_details + provider: Final = details.get("custom_llm_provider") + return with_deployment_debug_info( + error, + logger.model, + provider if isinstance(provider, str) and provider else request_provider, + details.get("litellm_params"), + ) + + def map_failure( error: Exception, request: Mapping[str, object], request_provider: str, asynchronous: bool = False ) -> Exception: @@ -775,12 +790,16 @@ def map_failure( if getattr(error, "messages_timeout", None) == "Body": # Python reads a stream's body outside its HTTP client's error mapping, so the transport's # own timeout reaches the caller unmapped. - return httpx.ReadTimeout(_BODY_READ_TIMEOUT if http_transport.uses_aiohttp_transport() else "") + return _with_deployment_debug_info( + httpx.ReadTimeout(_BODY_READ_TIMEOUT if http_transport.uses_aiohttp_transport() else ""), + request, + request_provider, + ) body_cut: Final = getattr(error, "messages_body_cut", None) if body_cut is not None and request.get("stream") is True: # Python reads a stream's body outside its HTTP client's error mapping, so a body the # connection cut short reaches the caller as its transport raised it. - return _body_cut(*_BODY_CUT.validate_python(body_cut)) + return _with_deployment_debug_info(_body_cut(*_BODY_CUT.validate_python(body_cut)), request, request_provider) transport_timeout: Final = _TRANSPORT_TIMEOUTS.get(getattr(error, "messages_timeout", None)) if transport_timeout is not None: # Python's `litellm.Timeout` is raised while handling httpx's own timeout, which the proxy's diff --git a/tests/unit/rust_bridge/host/routes/test_messages.py b/tests/unit/rust_bridge/host/routes/test_messages.py index ed60951002d..bf11aa158b9 100644 --- a/tests/unit/rust_bridge/host/routes/test_messages.py +++ b/tests/unit/rust_bridge/host/routes/test_messages.py @@ -523,6 +523,41 @@ def test_a_complete_body_cut_is_a_synthesized_server_error( assert text in str(mapped) +def _deployment_logger() -> Logging: + logger: Final = Logging( + model=_ADAPTIVE_OPUS, + messages=[], + stream=True, + call_type="anthropic_messages", + start_time=datetime.datetime.now(), + litellm_call_id="cut", + function_id="cut", + ) + logger.model_call_details["custom_llm_provider"] = "anthropic" + logger.model_call_details["litellm_params"] = { + "api_base": "https://upstream.example/v1", + "metadata": {"model_group": "claude-group", "deployment": "anthropic/claude-opus-4-6"}, + } + return logger + + +@pytest.mark.parametrize("marker", ("body_cut", "body_timeout")) +def test_a_stream_transport_error_names_the_deployment_like_the_python_stream_iterator(marker: str) -> None: + native: Final = RuntimeError("upstream network error") + if marker == "body_cut": + native.messages_body_cut = (19, None, False) # pyright: ignore[reportAttributeAccessIssue] # marker the native host sets + else: + native.messages_timeout = "Body" # pyright: ignore[reportAttributeAccessIssue] # marker the native host sets + request: Final = {"model": _ADAPTIVE_OPUS, "stream": True, "litellm_logging_obj": _deployment_logger()} + + debug_info: Final = getattr(messages_host.map_failure(native, request, "anthropic"), "litellm_debug_info", None) + + assert isinstance(debug_info, str) + assert "API Base: `https://upstream.example/v1`" in debug_info + assert "model_group: `claude-group`" in debug_info + assert "deployment: `anthropic/claude-opus-4-6`" in debug_info + + _TRANSPORT: Final = "error sending request: client error"