From eac63695d55b24444288b2159d8fe9602946ad93 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:29:34 +0000 Subject: [PATCH] fix(routing_groups): build every group selector before swapping router state Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/router.py | 28 ++++++++++++------- .../test_router_routing_groups.py | 27 ++++++++++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index a00b919cf19..8f23b67d5c4 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -28,6 +28,7 @@ import anyio import httpx import openai from openai import AsyncOpenAI +from pydantic import ValidationError from typing_extensions import overload import litellm @@ -1030,6 +1031,12 @@ class Router: known_model_names=known_model_names, ) + built: Final = tuple((group, self._try_build_group_selector(group)) for group in groups) + failures: Final = tuple(outcome for _, outcome in built if isinstance(outcome, Exception)) + if failures: + self._unregister_router_selectors([outcome for _, outcome in built if not isinstance(outcome, Exception)]) + raise failures[0] + self._unregister_router_selectors( [sel for selectors in getattr(self, "_group_selectors", {}).values() for sel in selectors.values()] ) @@ -1040,19 +1047,20 @@ class Router: } self._group_selectors: dict[str, dict[str, Any]] = { group.group_name: ( - {} - if ( - selector := self._build_strategy_selector( - strategy=group.routing_strategy, - routing_strategy_args=group.routing_strategy_args or {}, - ) - ) - is None - else {self._normalize_strategy(group.routing_strategy) or "": selector} + {} if selector is None else {self._normalize_strategy(group.routing_strategy) or "": selector} ) - for group in groups + for group, selector in built } + def _try_build_group_selector(self, group: RoutingGroup) -> object | None: + try: + return self._build_strategy_selector( + strategy=group.routing_strategy, + routing_strategy_args=group.routing_strategy_args or {}, + ) + except ValidationError as build_error: + return build_error + _OVERRIDABLE_ROUTING_STRATEGIES: frozenset[str] = frozenset({"simple-shuffle", *_DEFAULT_SELECTOR_ATTR_BY_STRATEGY}) def _get_request_routing_strategy_override(self, request_kwargs: dict | None) -> str | None: diff --git a/tests/test_litellm/router_strategy/test_router_routing_groups.py b/tests/test_litellm/router_strategy/test_router_routing_groups.py index 8865bdf0172..c4d667a6af9 100644 --- a/tests/test_litellm/router_strategy/test_router_routing_groups.py +++ b/tests/test_litellm/router_strategy/test_router_routing_groups.py @@ -788,3 +788,30 @@ def test_invalid_group_strategy_does_not_leak_a_selector(): assert list(router._routing_groups) == ["g1"] assert any(id(cb) == id(selector) for cb in litellm.callbacks) + + +def test_unbuildable_group_selector_keeps_previous_groups(): + router = _build_router( + routing_groups=[ + {"group_name": "g1", "models": ["filtered-model"], "routing_strategy": "latency-based-routing"}, + ], + ) + selector = router._group_selectors["g1"]["latency-based-routing"] + + with pytest.raises(Exception): + router.update_settings( + routing_groups=[ + {"group_name": "g1", "models": ["filtered-model"], "routing_strategy": "latency-based-routing"}, + { + "group_name": "g2", + "models": ["other-model"], + "routing_strategy": "latency-based-routing", + "routing_strategy_args": {"ttl": "not-a-number"}, + }, + ], + ) + + assert list(router._routing_groups) == ["g1"] + assert router._model_to_group == {"filtered-model": "g1"} + assert router._group_selectors["g1"]["latency-based-routing"] is selector + assert sum(1 for cb in litellm.callbacks if type(cb) is type(selector)) == 1