Skip to content
Merged
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
77 changes: 75 additions & 2 deletions litellm/proxy/auth/model_checks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# What is this?
## Common checks for /v1/models and `/model/info`
from typing import Dict, List, Optional, Set
import copy
from typing import Any, Dict, List, Optional, Set

import litellm
from litellm._logging import verbose_proxy_logger
Expand Down Expand Up @@ -281,8 +282,18 @@ def _hydrate_litellm_credential_name(
def get_known_models_from_wildcard(
wildcard_model: str, litellm_params: Optional[LiteLLM_Params] = None
) -> List[str]:
wildcard_model_to_expand = (
litellm_params.model
if wildcard_model == "*"
and litellm_params is not None
and _check_wildcard_routing(litellm_params.model)
and "/" in litellm_params.model
else wildcard_model
)
try:
wildcard_provider_prefix, wildcard_suffix = wildcard_model.split("/", 1)
wildcard_provider_prefix, wildcard_suffix = wildcard_model_to_expand.split(
"/", 1
)
except ValueError: # safely fail
return []

Expand Down Expand Up @@ -341,6 +352,68 @@ def get_known_models_from_wildcard(
return suffix_appended_wildcard_models or []


def expand_wildcard_deployments_for_model_info(
deployments: list[dict[str, Any]],
) -> list[dict[str, Any]]:
"""Expand wildcard deployments into one row per known provider model.

PR #30025 changed /model/info to read from llm_router.model_list (correct,
so team-scoped rows are included). This function restores wildcard expansion
on top of that: a wildcard deployment like model_name="*" / litellm_params.model="openai/*"
becomes one entry per known openai model, matching /v1/models behaviour.
"""
expanded: list[dict[str, Any]] = []
for deployment in deployments:
model_name = str(deployment.get("model_name") or "")
raw_params = deployment.get("litellm_params")
litellm_params_dict: dict[str, Any] = (
raw_params if isinstance(raw_params, dict) else {}
)
litellm_model = str(litellm_params_dict.get("model") or "")

# Determine the wildcard pattern to expand.
# Branch order matters: only fall to litellm_model when model_name is
# also a wildcard, so a concrete model_name is never overwritten.
if _check_wildcard_routing(model_name) and "/" in model_name:
wildcard_pattern = model_name
elif _check_wildcard_routing(model_name) and _check_wildcard_routing(
litellm_model
):
wildcard_pattern = litellm_model
elif _check_wildcard_routing(model_name):
wildcard_pattern = model_name
else:
expanded.append(deployment)
continue

try:
litellm_params = (
LiteLLM_Params.model_validate(litellm_params_dict)
if litellm_params_dict
else None
)
except Exception:
expanded.append(deployment)
continue
expanded_names = get_known_models_from_wildcard(
wildcard_model=wildcard_pattern,
litellm_params=litellm_params,
)
if not expanded_names:
expanded.append(deployment)
continue

for name in expanded_names:
row = copy.deepcopy(deployment)
row["model_name"] = name
params = row.get("litellm_params")
if isinstance(params, dict):
params["model"] = name
expanded.append(row)

return expanded


def _get_wildcard_models(
unique_models: List[str],
return_wildcard_routes: Optional[bool] = False,
Expand Down
3 changes: 3 additions & 0 deletions litellm/proxy/proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ def generate_feedback_box():
from litellm.proxy.auth.handle_jwt import JWTHandler
from litellm.proxy.auth.litellm_license import LicenseCheck
from litellm.proxy.auth.model_checks import (
expand_wildcard_deployments_for_model_info,
get_all_fallbacks,
get_complete_model_list,
get_key_models,
Expand Down Expand Up @@ -13430,6 +13431,8 @@ async def model_info_v1(
alias_models = copy.deepcopy(llm_router.get_model_list_from_model_alias())
all_models.extend(alias_models)

all_models = expand_wildcard_deployments_for_model_info(all_models)

Comment thread
Sameerlite marked this conversation as resolved.
allowed_model_names = _get_v1_model_info_allowed_model_names(
user_api_key_dict=user_api_key_dict,
llm_router=llm_router,
Expand Down
71 changes: 70 additions & 1 deletion tests/test_litellm/proxy/auth/test_model_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,8 @@ def test_get_key_models_all_team_models_recursive_team():
from litellm.proxy._types import SpecialModelNames

user_api_key_dict = type(
"obj", (object,),
"obj",
(object,),
{
"models": [SpecialModelNames.all_team_models.value],
"team_id": "team-1",
Expand Down Expand Up @@ -617,3 +618,71 @@ def test_get_team_models_all_team_models_expands_with_access_groups():
assert "model-b" in result
assert "group-1" in result
assert "group-2" in result


def test_expand_wildcard_deployments_non_wildcard_passthrough():
"""Non-wildcard deployments must be returned unchanged."""
from litellm.proxy.auth.model_checks import (
expand_wildcard_deployments_for_model_info,
)

deployment = {"model_name": "gpt-4o", "litellm_params": {"model": "gpt-4o"}}
result = expand_wildcard_deployments_for_model_info([deployment])
assert result == [deployment]


def test_expand_wildcard_deployments_openai_wildcard():
"""openai/* should expand into ≥1 known openai model entries."""
from unittest.mock import patch

from litellm.proxy.auth.model_checks import (
expand_wildcard_deployments_for_model_info,
)

fake_models = ["openai/gpt-4o", "openai/gpt-4o-mini"]
deployment = {
"model_name": "openai/*",
"litellm_params": {"model": "openai/*"},
}
with patch(
"litellm.proxy.auth.model_checks.get_known_models_from_wildcard",
return_value=fake_models,
):
result = expand_wildcard_deployments_for_model_info([deployment])

assert len(result) == 2
assert all(r["model_name"] in fake_models for r in result)
assert all(r["litellm_params"]["model"] in fake_models for r in result)


def test_expand_wildcard_concrete_model_name_with_wildcard_litellm_params():
"""Concrete model_name must not be overwritten when only litellm_params.model is wildcard."""
from litellm.proxy.auth.model_checks import (
expand_wildcard_deployments_for_model_info,
)

deployment = {
"model_name": "my-custom-alias",
"litellm_params": {"model": "openai/*"},
}
result = expand_wildcard_deployments_for_model_info([deployment])
# model_name is not a wildcard, so the deployment passes through unchanged
assert result == [deployment]


def test_expand_wildcard_invalid_litellm_params_passthrough():
"""Deployments with invalid litellm_params must pass through unchanged (no 500)."""
from litellm.proxy.auth.model_checks import (
expand_wildcard_deployments_for_model_info,
)

deployment = {
"model_name": "openai/*",
"litellm_params": {
"model": "openai/*",
"max_retries": "not-an-int-field-that-breaks",
},
}
# Even if LiteLLM_Params construction fails the deployment should survive
result = expand_wildcard_deployments_for_model_info([deployment])
assert result == [deployment]
38 changes: 38 additions & 0 deletions tests/test_litellm/proxy/proxy_server/test_routes_model_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,44 @@ def test_v1_model_info_no_model_list_error(client, auth_as, null_router, path):
assert "LLM Model List not loaded" in response.text


def test_v1_model_info_star_wildcard_filter_keeps_provider_expansion(monkeypatch):
from litellm.proxy._types import SpecialModelNames, UserAPIKeyAuth
from litellm.proxy.auth import model_checks

def fake_get_provider_models(provider, litellm_params=None):
if provider == "openai":
return ["gpt-4o"]
return []

deployment = {
"model_name": "*",
"litellm_params": {"model": "openai/*"},
}
router = MagicMock()
router.get_model_access_groups = MagicMock(return_value={})
router.get_model_names = MagicMock(return_value=["*"])
router.get_model_list = MagicMock(return_value=[deployment])
monkeypatch.setattr(model_checks, "get_provider_models", fake_get_provider_models)

expanded_deployments = proxy_server.expand_wildcard_deployments_for_model_info(
[deployment]
)
allowed_model_names = proxy_server._get_v1_model_info_allowed_model_names(
user_api_key_dict=UserAPIKeyAuth(
api_key="sk-test",
models=[SpecialModelNames.all_proxy_models.value],
),
llm_router=router,
)

result = proxy_server._filter_v1_model_info_deployments(
all_models=expanded_deployments,
allowed_model_names=allowed_model_names,
)

assert [model["model_name"] for model in result] == ["openai/gpt-4o"]


# ---------------------------------------------------------------------------
# GET /model/info — team BYOK scoping (issue #30983)
# ---------------------------------------------------------------------------
Expand Down
Loading