From a9c422735fb8eb2f3e54510fac4796516e29107e Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 15 Sep 2026 05:39:23 -0700 Subject: [PATCH 1/4] fix(router): stop counting caller-set timeout 408s toward deployment cooldown A 408 produced by a timeout the caller set (a timeout body field or an x-litellm-timeout header, which the proxy marks as client_side_timeout) says nothing about the deployment's health, yet the router's primary failure callback counted it toward allowed_fails and cooled the deployment down. The fallback path already skipped it. The marker never reached that callback because get_litellm_params drops kwargs outside OPTIONAL_KWARGS_KEYS, so it is listed there now, and deployment_callback_on_failure returns before the failure counter when is_caller_timeout_408 holds. A 408 from a timeout the deployment or the provider set still counts and still cools the deployment down. --- .../litellm_core_utils/get_litellm_params.py | 1 + litellm/router.py | 8 ++ litellm/router_utils/cooldown_handlers.py | 4 + .../router_utils/fallback_event_handlers.py | 3 +- tests/test_litellm/test_router.py | 82 +++++++++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/get_litellm_params.py b/litellm/litellm_core_utils/get_litellm_params.py index edd2e88f95c..49fc9abc525 100644 --- a/litellm/litellm_core_utils/get_litellm_params.py +++ b/litellm/litellm_core_utils/get_litellm_params.py @@ -41,6 +41,7 @@ OPTIONAL_KWARGS_KEYS: Final = ( "azure_password", "azure_scope", "timeout", + "client_side_timeout", "gcs_bucket_name", "bucket_name", "vertex_credentials", diff --git a/litellm/router.py b/litellm/router.py index ef89d611075..fc698ecb4b9 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -167,6 +167,7 @@ from litellm.router_utils.cooldown_handlers import ( _get_cooldown_deployments, _set_cooldown_deployments, is_advisor_orchestration_failure, + is_caller_timeout_408, ) from litellm.router_utils.fallback_event_handlers import ( AttemptedFallbackTargets, @@ -8297,6 +8298,13 @@ class Router: litellm_params: Final = kwargs.get("litellm_params", {}) _model_info: Final = litellm_params.get("model_info", {}) + if is_caller_timeout_408(litellm_params.get("client_side_timeout"), exception_status): + verbose_router_logger.debug( + "Router: Exiting 'deployment_callback_on_failure' without cooldown. " + "A timeout the caller set caused this 408, not the deployment's health." + ) + return False + exception_headers: Final = litellm.litellm_core_utils.exception_mapping_utils._get_response_headers( original_exception=exception ) diff --git a/litellm/router_utils/cooldown_handlers.py b/litellm/router_utils/cooldown_handlers.py index 027f0a9ca05..e21567684eb 100644 --- a/litellm/router_utils/cooldown_handlers.py +++ b/litellm/router_utils/cooldown_handlers.py @@ -637,3 +637,7 @@ def cast_exception_status_to_int(exception_status: str | int) -> int: ) exception_status = 500 return exception_status + + +def is_caller_timeout_408(client_side_timeout: object, exception_status: str | int) -> bool: + return bool(client_side_timeout) and cast_exception_status_to_int(exception_status) == 408 diff --git a/litellm/router_utils/fallback_event_handlers.py b/litellm/router_utils/fallback_event_handlers.py index 7fda5d96fb0..527a6b484e0 100644 --- a/litellm/router_utils/fallback_event_handlers.py +++ b/litellm/router_utils/fallback_event_handlers.py @@ -20,6 +20,7 @@ from litellm.router_utils.cooldown_handlers import ( _set_cooldown_deployments, # pyright: ignore[reportPrivateUsage] - shared helper, used across router_utils cast_exception_status_to_int, is_advisor_orchestration_failure, + is_caller_timeout_408, ) from litellm.router_utils.router_callbacks.track_deployment_metrics import ( increment_deployment_failures_for_current_minute, @@ -80,7 +81,7 @@ def _trigger_cooldown_for_failed_deployment( # timeout, which litellm.Timeout reports as status 408 regardless of the deployment's # actual health. Left unguarded, a caller could force a 408 on every deployment in # the fallback chain from a single request with a near-zero timeout. - if kwargs.get("client_side_timeout") and cast_exception_status_to_int(exception_status) == 408: + if is_caller_timeout_408(kwargs.get("client_side_timeout"), exception_status): verbose_router_logger.debug( "Not triggering cooldown for fallback deployment: a caller-supplied " "x-litellm-timeout caused this 408, not deployment health." diff --git a/tests/test_litellm/test_router.py b/tests/test_litellm/test_router.py index c46a080976c..53eb91e6c63 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -8520,6 +8520,88 @@ class TestAdvisorSubCallCooldown: assert "dep-1" not in self._cooled_down_ids(router) +class TestCallerTimeoutCooldown: + """A timeout the caller set (the proxy's `timeout` body field or x-litellm-timeout + header) comes back as a 408 whatever the deployment's health, so it must neither + count toward allowed_fails nor bench the deployment. A 408 without that marker is + the provider's and keeps cooling the deployment down.""" + + def _router(self): + return litellm.Router( + model_list=[ + { + "model_name": "slow-model", + "litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"}, + "model_info": {"id": "dep-1"}, + } + ], + allowed_fails=0, + cooldown_time=120, + num_retries=0, + ) + + def _kwargs(self, marker): + exception = litellm.Timeout(message="Request timed out", model="gpt-5.6", llm_provider="openai") + return { + "exception": exception, + "litellm_params": {"model_info": {"id": "dep-1"}, "metadata": {}, **marker}, + } + + def _fail_count(self, router): + from litellm.router_utils.router_callbacks.track_deployment_metrics import ( + get_deployment_failures_for_current_minute, + ) + + return get_deployment_failures_for_current_minute(litellm_router_instance=router, deployment_id="dep-1") + + def _cooled_down_ids(self, router): + active = router.cooldown_cache.get_active_cooldowns(model_ids=["dep-1"], parent_otel_span=None) + return [entry[0] for entry in active] + + @pytest.mark.asyncio + async def test_caller_timeout_408_leaves_failure_counter_and_cooldown_untouched(self): + router = self._router() + now = datetime.now() + assert router.deployment_callback_on_failure(self._kwargs({"client_side_timeout": True}), None, now, now) is False + assert self._fail_count(router) == 0 + assert self._cooled_down_ids(router) == [] + + @pytest.mark.asyncio + async def test_provider_timeout_408_still_counts_and_cools_down(self): + router = self._router() + now = datetime.now() + assert router.deployment_callback_on_failure(self._kwargs({}), None, now, now) is True + assert self._fail_count(router) == 1 + assert self._cooled_down_ids(router) == ["dep-1"] + + @pytest.mark.asyncio + async def test_caller_timeout_marker_reaches_failure_callback_end_to_end(self): + router = self._router() + seen = [] + recorded = threading.Event() + + def record(kwargs, completion_response, start_time, end_time): + seen.append(kwargs) + recorded.set() + + litellm.failure_callback.append(record) + try: + with pytest.raises(litellm.Timeout): + await router.acompletion( + model="slow-model", + messages=[{"role": "user", "content": "hello"}], + mock_timeout=True, + timeout=0.001, + client_side_timeout=True, + ) + assert await asyncio.to_thread(recorded.wait, 5) + finally: + litellm.failure_callback.remove(record) + assert seen[0]["litellm_params"]["client_side_timeout"] is True + assert self._fail_count(router) == 0 + assert self._cooled_down_ids(router) == [] + + def test_stream_chunks_have_generated_content_detects_text_and_non_text(): from litellm.router import _stream_chunks_have_generated_content from litellm.types.utils import ( From 5da497f4acff18a13161c41359e962c2d92598dd Mon Sep 17 00:00:00 2001 From: yassin Date: Tue, 15 Sep 2026 20:19:20 +0000 Subject: [PATCH 2/4] fix(router): only exempt 408s that arrive after the caller's timeout from cooldown client_side_timeout records that the caller configured a timeout, not that the timeout fired. A 408 the provider returns before that deadline is a deployment failure and must still count toward cooldown. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/router.py | 2 +- litellm/router_utils/cooldown_handlers.py | 16 +++++- .../router_utils/fallback_event_handlers.py | 6 ++- .../test_fallback_event_handlers.py | 50 +++++++++++++++++++ tests/test_litellm/test_router.py | 30 ++++++++--- 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index fc698ecb4b9..2c20e810839 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -8298,7 +8298,7 @@ class Router: litellm_params: Final = kwargs.get("litellm_params", {}) _model_info: Final = litellm_params.get("model_info", {}) - if is_caller_timeout_408(litellm_params.get("client_side_timeout"), exception_status): + if is_caller_timeout_408(kwargs, exception_status): verbose_router_logger.debug( "Router: Exiting 'deployment_callback_on_failure' without cooldown. " "A timeout the caller set caused this 408, not the deployment's health." diff --git a/litellm/router_utils/cooldown_handlers.py b/litellm/router_utils/cooldown_handlers.py index e21567684eb..bef07c68e9e 100644 --- a/litellm/router_utils/cooldown_handlers.py +++ b/litellm/router_utils/cooldown_handlers.py @@ -9,6 +9,7 @@ Router cooldown handlers import asyncio import math from collections.abc import Mapping +from datetime import datetime from types import MappingProxyType from typing import TYPE_CHECKING, Any, Final @@ -639,5 +640,16 @@ def cast_exception_status_to_int(exception_status: str | int) -> int: return exception_status -def is_caller_timeout_408(client_side_timeout: object, exception_status: str | int) -> bool: - return bool(client_side_timeout) and cast_exception_status_to_int(exception_status) == 408 +def is_caller_timeout_408(model_call_details: Mapping[str, object], exception_status: str | int) -> bool: + """A 408 that arrives before the caller-set timeout could have fired came from the provider.""" + if cast_exception_status_to_int(exception_status) != 408: + return False + litellm_params: Final = model_call_details.get("litellm_params") + if not isinstance(litellm_params, Mapping) or not litellm_params.get("client_side_timeout"): + return False + timeout: Final = litellm_params.get("timeout") + started: Final = model_call_details.get("api_call_start_time") or model_call_details.get("start_time") + ended: Final = model_call_details.get("end_time") + if not isinstance(timeout, (int, float)) or not isinstance(started, datetime) or not isinstance(ended, datetime): + return False + return (ended - started).total_seconds() >= timeout diff --git a/litellm/router_utils/fallback_event_handlers.py b/litellm/router_utils/fallback_event_handlers.py index 527a6b484e0..eeea9b9faf8 100644 --- a/litellm/router_utils/fallback_event_handlers.py +++ b/litellm/router_utils/fallback_event_handlers.py @@ -3,6 +3,7 @@ import json from collections.abc import Mapping, Sequence from dataclasses import dataclass from enum import Enum +from types import MappingProxyType from typing import TYPE_CHECKING, Any, Final import litellm @@ -37,12 +38,14 @@ else: # Status codes a generic API call's caller-supplied resource id can trigger on its own # (e.g. a nonexistent file/batch/thread id), independent of the selected deployment's health. _REQUEST_SCOPED_STATUS_CODES: Final = frozenset((404,)) +_NO_MODEL_CALL_DETAILS: Final[Mapping[str, object]] = MappingProxyType({}) def _trigger_cooldown_for_failed_deployment( litellm_router: LitellmRouter, kwargs: Mapping[str, object], exception: Exception, + model_call_details: Mapping[str, object] = _NO_MODEL_CALL_DETAILS, ) -> None: """ Trigger cooldown for a failed fallback deployment. @@ -81,7 +84,7 @@ def _trigger_cooldown_for_failed_deployment( # timeout, which litellm.Timeout reports as status 408 regardless of the deployment's # actual health. Left unguarded, a caller could force a 408 on every deployment in # the fallback chain from a single request with a near-zero timeout. - if is_caller_timeout_408(kwargs.get("client_side_timeout"), exception_status): + if is_caller_timeout_408(model_call_details, exception_status): verbose_router_logger.debug( "Not triggering cooldown for fallback deployment: a caller-supplied " "x-litellm-timeout caused this 408, not deployment health." @@ -580,6 +583,7 @@ async def run_async_fallback( litellm_router=litellm_router, kwargs=kwargs, exception=e, + model_call_details=logging_obj.model_call_details, ) raise error_from_fallbacks diff --git a/tests/test_litellm/router_utils/test_fallback_event_handlers.py b/tests/test_litellm/router_utils/test_fallback_event_handlers.py index a4965c49f07..15db22dc758 100644 --- a/tests/test_litellm/router_utils/test_fallback_event_handlers.py +++ b/tests/test_litellm/router_utils/test_fallback_event_handlers.py @@ -1,4 +1,5 @@ import json +from datetime import datetime, timedelta from typing import NoReturn from unittest.mock import MagicMock, patch @@ -973,11 +974,60 @@ class TestTriggerCooldownForFailedDeployment: litellm_router=mock_router, kwargs={"client_side_timeout": True}, exception=exc, + model_call_details={ + "litellm_params": {"client_side_timeout": True, "timeout": 0.5}, + "api_call_start_time": datetime.now() - timedelta(seconds=1), + "end_time": datetime.now(), + }, ) mock_set_cooldown.assert_not_called() mock_increment.assert_not_called() + def test_still_cools_down_provider_408_before_caller_deadline(self): + """client_side_timeout only records that the caller configured a timeout. A 408 + that comes back before that deadline was raised by the provider itself, so it is + a real health signal and must still cool the deployment down.""" + from litellm.router_utils.router_callbacks.track_deployment_metrics import ( + get_deployment_failures_for_current_minute, + ) + + router = litellm.Router( + model_list=[ + { + "model_name": "fallback-model", + "litellm_params": {"model": "openai/gpt-5.6", "api_key": "sk-fake"}, + "model_info": {"id": "fallback-deployment"}, + } + ], + allowed_fails=0, + cooldown_time=60, + num_retries=0, + ) + exc = litellm.Timeout(message="timeout", model="gpt-5.6", llm_provider="openai") + exc.failed_deployment_id = "fallback-deployment" + started = datetime.now() + + _trigger_cooldown_for_failed_deployment( + litellm_router=router, + kwargs={"client_side_timeout": True}, + exception=exc, + model_call_details={ + "litellm_params": {"client_side_timeout": True, "timeout": 30}, + "api_call_start_time": started, + "end_time": started + timedelta(seconds=1), + }, + ) + + assert ( + get_deployment_failures_for_current_minute( + litellm_router_instance=router, deployment_id="fallback-deployment" + ) + == 1 + ) + active = router.cooldown_cache.get_active_cooldowns(model_ids=["fallback-deployment"], parent_otel_span=None) + assert [entry[0] for entry in active] == ["fallback-deployment"] + def test_still_cools_down_408_without_client_side_timeout_flag(self): """The client-side-timeout guard is scoped to caller-supplied timeouts only: a 408 that did not come from x-litellm-timeout (no client_side_timeout in kwargs) diff --git a/tests/test_litellm/test_router.py b/tests/test_litellm/test_router.py index 53eb91e6c63..fe01df04351 100644 --- a/tests/test_litellm/test_router.py +++ b/tests/test_litellm/test_router.py @@ -7,7 +7,7 @@ import os import sys import threading from collections.abc import Awaitable, Callable, Mapping -from datetime import datetime +from datetime import datetime, timedelta from types import SimpleNamespace from typing import Final, Literal from unittest.mock import AsyncMock, MagicMock, patch @@ -8523,8 +8523,9 @@ class TestAdvisorSubCallCooldown: class TestCallerTimeoutCooldown: """A timeout the caller set (the proxy's `timeout` body field or x-litellm-timeout header) comes back as a 408 whatever the deployment's health, so it must neither - count toward allowed_fails nor bench the deployment. A 408 without that marker is - the provider's and keeps cooling the deployment down.""" + count toward allowed_fails nor bench the deployment. A 408 without that marker, or + one that arrives before the caller's deadline could have fired, is the provider's + and keeps cooling the deployment down.""" def _router(self): return litellm.Router( @@ -8540,10 +8541,12 @@ class TestCallerTimeoutCooldown: num_retries=0, ) - def _kwargs(self, marker): + def _kwargs(self, marker, started=None, ended=None): exception = litellm.Timeout(message="Request timed out", model="gpt-5.6", llm_provider="openai") return { "exception": exception, + "api_call_start_time": started, + "end_time": ended, "litellm_params": {"model_info": {"id": "dep-1"}, "metadata": {}, **marker}, } @@ -8561,8 +8564,10 @@ class TestCallerTimeoutCooldown: @pytest.mark.asyncio async def test_caller_timeout_408_leaves_failure_counter_and_cooldown_untouched(self): router = self._router() - now = datetime.now() - assert router.deployment_callback_on_failure(self._kwargs({"client_side_timeout": True}), None, now, now) is False + started = datetime.now() + ended = started + timedelta(seconds=2.05) + kwargs = self._kwargs({"client_side_timeout": True, "timeout": 2}, started=started, ended=ended) + assert router.deployment_callback_on_failure(kwargs, None, started, ended) is False assert self._fail_count(router) == 0 assert self._cooled_down_ids(router) == [] @@ -8574,6 +8579,19 @@ class TestCallerTimeoutCooldown: assert self._fail_count(router) == 1 assert self._cooled_down_ids(router) == ["dep-1"] + @pytest.mark.asyncio + async def test_provider_408_before_caller_deadline_still_counts_and_cools_down(self): + """The marker only says the caller configured a timeout. A 408 that comes back + well before that deadline was raised by the provider, so it is a real health + signal and must not hide behind the caller's timeout.""" + router = self._router() + started = datetime.now() + ended = started + timedelta(seconds=0.4) + kwargs = self._kwargs({"client_side_timeout": True, "timeout": 30}, started=started, ended=ended) + assert router.deployment_callback_on_failure(kwargs, None, started, ended) is True + assert self._fail_count(router) == 1 + assert self._cooled_down_ids(router) == ["dep-1"] + @pytest.mark.asyncio async def test_caller_timeout_marker_reaches_failure_callback_end_to_end(self): router = self._router() From 2f719fec521cd6fb2ab281b17e08b876b0b09acb Mon Sep 17 00:00:00 2001 From: yassin Date: Tue, 15 Sep 2026 20:26:46 +0000 Subject: [PATCH 3/4] test(router): run the fallback provider-408 cooldown regression inside an event loop Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../test_litellm/router_utils/test_fallback_event_handlers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_litellm/router_utils/test_fallback_event_handlers.py b/tests/test_litellm/router_utils/test_fallback_event_handlers.py index 15db22dc758..783f8da31e9 100644 --- a/tests/test_litellm/router_utils/test_fallback_event_handlers.py +++ b/tests/test_litellm/router_utils/test_fallback_event_handlers.py @@ -984,7 +984,8 @@ class TestTriggerCooldownForFailedDeployment: mock_set_cooldown.assert_not_called() mock_increment.assert_not_called() - def test_still_cools_down_provider_408_before_caller_deadline(self): + @pytest.mark.asyncio + async def test_still_cools_down_provider_408_before_caller_deadline(self): """client_side_timeout only records that the caller configured a timeout. A 408 that comes back before that deadline was raised by the provider itself, so it is a real health signal and must still cool the deployment down.""" From 595bec46ff9099c8dae51ff9bb430baae8167c43 Mon Sep 17 00:00:00 2001 From: yassin Date: Tue, 15 Sep 2026 20:56:33 +0000 Subject: [PATCH 4/4] fix(router): time fallback-hop 408s against now, not the previous hop's end_time The failure logger skips fallback hops (has_logged_async_failure is already set), so model_call_details.end_time still belongs to the previous hop and predates this hop's api_call_start_time. The fallback cooldown guard measured a negative elapsed time and cooled down deployments for caller-set timeouts. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/router_utils/cooldown_handlers.py | 15 ++++++++++----- litellm/router_utils/fallback_event_handlers.py | 7 ++++++- .../router_utils/test_fallback_event_handlers.py | 8 ++++++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/litellm/router_utils/cooldown_handlers.py b/litellm/router_utils/cooldown_handlers.py index bef07c68e9e..6e6d4c253e9 100644 --- a/litellm/router_utils/cooldown_handlers.py +++ b/litellm/router_utils/cooldown_handlers.py @@ -640,8 +640,13 @@ def cast_exception_status_to_int(exception_status: str | int) -> int: return exception_status -def is_caller_timeout_408(model_call_details: Mapping[str, object], exception_status: str | int) -> bool: - """A 408 that arrives before the caller-set timeout could have fired came from the provider.""" +def is_caller_timeout_408( + model_call_details: Mapping[str, object], exception_status: str | int, ended: datetime | None = None +) -> bool: + """A 408 that arrives before the caller-set timeout could have fired came from the provider. + + ``ended`` overrides ``model_call_details["end_time"]`` for callers that run before the + failure logger has stamped the current API call's end time.""" if cast_exception_status_to_int(exception_status) != 408: return False litellm_params: Final = model_call_details.get("litellm_params") @@ -649,7 +654,7 @@ def is_caller_timeout_408(model_call_details: Mapping[str, object], exception_st return False timeout: Final = litellm_params.get("timeout") started: Final = model_call_details.get("api_call_start_time") or model_call_details.get("start_time") - ended: Final = model_call_details.get("end_time") - if not isinstance(timeout, (int, float)) or not isinstance(started, datetime) or not isinstance(ended, datetime): + finished: Final = ended if ended is not None else model_call_details.get("end_time") + if not isinstance(timeout, (int, float)) or not isinstance(started, datetime) or not isinstance(finished, datetime): return False - return (ended - started).total_seconds() >= timeout + return (finished - started).total_seconds() >= timeout diff --git a/litellm/router_utils/fallback_event_handlers.py b/litellm/router_utils/fallback_event_handlers.py index eeea9b9faf8..94164d0ea0c 100644 --- a/litellm/router_utils/fallback_event_handlers.py +++ b/litellm/router_utils/fallback_event_handlers.py @@ -2,6 +2,7 @@ import hashlib import json from collections.abc import Mapping, Sequence from dataclasses import dataclass +from datetime import datetime from enum import Enum from types import MappingProxyType from typing import TYPE_CHECKING, Any, Final @@ -84,7 +85,11 @@ def _trigger_cooldown_for_failed_deployment( # timeout, which litellm.Timeout reports as status 408 regardless of the deployment's # actual health. Left unguarded, a caller could force a 408 on every deployment in # the fallback chain from a single request with a near-zero timeout. - if is_caller_timeout_408(model_call_details, exception_status): + if is_caller_timeout_408( + model_call_details, + exception_status, + ended=datetime.now(), # noqa: DTZ005 # naive to match the logging pipeline's api_call_start_time + ): verbose_router_logger.debug( "Not triggering cooldown for fallback deployment: a caller-supplied " "x-litellm-timeout caused this 408, not deployment health." diff --git a/tests/test_litellm/router_utils/test_fallback_event_handlers.py b/tests/test_litellm/router_utils/test_fallback_event_handlers.py index 783f8da31e9..9318f306c89 100644 --- a/tests/test_litellm/router_utils/test_fallback_event_handlers.py +++ b/tests/test_litellm/router_utils/test_fallback_event_handlers.py @@ -956,7 +956,11 @@ class TestTriggerCooldownForFailedDeployment: """The proxy's x-litellm-timeout header lets a caller set an arbitrarily short timeout, which litellm.Timeout reports as status 408 regardless of the deployment's actual health. Without this guard, a caller could force a 408 on - every deployment in the fallback chain from a single request.""" + every deployment in the fallback chain from a single request. + + The failure logger never stamps end_time for a fallback hop (has_logged_async_failure + is already set), so model_call_details still carries the previous hop's end_time, which + predates this hop's api_call_start_time. The guard must not trust it.""" mock_router = MagicMock() mock_router.cooldown_time = 60.0 mock_router.get_model_info.return_value = None @@ -977,7 +981,7 @@ class TestTriggerCooldownForFailedDeployment: model_call_details={ "litellm_params": {"client_side_timeout": True, "timeout": 0.5}, "api_call_start_time": datetime.now() - timedelta(seconds=1), - "end_time": datetime.now(), + "end_time": datetime.now() - timedelta(seconds=5), }, )