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
This commit is contained in:
Jonathan Wrede 2026-05-07 21:35:21 +00:00
parent fa81017e12
commit 54beb8af3d
2 changed files with 91 additions and 8 deletions

View file

@ -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

View file

@ -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