fix(scim): apply default_team_params (incl. models) to SCIM-created teams (#38433)

* fix(scim): apply default_team_params (incl. models) to SCIM-created teams

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(scim): annotate default_team_params regression test parameters

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-08-26 20:49:21 -07:00 committed by GitHub
parent 172e3aceaf
commit 2e2c8200ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 2 deletions

View file

@ -6,6 +6,7 @@ This is an enterprise feature and requires a premium license.
import re
from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
from copy import deepcopy
from dataclasses import dataclass
from functools import partial
from itertools import chain
@ -2375,6 +2376,37 @@ async def get_group(
raise handle_exception_on_proxy(e)
def _new_team_request_with_defaults(
team_id: str,
team_alias: str | None,
members_with_roles: Sequence[Member],
) -> NewTeamRequest:
"""Build the SCIM group's team request, applying litellm.default_team_params
(including models) the same way SSO auto-created teams do."""
default_params: Final = litellm.default_team_params
defaults: Final[Mapping[str, object]] = (
deepcopy(default_params)
if isinstance(default_params, dict)
else default_params.model_dump(exclude_none=True)
if default_params is not None
else {}
)
default_metadata: Final = defaults.get("metadata")
metadata: Final = {
**(default_metadata if isinstance(default_metadata, dict) else {}),
SCIM_MANAGED_TEAM_METADATA_KEY: True,
}
return NewTeamRequest.model_validate(
{
**defaults,
"team_id": team_id,
"team_alias": team_alias,
"members_with_roles": members_with_roles,
"metadata": metadata,
}
)
@scim_router.post(
"/Groups",
response_model=SCIMGroup,
@ -2412,11 +2444,10 @@ async def create_group(
# Create team in database
created_team: Final = await new_team(
data=NewTeamRequest(
data=_new_team_request_with_defaults(
team_id=team_id,
team_alias=group.displayName,
members_with_roles=members_with_roles,
metadata={SCIM_MANAGED_TEAM_METADATA_KEY: True},
),
http_request=Request(scope={"type": "http", "path": "/scim/v2/Groups"}),
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),

View file

@ -4415,6 +4415,69 @@ async def test_create_group_stamps_scim_provenance(mocker, scim_upsert_user_enab
assert new_team_mock.call_args.kwargs["data"].metadata == {SCIM_MANAGED_TEAM_METADATA_KEY: True}
@pytest.mark.asyncio
@pytest.mark.parametrize("as_pydantic", [False, True])
async def test_create_group_applies_default_team_params(
mocker: MockerFixture,
monkeypatch: pytest.MonkeyPatch,
scim_upsert_user_enabled: None,
as_pydantic: bool,
):
"""SCIM-created teams must honor litellm_settings.default_team_params, including
models, the same way SSO auto-created teams do."""
import litellm
from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams
default_params = {
"models": ["no-default-models"],
"max_budget": 25.0,
"budget_duration": "30d",
"tpm_limit": 100,
"rpm_limit": 10,
}
monkeypatch.setattr(
litellm,
"default_team_params",
DefaultTeamSSOParams(**default_params) if as_pydantic else default_params,
)
scim_group = SCIMGroup(
schemas=["urn:ietf:params:scim:schemas:core:2.0:Group"],
id="defaults-group",
displayName="Defaults.Apps",
members=[],
)
mocker.patch( # test-quality-ok: endpoint collaborators are module-level, not injectable into create_group
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
AsyncMock(return_value=_member_resolution_prisma(mocker, users=set(), teams=set())),
)
new_team_mock = mocker.patch( # test-quality-ok: endpoint collaborators are module-level, not injectable into create_group
"litellm.proxy.management_endpoints.scim.scim_v2.new_team",
AsyncMock(return_value=mocker.MagicMock()),
)
mocker.patch( # test-quality-ok: endpoint collaborators are module-level, not injectable into create_group
"litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_team_to_scim_group",
AsyncMock(return_value=scim_group),
)
mocker.patch( # test-quality-ok: endpoint collaborators are module-level, not injectable into create_group
"litellm.proxy.management_endpoints.scim.scim_v2._recompute_scim_member_roles",
AsyncMock(),
)
await create_group(group=scim_group)
team_request = new_team_mock.call_args.kwargs["data"]
assert team_request.models == ["no-default-models"]
assert team_request.max_budget == 25.0
assert team_request.budget_duration == "30d"
assert team_request.tpm_limit == 100
assert team_request.rpm_limit == 10
assert team_request.team_id == "defaults-group"
assert team_request.team_alias == "Defaults.Apps"
assert team_request.metadata == {SCIM_MANAGED_TEAM_METADATA_KEY: True}
@pytest.mark.asyncio
async def test_update_group_stamps_scim_provenance(mocker, scim_upsert_user_enabled):
"""A PUT full sync adopts a team the identity provider now owns, and the stamp has