Repository navigation
[Fix] Request Timeout needs to be also fetched from litellm_settings.request_timeout
#25591
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
ff33fee
645e43b
2345baf
862b693
de20182
c2b26ed
fdccbcb
0fa9cd1
2c14d42
6a3ca9a
9ae30ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| """Completion HTTP timeout resolution (kept out of ``main.py`` to limit import cycles).""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Callable, Optional, Union | ||
|
|
||
| import httpx | ||
|
|
||
| from litellm.constants import DEFAULT_REQUEST_TIMEOUT_SECONDS | ||
|
|
||
|
|
||
| class CompletionTimeout: | ||
| """Resolves HTTP timeout for ``completion()`` from model vs global settings.""" | ||
|
|
||
| @staticmethod | ||
| def resolve( | ||
| model_timeout: Optional[Union[float, str, httpx.Timeout]], | ||
| kwargs: dict, | ||
| custom_llm_provider: str, | ||
| *, | ||
| global_timeout: Optional[Union[float, str, httpx.Timeout]], | ||
| supports_httpx_timeout: Callable[[str], bool], | ||
| ) -> Union[float, httpx.Timeout]: | ||
| """ | ||
| Order: ``model_timeout`` (call argument / merged ``litellm_params``), then | ||
| ``kwargs["timeout"]``, ``kwargs["request_timeout"]``, then ``global_timeout`` | ||
| (e.g. :attr:`litellm.request_timeout` from proxy ``litellm_settings``), else ``600``. | ||
|
|
||
| Coerce :class:`httpx.Timeout` when the provider does not support it. If the value | ||
| came only from ``global_timeout`` (no model/kwargs timeout) and equals ``6000`` | ||
| (:data:`~litellm.constants.DEFAULT_REQUEST_TIMEOUT_SECONDS`), use ``600`` for | ||
| completion so chat calls do not inherit the long package default; explicit | ||
| ``model_timeout`` / kwargs values of ``6000`` are left unchanged. | ||
| """ | ||
| timeout_from_global_only = False | ||
| if model_timeout is not None: | ||
| resolved: Union[float, str, httpx.Timeout] = model_timeout | ||
| elif kwargs.get("timeout") is not None: | ||
| resolved = kwargs["timeout"] | ||
| elif kwargs.get("request_timeout") is not None: | ||
| resolved = kwargs["request_timeout"] | ||
| else: | ||
| resolved = global_timeout if global_timeout is not None else 600 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this looks odd, can you write this cleanly
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure, will look into this again
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic for this is cleaned up There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 600, should be a constant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is it not DEFAULT_REQUEST_TIMEOUT_SECONDS ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if Agreed, will make it into a const. We did not define default for chat completion in constants before, we can do it now. Will fix
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fixed |
||
| timeout_from_global_only = True | ||
|
|
||
| if isinstance(resolved, httpx.Timeout) and not supports_httpx_timeout( | ||
| custom_llm_provider | ||
| ): | ||
| read_timeout = resolved.read | ||
| resolved = ( | ||
| float(read_timeout) if read_timeout is not None else 600.0 | ||
| ) # default 10 min timeout | ||
| elif not isinstance(resolved, httpx.Timeout): | ||
| resolved = float(resolved) # type: ignore | ||
|
|
||
| if ( | ||
| timeout_from_global_only | ||
| and not isinstance(resolved, httpx.Timeout) | ||
| and float(resolved) == float(DEFAULT_REQUEST_TIMEOUT_SECONDS) | ||
| ): | ||
| resolved = 600.0 | ||
|
|
||
| return resolved | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| """ | ||
| ``_get_httpx_client`` + ``HTTPHandler.post`` (same pattern as Azure Anthropic sync path: | ||
| ``_get_httpx_client(params={"timeout": ...})`` then ``post(..., timeout=...)``). | ||
|
|
||
| Uses https://httpbin.org/delay/10 with ``timeout=5`` — the handler must raise :class:`~litellm.exceptions.Timeout` | ||
| before the 10s delay completes. Skips if httpbin is unreachable. | ||
|
|
||
| Lives under ``local_testing`` (not ``make test-unit``). | ||
| """ | ||
|
|
||
| import json | ||
| import os | ||
| import sys | ||
|
|
||
| import httpx | ||
| import pytest | ||
|
|
||
| sys.path.insert( | ||
| 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) | ||
| ) | ||
|
|
||
| from litellm.exceptions import Timeout as LitellmTimeout | ||
| from litellm.llms.custom_httpx.http_handler import _get_httpx_client | ||
|
|
||
| _HTTPBIN_DELAY_S = 10 | ||
| _PER_REQUEST_TIMEOUT_S = 5.0 | ||
| _CLIENT_DEFAULT_TIMEOUT_S = 60.0 | ||
|
|
||
|
|
||
| def test_post_delay_exceeds_per_request_timeout_raises(): | ||
| try: | ||
| httpx.get("https://httpbin.org/get", timeout=5.0) | ||
| except Exception as e: | ||
| pytest.skip(f"httpbin.org unreachable: {e}") | ||
|
|
||
| handler = _get_httpx_client(params={"timeout": _CLIENT_DEFAULT_TIMEOUT_S}) | ||
| try: | ||
| with pytest.raises(LitellmTimeout): | ||
| handler.post( | ||
| f"https://httpbin.org/delay/{_HTTPBIN_DELAY_S}", | ||
| headers={"content-type": "application/json"}, | ||
| data=json.dumps({"model": "claude", "messages": []}), | ||
| timeout=_PER_REQUEST_TIMEOUT_S, | ||
| ) | ||
| finally: | ||
| handler.close() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| """ | ||
| Ensure litellm.completion() forwards timeout to Azure Anthropic handler (main.py dispatch). | ||
| """ | ||
|
|
||
| import os | ||
| import sys | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| sys.path.insert( | ||
| 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..")) | ||
| ) | ||
|
|
||
| from litellm import completion | ||
| from litellm.types.utils import ModelResponse | ||
|
|
||
|
|
||
| def test_main_azure_ai_claude_completion_passes_timeout_to_azure_anthropic_handler(): | ||
| captured: dict = {} | ||
|
|
||
| def fake_azure_anthropic_completion(**kwargs): | ||
| captured.update(kwargs) | ||
| return ModelResponse() | ||
|
|
||
| with patch( | ||
| "litellm.main.azure_anthropic_chat_completions" | ||
| ) as mock_azure_anthropic: | ||
| mock_azure_anthropic.completion = MagicMock( | ||
| side_effect=fake_azure_anthropic_completion | ||
| ) | ||
|
|
||
| completion( | ||
| model="azure_ai/claude-sonnet-4-5", | ||
| messages=[{"role": "user", "content": "hi"}], | ||
| api_base="https://example.services.ai.azure.com/anthropic", | ||
| api_key="test-key", | ||
| timeout=42.5, | ||
| ) | ||
|
|
||
| mock_azure_anthropic.completion.assert_called_once() | ||
| assert captured["timeout"] == 42.5 | ||
| assert captured["model"] == "claude-sonnet-4-5" | ||
| assert captured["custom_llm_provider"] == "azure_ai" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
request_timeoutdefault change widens scope beyondcompletion()Changing the default from
6000to600fixes thecompletion()regression flagged in the previous thread, butlitellm.request_timeoutis also the default forRouter.__init__(self.timeout = timeout or litellm.request_timeout,router.py:530),speech()(main.py:6760), and the Anthropic / Azure-Anthropic / OpenAI count-token handlers. All of these would silently drop from a 6000 s ceiling to 600 s for users who have not set an explicit timeout. Long-running router calls or TTS jobs that complete in 600–6000 s will now time out.Per the "avoid backwards-incompatible changes without user-controlled flags" rule, consider keeping the constant at
6000(or introducing a separateCOMPLETION_REQUEST_TIMEOUTconstant) and only using the explicit-600 fallback inside_resolve_completion_timeout()itself, where you control the scope.Rule Used: What: avoid backwards-incompatible changes without... (source)