fix(router): handle non-standard fallback formats with order-based fallback

When fallbacks use non-standard formats (e.g. ["claude-3-haiku"] or
[{"model": "...", "messages": [...]}]), detect them with
_check_non_standard_fallback_format and pass them through directly
instead of trying to parse with get_fallback_model_group which only
handles the standard dict-keyed format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sameer Kankute 2026-03-26 12:28:25 +05:30
parent 6295e6b3d8
commit 2950677e5d
No known key found for this signature in database
2 changed files with 59 additions and 7 deletions

View file

@ -5313,15 +5313,20 @@ class Router:
for o in order_values
if o > skip_up_to
]
# Get external fallbacks
# Get external fallbacks — handle both standard and non-standard formats
external_fallback_group: Optional[List] = None
if fallbacks is not None and model_group is not None:
external_fallback_group, generic_idx = get_fallback_model_group(
fallbacks=fallbacks,
model_group=cast(str, model_group),
)
if external_fallback_group is None and generic_idx is not None:
external_fallback_group = fallbacks[generic_idx]["*"]
if _check_non_standard_fallback_format(fallbacks=fallbacks):
# Non-standard formats (e.g. ["claude-3-haiku"] or
# [{"model": "...", "messages": [...]}]) are passed through directly
external_fallback_group = fallbacks
else:
external_fallback_group, generic_idx = get_fallback_model_group(
fallbacks=fallbacks,
model_group=cast(str, model_group),
)
if external_fallback_group is None and generic_idx is not None:
external_fallback_group = fallbacks[generic_idx]["*"]
# Combined list: order fallbacks first, then external
combined_fallbacks = order_fallback_entries + (

View file

@ -282,3 +282,50 @@ async def test_router_order_fallback_then_external_fallback():
messages=[{"role": "user", "content": "hi"}],
)
assert response._hidden_params["model_id"] == "fallback"
@pytest.mark.asyncio
async def test_router_order_fallback_with_non_standard_fallbacks():
"""Non-standard fallback formats (e.g. fallbacks=["model-name"]) passed
per-request should still be tried after all order levels are exhausted."""
router = Router(
model_list=[
{
"model_name": "test-model",
"litellm_params": {
"model": "gpt-4o",
"api_key": "bad",
"mock_response": Exception("fail order 1"),
"order": 1,
},
"model_info": {"id": "1"},
},
{
"model_name": "test-model",
"litellm_params": {
"model": "gpt-4o",
"api_key": "bad",
"mock_response": Exception("fail order 2"),
"order": 2,
},
"model_info": {"id": "2"},
},
{
"model_name": "fallback-model",
"litellm_params": {
"model": "gpt-4o",
"api_key": "good",
"mock_response": "success from non-standard fallback",
},
"model_info": {"id": "fallback"},
},
],
num_retries=0,
)
response = await router.acompletion(
model="test-model",
messages=[{"role": "user", "content": "hi"}],
fallbacks=["fallback-model"], # non-standard format, passed per-request
)
assert response._hidden_params["model_id"] == "fallback"