fix(proxy): list each model id once and keep a named deployment's row over a wildcard's

This commit is contained in:
mateo-berri 2026-10-29 12:53:43 -07:00
parent 4b3c273fad
commit b3f386bb0f
2 changed files with 88 additions and 17 deletions

View file

@ -267,9 +267,7 @@ def get_complete_model_list(
expand_wildcard_routes=expand_wildcard_routes,
)
complete_model_list: Final = unique_models + all_wildcard_models
return complete_model_list
return list(dict.fromkeys(unique_models + all_wildcard_models))
def _hydrate_litellm_credential_name(
@ -399,18 +397,15 @@ def _deployment_team_id(deployment: Mapping[str, object]) -> str | None:
def _cached_wildcard_routing(llm_router: Router | None) -> Callable[[str, str | None], frozenset[str]] | None:
return None if llm_router is None else functools.cache(llm_router.wildcard_routed_models)
def _deployed_by_name(llm_router: Router | None) -> Callable[[str, str | None], bool]:
if llm_router is None:
return None
return lambda _model, _team_id: False
deployed_names: Final = frozenset(llm_router.model_names)
team_deployed_names: Final = frozenset(llm_router.team_model_to_deployment_indices)
wildcard_routed_models: Final = llm_router.wildcard_routed_models
def routed_models(model: str, team_id: str | None) -> frozenset[str]:
if model in deployed_names or (team_id, model) in team_deployed_names:
return frozenset()
return wildcard_routed_models(model, team_id)
return functools.cache(routed_models)
return lambda model, team_id: model in deployed_names or (team_id, model) in team_deployed_names
def _routes_to_model(
@ -522,6 +517,25 @@ def _routed_deployment_model(name_pattern: str, listed_name: str, deployment_mod
)
def _names_listed_under_wildcard(
deployment: Mapping[str, object],
wildcard_pattern: str,
litellm_params: LiteLLM_Params | None,
routed_models: Callable[[str, str | None], frozenset[str]] | None,
deployed_by_name: Callable[[str, str | None], bool],
) -> tuple[str, ...]:
team_id: Final = _deployment_team_id(deployment)
return tuple(
name
for name in get_known_models_from_wildcard(
wildcard_model=wildcard_pattern,
litellm_params=litellm_params,
routes_to_model=_routes_to_model(routed_models, team_id),
)
if not deployed_by_name(name, team_id)
)
def expand_wildcard_deployments_for_model_info(
deployments: list[dict[str, object]],
llm_router: Router | None = None,
@ -535,6 +549,7 @@ def expand_wildcard_deployments_for_model_info(
"""
expanded: Final[list[dict[str, object]]] = []
routed_models: Final = _cached_wildcard_routing(llm_router)
deployed_by_name: Final = _deployed_by_name(llm_router)
for deployment in deployments:
model_name = str(deployment.get("model_name") or "")
raw_params = deployment.get("litellm_params")
@ -559,10 +574,8 @@ def expand_wildcard_deployments_for_model_info(
except Exception:
expanded.append(deployment)
continue
expanded_names = get_known_models_from_wildcard(
wildcard_model=wildcard_pattern,
litellm_params=litellm_params,
routes_to_model=_routes_to_model(routed_models, _deployment_team_id(deployment)),
expanded_names = _names_listed_under_wildcard(
deployment, wildcard_pattern, litellm_params, routed_models, deployed_by_name
)
if not expanded_names:
expanded.append(deployment)

View file

@ -1248,7 +1248,7 @@ def _openai_catalog_behind_a_catch_all(
("model_name_team-x_gpt-4o", {"team_id": "team-x", "team_public_model_name": "gpt-4o"}, "team-x"),
],
)
def test_catch_all_wildcard_never_lists_a_name_a_concrete_deployment_serves(
def test_catch_all_wildcard_lists_a_name_a_concrete_deployment_serves_once(
monkeypatch: pytest.MonkeyPatch, concrete_name: str, concrete_model_info: Mapping[str, str], team_id: str | None
) -> None:
from litellm.proxy.auth.model_checks import get_complete_model_list
@ -1283,6 +1283,64 @@ def test_catch_all_wildcard_model_info_keeps_only_the_concrete_row_for_a_deploye
]
@contextmanager
def _openrouter_catalog_beside_a_named_deployment(
monkeypatch: pytest.MonkeyPatch, wildcard_name: str, named: str
) -> Generator[Router, None, None]:
import litellm
monkeypatch.setattr(litellm, "check_provider_endpoint", True)
monkeypatch.delenv("OPENROUTER_API_BASE", raising=False)
with respx.mock(assert_all_called=False) as respx_mock:
respx_mock.get("https://openrouter.ai/api/v1/models/user").mock(
return_value=httpx.Response(
200, json={"data": [{"id": "openai/gpt-5.4-mini"}, {"id": "anthropic/claude-haiku-5-5"}]}
)
)
yield Router(
model_list=[
{
"model_name": wildcard_name,
"litellm_params": {"model": "openrouter/*", "api_key": f"sk-or-wildcard-{wildcard_name}"},
},
{
"model_name": named,
"litellm_params": {"model": "openrouter/openai/gpt-5.4-mini", "api_key": "sk-or-named"},
},
]
)
@pytest.mark.parametrize(
("wildcard_name", "named"),
[("openrouter/*", "openrouter/openai/gpt-5.4-mini"), ("or-team/*", "or-team/openai/gpt-5.4-mini")],
)
def test_provider_wildcard_lists_a_name_a_named_deployment_serves_once(
monkeypatch: pytest.MonkeyPatch, wildcard_name: str, named: str
) -> None:
from litellm.proxy.auth.model_checks import expand_wildcard_deployments_for_model_info, get_complete_model_list
with _openrouter_catalog_beside_a_named_deployment(monkeypatch, wildcard_name, named) as router:
listed: Final = get_complete_model_list(
key_models=[],
team_models=[],
proxy_model_list=list(router.model_names),
user_model=None,
infer_model_from_keys=False,
llm_router=router,
)
rows: Final = expand_wildcard_deployments_for_model_info(
[dict(deployment) for deployment in router.get_model_list() or ()], llm_router=router
)
vendor_prefix: Final = wildcard_name.removesuffix("*")
assert sorted(listed) == [f"{vendor_prefix}anthropic/claude-haiku-5-5", named]
assert sorted((row["model_name"], row["litellm_params"]["api_key"]) for row in rows) == [
(f"{vendor_prefix}anthropic/claude-haiku-5-5", f"sk-or-wildcard-{wildcard_name}"),
(named, "sk-or-named"),
]
@respx.mock
def test_custom_prefix_that_starts_a_discovered_id_still_prefixes_every_listed_model(
monkeypatch: pytest.MonkeyPatch,