diff --git a/litellm/proxy/auth/model_checks.py b/litellm/proxy/auth/model_checks.py index 9ac68087374..5dde12f16bd 100644 --- a/litellm/proxy/auth/model_checks.py +++ b/litellm/proxy/auth/model_checks.py @@ -274,6 +274,8 @@ def _hydrate_litellm_credential_name( def _ids_routing_to_catalog_models( name_pattern: str, deployment_pattern: str, provider: str, catalog_ids: Sequence[str] ) -> tuple[str, ...]: + if name_pattern.count("*") != deployment_pattern.count("*"): + return () deployment_regex: Final = re.compile(PatternMatchRouter().pattern_to_regex(deployment_pattern)) provider_id_prefix: Final = f"{provider}/" catalog_matches: Final = ( @@ -291,6 +293,13 @@ def _ids_routing_to_catalog_models( ) +def _deployment_pattern_naming_listed_ids(wildcard_suffix: str, litellm_params: LiteLLM_Params | None) -> str | None: + if litellm_params is None or not _check_wildcard_routing(litellm_params.model): + return None + deployment_suffix: Final = litellm_params.model.partition("/")[2] + return litellm_params.model if wildcard_suffix != "*" or deployment_suffix != "*" else None + + def get_known_models_from_wildcard(wildcard_model: str, litellm_params: LiteLLM_Params | None = None) -> list[str]: wildcard_model_to_expand: Final = ( litellm_params.model @@ -304,6 +313,9 @@ def get_known_models_from_wildcard(wildcard_model: str, litellm_params: LiteLLM_ wildcard_provider_prefix, wildcard_suffix = wildcard_model_to_expand.split("/", 1) except ValueError: # safely fail return [] + deployment_pattern_naming_listed_ids: Final = _deployment_pattern_naming_listed_ids(wildcard_suffix, litellm_params) + if deployment_pattern_naming_listed_ids is not None and wildcard_model == "*": + return [] # Use provider from litellm_params when available, otherwise from wildcard prefix # (e.g., "openai" from "openai/*" - needed for BYOK where wildcard isn't in router) @@ -321,16 +333,16 @@ def get_known_models_from_wildcard(wildcard_model: str, litellm_params: LiteLLM_ if wildcard_models is None: return [] - if wildcard_suffix != "*": - if litellm_params is not None and _check_wildcard_routing(litellm_params.model): - return list( - _ids_routing_to_catalog_models( - name_pattern=wildcard_model, - deployment_pattern=litellm_params.model, - provider=provider, - catalog_ids=wildcard_models, - ) + if deployment_pattern_naming_listed_ids is not None: + return list( + _ids_routing_to_catalog_models( + name_pattern=wildcard_model, + deployment_pattern=deployment_pattern_naming_listed_ids, + provider=provider, + catalog_ids=wildcard_models, ) + ) + if wildcard_suffix != "*": model_prefix: Final = wildcard_suffix.replace("*", "") provider_id_prefix: Final = f"{provider}/" filtered_wildcard_models: Final = [ @@ -363,6 +375,17 @@ def get_known_models_from_wildcard(wildcard_model: str, litellm_params: LiteLLM_ return list(dict.fromkeys(suffix_appended_wildcard_models)) +def _deployment_model_serving(listed_id: str, name_pattern: str, deployment_model: str) -> str: + listed_match: Final = ( + None if name_pattern == "*" else re.fullmatch(PatternMatchRouter().pattern_to_regex(name_pattern), listed_id) + ) + if listed_match is None: + return listed_id + return PatternMatchRouter.set_deployment_model_name( + matched_pattern=listed_match, litellm_deployment_litellm_model=deployment_model + ) + + def expand_wildcard_deployments_for_model_info( deployments: list[dict[str, object]], ) -> list[dict[str, object]]: @@ -411,7 +434,9 @@ def expand_wildcard_deployments_for_model_info( row["model_name"] = name params = row.get("litellm_params") if isinstance(params, dict): - params["model"] = name + params["model"] = _deployment_model_serving( + name, name_pattern=model_name, deployment_model=litellm_model + ) expanded.append(row) return expanded diff --git a/tests/unit/proxy/auth/test_model_checks.py b/tests/unit/proxy/auth/test_model_checks.py index 8cf5708ba7c..bf3d52d650d 100644 --- a/tests/unit/proxy/auth/test_model_checks.py +++ b/tests/unit/proxy/auth/test_model_checks.py @@ -1284,11 +1284,13 @@ _ROUTE_CF_IDS: Final = {_ROUTE_CF_ID, _ROUTE_CF_SUFFIXED_ID} ), pytest.param("cloudflare/team-*", "cloudflare/*", None, {*_ROUTE_CF_IDS, _ROUTE_HF_ID}, id="alias"), pytest.param("cloudflare/cf-*", "cloudflare/@cf/*", None, _ROUTE_CF_IDS, id="alias-over-partial-deployment"), - pytest.param("*", "cloudflare/@cf/*", None, _ROUTE_CF_IDS, id="catch-all-over-partial-deployment"), + pytest.param("my-cf/*", "cloudflare/@cf/*", None, _ROUTE_CF_IDS, id="bare-alias-over-partial-deployment"), + pytest.param( + "cloudflare/team-*", "cloudflare/@cf/*/*", None, set(), id="alias-with-fewer-wildcards-than-its-deployment" + ), pytest.param( "cloudflare/*", "cloudflare/*", "cloudflare/@cf/*", _ROUTE_CF_IDS, id="key-filter-over-provider-wildcard" ), - pytest.param("*", "gemini/gemini-partial-route-9*", None, {_ROUTE_GEMINI_ID}, id="catch-all-over-bare-id"), pytest.param("gemini/gemini-*", "gemini/*", None, {_ROUTE_GEMINI_ID}, id="alias-over-bare-id"), pytest.param( "gemini/*", "gemini/*", "gemini/gemini-partial-route-9*", {_ROUTE_GEMINI_ID}, id="key-filter-over-bare-id" @@ -1337,15 +1339,57 @@ def test_every_listed_partial_wildcard_id_routes_to_the_catalog_model_it_names( infer_model_from_keys=False, llm_router=router, ) - model_info_names: Final = [ - str(row["model_name"]) + model_info_rows: Final = tuple( + (str(row["model_name"]), TypeAdapter(LiteLLM_Params).validate_python(row["litellm_params"]).model) for row in expand_wildcard_deployments_for_model_info( [dict(deployment) for deployment in router.get_model_list() or []] ) - ] + ) listed_v1_models: Final = routed_catalog_ids(v1_models) - listed_model_info_names: Final = routed_catalog_ids(model_info_names) + listed_model_info_names: Final = routed_catalog_ids([name for name, _ in model_info_rows]) assert listed_v1_models == sorted(reachable) + assert [shown_model for _, shown_model in model_info_rows] == [routed_model(name) for name, _ in model_info_rows] if key_model is None: assert listed_model_info_names == sorted(reachable) + + +@pytest.mark.parametrize("deployment_model", ["cloudflare/@cf/*", "gemini/gemini-partial-route-9*"]) +def test_catch_all_over_a_partial_deployment_lists_no_catalog_ids(deployment_model: str) -> None: + from litellm import Router + from litellm.proxy.auth.model_checks import expand_wildcard_deployments_for_model_info, get_complete_model_list + + router: Final = Router(model_list=[{"model_name": "*", "litellm_params": {"model": deployment_model}}]) + deployments: Final = [dict(deployment) for deployment in router.get_model_list() or []] + with _registered_chat_models({_ROUTE_CF_ID: "cloudflare", _ROUTE_GEMINI_ID: "gemini"}): + v1_models: Final = get_complete_model_list( + key_models=[], + team_models=[], + proxy_model_list=["*"], + user_model=None, + infer_model_from_keys=False, + llm_router=router, + ) + model_info_rows: Final = expand_wildcard_deployments_for_model_info(deployments) + + assert v1_models == [] + assert model_info_rows == deployments + + +def test_catch_all_model_info_rows_show_the_listed_provider_model() -> None: + from pydantic import TypeAdapter + + from litellm.proxy.auth.model_checks import expand_wildcard_deployments_for_model_info + from litellm.types.router import LiteLLM_Params + + listed_id: Final = "openai/partial-route-catch-all-model" + with _registered_chat_models({listed_id.removeprefix("openai/"): "openai"}): + model_info_rows: Final = expand_wildcard_deployments_for_model_info( + [{"model_name": "*", "litellm_params": {"model": "openai/*"}}] + ) + + shown_models: Final = { + str(row["model_name"]): TypeAdapter(LiteLLM_Params).validate_python(row["litellm_params"]).model + for row in model_info_rows + } + assert shown_models[listed_id] == listed_id