From 86f9b5d59c44e02aafd72d65197a5e8b8be7ab79 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Mon, 23 Mar 2026 22:17:57 -0700 Subject: [PATCH] fix(proxy): resolve all-team-models for project model access checks Projects with models=["all-team-models"] were failing with 401 because _check_model_access_helper only recognized "all-proxy-models" as a special value. The literal string "all-team-models" was compared against the requested model name, which always failed. - Update can_project_access_model to accept team_models param and resolve "all-team-models" to the team's actual model list before passing to the access check helper - Pass team_models from valid_token in _run_project_checks - Fix circular no-op in get_team_models where all-team-models sentinel caused a self-update; now strips it instead --- litellm/proxy/auth/auth_checks.py | 11 ++- litellm/proxy/auth/model_checks.py | 5 +- .../proxy/auth/test_auth_checks.py | 97 +++++++++++++++++++ .../proxy/auth/test_model_checks.py | 20 ++++ 4 files changed, 131 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 815393467de..5a19aecd16b 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -208,6 +208,7 @@ async def _run_project_checks( model=_model, project_object=project_object, llm_router=llm_router, + team_models=valid_token.team_models if valid_token else None, ) if not skip_budget_checks: @@ -2802,16 +2803,24 @@ def can_project_access_model( model: Union[str, List[str]], project_object: LiteLLM_ProjectTableCachedObj, llm_router: Optional[Router], + team_models: Optional[List[str]] = None, ) -> Literal[True]: """ Returns True if the project can access a specific model. Raises ProxyException if access is denied. """ + project_models = list(project_object.models) if project_object else [] + + # Resolve "all-team-models" sentinel to the team's actual model list, + # matching the pattern used in get_key_models() for key-level resolution. + if SpecialModelNames.all_team_models.value in project_models: + project_models = list(team_models) if team_models else [] + return _can_object_call_model( model=model, llm_router=llm_router, - models=project_object.models if project_object else [], + models=project_models, object_type="project", ) diff --git a/litellm/proxy/auth/model_checks.py b/litellm/proxy/auth/model_checks.py index bf76f99db69..761e4c61713 100644 --- a/litellm/proxy/auth/model_checks.py +++ b/litellm/proxy/auth/model_checks.py @@ -148,7 +148,10 @@ def get_team_models( if len(team_models) > 0: all_models_set.update(team_models) if SpecialModelNames.all_team_models.value in all_models_set: - all_models_set.update(team_models) + # "all-team-models" is a key/project-level sentinel meaning + # "inherit the team's models". It has no meaning on the team + # itself, so just strip it out. + all_models_set.discard(SpecialModelNames.all_team_models.value) if SpecialModelNames.all_proxy_models.value in all_models_set: all_models_set.update(proxy_model_list) if include_model_access_groups: diff --git a/tests/test_litellm/proxy/auth/test_auth_checks.py b/tests/test_litellm/proxy/auth/test_auth_checks.py index 69188fd200e..eddc6c26d12 100644 --- a/tests/test_litellm/proxy/auth/test_auth_checks.py +++ b/tests/test_litellm/proxy/auth/test_auth_checks.py @@ -1629,3 +1629,100 @@ async def test_custom_auth_common_checks_opt_in(): parent_otel_span=None, ) mock_common.assert_called_once() + + +def test_can_project_access_model_all_team_models_resolves(): + """ + Regression test: project with models=["all-team-models"] should resolve + to the team's actual models, not compare the literal string. + """ + from litellm.proxy._types import LiteLLM_ProjectTableCachedObj + from litellm.proxy.auth.auth_checks import can_project_access_model + + project = LiteLLM_ProjectTableCachedObj( + project_id="test-project", + models=["all-team-models"], + created_by="test", + updated_by="test", + ) + + # Team has gpt-5 → project should be able to access it + result = can_project_access_model( + model="gpt-5", + project_object=project, + llm_router=None, + team_models=["gpt-5", "gpt-4"], + ) + assert result is True + + +def test_can_project_access_model_all_team_models_denies_unlisted(): + """ + Project with models=["all-team-models"] should deny a model that + the team doesn't have. + """ + from litellm.proxy._types import LiteLLM_ProjectTableCachedObj + from litellm.proxy.auth.auth_checks import can_project_access_model + + project = LiteLLM_ProjectTableCachedObj( + project_id="test-project", + models=["all-team-models"], + created_by="test", + updated_by="test", + ) + + with pytest.raises(Exception): + can_project_access_model( + model="gpt-5", + project_object=project, + llm_router=None, + team_models=["gpt-4"], + ) + + +def test_can_project_access_model_all_team_models_with_all_proxy_models(): + """ + Project has all-team-models, team has all-proxy-models. + Should grant access to any model. + """ + from litellm.proxy._types import LiteLLM_ProjectTableCachedObj + from litellm.proxy.auth.auth_checks import can_project_access_model + + project = LiteLLM_ProjectTableCachedObj( + project_id="test-project", + models=["all-team-models"], + created_by="test", + updated_by="test", + ) + + result = can_project_access_model( + model="gpt-5", + project_object=project, + llm_router=None, + team_models=["all-proxy-models"], + ) + assert result is True + + +def test_can_project_access_model_all_team_models_no_team(): + """ + Project has all-team-models but team_models is None/empty. + Empty resolved list → all access (matches existing behavior for empty models). + """ + from litellm.proxy._types import LiteLLM_ProjectTableCachedObj + from litellm.proxy.auth.auth_checks import can_project_access_model + + project = LiteLLM_ProjectTableCachedObj( + project_id="test-project", + models=["all-team-models"], + created_by="test", + updated_by="test", + ) + + result = can_project_access_model( + model="gpt-5", + project_object=project, + llm_router=None, + team_models=None, + ) + assert result is True diff --git a/tests/test_litellm/proxy/auth/test_model_checks.py b/tests/test_litellm/proxy/auth/test_model_checks.py index c43621d7f71..c83e4933453 100644 --- a/tests/test_litellm/proxy/auth/test_model_checks.py +++ b/tests/test_litellm/proxy/auth/test_model_checks.py @@ -220,3 +220,23 @@ def test_get_complete_model_list_byok_wildcard_expansion(): assert len(result) > 0 assert all(m.startswith("openai/") for m in result) assert "openai/*" not in result + + +def test_get_team_models_strips_all_team_models_sentinel(): + """ + Regression test: 'all-team-models' is a key/project-level sentinel and + should be stripped from team model lists rather than causing a circular + self-update. + """ + from litellm.proxy.auth.model_checks import get_team_models + + result = get_team_models( + team_models=["all-team-models", "gpt-4"], + proxy_model_list=["model1", "model2"], + model_access_groups={}, + include_model_access_groups=False, + ) + assert "all-team-models" not in result + assert "gpt-4" in result + # Should NOT include proxy models since all-proxy-models wasn't set + assert "model1" not in result