From 2950677e5d4e7339f9484eead6c7fb6917f377f4 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 26 Mar 2026 12:28:25 +0530 Subject: [PATCH] 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 --- litellm/router.py | 19 +++++--- .../test_router_order_fallback.py | 47 +++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index f63d08598a6..6b16a79ec85 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -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 + ( diff --git a/tests/test_litellm/test_router_order_fallback.py b/tests/test_litellm/test_router_order_fallback.py index 51813fbc53b..760766a7461 100644 --- a/tests/test_litellm/test_router_order_fallback.py +++ b/tests/test_litellm/test_router_order_fallback.py @@ -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"