From c2265b0ef3569b72767359c33516a91aef7db7fb Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:55:59 -0700 Subject: [PATCH] fix(proxy): return persisted team memberships from /user/new so first CLI login gets the default team (#39545) * fix(proxy): return persisted team memberships from /user/new new_user attached default teams after building its response from the pre-membership snapshot, so NewUserResponse.teams was always empty for users created with default_internal_user_params.teams. The CLI SSO flow reads that response on a user's first login and minted a teamless JWT, which skipped the default team's model allowlist. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(proxy): return team ids as a tuple to satisfy LIT001 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../management_endpoints/internal_user_endpoints.py | 12 ++++++++++++ .../test_internal_user_endpoints.py | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 5326074ad3c..93423ca5a1a 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -426,6 +426,11 @@ async def add_new_user_to_default_team( await asyncio.gather(*tasks, return_exceptions=True) +async def _fetch_user_team_ids(user_id: str, prisma_client: "PrismaClient") -> tuple[str, ...]: + user_row: Final = await _user_table(prisma_client).find_unique(where={"user_id": user_id}) + return tuple(user_row.teams) if user_row is not None else () + + @router.post( "/user/new", tags=["Internal User management"], @@ -580,6 +585,11 @@ async def new_user( ) user_id: Final = cast(str | None, response.get("user_id", None)) + attached_team_ids: Final = ( + await _fetch_user_team_ids(user_id=user_id, prisma_client=prisma_client) + if user_id is not None and (_team_id is not None or teams is not None) + else None + ) if organization_ids is not None and user_id is not None: await _add_user_to_organizations( @@ -596,6 +606,8 @@ async def new_user( response_dict[key] = value response_dict["key"] = response.get("token", "") + if attached_team_ids is not None: + response_dict["teams"] = list(attached_team_ids) new_user_response: Final = NewUserResponse.model_validate(response_dict) diff --git a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py index f231eb66a50..022aeff4e20 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py @@ -1449,6 +1449,11 @@ async def test_new_user_default_teams_flow(mocker): return 5 # Low user count, under limit mock_prisma_client.db.litellm_usertable.count = mock_count + persisted_user_row = mocker.MagicMock() + persisted_user_row.teams = ["96fed65b-0182-4ff4-8429-2721cd7d42af"] + mock_prisma_client.db.litellm_usertable.find_unique = mocker.AsyncMock( + return_value=persisted_user_row + ) # Mock duplicate checks to pass async def mock_check_duplicate_user_email(*args, **kwargs): @@ -1477,6 +1482,7 @@ async def test_new_user_default_teams_flow(mocker): "token": "sk-test-token-123", "expires": None, "max_budget": 100, + "teams": [], } # Mock _add_user_to_team @@ -1551,6 +1557,7 @@ async def test_new_user_default_teams_flow(mocker): # Verify response structure assert response.user_id == "test-user-123" assert response.key == "sk-test-token-123" + assert response.teams == ["96fed65b-0182-4ff4-8429-2721cd7d42af"] finally: # Restore original default params (always assign, never delattr — the attribute