This commit is contained in:
Ben Younes 2026-08-27 19:37:19 -05:00 • committed by GitHub
commit 16f6b9b318
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 126 additions and 39 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
@ -232,14 +232,14 @@ def get_complete_model_list(
model_access_groups_to_return.append(model)
return model_access_groups_to_return
all_wildcard_models: Final = _get_wildcard_models(
wildcard: Final = _get_wildcard_models(
unique_models=unique_models,
return_wildcard_routes=return_wildcard_routes,
llm_router=llm_router,
team_id=team_id,
)
complete_model_list: Final = unique_models + all_wildcard_models
complete_model_list: Final = list(wildcard.kept_models + wildcard.expanded_models)
return complete_model_list
@ -377,48 +377,64 @@ 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)
class _WildcardModels(NamedTuple):
kept_models: tuple[str, ...]
expanded_models: tuple[str, ...]
def _get_wildcard_models(
unique_models: list[str],
unique_models: Sequence[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)
) -> _WildcardModels:
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)
expanded_models: Final = tuple(
item
for model, expansion in expansions.items()
for item in [*([model] if return_wildcard_routes else []), *expansion.concrete_models]
)
if wildcard_models:
models_to_remove.add(model)
all_wildcard_models.extend(wildcard_models)
for model in models_to_remove:
unique_models.remove(model)
return all_wildcard_models
literals_to_drop: Final = frozenset(
model for model, expansion in expansions.items() if expansion.has_router_deployment or expansion.concrete_models
)
kept_models: Final = tuple(model for model in unique_models if model not in literals_to_drop)
return _WildcardModels(kept_models=kept_models, expanded_models=expanded_models)
def get_all_fallbacks(

View file

@ -803,3 +803,74 @@ 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)
def test_get_wildcard_models_does_not_mutate_input_and_drops_all_literals():
"""Regression for #13752: _get_wildcard_models must not mutate the caller's list and
must drop every expanded wildcard literal from kept_models, even when the same literal
appears more than once, so no literal route leaks back into the listing."""
from unittest.mock import MagicMock
from litellm.proxy.auth.model_checks import _get_wildcard_models
router = MagicMock()
router.get_model_list.return_value = [{"litellm_params": {"model": "openai/*"}}]
unique_models = ["openai/*", "openai/*"]
original = list(unique_models)
result = _get_wildcard_models(unique_models=unique_models, llm_router=router)
assert unique_models == original
assert "openai/*" not in result.kept_models
assert result.expanded_models
assert all(m != "openai/*" for m in result.expanded_models)