diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 0bead9fb2f4..9453140f472 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -3940,10 +3940,13 @@ def _check_model_access_helper( ## check if model in allowed model names from collections import defaultdict + team_alias_target: Final = team_model_aliases.get(model) if team_model_aliases else None + effective_model: Final = team_alias_target if team_alias_target is not None else model + access_groups: dict[str, list[str]] = defaultdict(list) if llm_router: - access_groups = llm_router.get_model_access_groups(model_name=model, team_id=team_id) + access_groups = llm_router.get_model_access_groups(model_name=effective_model, team_id=team_id) models = _resolve_all_team_model_sentinel_for_auth_check( models=models, @@ -3959,16 +3962,7 @@ def _check_model_access_helper( # Filter out models that are access_groups filtered_models: Final = [m for m in models if m not in access_groups] - team_alias_target: Final = team_model_aliases.get(model) if team_model_aliases else None - if team_alias_target is not None and _check_model_access_helper( - model=team_alias_target, - llm_router=llm_router, - models=models, - team_id=team_id, - ): - return True - - if _model_matches_any_wildcard_pattern_in_list(model=model, allowed_model_list=filtered_models): + if _model_matches_any_wildcard_pattern_in_list(model=effective_model, allowed_model_list=filtered_models): return True all_model_access: bool = False @@ -3979,7 +3973,7 @@ def _check_model_access_helper( if SpecialModelNames.all_proxy_models.value in filtered_models: all_model_access = True - if model is not None and model not in filtered_models and all_model_access is False: + if effective_model is not None and effective_model not in filtered_models and all_model_access is False: return False return True @@ -4024,11 +4018,13 @@ def _can_object_call_model( ) return True - potential_models: Final = [model] - if model in litellm.model_alias_map: - potential_models.append(litellm.model_alias_map[model]) - elif llm_router and model in llm_router.model_group_alias: - _model: Final = llm_router._get_model_from_alias(model) + team_alias_target: Final = team_model_aliases.get(model) if team_model_aliases else None + resolved_model: Final = team_alias_target if team_alias_target is not None else model + potential_models: Final = [resolved_model] + if resolved_model in litellm.model_alias_map: + potential_models.append(litellm.model_alias_map[resolved_model]) + elif llm_router and resolved_model in llm_router.model_group_alias: + _model: Final = llm_router._get_model_from_alias(resolved_model) if _model: potential_models.append(_model) @@ -4038,7 +4034,6 @@ def _can_object_call_model( model=m, llm_router=llm_router, models=models, - team_model_aliases=team_model_aliases, team_id=team_id, ): return True diff --git a/tests/test_litellm/proxy/auth/test_auth_checks.py b/tests/test_litellm/proxy/auth/test_auth_checks.py index c2a66eece87..94c952839d1 100644 --- a/tests/test_litellm/proxy/auth/test_auth_checks.py +++ b/tests/test_litellm/proxy/auth/test_auth_checks.py @@ -519,6 +519,75 @@ def test_check_model_access_helper_self_referential_alias_terminates(): ) +def _router_with_group_alias(alias: str, target: str) -> "Router": + from litellm import Router + + return Router( + model_list=[{"model_name": target, "litellm_params": {"model": "gpt-4o", "api_key": "test-api-key"}}], + model_group_alias={alias: {"model": target, "hidden": False}}, + ) + + +@pytest.mark.asyncio +async def test_can_key_call_model_team_alias_name_in_key_models_is_not_enough(): + """A key that lists the alias name but not its target is denied.""" + from litellm.proxy.auth.auth_checks import can_key_call_model + + for models in (["smart"], ["sm*"]): + valid_token: Final = _aliased_key(models=models) + with pytest.raises(ProxyException) as exc_info: + await can_key_call_model(model="smart", llm_model_list=None, valid_token=valid_token, llm_router=None) + assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied + assert "Tried to access smart" in exc_info.value.message + + +def test_can_object_call_model_team_alias_target_expands_router_alias(): + """A team alias whose target is a router alias is allowed when the key lists the underlying model.""" + llm_router: Final = _router_with_group_alias(alias="router-fast", target="gpt-4o-group") + + assert _can_object_call_model( + model="team-fast", + llm_router=llm_router, + models=["gpt-4o-group"], + team_model_aliases={"team-fast": "router-fast"}, + object_type="key", + ) + with pytest.raises(ProxyException) as exc_info: + _can_object_call_model( + model="team-fast", + llm_router=llm_router, + models=["other"], + team_model_aliases={"team-fast": "router-fast"}, + object_type="key", + ) + assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied + + +def test_can_object_call_model_team_alias_takes_precedence_over_router_alias(): + """A name that is both a team alias and a router alias is checked against the team alias target only.""" + llm_router: Final = _router_with_group_alias(alias="fast", target="gpt-4o-group") + + with pytest.raises(ProxyException) as exc_info: + _can_object_call_model( + model="fast", + llm_router=llm_router, + models=["gpt-4o-group"], + team_model_aliases={"fast": "openai/gpt-4.1-mini"}, + object_type="key", + ) + assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied + assert _can_object_call_model( + model="fast", + llm_router=llm_router, + models=["openai/gpt-4.1-mini"], + team_model_aliases={"fast": "openai/gpt-4.1-mini"}, + object_type="key", + ) + assert _can_object_call_model( + model="fast", llm_router=llm_router, models=["gpt-4o-group"], team_model_aliases=None, object_type="key" + ) + + def test_resolve_key_models_teamless_all_team_models_returns_empty(): """_resolve_key_models_for_auth_check must return [] for a teamless key with all-team-models, making it equivalent to an unscoped key (unrestricted