fix(router): reject string fallback targets at Router init (LIT-10238) (#48943)

Backport of #48943 to lts/1.108.x.
Cherry-picked from 96fe78f96e (main).

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Kerry Lu 2026-11-04 23:50:32 +00:00 • committed by kerry
parent 825a555b5b
commit 8304f16942
2 changed files with 17 additions and 8 deletions

View file

@ -1224,7 +1224,7 @@ class Router:
},
]
router = Router(model_list=model_list, fallbacks=[{"azure-gpt-3.5-turbo": "openai-gpt-3.5-turbo"}])
router = Router(model_list=model_list, fallbacks=[{"azure-gpt-3.5-turbo": ["openai-gpt-3.5-turbo"]}])
```
"""
@ -1431,6 +1431,10 @@ class Router:
if default_fallbacks is not None or litellm.default_fallbacks is not None:
_fallbacks = default_fallbacks or litellm.default_fallbacks
if isinstance(_fallbacks, str):
raise TypeError(
f"default_fallbacks must be a list of model names, got '{_fallbacks}'. Use ['{_fallbacks}']"
)
if self.fallbacks is not None:
self.fallbacks.append({"*": _fallbacks})
else:
@ -2564,7 +2568,7 @@ class Router:
self._initialize_core_endpoints()
self._initialize_specialized_endpoints()
def validate_fallbacks(self, fallback_param: list | None):
def validate_fallbacks(self, fallback_param: Sequence[Mapping[str, object]] | None):
"""
Validate the fallbacks parameter.
"""
@ -2577,6 +2581,13 @@ class Router:
raise ValueError(
f"Dictionary '{fallback_dict}' must have exactly one key, but has {len(fallback_dict)} keys."
)
((model_group, targets),) = fallback_dict.items()
if isinstance(targets, list) or check_non_standard_fallback_format(fallbacks=[fallback_dict]):
continue
raise TypeError(
f"Fallbacks for '{model_group}' must be a list of model names, got '{targets}'. "
f"Use {{'{model_group}': ['{targets}']}}"
)
def _add_encrypted_content_affinity_check(self, enable_global_affinity: bool) -> None:
def _move_before_deployment_affinity(

View file

@ -2,7 +2,7 @@ import asyncio
import importlib
import random
import time
from collections.abc import Mapping
from collections.abc import Mapping, Sequence
from typing import Final
from unittest.mock import MagicMock, patch
@ -42,6 +42,7 @@ from litellm.router_utils.router_callbacks.track_deployment_metrics import (
increment_deployment_successes_for_current_minute,
)
from litellm.types.router import (
RouterRateLimitError,
AllowedFailsPolicy,
DeploymentTypedDict,
LiteLLMParamsTypedDict,
@ -2972,8 +2973,6 @@ BENCHED: Final = (litellm.Timeout, litellm.Timeout, RouterRateLimitError, Router
{},
BENCHED,
),
({"allowed_fails": 1, "fallbacks": [{"test-model": "backup-model"}]}, {}, BENCHED),
({"allowed_fails": 1, "default_fallbacks": "backup-model"}, {}, BENCHED),
],
ids=[
"allowed_fails",
@ -2982,8 +2981,6 @@ BENCHED: Final = (litellm.Timeout, litellm.Timeout, RouterRateLimitError, Router
"policy_names_timeout",
"allowed_fails_with_a_fallback_group",
"allowed_fails_with_a_dict_fallback_target",
"allowed_fails_with_a_string_fallback_target",
"allowed_fails_with_a_string_default_fallbacks",
],
)
@pytest.mark.asyncio
@ -3029,7 +3026,8 @@ async def test_router_allowed_fails_benches_a_lone_deployment_only_when_the_poli
def test_has_fallback_group_reads_string_targets_and_skips_unparseable_entries(
fallbacks: Sequence[Mapping[str, object]], expected: bool
) -> None:
router: Final = Router(model_list=[TIMED_OUT_DEPLOYMENT], fallbacks=list(fallbacks))
router: Final = Router(model_list=[TIMED_OUT_DEPLOYMENT])
router.fallbacks = list(fallbacks)
assert _has_fallback_group(router, "test-model") is expected