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
This commit is contained in:
Ryan Crabbe 2026-03-23 22:17:57 -07:00
parent e40f68aec4
commit 86f9b5d59c
4 changed files with 131 additions and 2 deletions

View file

@ -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",
)

View file

@ -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:

View file

@ -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

View file

@ -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