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
This commit is contained in:
CJYLZS 2026-03-18 09:55:38 +08:00
parent 7df4a236f4
commit f69fa5aca1
2 changed files with 18 additions and 4 deletions

View file

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

View file

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