From 4f17351d312eb6ad1da88c9e13ffd09e1bca47d9 Mon Sep 17 00:00:00 2001 From: Jonathan Wrede Date: Sat, 9 May 2026 17:26:05 +0000 Subject: [PATCH 1/2] fix(router): resolve model_group_alias before pre-routing hook lookups When a model_group_alias points to a complexity_router (or auto_router, quality_router, adaptive_router) group, calling completion() with the alias name skips the pre-routing hook because the alias is not found in the router maps. The hook returns None, the raw auto_router deployment is selected, and litellm.acompletion fails with "Unmapped LLM provider". Resolve aliases at the top of async_pre_routing_hook using the existing _get_model_from_alias() utility so the resolved group name is used for all router map lookups. Fixes #27473 --- litellm/router.py | 5 + .../test_pre_routing_hook_alias_resolution.py | 210 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 tests/test_litellm/router_utils/test_pre_routing_hook_alias_resolution.py diff --git a/litellm/router.py b/litellm/router.py index 7512ee387dca..0ad7b391e9b9 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -10287,6 +10287,11 @@ async def async_pre_routing_hook( Used for the litellm auto-router to modify the request before the routing decision is made. """ + # Resolve model_group_alias so aliased names hit the correct router map + _resolved = self._get_model_from_alias(model) + if _resolved is not None: + model = _resolved + ######################################################### # Check if any auto-router should be used ######################################################### diff --git a/tests/test_litellm/router_utils/test_pre_routing_hook_alias_resolution.py b/tests/test_litellm/router_utils/test_pre_routing_hook_alias_resolution.py new file mode 100644 index 000000000000..538af9ab621b --- /dev/null +++ b/tests/test_litellm/router_utils/test_pre_routing_hook_alias_resolution.py @@ -0,0 +1,210 @@ +""" +Tests that Router.async_pre_routing_hook resolves model_group_alias +before looking up router maps (complexity_routers, auto_routers, etc.). + +Without alias resolution, calling completion(model="my-alias") when +"my-alias" maps to a complexity_router group causes an "Unmapped LLM +provider" 400 error because the alias name is not found in the router +maps, so the pre-routing hook returns None and the raw +auto_router/complexity_router deployment is selected as-is. + +See: https://github.com/BerriAI/litellm/issues/27473 +""" + +import os +import sys +from unittest.mock import AsyncMock, MagicMock + +import pytest + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +from litellm import Router +from litellm.types.router import PreRoutingHookResponse + + +def _make_router_with_alias( + alias_name: str, + target_group: str, + router_type: str = "complexity", +) -> Router: + """Build a Router with a model_group_alias and a registered router.""" + router = Router( + model_list=[ + { + "model_name": "gpt-4o-mini", + "litellm_params": {"model": "openai/gpt-4o-mini"}, + } + ], + model_group_alias={alias_name: target_group}, + ) + + mock_sub_router = MagicMock() + mock_sub_router.async_pre_routing_hook = AsyncMock( + return_value=PreRoutingHookResponse( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "test"}], + ) + ) + + if router_type == "complexity": + router.complexity_routers[target_group] = mock_sub_router + elif router_type == "auto": + router.auto_routers[target_group] = mock_sub_router + elif router_type == "quality": + router.quality_routers[target_group] = mock_sub_router + + return router + + +class TestPreRoutingHookAliasResolution: + """Router.async_pre_routing_hook must resolve model_group_alias.""" + + @pytest.mark.asyncio + async def test_alias_resolves_to_complexity_router(self): + """An aliased model name should match the complexity_router for + the resolved group name.""" + router = _make_router_with_alias( + alias_name="my-alias", + target_group="auto_router/complexity_router/my-router", + router_type="complexity", + ) + + result = await router.async_pre_routing_hook( + model="my-alias", + request_kwargs={}, + messages=[{"role": "user", "content": "Hello"}], + ) + + assert result is not None + assert result.model == "gpt-4o-mini" + mock = router.complexity_routers["auto_router/complexity_router/my-router"] + mock.async_pre_routing_hook.assert_awaited_once() + + @pytest.mark.asyncio + async def test_alias_resolves_to_auto_router(self): + """An aliased model name should match the auto_router for the + resolved group name.""" + router = _make_router_with_alias( + alias_name="smart", + target_group="auto_router/my-auto-router", + router_type="auto", + ) + + result = await router.async_pre_routing_hook( + model="smart", + request_kwargs={}, + messages=[{"role": "user", "content": "Hello"}], + ) + + assert result is not None + mock = router.auto_routers["auto_router/my-auto-router"] + mock.async_pre_routing_hook.assert_awaited_once() + + @pytest.mark.asyncio + async def test_alias_resolves_to_quality_router(self): + """An aliased model name should match the quality_router for the + resolved group name.""" + router = _make_router_with_alias( + alias_name="best", + target_group="quality-router-group", + router_type="quality", + ) + + result = await router.async_pre_routing_hook( + model="best", + request_kwargs={}, + messages=[{"role": "user", "content": "Hello"}], + ) + + assert result is not None + mock = router.quality_routers["quality-router-group"] + mock.async_pre_routing_hook.assert_awaited_once() + + @pytest.mark.asyncio + async def test_non_alias_model_still_works(self): + """A model name that is NOT an alias should still match the router + map directly (no regression).""" + router = Router( + model_list=[ + { + "model_name": "gpt-4o-mini", + "litellm_params": {"model": "openai/gpt-4o-mini"}, + } + ], + ) + mock_sub_router = MagicMock() + mock_sub_router.async_pre_routing_hook = AsyncMock( + return_value=PreRoutingHookResponse( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "test"}], + ) + ) + router.complexity_routers["my-router"] = mock_sub_router + + result = await router.async_pre_routing_hook( + model="my-router", + request_kwargs={}, + messages=[{"role": "user", "content": "Hello"}], + ) + + assert result is not None + mock_sub_router.async_pre_routing_hook.assert_awaited_once() + + @pytest.mark.asyncio + async def test_no_alias_no_router_returns_none(self): + """When the model is not an alias and not in any router map, + async_pre_routing_hook should return None.""" + router = Router( + model_list=[ + { + "model_name": "gpt-4o-mini", + "litellm_params": {"model": "openai/gpt-4o-mini"}, + } + ], + ) + + result = await router.async_pre_routing_hook( + model="some-unknown-model", + request_kwargs={}, + messages=[{"role": "user", "content": "Hello"}], + ) + + assert result is None + + @pytest.mark.asyncio + async def test_alias_dict_format_resolves(self): + """model_group_alias supports dict format with a 'model' key.""" + router = Router( + model_list=[ + { + "model_name": "gpt-4o-mini", + "litellm_params": {"model": "openai/gpt-4o-mini"}, + } + ], + model_group_alias={ + "my-alias": { + "model": "auto_router/complexity_router/cr", + "hidden": False, + } + }, + ) + mock_sub_router = MagicMock() + mock_sub_router.async_pre_routing_hook = AsyncMock( + return_value=PreRoutingHookResponse( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "test"}], + ) + ) + router.complexity_routers["auto_router/complexity_router/cr"] = mock_sub_router + + result = await router.async_pre_routing_hook( + model="my-alias", + request_kwargs={}, + messages=[{"role": "user", "content": "Hello"}], + ) + + assert result is not None + mock_sub_router.async_pre_routing_hook.assert_awaited_once() From 55d393976c8db4eba3620298944ef9032ddc4f3c Mon Sep 17 00:00:00 2001 From: Jonathan Wrede Date: Sat, 9 May 2026 17:45:26 +0000 Subject: [PATCH 2/2] test: add adaptive_routers alias resolution test Cover the adaptive_routers branch in async_pre_routing_hook, completing test coverage for all four router map types. --- .../test_pre_routing_hook_alias_resolution.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_litellm/router_utils/test_pre_routing_hook_alias_resolution.py b/tests/test_litellm/router_utils/test_pre_routing_hook_alias_resolution.py index 538af9ab621b..f2311a83b340 100644 --- a/tests/test_litellm/router_utils/test_pre_routing_hook_alias_resolution.py +++ b/tests/test_litellm/router_utils/test_pre_routing_hook_alias_resolution.py @@ -55,6 +55,8 @@ def _make_router_with_alias( router.auto_routers[target_group] = mock_sub_router elif router_type == "quality": router.quality_routers[target_group] = mock_sub_router + elif router_type == "adaptive": + router.adaptive_routers[target_group] = mock_sub_router return router @@ -123,6 +125,26 @@ async def test_alias_resolves_to_quality_router(self): mock = router.quality_routers["quality-router-group"] mock.async_pre_routing_hook.assert_awaited_once() + @pytest.mark.asyncio + async def test_alias_resolves_to_adaptive_router(self): + """An aliased model name should match the adaptive_router for the + resolved group name.""" + router = _make_router_with_alias( + alias_name="adaptive", + target_group="adaptive-router-group", + router_type="adaptive", + ) + + result = await router.async_pre_routing_hook( + model="adaptive", + request_kwargs={}, + messages=[{"role": "user", "content": "Hello"}], + ) + + assert result is not None + mock = router.adaptive_routers["adaptive-router-group"] + mock.async_pre_routing_hook.assert_awaited_once() + @pytest.mark.asyncio async def test_non_alias_model_still_works(self): """A model name that is NOT an alias should still match the router