mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(messages): bill a native read timeout's prompt on the failure row like python
The proxy bills the estimated prompt of a failed call only when the Timeout was caused by an httpx ReadTimeout. The native timeout carried no transport cause, so its failure SpendLogs row and spend counters showed 0 where Python bills the prompt. The native timeout now chains the same httpx timeout Python's client raises. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013FDwtkaNTifWAP4W7JZavE
This commit is contained in:
parent
1d46ade6da
commit
788c4dae51
2 changed files with 19 additions and 0 deletions
|
|
@ -128,6 +128,10 @@ _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
|
||||
_TRANSPORT_TIMEOUTS: Final[Mapping[object, type[httpx.TimeoutException]]] = {
|
||||
"Connection": httpx.ConnectTimeout,
|
||||
"Read": httpx.ReadTimeout,
|
||||
}
|
||||
_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` "
|
||||
|
|
@ -232,6 +236,11 @@ def map_failure(error: Exception, request: Mapping[str, object], request_provide
|
|||
# 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)
|
||||
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
|
||||
# failure spend reads to bill a read timeout's prompt.
|
||||
error.__context__ = transport_timeout(str(error))
|
||||
if getattr(error, "messages_late_rejection", False):
|
||||
_log_resolved_call(request, request_provider)
|
||||
if getattr(error, "messages_request_error", False):
|
||||
|
|
|
|||
|
|
@ -207,6 +207,16 @@ def test_a_native_read_timeout_maps_to_the_public_timeout_with_python_text() ->
|
|||
assert "Read timed out. Timeout passed=1.0" in str(mapped)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("phase", "billed"), (("Read", True), ("Connection", False)))
|
||||
def test_a_native_timeout_is_billed_on_the_failure_row_as_python_bills_it(phase: str, billed: bool) -> None:
|
||||
from litellm.proxy.utils import _provider_billed_the_failure
|
||||
|
||||
mapped: Final = messages_host.map_failure(_marked(phase), {"model": _ADAPTIVE_OPUS, "timeout": 1.0}, "anthropic")
|
||||
|
||||
assert isinstance(mapped, litellm.Timeout)
|
||||
assert _provider_billed_the_failure(mapped) is billed
|
||||
|
||||
|
||||
def test_an_upstream_status_maps_through_the_provider_error_class() -> None:
|
||||
upstream: Final = ValueError(429, "slow down")
|
||||
upstream.headers = [("retry-after", "3")] # pyright: ignore[reportAttributeAccessIssue] # set by the native host
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue