Merge remote-tracking branch 'origin/nate_messages_native_connect_error_class' into nate_messages_integration_local

This commit is contained in:
x 2026-11-06 07:58:07 +00:00
commit 935a6fd0cd
No known key found for this signature in database
2 changed files with 148 additions and 3 deletions

View file

@ -2,7 +2,9 @@ from __future__ import annotations
import datetime
import errno
import ipaddress
import os
import socket
from collections.abc import Callable, Mapping, Sequence
from dataclasses import asdict, dataclass
from typing import Final, TypeAlias, cast # noqa: TID251 # narrows native payloads to public TypedDicts
@ -17,7 +19,7 @@ from litellm.litellm_core_utils.core_helpers import normalize_drop_params
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.reasoning_effort_capability import deployment_reasoning_effort_levels
from litellm.litellm_core_utils.request_timeout_resolver import get_configured_request_timeout
from litellm.llms.anthropic.common_utils import AnthropicError
from litellm.llms.anthropic.common_utils import AnthropicError, anthropic_pass_through_api_base
from litellm.llms.anthropic.messages.native import handler
from litellm.llms.anthropic.messages.native.handler import (
_native_messages_config as native_messages_config, # pyright: ignore[reportPrivateUsage] # the handler's own config lookup
@ -26,6 +28,8 @@ from litellm.llms.anthropic.messages.native.handler import (
_with_deployment_output_effort as _deployment_output_effort, # pyright: ignore[reportPrivateUsage] # the handler's own effort rounding
)
from litellm.llms.anthropic.messages.utils import is_reasoning_auto_summary_enabled
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.custom_httpx.http_handler import get_ssl_configuration
from litellm.rust_bridge import failures
from litellm.rust_bridge.host import callbacks_legacy_python, http_transport
from litellm.rust_bridge.public_call import known_params, optional_str
@ -132,6 +136,13 @@ _exception_type: Final = cast( # cast-ok: the legacy public exception mapper is
Callable[..., Exception], exception_mapping_utils.exception_type
)
_BODY_READ_TIMEOUT: Final = "Timeout on reading data from socket" # aiohttp's SocketTimeoutError text
_UNREACHABLE: Final = "could not reach the provider: "
_SEND_FAILED: Final = "upstream network error: error sending request: client error (SendRequest): "
_DNS_FAILURE: Final = "failed to lookup address information: "
_HTTPX_DNS_ERRNO: Final = {
"Name or service not known": socket.EAI_NONAME,
"Temporary failure in name resolution": socket.EAI_AGAIN,
}
_MISSING_KEY: Final = (
"Missing Anthropic API Key - A call is being made to anthropic but no key is set "
"either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` "
@ -199,6 +210,59 @@ def _body_cut(received: int, expected: int | None, reset: bool) -> httpx.Transpo
return httpx.ReadError(f"{text}. {ConnectionResetError(errno.ECONNRESET, os.strerror(errno.ECONNRESET))!r}")
def _connect_failure(native: str, request: Mapping[str, object], request_provider: str) -> str | None:
api_base: Final = request.get("api_base")
if not isinstance(api_base, str) and request_provider != "anthropic":
return None
refused: Final = "tcp connect error: Connection refused" in native
dns_reason: Final = native.rsplit(_DNS_FAILURE, 1)[1] if _DNS_FAILURE in native else None
tls_eof: Final = native.endswith("tls handshake eof")
if not http_transport.uses_aiohttp_transport():
if refused:
return "All connection attempts failed"
dns_errno: Final = None if dns_reason is None else _HTTPX_DNS_ERRNO.get(dns_reason)
if dns_errno is not None:
return f"[Errno {dns_errno}] {dns_reason}"
return "" if tls_eof else None
url: Final = httpx.URL(api_base if isinstance(api_base, str) else anthropic_pass_through_api_base())
port: Final = url.port or (443 if url.scheme == "https" else 80)
reason: str | None # rebind-ok: one branch per connect failure kind
if refused:
try:
address: Final = ipaddress.ip_address(url.host)
except ValueError:
return None
reason = f"Connect call failed {(str(address), port) if address.version == 4 else (str(address), port, 0, 0)}"
elif dns_reason is not None:
reason = dns_reason
elif tls_eof:
reason = None
else:
return None
ssl_verify: Final = request.get("ssl_verify")
ssl_config: Final = get_ssl_configuration(ssl_verify if isinstance(ssl_verify, bool | str) else None)
return f"Cannot connect to host {url.host}:{port} ssl:{'default' if ssl_config is True else ssl_config} [{reason}]"
def _transport_failure(error: Exception, request: Mapping[str, object], request_provider: str) -> str | None:
"""The text Python's HTTP client raises for a call that failed before any response arrived."""
native: Final = str(error)
if native.startswith(_SEND_FAILED):
reset: Final = native.endswith(f"Connection reset by peer (os error {errno.ECONNRESET})")
closed: Final = native.endswith("connection closed before message completed")
if http_transport.uses_aiohttp_transport():
if reset:
return str(ConnectionResetError(errno.ECONNRESET, os.strerror(errno.ECONNRESET)))
return "Server disconnected" if closed else native
if reset:
return ""
return "Server disconnected without sending a response." if closed else native
if not native.startswith(_UNREACHABLE):
return None
connect: Final = _connect_failure(native, request, request_provider)
return native if connect is None else connect
def _upstream_failure(
error: Exception, request: Mapping[str, object]
) -> tuple[int, str, Sequence[tuple[str, str]]] | None:
@ -215,7 +279,8 @@ def _upstream_failure(
def _provider_error(error: Exception, request: Mapping[str, object], request_provider: str) -> Exception | None:
"""The provider exception Python's `_handle_error` builds for the same failure."""
failure: Final = _upstream_failure(error, request)
transport: Final = _transport_failure(error, request, request_provider)
failure: Final = _upstream_failure(error, request) if transport is None else (500, transport, ())
if failure is None:
return None
status, text, headers = failure
@ -226,13 +291,17 @@ def _provider_error(error: Exception, request: Mapping[str, object], request_pro
request.get("model_info"),
api_base if isinstance(api_base, str) else None,
)
return (
provider_error: Final = (
cast(_ERROR_CLASS, config.get_error_class)( # cast-ok: the config's untyped headers parameter
error_message=text, status_code=status, headers=dict(headers)
)
if config is not None
else AnthropicError(status_code=status, message=text, headers=httpx.Headers(headers))
)
if transport is not None and isinstance(provider_error, BaseLLMException):
# Python's `_handle_error` gives a failure without a response the status 500.
provider_error.status_code_is_synthesized = True
return provider_error
def _log_resolved_call(request: Mapping[str, object], request_provider: str) -> None:

View file

@ -299,3 +299,79 @@ def test_a_stream_body_cut_reaches_the_caller_as_the_transport_raised_it(
assert type(mapped) is getattr(httpx, error_class)
assert str(mapped) == text
_TRANSPORT: Final = "error sending request: client error"
@pytest.mark.parametrize(
("aiohttp", "native", "text"),
(
(
True,
f"could not reach the provider: {_TRANSPORT} (Connect): tcp connect error: Connection refused (os error 111)",
"Cannot connect to host 127.0.0.1:9 ssl:<ssl> [Connect call failed ('127.0.0.1', 9)]",
),
(
True,
f"could not reach the provider: {_TRANSPORT} (Connect): dns error: error resolving DNS: "
"failed to lookup address information: Name or service not known",
"Cannot connect to host 127.0.0.1:9 ssl:<ssl> [Name or service not known]",
),
(
True,
f"could not reach the provider: {_TRANSPORT} (Connect): tls handshake eof",
"Cannot connect to host 127.0.0.1:9 ssl:<ssl> [None]",
),
(
True,
f"upstream network error: {_TRANSPORT} (SendRequest): connection error: Connection reset by peer (os error 104)",
"[Errno 104] Connection reset by peer",
),
(
True,
f"upstream network error: {_TRANSPORT} (SendRequest): connection closed before message completed",
"Server disconnected",
),
(
False,
f"could not reach the provider: {_TRANSPORT} (Connect): tcp connect error: Connection refused (os error 111)",
"All connection attempts failed",
),
(
False,
f"could not reach the provider: {_TRANSPORT} (Connect): dns error: error resolving DNS: "
"failed to lookup address information: Name or service not known",
"[Errno -2] Name or service not known",
),
(False, f"could not reach the provider: {_TRANSPORT} (Connect): tls handshake eof", ""),
(
False,
f"upstream network error: {_TRANSPORT} (SendRequest): connection error: Connection reset by peer (os error 104)",
"",
),
(
False,
f"upstream network error: {_TRANSPORT} (SendRequest): connection closed before message completed",
"Server disconnected without sending a response.",
),
),
)
def test_a_failure_before_any_response_maps_like_the_python_transport_error(
monkeypatch: pytest.MonkeyPatch, aiohttp: bool, native: str, text: str
) -> None:
from litellm.rust_bridge.host import http_transport
monkeypatch.setattr(http_transport, "uses_aiohttp_transport", lambda: aiohttp)
monkeypatch.setattr(messages_host, "get_ssl_configuration", lambda _ssl_verify: "<ssl>")
mapped: Final = messages_host.map_failure(
RuntimeError(native), {"model": _ADAPTIVE_OPUS, "api_base": "http://127.0.0.1:9"}, "anthropic"
)
assert type(mapped) is litellm.InternalServerError
assert mapped.status_code == 500
assert mapped.message == (
f"litellm.InternalServerError: AnthropicException - {text}. Handle with `litellm.InternalServerError`."
)
assert "This looks like a bug in LiteLLM" not in str(mapped)