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.
This commit is contained in:
Nate Armstrong 2026-11-12 09:29:37 -08:00 • committed by GitHub
parent b60df93c46
commit 2845677822
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 2 deletions

View file

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

View file

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