From c8d36acf265ce4d2e5368b15135b6772f364e180 Mon Sep 17 00:00:00 2001 From: Alvin Tang <104285249+alvinttang@users.noreply.github.com> Date: Sun, 31 May 2026 14:16:14 +0800 Subject: [PATCH] fix(router): resolve model_group_alias before fallback lookup When a request is routed via `model_group_alias`, the alias name was passed straight to `get_fallback_model_group`, whose exact-match check never matched fallbacks declared against the underlying real model group. The router then raised the original exception with "No fallback model group found for original model_group=", silently dropping the configured fallback chain. Resolve the alias to its real model group inside `async_function_with_fallbacks_common_utils` before any fallback lookup. `original_model_group` intentionally keeps the alias name so logging and the `x-litellm-attempted-fallbacks` headers remain user-facing. Refs #10317 --- litellm/router.py | 10 ++++ tests/local_testing/test_router_fallbacks.py | 54 ++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/litellm/router.py b/litellm/router.py index debccb0e83fe..94b4e737e25a 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -6339,6 +6339,16 @@ async def async_function_with_fallbacks_common_utils( # noqa: PLR0915 if disable_fallbacks is True or original_model_group is None: raise e + # If the caller routed to a `model_group_alias`, fallbacks may be + # declared against the underlying real model group (see #10317). + # Resolve the alias here so the downstream exact-match lookup in + # `get_fallback_model_group` works in both cases. `original_model_group` + # intentionally keeps the alias name for logging / fallback headers. + if model_group is not None: + aliased_model_group = self._get_model_from_alias(model=model_group) + if aliased_model_group is not None: + model_group = aliased_model_group + input_kwargs = { "litellm_router": self, "original_exception": original_exception, diff --git a/tests/local_testing/test_router_fallbacks.py b/tests/local_testing/test_router_fallbacks.py index a14e53adbc49..2c365c71d570 100644 --- a/tests/local_testing/test_router_fallbacks.py +++ b/tests/local_testing/test_router_fallbacks.py @@ -1626,3 +1626,57 @@ async def test_router_attempted_fallbacks_in_response(expected_attempted_fallbac resp._hidden_params["additional_headers"]["x-litellm-attempted-fallbacks"] == expected_attempted_fallbacks ) + + +@pytest.mark.parametrize("sync_mode", [True, False]) +@pytest.mark.asyncio +async def test_model_group_alias_respects_fallbacks(sync_mode): + """ + Related issue - https://github.com/BerriAI/litellm/issues/10317 + + When a request targets a model via ``model_group_alias``, fallbacks + declared against the underlying real model group should still fire. + Previously the fallback lookup was performed against the alias name + (e.g. ``"alias-model"``) which never matched a fallback keyed by the + real model (``"real-bad-model"``), so the router raised the original + error with ``No fallback model group found for original + model_group=alias-model``. + """ + router = Router( + model_list=[ + { + "model_name": "real-bad-model", + "litellm_params": { + "model": "openai/my-bad-model", + "api_key": "my-bad-api-key", + }, + }, + { + "model_name": "my-good-model", + "litellm_params": { + "model": "gpt-4o", + "api_key": os.getenv("OPENAI_API_KEY"), + }, + }, + ], + model_group_alias={"alias-model": "real-bad-model"}, + fallbacks=[{"real-bad-model": ["my-good-model"]}], + ) + + if sync_mode: + response = router.completion( + model="alias-model", + messages=[{"role": "user", "content": "Hey, how's it going?"}], + mock_testing_fallbacks=True, + mock_response="Hey! nice day", + ) + else: + response = await router.acompletion( + model="alias-model", + messages=[{"role": "user", "content": "Hey, how's it going?"}], + mock_testing_fallbacks=True, + mock_response="Hey! nice day", + ) + + assert isinstance(response, litellm.ModelResponse) + assert response.model == "gpt-4o"