litellm/proxy: preserve model order of /v1/models and /model_group/info (#13178)

Closes #12644

Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>
This commit is contained in:
Alexander Yastrebov 2025-08-02 17:57:38 +02:00 • committed by GitHub
parent c427531c9c
commit 825923e7be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 107 additions and 24 deletions

View file

@ -175,22 +175,27 @@ def get_complete_model_list(
If list contains wildcard -> return known provider models
"""
unique_models: Set[str] = set()
unique_models = []
def append_unique(models):
for model in models:
if model not in unique_models:
unique_models.append(model)
if key_models:
unique_models.update(key_models)
append_unique(key_models)
elif team_models:
unique_models.update(team_models)
append_unique(team_models)
else:
unique_models.update(proxy_model_list)
append_unique(proxy_model_list)
if include_model_access_groups:
unique_models.update(model_access_groups.keys())
append_unique(list(model_access_groups.keys())) # TODO: keys order
if user_model:
unique_models.add(user_model)
append_unique([user_model])
if infer_model_from_keys:
valid_models = get_valid_models()
unique_models.update(valid_models)
append_unique(valid_models)
if only_model_access_groups:
model_access_groups_to_return: List[str] = []
@ -205,7 +210,7 @@ def get_complete_model_list(
llm_router=llm_router,
)
complete_model_list = list(unique_models) + all_wildcard_models
complete_model_list = unique_models + all_wildcard_models
return complete_model_list
@ -261,7 +266,7 @@ def get_known_models_from_wildcard(
def _get_wildcard_models(
unique_models: Set[str],
unique_models: List[str],
return_wildcard_routes: Optional[bool] = False,
llm_router: Optional[Router] = None,
) -> List[str]:

View file

@ -6469,18 +6469,18 @@ async def model_metrics_exceptions(
"""
sql_query = """
WITH cte AS (
SELECT
SELECT
CASE WHEN api_base = '' THEN litellm_model_name ELSE CONCAT(litellm_model_name, '-', api_base) END AS combined_model_api_base,
exception_type,
COUNT(*) AS num_rate_limit_exceptions
FROM "LiteLLM_ErrorLogs"
WHERE
"startTime" >= $1::timestamp
AND "endTime" <= $2::timestamp
WHERE
"startTime" >= $1::timestamp
AND "endTime" <= $2::timestamp
AND model_group = $3
GROUP BY combined_model_api_base, exception_type
)
SELECT
SELECT
combined_model_api_base,
COUNT(*) AS total_exceptions,
json_object_agg(exception_type, num_rate_limit_exceptions) AS exception_counts
@ -6711,10 +6711,13 @@ def _get_model_group_info(
llm_router: Router, all_models_str: List[str], model_group: Optional[str]
) -> List[ModelGroupInfoProxy]:
model_groups: List[ModelGroupInfoProxy] = []
# ensure all_models_str is a set
all_models_str_set = set(all_models_str)
for model in all_models_str_set:
unique_models = []
for model in all_models_str:
if model not in unique_models:
unique_models.append(model)
for model in unique_models:
if model_group is not None and model_group != model:
continue
@ -7304,7 +7307,7 @@ async def login(request: Request): # noqa: PLR0915
get_disabled_non_admin_personal_key_creation()
)
"""
To login to Admin UI, we support the following
To login to Admin UI, we support the following
- Login with UI_USERNAME and UI_PASSWORD
- Login with Invite Link `user_email` and `password` combination
"""
@ -8131,8 +8134,8 @@ async def update_config_general_settings(
"""
- Check if prisma_client is None
- Check if user allowed to call this endpoint (admin-only)
- Check if param in general settings
- Check if config value is valid type
- Check if param in general settings
- Check if config value is valid type
"""
if prisma_client is None:
@ -8208,7 +8211,7 @@ async def get_config_general_settings(
"""
- Check if prisma_client is None
- Check if user allowed to call this endpoint (admin-only)
- Check if param in general settings
- Check if param in general settings
"""
if prisma_client is None:
raise HTTPException(
@ -8272,7 +8275,7 @@ async def get_config_list(
"""
- Check if prisma_client is None
- Check if user allowed to call this endpoint (admin-only)
- Check if param in general settings
- Check if param in general settings
"""
if prisma_client is None:
raise HTTPException(
@ -8408,7 +8411,7 @@ async def delete_config_general_settings(
"""
- Check if prisma_client is None
- Check if user allowed to call this endpoint (admin-only)
- Check if param in general settings
- Check if param in general settings
"""
if prisma_client is None:
raise HTTPException(
@ -8592,7 +8595,7 @@ async def get_config(): # noqa: PLR0915
},
}
]
"""
for _callback in _success_callbacks:
if _callback != "langfuse":

View file

@ -19,3 +19,46 @@ def test_get_team_models_for_all_models_and_team_only_models():
)
combined_models = team_models + proxy_model_list
assert set(result) == set(combined_models)
@pytest.mark.parametrize(
"key_models,team_models,proxy_model_list,model_list,expected",
[
(
["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"],
[],
[],
[{"model_name": "anthropic/*", "litellm_params": {"model": "anthropic/*"}}],
["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"]
),
(
[],
["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"],
[],
[{"model_name": "anthropic/*", "litellm_params": {"model": "anthropic/*"}}],
["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"]
),
(
[],
[],
["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"],
[{"model_name": "anthropic/*", "litellm_params": {"model": "anthropic/*"}}],
["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"]
),
],
)
def test_get_complete_model_list_order(key_models, team_models, proxy_model_list, model_list, expected):
"""
Test that get_complete_model_list preserves order
"""
from litellm.proxy.auth.model_checks import get_complete_model_list
from litellm import Router
assert get_complete_model_list(
proxy_model_list=proxy_model_list,
key_models=key_models,
team_models=team_models,
user_model=None,
infer_model_from_keys=False,
llm_router=Router(model_list=model_list),
) == expected

View file

@ -57,3 +57,35 @@ def test_proxy_only_error_false_for_other_error_type():
)
is False
)
def test_get_model_group_info_order():
from litellm.proxy.proxy_server import _get_model_group_info
from litellm import Router
router = Router(
model_list=[
{
"model_name": "openai/tts-1",
"litellm_params": {
"model": "openai/tts-1",
"api_key": "sk-1234",
},
},
{
"model_name": "openai/gpt-3.5-turbo",
"litellm_params": {
"model": "openai/gpt-3.5-turbo",
"api_key": "sk-1234",
},
},
]
)
model_list = _get_model_group_info(
llm_router=router,
all_models_str=["openai/tts-1", "openai/gpt-3.5-turbo"],
model_group=None,
)
model_groups = [m.model_group for m in model_list]
assert model_groups == ["openai/tts-1", "openai/gpt-3.5-turbo"]