address review: validate defaulted models against org limits, preserve explicit empty models

Apply default_team_params before _check_org_team_limits so inherited
defaults (including models) are validated against the organization's
allowlist, and use model_fields_set to distinguish an omitted models field
(apply default) from an explicit 'models: []' (unrestricted — preserved).
Adds a regression test for the explicit-empty-list case.
This commit is contained in:
Joaquin Hui Gomez 2026-07-05 14:12:02 +01:00
parent 301459adf7
commit 27ed550e47
2 changed files with 51 additions and 22 deletions

View file

@ -1010,6 +1010,31 @@ async def new_team( # noqa: PLR0915
},
)
# Apply defaults from litellm.default_team_params for any fields
# not explicitly provided in the request. This runs before the org
# limit checks below so inherited defaults (e.g. models) are validated
# against the organization's allowlist like explicit values.
for field in (
"max_budget",
"budget_duration",
"tpm_limit",
"rpm_limit",
"team_member_permissions",
):
if getattr(data, field, None) is None:
default_value = _get_default_team_param(field)
if default_value is not None:
setattr(data, field, default_value)
# `models` defaults to an empty list rather than None on the request, so
# use model_fields_set to distinguish "field omitted" (apply the default,
# matching SSO-provisioned team behavior) from an explicit `models: []`
# (meaning unrestricted access, which must be preserved).
if "models" not in data.model_fields_set:
default_models = _get_default_team_param("models")
if default_models is not None:
data.models = default_models
# check org key limits - done here to handle inheriting org id from team
if data.organization_id is not None and prisma_client is not None:
org_table = await get_org_object(
@ -1029,28 +1054,6 @@ async def new_team( # noqa: PLR0915
prisma_client=prisma_client,
)
# Apply defaults from litellm.default_team_params for any fields
# not explicitly provided in the request.
for field in (
"max_budget",
"budget_duration",
"tpm_limit",
"rpm_limit",
"team_member_permissions",
):
if getattr(data, field, None) is None:
default_value = _get_default_team_param(field)
if default_value is not None:
setattr(data, field, default_value)
# `models` defaults to an empty list rather than None on the request, so it
# needs its own check: apply the configured default when the request did not
# specify any models (matches SSO-provisioned team behavior).
if not getattr(data, "models", None):
default_models = _get_default_team_param("models")
if default_models is not None:
data.models = default_models
# Legacy fallback: apply max_budget from default_team_settings (YAML config)
# if still not set after checking default_team_params.
if data.max_budget is None:

View file

@ -342,6 +342,32 @@ class TestNewTeamDefaultParamsApplied:
assert data.models == ["claude-3-5-sonnet"]
@pytest.mark.asyncio
async def test_models_explicit_empty_list_not_overridden(self, monkeypatch):
"""An explicit `models: []` means unrestricted access and must be preserved,
distinguished from an omitted field via model_fields_set."""
from litellm.proxy.management_endpoints.team_endpoints import new_team
monkeypatch.setattr(
litellm,
"default_team_params",
{"models": ["gpt-4"]},
)
data = NewTeamRequest(team_alias="my-team", models=[])
auth = self._make_admin_auth()
try:
await new_team(
data=data,
user_api_key_dict=auth,
http_request=MagicMock(),
)
except Exception:
pass
assert data.models == []
@pytest.mark.asyncio
async def test_no_defaults_when_config_is_none(self, monkeypatch):
"""When default_team_params is None, no defaults applied."""