diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index c82e47d14a5..61bfeba42da 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -926,9 +926,17 @@ class ComplexityRouter(CustomLogger): Reads the router's in-process deployment list, so it costs no I/O: the cooldown set was already fetched once for the whole request. + + A deployment with no id counts as live. Cooldowns are keyed by id, so one that + cannot be identified can never be known to be cooling, and the ladder's rule + everywhere else is that an absent health signal means live. Excluding it instead + would let missing metadata empty a tier and push traffic to a pricier one. """ deployments = self.litellm_router_instance.get_model_list(model_name=model) or () - return any(self._deployment_id(deployment) not in cooled_down for deployment in deployments) + return any( + deployment_id is None or deployment_id not in cooled_down + for deployment_id in map(self._deployment_id, deployments) + ) @staticmethod def _deployment_id(deployment: Mapping[str, Any]) -> str | None: diff --git a/tests/test_litellm/router_strategy/test_complexity_router.py b/tests/test_litellm/router_strategy/test_complexity_router.py index 3188d68cd2d..634c562a38b 100644 --- a/tests/test_litellm/router_strategy/test_complexity_router.py +++ b/tests/test_litellm/router_strategy/test_complexity_router.py @@ -4933,6 +4933,28 @@ class TestTierFallbackLadder: assert response.model == "medium-model" assert response.routing_decision["tier_fallback_from"] == "SIMPLE" + @pytest.mark.asyncio + async def test_a_deployment_with_no_id_counts_as_live(self): + """Cooldowns are keyed by deployment id, so one without an id can never be known to + be cooling. It counts as live, matching the rule everywhere else that an absent + health signal means live; excluding it would let missing metadata empty a tier and + push traffic to a pricier one.""" + + class IdlessRouter(StubRouter): + def get_model_list(self, model_name=None, team_id=None): + self.listed.append(model_name) + return [{"model_info": {}}] if model_name in self.registered else [] + + router = ComplexityRouter( + model_name="test-ladder-router", + litellm_router_instance=IdlessRouter(live=set()), + complexity_router_config={"tiers": dict(LADDER_TIERS), "session_affinity": False}, + ) + response = await _route(router, "What is 2+2?") + assert response.model in {"simple-a", "simple-b"} + assert "tier_fallback_from" not in response.routing_decision + assert "resolved_by" not in response.routing_decision + @pytest.mark.asyncio async def test_health_lookup_failure_keeps_the_classified_tier(self): """A health view that cannot be read must not empty every tier and push traffic up