From f69fa5aca1d6c4f0e49155b1658cbd75eb69bdce Mon Sep 17 00:00:00 2001 From: CJYLZS <691086891@qq.com> Date: Wed, 18 Mar 2026 09:55:38 +0800 Subject: [PATCH] fix(router): return None instead of raising ValueError in least-busy strategy - Replace raise ValueError with return None when no healthy deployments are available, matching the None-return contract used by lowest_tpm and lowest_latency strategies; the router's `if deployment is None` path then raises the proper RouterRateLimitError with cooldown context - Add Optional[dict] return type annotation to _get_available_deployments - Add test_should_return_none_when_no_healthy_deployments to cover the None-return path - Seed random.seed(42) in probabilistic distribution tests for reproducible CI runs Made-with: Cursor --- litellm/router_strategy/least_busy.py | 10 ++++++---- .../test_litellm/router_strategy/test_least_busy.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/litellm/router_strategy/least_busy.py b/litellm/router_strategy/least_busy.py index e4487854f3b..a7ca5664dbd 100644 --- a/litellm/router_strategy/least_busy.py +++ b/litellm/router_strategy/least_busy.py @@ -193,9 +193,11 @@ class LeastBusyLoggingHandler(CustomLogger): self, healthy_deployments: list, all_deployments: dict, - ): + ) -> Optional[dict]: """ - Helper to get deployments using least busy strategy + Helper to get deployments using least busy strategy. + Returns None when no healthy deployments are available, consistent + with other routing strategies (lowest_tpm, lowest_latency). """ healthy_ids = set() for d in healthy_deployments: @@ -216,14 +218,14 @@ class LeastBusyLoggingHandler(CustomLogger): min_deployment_ids.append(k) if not min_deployment_ids: - raise ValueError("No healthy deployments available") + return None chosen_id = random.choice(min_deployment_ids) for m in healthy_deployments: if m["model_info"]["id"] == chosen_id: return m - raise ValueError(f"Chosen deployment id {chosen_id!r} not found in healthy_deployments") + return None def get_available_deployments( self, diff --git a/tests/test_litellm/router_strategy/test_least_busy.py b/tests/test_litellm/router_strategy/test_least_busy.py index f434f6a6f77..9d36cf4e604 100644 --- a/tests/test_litellm/router_strategy/test_least_busy.py +++ b/tests/test_litellm/router_strategy/test_least_busy.py @@ -1,4 +1,5 @@ import os +import random import sys from collections import Counter @@ -24,6 +25,7 @@ class TestLeastBusyTieBreaking: """Tests that least-busy strategy distributes requests across tied deployments.""" def test_should_randomly_distribute_when_all_counts_are_zero(self): + random.seed(42) cache = DualCache() handler = LeastBusyLoggingHandler(router_cache=cache) deployments = [_make_deployment(i) for i in range(3)] @@ -44,6 +46,7 @@ class TestLeastBusyTieBreaking: ) def test_should_randomly_distribute_when_counts_are_tied(self): + random.seed(42) cache = DualCache() handler = LeastBusyLoggingHandler(router_cache=cache) deployments = [_make_deployment(i) for i in range(2)] @@ -65,6 +68,15 @@ class TestLeastBusyTieBreaking: f"Deployment {dep_id} selected only {count}/200 times — distribution is too skewed" ) + def test_should_return_none_when_no_healthy_deployments(self): + cache = DualCache() + handler = LeastBusyLoggingHandler(router_cache=cache) + + result = handler._get_available_deployments( + healthy_deployments=[], all_deployments={} + ) + assert result is None + def test_should_pick_unique_minimum(self): cache = DualCache() handler = LeastBusyLoggingHandler(router_cache=cache)