Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions litellm/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
#########################################################
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
"""
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
elif router_type == "adaptive":
router.adaptive_routers[target_group] = mock_sub_router

return router

Comment on lines +55 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No test for adaptive_routers alias resolution

async_pre_routing_hook has four router-map branches: auto_routers, complexity_routers, adaptive_routers, and quality_routers. The suite covers three of them but skips adaptive_routers. If a regression were introduced that accidentally excluded adaptive_routers from the alias-resolution path, these tests would not catch it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four router types are covered -- see test_alias_resolves_to_adaptive_router in the test file. The _make_router_with_alias helper accepts router_type="adaptive" and the test class has a dedicated test case for it.


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_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
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()
Loading