From 54beb8af3d2de91a33d38ba3d93f1a79776d16e1 Mon Sep 17 00:00:00 2001 From: Jonathan Wrede Date: Thu, 7 May 2026 21:35:21 +0000 Subject: [PATCH] fix(router): allow APIConnectionError to trigger cooldown for failover Remove APIConnectionError from the ignored_strings list in _is_cooldown_required(). The blanket exclusion prevented unreachable hosts from being cooled down, so the router retried the same dead deployment on every attempt instead of failing over to healthy ones. APIConnectionError indicates the host is unreachable -- exactly the scenario where cooldown and failover should activate. Single-deployment model groups are already protected from unnecessary cooldown by the traffic-based logic in _should_cooldown_deployment(). Fixes #27362 --- litellm/router_utils/cooldown_handlers.py | 8 -- .../router_utils/test_cooldown_handlers.py | 91 +++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 tests/test_litellm/router_utils/test_cooldown_handlers.py diff --git a/litellm/router_utils/cooldown_handlers.py b/litellm/router_utils/cooldown_handlers.py index 81bfac2ad19..2be80cd4232 100644 --- a/litellm/router_utils/cooldown_handlers.py +++ b/litellm/router_utils/cooldown_handlers.py @@ -54,14 +54,6 @@ def _is_cooldown_required( bool: True if a cooldown is required, False otherwise. """ try: - ignored_strings = ["APIConnectionError"] - if ( - exception_str is not None - ): # don't cooldown on litellm api connection errors errors - for ignored_string in ignored_strings: - if ignored_string in exception_str: - return False - if isinstance(exception_status, str): if len(exception_status) == 0: return False diff --git a/tests/test_litellm/router_utils/test_cooldown_handlers.py b/tests/test_litellm/router_utils/test_cooldown_handlers.py new file mode 100644 index 00000000000..5d2e417f315 --- /dev/null +++ b/tests/test_litellm/router_utils/test_cooldown_handlers.py @@ -0,0 +1,91 @@ +""" +Unit tests for cooldown handler logic in litellm.router_utils.cooldown_handlers +""" + +import os +import sys +from unittest.mock import MagicMock + +import pytest + +sys.path.insert(0, os.path.abspath("../../..")) + +import litellm +from litellm.router_utils.cooldown_handlers import _is_cooldown_required + + +@pytest.fixture +def mock_router(): + router = MagicMock() + router.allowed_fails = None + router.allowed_fails_policy = None + return router + + +class TestIsCooldownRequired: + """Tests for _is_cooldown_required behavior with connection errors.""" + + def test_api_connection_error_triggers_cooldown(self, mock_router): + """ + APIConnectionError (unreachable host) should trigger cooldown so the + router can fail over to healthy deployments. + + Regression test for https://github.com/BerriAI/litellm/issues/27362 + """ + exception = litellm.APIConnectionError( + message="Cannot connect to host example:11434", + llm_provider="ollama", + model="llama3.1:8b", + ) + + result = _is_cooldown_required( + litellm_router_instance=mock_router, + model_id="test_deployment", + exception_status=500, + exception_str=str(exception), + ) + + assert result is True + + def test_server_error_triggers_cooldown(self, mock_router): + """500-level errors should trigger cooldown.""" + result = _is_cooldown_required( + litellm_router_instance=mock_router, + model_id="test_deployment", + exception_status=500, + exception_str="Internal Server Error", + ) + + assert result is True + + def test_rate_limit_triggers_cooldown(self, mock_router): + """429 errors should trigger cooldown.""" + result = _is_cooldown_required( + litellm_router_instance=mock_router, + model_id="test_deployment", + exception_status=429, + exception_str="Rate limit exceeded", + ) + + assert result is True + + def test_bad_request_does_not_trigger_cooldown(self, mock_router): + """400 errors (except 401, 404, 408, 429) should not trigger cooldown.""" + result = _is_cooldown_required( + litellm_router_instance=mock_router, + model_id="test_deployment", + exception_status=400, + exception_str="Bad Request", + ) + + assert result is False + + def test_empty_exception_status_does_not_trigger_cooldown(self, mock_router): + """Empty string exception status should not trigger cooldown.""" + result = _is_cooldown_required( + litellm_router_instance=mock_router, + model_id="test_deployment", + exception_status="", + ) + + assert result is False