mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
Merge 9e650d138a into 87e961fad0
This commit is contained in:
commit
8b4ac9f52b
2 changed files with 101 additions and 11 deletions
|
|
@ -178,6 +178,23 @@ def _default_cached_client_timeout() -> httpx.Timeout:
|
|||
return httpx.Timeout(timeout=configured, connect=HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS)
|
||||
|
||||
|
||||
def _resolve_effective_timeout(
|
||||
timeout: float | httpx.Timeout | None,
|
||||
fallback_timeout: float | httpx.Timeout | None = None,
|
||||
) -> float | httpx.Timeout:
|
||||
"""Resolve the timeout actually in effect for a request.
|
||||
|
||||
`None` means "use the client default", which `create_client` maps to
|
||||
`_DEFAULT_TIMEOUT`. An explicit `timeout=None` handed to httpx bypasses the
|
||||
client default entirely (no timeouts are enforced), so `None` must be
|
||||
resolved before building a request or rendering a Timeout error message.
|
||||
`fallback_timeout` lets error messages preserve a handler-level timeout.
|
||||
"""
|
||||
if timeout is not None:
|
||||
return timeout
|
||||
return fallback_timeout if fallback_timeout is not None else _DEFAULT_TIMEOUT
|
||||
|
||||
|
||||
_CLIENT_REFCOUNT_WHEN_HANDLER_IS_SOLE_REFERRER: Final = 2
|
||||
|
||||
|
||||
|
|
@ -827,7 +844,7 @@ class AsyncHTTPHandler:
|
|||
start_time: Final = time.time()
|
||||
try:
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
timeout = _resolve_effective_timeout(self.timeout)
|
||||
|
||||
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
|
||||
request_data, request_content = _prepare_request_data_and_content(data, content)
|
||||
|
|
@ -899,7 +916,7 @@ class AsyncHTTPHandler:
|
|||
_follow_redirects: Final = follow_redirects if follow_redirects is not None else USE_CLIENT_DEFAULT
|
||||
try:
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
timeout = _resolve_effective_timeout(self.timeout)
|
||||
|
||||
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
|
||||
request_data, request_content = _prepare_request_data_and_content(data, content)
|
||||
|
|
@ -971,7 +988,7 @@ class AsyncHTTPHandler:
|
|||
):
|
||||
try:
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
timeout = _resolve_effective_timeout(self.timeout)
|
||||
|
||||
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
|
||||
request_data, request_content = _prepare_request_data_and_content(data, content)
|
||||
|
|
@ -1036,7 +1053,7 @@ class AsyncHTTPHandler:
|
|||
):
|
||||
try:
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
timeout = _resolve_effective_timeout(self.timeout)
|
||||
|
||||
# Prepare data/content parameters to prevent httpx DeprecationWarning (memory leak fix)
|
||||
request_data, request_content = _prepare_request_data_and_content(data, content)
|
||||
|
|
@ -1540,7 +1557,7 @@ class HTTPHandler:
|
|||
return response
|
||||
except httpx.TimeoutException:
|
||||
raise litellm.Timeout(
|
||||
message=f"Connection timed out after {timeout} seconds.",
|
||||
message=f"Connection timed out after {_resolve_effective_timeout(timeout, self.timeout)} seconds.",
|
||||
model="default-model-name",
|
||||
llm_provider="litellm-httpx-handler",
|
||||
)
|
||||
|
|
@ -1592,7 +1609,7 @@ class HTTPHandler:
|
|||
return response
|
||||
except httpx.TimeoutException:
|
||||
raise litellm.Timeout(
|
||||
message=f"Connection timed out after {timeout} seconds.",
|
||||
message=f"Connection timed out after {_resolve_effective_timeout(timeout, self.timeout)} seconds.",
|
||||
model="default-model-name",
|
||||
llm_provider="litellm-httpx-handler",
|
||||
)
|
||||
|
|
@ -1643,7 +1660,7 @@ class HTTPHandler:
|
|||
return response
|
||||
except httpx.TimeoutException:
|
||||
raise litellm.Timeout(
|
||||
message=f"Connection timed out after {timeout} seconds.",
|
||||
message=f"Connection timed out after {_resolve_effective_timeout(timeout, self.timeout)} seconds.",
|
||||
model="default-model-name",
|
||||
llm_provider="litellm-httpx-handler",
|
||||
)
|
||||
|
|
@ -1695,7 +1712,7 @@ class HTTPHandler:
|
|||
return response
|
||||
except httpx.TimeoutException:
|
||||
raise litellm.Timeout(
|
||||
message=f"Connection timed out after {timeout} seconds.",
|
||||
message=f"Connection timed out after {_resolve_effective_timeout(timeout, self.timeout)} seconds.",
|
||||
model="default-model-name",
|
||||
llm_provider="litellm-httpx-handler",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import pytest
|
|||
from aiohttp import ClientSession, TCPConnector
|
||||
|
||||
import litellm
|
||||
from litellm.constants import COMPLETION_HTTP_FALLBACK_SECONDS, HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS
|
||||
from litellm.llms.custom_httpx.aiohttp_transport import LiteLLMAiohttpTransport
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
_CLIENT_REFCOUNT_WHEN_HANDLER_IS_SOLE_REFERRER,
|
||||
|
|
@ -1724,8 +1725,6 @@ def test_sync_force_ipv4_https_proxy_mount_uses_handler_ca_bundle(
|
|||
handler.close()
|
||||
|
||||
assert response.text == "ok-tls"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_can_refuse_to_follow_a_redirect():
|
||||
"""The client follows redirects by default; a caller uploading to a URL it did not choose must be able to opt out."""
|
||||
|
|
@ -2031,7 +2030,6 @@ def test_post_delay_exceeds_per_request_timeout_raises():
|
|||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
def test_realtime_ssl_for_url_sends_no_tls_argument_for_a_plain_ws_endpoint() -> None:
|
||||
assert (
|
||||
realtime_ssl_for_url("ws://127.0.0.1:8080/ws/google.cloud.aiplatform.v1.LlmBidiService/BidiGenerateContent")
|
||||
|
|
@ -2051,3 +2049,78 @@ def test_realtime_ssl_for_url_turns_ssl_verify_false_into_an_unverified_tls_cont
|
|||
assert isinstance(selected, ssl.SSLContext)
|
||||
assert selected.verify_mode == ssl.CERT_NONE
|
||||
assert selected.check_hostname is False
|
||||
|
||||
|
||||
class _TimeoutRecordingAsyncClient(httpx.AsyncClient):
|
||||
async def send(self, request, **kwargs):
|
||||
self.sent_timeout_extensions = request.extensions.get("timeout")
|
||||
raise httpx.ConnectTimeout("connect timed out", request=request)
|
||||
|
||||
|
||||
class _RecordingSyncClient(httpx.Client):
|
||||
def send(self, request, **kwargs):
|
||||
raise httpx.ReadTimeout("read timed out", request=request)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_post_without_timeout_enforces_default_and_reports_it():
|
||||
"""
|
||||
A handler built without a timeout used to pass timeout=None straight to
|
||||
httpx, which bypasses the client default entirely (no timeouts enforced)
|
||||
and produced "Connection timed out after None seconds." on failure.
|
||||
"""
|
||||
handler = AsyncHTTPHandler()
|
||||
assert handler.timeout is None
|
||||
client = _TimeoutRecordingAsyncClient()
|
||||
handler.client = client
|
||||
|
||||
with pytest.raises(litellm.Timeout) as exc_info:
|
||||
await handler.post("https://example.test/v1/chat", json={"ping": True})
|
||||
|
||||
assert client.sent_timeout_extensions["connect"] == HTTP_HANDLER_CONNECT_TIMEOUT_SECONDS
|
||||
assert client.sent_timeout_extensions["read"] == COMPLETION_HTTP_FALLBACK_SECONDS
|
||||
assert "None seconds" not in str(exc_info.value)
|
||||
assert f"{COMPLETION_HTTP_FALLBACK_SECONDS}" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_post_explicit_timeout_is_enforced_and_reported():
|
||||
handler = AsyncHTTPHandler()
|
||||
handler.client = _TimeoutRecordingAsyncClient()
|
||||
|
||||
with pytest.raises(litellm.Timeout) as exc_info:
|
||||
await handler.post("https://example.test/v1/chat", json={"ping": True}, timeout=3.5)
|
||||
|
||||
assert "None seconds" not in str(exc_info.value)
|
||||
assert "3.5" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_sync_post_timeout_message_reports_client_default_when_timeout_unset():
|
||||
handler = HTTPHandler()
|
||||
handler.client = _RecordingSyncClient()
|
||||
|
||||
with pytest.raises(litellm.Timeout) as exc_info:
|
||||
handler.post("https://example.test/v1/chat", json={"ping": True})
|
||||
|
||||
assert "None seconds" not in str(exc_info.value)
|
||||
assert f"{COMPLETION_HTTP_FALLBACK_SECONDS}" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_sync_post_timeout_message_reports_handler_timeout_when_request_timeout_unset():
|
||||
handler = HTTPHandler(timeout=7.25)
|
||||
handler.client = _RecordingSyncClient()
|
||||
|
||||
with pytest.raises(litellm.Timeout) as exc_info:
|
||||
handler.post("https://example.test/v1/chat", json={"ping": True})
|
||||
|
||||
assert "7.25 seconds" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_sync_post_timeout_message_reports_explicit_timeout():
|
||||
handler = HTTPHandler()
|
||||
handler.client = _RecordingSyncClient()
|
||||
|
||||
with pytest.raises(litellm.Timeout) as exc_info:
|
||||
handler.post("https://example.test/v1/chat", json={"ping": True}, timeout=3.5)
|
||||
|
||||
assert "3.5 seconds" in str(exc_info.value)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue