fix(timeout): preserve httpx.Timeout connect/read for all providers

supports_httpx_timeout was an allowlist of only openai/azure/bedrock.
For every other provider (mistral, anthropic, ...), CompletionTimeout._coerce
collapsed httpx.Timeout(10, connect=1) to the single float 10.0, so the
connect timeout was ignored and stalled TCP connects waited the full
read timeout.

LiteLLM's own httpx handlers accept httpx.Timeout objects, so returning
True for all providers is correct. Update the unit tests that previously
asserted coercion for azure_ai.

Fixes #47822
This commit is contained in:
Om Patil 2026-10-22 01:46:11 +00:00
parent b6f91a221b
commit 7a95037c75
2 changed files with 32 additions and 24 deletions

View file

@ -2848,14 +2848,19 @@ def token_counter(
def supports_httpx_timeout(custom_llm_provider: str) -> bool:
"""
Helper function to know if a provider implementation supports httpx timeout
Helper function to know if a provider implementation supports httpx.Timeout.
LiteLLM's own httpx handlers (and the OpenAI/Azure/Bedrock SDKs) accept
``httpx.Timeout`` objects, preserving connect/read/write/pool components.
Returning False collapses the object to a single float (the read timeout),
which drops the connect timeout and is incorrect for every provider that
uses httpx under the hood.
Historically this was an allowlist of three providers; that caused
connect timeouts to be ignored for mistral and many others. Prefer True
unless a specific provider is known to reject ``httpx.Timeout``.
"""
supported_providers: Final = ["openai", "azure", "bedrock"]
if custom_llm_provider in supported_providers:
return True
return False
return True
def supports_system_messages(model: str, custom_llm_provider: str | None) -> bool:

View file

@ -129,18 +129,20 @@ def test_fallback_600_when_no_global_timeout():
)
def test_httpx_timeout_coerced_for_provider_without_httpx_timeout_support():
def test_httpx_timeout_preserved_for_non_openai_providers():
"""httpx.Timeout must keep connect/read components for all providers (not only openai/azure/bedrock)."""
t = httpx.Timeout(50.0, connect=2.0)
out = CompletionTimeout.resolve(
t,
{},
"azure_ai",
"mistral",
global_timeout=None,
supports_httpx_timeout=supports_httpx_timeout,
stream_kind=StreamKind.NOT_STREAMED,
)
assert out == 50.0
assert not isinstance(out, httpx.Timeout)
assert out is t
assert isinstance(out, httpx.Timeout)
assert out.connect == 2.0
def test_httpx_timeout_preserved_for_openai():
@ -174,21 +176,22 @@ def test_sdk_timeout_preserves_components_for_httpx_providers(
assert resolved.as_dict() == timeout.as_dict()
@pytest.mark.parametrize("read_timeout, expected", ((23.0, 23.0), (None, 600.0)))
def test_sdk_timeout_coerces_read_timeout_for_providers_without_httpx_support(
read_timeout: float | None, expected: float
@pytest.mark.parametrize("read_timeout", (23.0, None))
def test_sdk_timeout_preserves_components_for_all_providers(
read_timeout: float | None,
) -> None:
assert (
CompletionTimeout.resolve(
SDKTimeout(connect=2.0, read=read_timeout, write=5.0, pool=7.0),
{},
"azure_ai",
global_timeout=None,
supports_httpx_timeout=supports_httpx_timeout,
stream_kind=StreamKind.NOT_STREAMED,
)
== expected
"""SDK Timeout objects must keep their components for every provider."""
timeout = SDKTimeout(connect=2.0, read=read_timeout, write=5.0, pool=7.0)
resolved = CompletionTimeout.resolve(
timeout,
{},
"mistral",
global_timeout=None,
supports_httpx_timeout=supports_httpx_timeout,
stream_kind=StreamKind.NOT_STREAMED,
)
assert isinstance(resolved, httpx.Timeout)
assert resolved.as_dict() == timeout.as_dict()
@pytest.mark.parametrize(