From 90f95d2aa854f9202a29ce5aae97e3415e37f0c0 Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Mon, 31 Aug 2026 17:52:45 -0700 Subject: [PATCH 1/3] fix(proxy): include litellm_model_table in GET /v2/team/list GET /v2/team/list built its find_many queries without joining the LiteLLM_ModelTable relation, so litellm_model_table (and the model_aliases it carries) always read back as null there, same bug class as GH #26312 which PR #33047 fixed on /team/info and /team/list but never touched this endpoint. --- .../management_endpoints/team_endpoints.py | 2 + .../test_team_endpoints.py | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index c6d7975b75e6..722e350fa3a3 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -5153,6 +5153,7 @@ async def list_team_v2( skip=skip, take=page_size, order=order_by if order_by else {"created_at": "desc"}, # Default sort + include=_INCLUDE_MODEL_TABLE, ) # Get total count for pagination total_count = await _deleted_team_db(prisma_client).count(where=where_conditions) @@ -5162,6 +5163,7 @@ async def list_team_v2( skip=skip, take=page_size, order=order_by if order_by else {"created_at": "desc"}, # Default sort + include=_INCLUDE_MODEL_TABLE, ) # Get total count for pagination total_count = await _team_db(prisma_client).count(where=where_conditions) diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py index ffa6bc601e90..aab080ed4c8d 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py @@ -3741,6 +3741,62 @@ async def test_list_team_v2_with_status_deleted(): assert len(result["teams"]) == 2 +@pytest.mark.asyncio +async def test_list_team_v2_includes_litellm_model_table(): + """ + Regression test for GH #26312: GET /v2/team/list must eagerly load the + litellm_model_table relation, same as /team/info and /team/list, or a + team's model_aliases always read back as null from this endpoint. + """ + from unittest.mock import AsyncMock, Mock, patch + + from fastapi import Request + + from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth + from litellm.proxy.management_endpoints.team_endpoints import ( + _INCLUDE_MODEL_TABLE, + list_team_v2, + ) + + mock_request = Mock(spec=Request) + mock_user_api_key_dict_admin = UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, + user_id="admin_user_123", + ) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client: + mock_db = Mock() + mock_prisma_client.db = mock_db + + mock_db.litellm_teamtable.find_many = AsyncMock(return_value=[]) + mock_db.litellm_teamtable.count = AsyncMock(return_value=0) + + await list_team_v2( + http_request=mock_request, + user_id=None, + user_api_key_dict=mock_user_api_key_dict_admin, + page=1, + page_size=10, + status=None, + ) + + assert mock_db.litellm_teamtable.find_many.call_args.kwargs["include"] == _INCLUDE_MODEL_TABLE + + mock_db.litellm_deletedteamtable.find_many = AsyncMock(return_value=[]) + mock_db.litellm_deletedteamtable.count = AsyncMock(return_value=0) + + await list_team_v2( + http_request=mock_request, + user_id=None, + user_api_key_dict=mock_user_api_key_dict_admin, + page=1, + page_size=10, + status="deleted", + ) + + assert mock_db.litellm_deletedteamtable.find_many.call_args.kwargs["include"] == _INCLUDE_MODEL_TABLE + + @pytest.mark.asyncio async def test_list_team_v2_org_admin_sees_org_teams(): """ From dadfdc66a3c4d013315651c3ad857bb386cd9d07 Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Mon, 31 Aug 2026 18:08:34 -0700 Subject: [PATCH 2/3] fix(test): assert observable output, not mock calls, in v2 team list test The test-quality gate flagged the regression test for asserting on find_many's call args instead of what the caller gets back. Rewritten so the fake find_many only attaches litellm_model_table when its own include kwarg asks for it, so the assertions are on the response. --- .../test_team_endpoints.py | 55 ++++++++++++++----- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py index aab080ed4c8d..fb13da59bfac 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py @@ -3747,16 +3747,17 @@ async def test_list_team_v2_includes_litellm_model_table(): Regression test for GH #26312: GET /v2/team/list must eagerly load the litellm_model_table relation, same as /team/info and /team/list, or a team's model_aliases always read back as null from this endpoint. + + The fake find_many below only attaches litellm_model_table when its own + `include` kwarg actually asks for the relation, so the assertions below + are on what the caller gets back, not on how find_many was called. """ from unittest.mock import AsyncMock, Mock, patch from fastapi import Request from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth - from litellm.proxy.management_endpoints.team_endpoints import ( - _INCLUDE_MODEL_TABLE, - list_team_v2, - ) + from litellm.proxy.management_endpoints.team_endpoints import list_team_v2 mock_request = Mock(spec=Request) mock_user_api_key_dict_admin = UserAPIKeyAuth( @@ -3764,14 +3765,38 @@ async def test_list_team_v2_includes_litellm_model_table(): user_id="admin_user_123", ) - with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client: + def _team_row(team_id: str, include) -> Mock: + model_table = ( + { + "id": 1, + "model_aliases": {"my-fast-model": "fake-model"}, + "created_by": "u", + "updated_by": "u", + "team": None, + } + if (include or {}).get("litellm_model_table") + else None + ) + return Mock( + team_id=team_id, + model_dump=lambda: { + "team_id": team_id, + "team_alias": "t", + "litellm_model_table": model_table, + }, + ) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client: # test-quality-ok: this file's DB-mock convention mock_db = Mock() mock_prisma_client.db = mock_db - mock_db.litellm_teamtable.find_many = AsyncMock(return_value=[]) - mock_db.litellm_teamtable.count = AsyncMock(return_value=0) + mock_db.litellm_teamtable.find_many = AsyncMock( + side_effect=lambda **kw: [_team_row("team_1", kw.get("include"))] + ) + mock_db.litellm_teamtable.count = AsyncMock(return_value=1) + mock_db.litellm_verificationtoken.group_by = AsyncMock(return_value=[]) - await list_team_v2( + result = await list_team_v2( http_request=mock_request, user_id=None, user_api_key_dict=mock_user_api_key_dict_admin, @@ -3780,12 +3805,15 @@ async def test_list_team_v2_includes_litellm_model_table(): status=None, ) - assert mock_db.litellm_teamtable.find_many.call_args.kwargs["include"] == _INCLUDE_MODEL_TABLE + assert result["teams"][0].litellm_model_table is not None + assert result["teams"][0].litellm_model_table.model_aliases == {"my-fast-model": "fake-model"} - mock_db.litellm_deletedteamtable.find_many = AsyncMock(return_value=[]) - mock_db.litellm_deletedteamtable.count = AsyncMock(return_value=0) + mock_db.litellm_deletedteamtable.find_many = AsyncMock( + side_effect=lambda **kw: [_team_row("team_2", kw.get("include"))] + ) + mock_db.litellm_deletedteamtable.count = AsyncMock(return_value=1) - await list_team_v2( + result = await list_team_v2( http_request=mock_request, user_id=None, user_api_key_dict=mock_user_api_key_dict_admin, @@ -3794,7 +3822,8 @@ async def test_list_team_v2_includes_litellm_model_table(): status="deleted", ) - assert mock_db.litellm_deletedteamtable.find_many.call_args.kwargs["include"] == _INCLUDE_MODEL_TABLE + assert result["teams"][0].litellm_model_table is not None + assert result["teams"][0].litellm_model_table.model_aliases == {"my-fast-model": "fake-model"} @pytest.mark.asyncio From aab515d23bbe67a7a50892c2deb40e0b1d0be27b Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Mon, 31 Aug 2026 18:21:18 -0700 Subject: [PATCH 3/3] fix(proxy): drop invalid litellm_model_table include on deleted-team query Greptile caught that LiteLLM_DeletedTeamTable has no litellm_model_table relation in the Prisma schema, so passing that include on the deleted-team find_many raised UnknownRelationalFieldError against a real database on every GET /v2/team/list?status=deleted call. Confirmed live against Postgres. Scope the fix to the active-team branch only, where the relation exists; update the test to reflect that and assert the deleted branch no longer requests it. --- litellm/proxy/management_endpoints/team_endpoints.py | 2 +- .../management_endpoints/test_team_endpoints.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 722e350fa3a3..714cf252e699 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -5148,12 +5148,12 @@ async def list_team_v2( # Get teams with pagination if use_deleted_table: + # LiteLLM_DeletedTeamTable has no litellm_model_table relation, unlike below teams = await _deleted_team_db(prisma_client).find_many( where=where_conditions, skip=skip, take=page_size, order=order_by if order_by else {"created_at": "desc"}, # Default sort - include=_INCLUDE_MODEL_TABLE, ) # Get total count for pagination total_count = await _deleted_team_db(prisma_client).count(where=where_conditions) diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py index fb13da59bfac..30b2ab86b9ad 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py @@ -3745,8 +3745,11 @@ async def test_list_team_v2_with_status_deleted(): async def test_list_team_v2_includes_litellm_model_table(): """ Regression test for GH #26312: GET /v2/team/list must eagerly load the - litellm_model_table relation, same as /team/info and /team/list, or a - team's model_aliases always read back as null from this endpoint. + litellm_model_table relation for active teams, same as /team/info and + /team/list, or a team's model_aliases always read back as null from this + endpoint. Deleted teams are excluded: LiteLLM_DeletedTeamTable has no such + relation in the Prisma schema, so requesting it there raises + UnknownRelationalFieldError against a real database. The fake find_many below only attaches litellm_model_table when its own `include` kwarg actually asks for the relation, so the assertions below @@ -3813,7 +3816,7 @@ def _team_row(team_id: str, include) -> Mock: ) mock_db.litellm_deletedteamtable.count = AsyncMock(return_value=1) - result = await list_team_v2( + await list_team_v2( http_request=mock_request, user_id=None, user_api_key_dict=mock_user_api_key_dict_admin, @@ -3822,8 +3825,7 @@ def _team_row(team_id: str, include) -> Mock: status="deleted", ) - assert result["teams"][0].litellm_model_table is not None - assert result["teams"][0].litellm_model_table.model_aliases == {"my-fast-model": "fake-model"} + assert "include" not in mock_db.litellm_deletedteamtable.find_many.call_args.kwargs @pytest.mark.asyncio