mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(router): resolve stream_timeout before generic timeouts on the passthrough route
This commit is contained in:
parent
efb2bcd87f
commit
73fddb999e
4 changed files with 80 additions and 79 deletions
|
|
@ -1,9 +1,20 @@
|
|||
import sys
|
||||
from typing import Final
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
DEFAULT_PASS_THROUGH_REQUEST_TIMEOUT_SECONDS: Final = 600.0
|
||||
|
||||
|
||||
class _TimeoutFields(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
stream: bool = False
|
||||
stream_timeout: float | None = None
|
||||
timeout: float | None = None
|
||||
request_timeout: float | None = None
|
||||
|
||||
|
||||
def resolve_pass_through_request_timeout(
|
||||
endpoint_timeout: float | None = None,
|
||||
) -> float:
|
||||
|
|
@ -33,8 +44,8 @@ def resolve_pass_through_request_timeout(
|
|||
def resolve_llm_passthrough_timeout(
|
||||
kwargs: dict | None = None,
|
||||
litellm_params: dict | None = None,
|
||||
router_timeout: float | None = None,
|
||||
router_stream_timeout: float | None = None,
|
||||
router_timeout: float | str | None = None,
|
||||
router_stream_timeout: float | str | None = None,
|
||||
) -> float:
|
||||
"""
|
||||
Resolve upstream httpx timeout for SDK native passthrough (e.g. Bedrock /converse,
|
||||
|
|
@ -44,28 +55,23 @@ def resolve_llm_passthrough_timeout(
|
|||
timeout/request_timeout -> router_timeout -> general_settings.pass_through_request_timeout
|
||||
-> 600s default.
|
||||
|
||||
Streaming (``kwargs["stream"]`` truthy) additionally consults ``stream_timeout`` at each
|
||||
level before the non-streaming key, matching ``Router._get_stream_timeout`` on the
|
||||
completion route: kwargs stream_timeout -> kwargs timeout/request_timeout ->
|
||||
litellm_params stream_timeout -> litellm_params timeout/request_timeout ->
|
||||
router_stream_timeout -> router_timeout -> pass_through_request_timeout -> 600s.
|
||||
Streaming (``kwargs["stream"]`` truthy) resolves ``stream_timeout`` at every level before
|
||||
any generic timeout, matching ``Router._get_stream_timeout`` on the completion route:
|
||||
kwargs stream_timeout -> litellm_params stream_timeout -> router_stream_timeout, then the
|
||||
non-streaming chain above.
|
||||
"""
|
||||
kwargs = kwargs or {}
|
||||
litellm_params = litellm_params or {}
|
||||
is_stream: Final[bool] = bool(kwargs.get("stream", False))
|
||||
|
||||
keys: Final[tuple[str, ...]] = (
|
||||
("stream_timeout", "timeout", "request_timeout") if is_stream else ("timeout", "request_timeout")
|
||||
request: Final = _TimeoutFields.model_validate(kwargs or {})
|
||||
deployment: Final = _TimeoutFields.model_validate(litellm_params or {})
|
||||
stream_candidates: Final = (
|
||||
(request.stream_timeout, deployment.stream_timeout, router_stream_timeout) if request.stream else ()
|
||||
)
|
||||
for source in (kwargs, litellm_params):
|
||||
for key in keys:
|
||||
val = source.get(key)
|
||||
if val is not None:
|
||||
return float(val)
|
||||
|
||||
if is_stream and router_stream_timeout is not None:
|
||||
return float(router_stream_timeout)
|
||||
if router_timeout is not None:
|
||||
return float(router_timeout)
|
||||
|
||||
return resolve_pass_through_request_timeout()
|
||||
candidates: Final = (
|
||||
*stream_candidates,
|
||||
request.timeout,
|
||||
request.request_timeout,
|
||||
deployment.timeout,
|
||||
deployment.request_timeout,
|
||||
router_timeout,
|
||||
)
|
||||
resolved: Final = next((float(val) for val in candidates if val is not None), None)
|
||||
return resolved if resolved is not None else resolve_pass_through_request_timeout()
|
||||
|
|
|
|||
|
|
@ -3880,7 +3880,9 @@ class Router:
|
|||
float(self._explicit_timeout) if isinstance(self._explicit_timeout, (int, float)) else None
|
||||
)
|
||||
_router_stream_timeout: Final = (
|
||||
float(self.stream_timeout) if isinstance(self.stream_timeout, (int, float)) else None
|
||||
self.stream_timeout
|
||||
if self.stream_timeout is not None
|
||||
else self.default_litellm_params.get("stream_timeout")
|
||||
)
|
||||
kwargs["timeout"] = resolve_llm_passthrough_timeout(
|
||||
kwargs=kwargs,
|
||||
|
|
|
|||
|
|
@ -1120,7 +1120,6 @@ def test_resolve_llm_passthrough_timeout_precedence():
|
|||
|
||||
|
||||
def test_resolve_llm_passthrough_timeout_stream_timeout_precedence():
|
||||
# streaming: stream_timeout wins at each level, then falls through to the non-stream keys
|
||||
assert (
|
||||
resolve_llm_passthrough_timeout(
|
||||
kwargs={"stream": True, "stream_timeout": 1800, "timeout": 45},
|
||||
|
|
@ -1129,36 +1128,35 @@ def test_resolve_llm_passthrough_timeout_stream_timeout_precedence():
|
|||
)
|
||||
assert (
|
||||
resolve_llm_passthrough_timeout(
|
||||
kwargs={"stream": True},
|
||||
kwargs={"stream": True, "timeout": 45},
|
||||
litellm_params={"stream_timeout": 1800, "timeout": 90},
|
||||
)
|
||||
== 1800.0
|
||||
)
|
||||
assert (
|
||||
resolve_llm_passthrough_timeout(
|
||||
kwargs={"stream": True, "timeout": 45},
|
||||
litellm_params={"timeout": 90},
|
||||
router_timeout=120,
|
||||
router_stream_timeout=1800,
|
||||
)
|
||||
== 1800.0
|
||||
)
|
||||
assert (
|
||||
resolve_llm_passthrough_timeout(
|
||||
kwargs={"stream": True},
|
||||
router_stream_timeout="1800",
|
||||
)
|
||||
== 1800.0
|
||||
)
|
||||
assert (
|
||||
resolve_llm_passthrough_timeout(
|
||||
kwargs={"stream": True},
|
||||
litellm_params={"timeout": 90},
|
||||
router_stream_timeout=1800,
|
||||
router_timeout=120,
|
||||
)
|
||||
== 90.0
|
||||
)
|
||||
assert (
|
||||
resolve_llm_passthrough_timeout(
|
||||
kwargs={"stream": True},
|
||||
router_timeout=120,
|
||||
router_stream_timeout=1800,
|
||||
)
|
||||
== 1800.0
|
||||
)
|
||||
assert (
|
||||
resolve_llm_passthrough_timeout(
|
||||
kwargs={"stream": True},
|
||||
router_timeout=120,
|
||||
)
|
||||
== 120.0
|
||||
)
|
||||
|
||||
# non-streaming: stream_timeout is ignored everywhere
|
||||
assert (
|
||||
resolve_llm_passthrough_timeout(
|
||||
kwargs={"stream": False, "stream_timeout": 1800},
|
||||
|
|
|
|||
|
|
@ -5480,13 +5480,17 @@ def test_update_kwargs_with_deployment_uses_pass_through_request_timeout():
|
|||
assert kwargs["timeout"] == 6.0
|
||||
|
||||
|
||||
def _passthrough_timeout(router: litellm.Router, deployment: dict, stream: bool) -> float:
|
||||
kwargs: Final[dict] = {"stream": stream}
|
||||
router._update_kwargs_with_deployment(
|
||||
deployment=deployment,
|
||||
kwargs=kwargs,
|
||||
function_name="_ageneric_api_call_with_fallbacks",
|
||||
)
|
||||
return kwargs["timeout"]
|
||||
|
||||
|
||||
def test_update_kwargs_with_deployment_passthrough_honors_stream_timeout():
|
||||
"""
|
||||
The SDK-native passthrough route (anthropic /v1/messages, bedrock /converse) resolves
|
||||
its upstream timeout separately from the completion route. A streaming call must get
|
||||
stream_timeout (deployment litellm_params first, then router_settings), while a
|
||||
non-streaming call on the same deployment keeps the non-stream resolution.
|
||||
"""
|
||||
router = litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
|
|
@ -5503,6 +5507,7 @@ def test_update_kwargs_with_deployment_passthrough_honors_stream_timeout():
|
|||
"litellm_params": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"api_key": "fake-key",
|
||||
"timeout": 60,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -5511,37 +5516,27 @@ def test_update_kwargs_with_deployment_passthrough_honors_stream_timeout():
|
|||
)
|
||||
per_deployment, router_default = router.model_list
|
||||
|
||||
kwargs: dict = {"stream": True}
|
||||
router._update_kwargs_with_deployment(
|
||||
deployment=per_deployment,
|
||||
kwargs=kwargs,
|
||||
function_name="_ageneric_api_call_with_fallbacks",
|
||||
)
|
||||
assert kwargs["timeout"] == 1800.0
|
||||
assert _passthrough_timeout(router, per_deployment, stream=True) == 1800.0
|
||||
assert _passthrough_timeout(router, router_default, stream=True) == 900.0
|
||||
assert _passthrough_timeout(router, per_deployment, stream=False) == 60.0
|
||||
assert _passthrough_timeout(router, router_default, stream=False) == 60.0
|
||||
|
||||
kwargs = {"stream": True}
|
||||
router._update_kwargs_with_deployment(
|
||||
deployment=router_default,
|
||||
kwargs=kwargs,
|
||||
function_name="_ageneric_api_call_with_fallbacks",
|
||||
)
|
||||
assert kwargs["timeout"] == 900.0
|
||||
|
||||
kwargs = {"stream": False}
|
||||
router._update_kwargs_with_deployment(
|
||||
deployment=per_deployment,
|
||||
kwargs=kwargs,
|
||||
function_name="_ageneric_api_call_with_fallbacks",
|
||||
def test_update_kwargs_with_deployment_passthrough_router_stream_timeout_sources():
|
||||
deployment: Final[dict] = {
|
||||
"model_name": "anthropic-router-default",
|
||||
"litellm_params": {"model": "anthropic/claude-sonnet-4-5", "api_key": "fake-key"},
|
||||
}
|
||||
string_router = litellm.Router(model_list=[deployment], timeout=120, stream_timeout="900")
|
||||
default_router = litellm.Router(
|
||||
model_list=[deployment],
|
||||
timeout=120,
|
||||
default_litellm_params={"stream_timeout": 700},
|
||||
)
|
||||
assert kwargs["timeout"] == 60.0
|
||||
|
||||
kwargs = {"stream": False}
|
||||
router._update_kwargs_with_deployment(
|
||||
deployment=router_default,
|
||||
kwargs=kwargs,
|
||||
function_name="_ageneric_api_call_with_fallbacks",
|
||||
)
|
||||
assert kwargs["timeout"] == 120.0
|
||||
assert _passthrough_timeout(string_router, string_router.model_list[0], stream=True) == 900.0
|
||||
assert _passthrough_timeout(default_router, default_router.model_list[0], stream=True) == 700.0
|
||||
assert _passthrough_timeout(default_router, default_router.model_list[0], stream=False) == 120.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue