fix(proxy): stop listing literal wildcard routes as models

get_complete_model_list expanded wildcard routes like openai/* into concrete
models but, when the router held a deployment for the pattern, left the literal
openai/* in the listing too. The /models endpoint then advertised openai/* as a
callable model, misleading clients. Drop the literal in the router-backed branch
like the other expansion branches already do; it is still surfaced when
return_wildcard_routes is set. Also type the local accumulators.

Fixes #13752
This commit is contained in:
Ben Younes 2026-08-10 12:34:28 +00:00 • committed by Ousama Ben Younes
parent 973329e986
commit 95b6cf50a7
2 changed files with 96 additions and 31 deletions

View file

@ -2,7 +2,7 @@
## Common checks for /v1/models and `/model/info`
import copy
from collections.abc import Sequence
from typing import Any, Final
from typing import Any, Final, NamedTuple
import litellm
from litellm._logging import verbose_proxy_logger
@ -377,44 +377,60 @@ def expand_wildcard_deployments_for_model_info(
return expanded
class _WildcardExpansion(NamedTuple):
concrete_models: tuple[str, ...]
has_router_deployment: bool
def _expand_wildcard_model(model: str, llm_router: Router | None, team_id: str | None) -> _WildcardExpansion:
if llm_router is None:
return _WildcardExpansion(
tuple(get_known_models_from_wildcard(wildcard_model=model, litellm_params=None)), False
)
router_deployments: Final = llm_router.get_model_list(model_name=model, team_id=team_id)
if not router_deployments:
# Router has no deployment for this wildcard (e.g. BYOK team models); fall
# back to expanding from known provider models.
return _WildcardExpansion(
tuple(get_known_models_from_wildcard(wildcard_model=model, litellm_params=None)), False
)
concrete_models: Final = tuple(
concrete
for router_model in router_deployments
for concrete in get_known_models_from_wildcard(
wildcard_model=model,
litellm_params=LiteLLM_Params(**router_model["litellm_params"]),
)
)
return _WildcardExpansion(concrete_models, True)
def _get_wildcard_models(
unique_models: list[str],
return_wildcard_routes: bool | None = False,
llm_router: Router | None = None,
team_id: str | None = None,
) -> list[str]:
models_to_remove: Final = set()
all_wildcard_models: Final = []
for model in unique_models:
if _check_wildcard_routing(model=model):
if return_wildcard_routes: # will add the wildcard route to the list eg: anthropic/*.
all_wildcard_models.append(model)
expansions: Final = {
model: _expand_wildcard_model(model=model, llm_router=llm_router, team_id=team_id)
for model in unique_models
if _check_wildcard_routing(model=model)
}
## get litellm params from model
if llm_router is not None:
model_list = llm_router.get_model_list(model_name=model, team_id=team_id)
if model_list:
for router_model in model_list:
wildcard_models = get_known_models_from_wildcard(
wildcard_model=model,
litellm_params=LiteLLM_Params(**router_model["litellm_params"]),
)
all_wildcard_models.extend(wildcard_models)
else:
# Router has no deployment for this wildcard (e.g., BYOK team models)
# Fall back to expanding from known provider models
wildcard_models = get_known_models_from_wildcard(wildcard_model=model, litellm_params=None)
if wildcard_models:
models_to_remove.add(model)
all_wildcard_models.extend(wildcard_models)
else:
# get all known provider models
wildcard_models = get_known_models_from_wildcard(wildcard_model=model, litellm_params=None)
if wildcard_models:
models_to_remove.add(model)
all_wildcard_models.extend(wildcard_models)
all_wildcard_models: Final = [
item
for model, expansion in expansions.items()
for item in [*([model] if return_wildcard_routes else []), *expansion.concrete_models]
]
# The literal pattern (e.g. "openai/*") is a routing directive, not a callable
# model, so drop it from the listing once expanded. It is re-surfaced above only
# when return_wildcard_routes is set (#13752).
models_to_remove: Final = {
model for model, expansion in expansions.items() if expansion.has_router_deployment or expansion.concrete_models
}
for model in models_to_remove:
unique_models.remove(model)

View file

@ -804,3 +804,52 @@ def test_get_complete_model_list_sentinel_only_grants_nothing():
infer_model_from_keys=False,
)
assert result == []
def test_get_complete_model_list_omits_literal_wildcard_when_router_has_deployment():
"""Regression for #13752: a configured wildcard route (e.g. openai/*) with a real
router deployment must expand to concrete models without listing the literal pattern
itself, unless return_wildcard_routes is requested."""
from unittest.mock import MagicMock
from litellm.proxy.auth.model_checks import get_complete_model_list
router = MagicMock()
router.get_model_list.return_value = [{"litellm_params": {"model": "openai/*"}}]
default_result = get_complete_model_list(
key_models=[],
team_models=[],
proxy_model_list=["openai/*"],
user_model=None,
infer_model_from_keys=False,
return_wildcard_routes=False,
llm_router=router,
)
assert "openai/*" not in default_result
assert any(m.startswith("openai/") and m != "openai/*" for m in default_result)
with_routes_result = get_complete_model_list(
key_models=[],
team_models=[],
proxy_model_list=["openai/*"],
user_model=None,
infer_model_from_keys=False,
return_wildcard_routes=True,
llm_router=router,
)
assert with_routes_result.count("openai/*") == 1
# No router at all: the wildcard still expands from known provider models and
# the literal is not listed.
no_router_result = get_complete_model_list(
key_models=[],
team_models=[],
proxy_model_list=["openai/*"],
user_model=None,
infer_model_from_keys=False,
return_wildcard_routes=False,
llm_router=None,
)
assert "openai/*" not in no_router_result
assert any(m.startswith("openai/") and m != "openai/*" for m in no_router_result)