mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
fix: pass request_timeout to httpx in async_anthropic_messages_handler
The async_anthropic_messages_handler was the only path in llm_http_handler.py that did not forward a per-request timeout to the underlying httpx POST call. With no explicit timeout, the cached AsyncHTTPHandler client default of 600 s was used, so a silent upstream (one that accepts the TCP connection but never writes a byte) would hold the request open for up to 10 minutes regardless of litellm_settings.request_timeout. The fix resolves the timeout from kwargs[request_timeout] (set by the proxy from litellm_settings.request_timeout), then litellm_params.timeout, then litellm.request_timeout, and passes it explicitly to async_httpx_client.post. Fixes #38358 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com>
This commit is contained in:
parent
2c0d20b327
commit
44d3d53f95
2 changed files with 95 additions and 0 deletions
|
|
@ -1954,6 +1954,16 @@ class BaseLLMHTTPHandler:
|
|||
},
|
||||
)
|
||||
|
||||
# Resolve the request timeout so a silent upstream is not waited on
|
||||
# indefinitely. Priority: per-request kwargs["request_timeout"] >
|
||||
# litellm_params.timeout > litellm.request_timeout > client default.
|
||||
_request_timeout: Optional[Union[float, httpx.Timeout]] = (
|
||||
kwargs.get("request_timeout")
|
||||
or litellm_params.get("timeout")
|
||||
or litellm.request_timeout
|
||||
or None
|
||||
)
|
||||
|
||||
try:
|
||||
response = await async_httpx_client.post(
|
||||
url=request_url,
|
||||
|
|
@ -1961,6 +1971,7 @@ class BaseLLMHTTPHandler:
|
|||
data=signed_json_body or json.dumps(request_body),
|
||||
stream=stream or False,
|
||||
logging_obj=logging_obj,
|
||||
timeout=_request_timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -288,3 +288,87 @@ async def test_async_anthropic_messages_handler_header_priority():
|
|||
assert captured_headers["X-Forwarded-Only"] == "keep"
|
||||
assert captured_headers["X-Extra-Only"] == "also-keep"
|
||||
assert captured_headers["X-Provider-Only"] == "keep-this-too"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_anthropic_messages_handler_passes_request_timeout():
|
||||
"""
|
||||
Regression test for https://github.com/BerriAI/litellm/issues/38358.
|
||||
|
||||
async_anthropic_messages_handler must forward the per-request timeout
|
||||
(from kwargs["request_timeout"] or litellm_params.timeout) to the
|
||||
underlying httpx POST so that a silent upstream is abandoned at the
|
||||
configured deadline instead of waiting up to 600 s.
|
||||
"""
|
||||
handler = BaseLLMHTTPHandler()
|
||||
|
||||
mock_config = Mock()
|
||||
mock_config.validate_anthropic_messages_environment = Mock(
|
||||
return_value=({"x-api-key": "test-key"}, "https://api.anthropic.com")
|
||||
)
|
||||
mock_config.transform_anthropic_messages_request = Mock(
|
||||
return_value={"model": "claude-sonnet-4-20250514", "messages": []}
|
||||
)
|
||||
mock_config.get_complete_url = Mock(
|
||||
return_value="https://api.anthropic.com/v1/messages"
|
||||
)
|
||||
mock_config.sign_request = Mock(return_value=({"x-api-key": "test-key"}, None))
|
||||
mock_config.get_async_streaming_response_iterator = Mock(return_value=iter([]))
|
||||
|
||||
# Build a mock that mimics AsyncHTTPHandler so the handler uses it directly
|
||||
# instead of calling get_async_httpx_client().
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
||||
|
||||
mock_http_handler = Mock(spec=AsyncHTTPHandler)
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.raise_for_status = Mock()
|
||||
mock_response.json.return_value = {
|
||||
"id": "msg_123",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [{"type": "text", "text": "Hello!"}],
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"stop_reason": "end_turn",
|
||||
"stop_sequence": None,
|
||||
"usage": {"input_tokens": 10, "output_tokens": 5},
|
||||
}
|
||||
mock_http_handler.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
mock_logging_obj = Mock()
|
||||
mock_logging_obj.update_from_kwargs = Mock()
|
||||
mock_logging_obj.model_call_details = {}
|
||||
mock_logging_obj.stream = False
|
||||
|
||||
# Provide a request_timeout in kwargs, simulating litellm_settings.request_timeout
|
||||
kwargs = {"request_timeout": 8.0}
|
||||
|
||||
with patch(
|
||||
"litellm.litellm_core_utils.get_provider_specific_headers.ProviderSpecificHeaderUtils.get_provider_specific_headers"
|
||||
) as mock_provider_headers:
|
||||
mock_provider_headers.return_value = None
|
||||
|
||||
try:
|
||||
await handler.async_anthropic_messages_handler(
|
||||
model="claude-sonnet-4-20250514",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
anthropic_messages_provider_config=mock_config,
|
||||
anthropic_messages_optional_request_params={},
|
||||
custom_llm_provider="anthropic",
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
logging_obj=mock_logging_obj,
|
||||
client=mock_http_handler,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# The httpx post must have been called with the timeout from kwargs
|
||||
assert mock_http_handler.post.call_count >= 1
|
||||
call_kwargs = mock_http_handler.post.call_args
|
||||
assert call_kwargs is not None
|
||||
passed_timeout = call_kwargs.kwargs.get("timeout") if call_kwargs.kwargs else None
|
||||
assert passed_timeout == 8.0, (
|
||||
f"Expected timeout=8.0 to be forwarded to httpx.post, got {passed_timeout!r}. "
|
||||
"A missing timeout causes silent upstreams to hang for 600 s (issue #38358)."
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue