Skip to content

Commit 4508f55

Browse files
Fix team_member_budget update logic (#12843)
* fix(team_endpoints.py): always remove team member budget from updated_kv this is not a field for the litellm team table Prevents startup issue * test(test_team_endpoints.py): add unit test to ensure 'team_member_budget' is never in update to table - separate logic * refactor: cleanup
1 parent db1d71a commit 4508f55

File tree

3 files changed

+164
-3
lines changed

3 files changed

+164
-3
lines changed

litellm/proxy/_types.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,6 @@ class LiteLLMRoutes(enum.Enum):
307307
"/v1/responses/{response_id}",
308308
"/responses/{response_id}/input_items",
309309
"/v1/responses/{response_id}/input_items",
310-
311310
# vector stores
312311
"/vector_stores",
313312
"/v1/vector_stores",

litellm/proxy/management_endpoints/team_endpoints.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,14 +212,15 @@ async def _upsert_team_member_budget_table(
212212
if updated_kv.get("metadata") is None:
213213
updated_kv["metadata"] = {}
214214
updated_kv["metadata"]["team_member_budget_id"] = budget_row.budget_id
215-
updated_kv.pop("team_member_budget", None)
215+
216216
else: # budget does not exist
217217
updated_kv = await _create_team_member_budget_table(
218218
data=team_table,
219219
new_team_data_json=updated_kv,
220220
user_api_key_dict=user_api_key_dict,
221221
team_member_budget=team_member_budget,
222222
)
223+
updated_kv.pop("team_member_budget", None)
223224
return updated_kv
224225

225226

@@ -798,6 +799,8 @@ async def update_team(
798799
team_member_budget=data.team_member_budget,
799800
user_api_key_dict=user_api_key_dict,
800801
)
802+
else:
803+
updated_kv.pop("team_member_budget", None)
801804

802805
# Check object permission
803806
if data.object_permission is not None:

tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,9 @@ async def test_new_team_with_object_permission(mock_db_client, mock_admin_auth):
291291
mock_db_client.db.litellm_teamtable = MagicMock()
292292
mock_db_client.db.litellm_teamtable.create = mock_team_create
293293
mock_db_client.db.litellm_teamtable.count = mock_team_count
294-
mock_db_client.db.litellm_teamtable.update = AsyncMock(return_value=team_create_result)
294+
mock_db_client.db.litellm_teamtable.update = AsyncMock(
295+
return_value=team_create_result
296+
)
295297

296298
# 4. Mock user table update behaviour (called for each member)
297299
mock_db_client.db.litellm_usertable = MagicMock()
@@ -1013,3 +1015,160 @@ def test_add_new_models_to_team_with_existing_models():
10131015
)
10141016

10151017
assert updated_models.sort() == ["model1", "model2", "model3", "model4"].sort()
1018+
1019+
1020+
@pytest.mark.asyncio
1021+
async def test_update_team_team_member_budget_not_passed_to_db():
1022+
"""
1023+
Test that 'team_member_budget' is never passed to prisma_client.db.litellm_teamtable.update
1024+
regardless of whether the value is set or None.
1025+
1026+
This ensures that team_member_budget is properly handled via the separate budget table
1027+
and not accidentally passed to the team table update operation.
1028+
"""
1029+
from unittest.mock import AsyncMock, MagicMock, Mock, patch
1030+
1031+
from fastapi import Request
1032+
1033+
from litellm.proxy._types import LitellmUserRoles, UpdateTeamRequest, UserAPIKeyAuth
1034+
from litellm.proxy.management_endpoints.team_endpoints import update_team
1035+
1036+
# Mock dependencies
1037+
mock_request = Mock(spec=Request)
1038+
mock_user_api_key_dict = UserAPIKeyAuth(
1039+
user_role=LitellmUserRoles.PROXY_ADMIN, user_id="test_user_id"
1040+
)
1041+
1042+
with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client, patch(
1043+
"litellm.proxy.proxy_server.llm_router"
1044+
) as mock_llm_router, patch(
1045+
"litellm.proxy.proxy_server.user_api_key_cache"
1046+
) as mock_cache, patch(
1047+
"litellm.proxy.proxy_server.proxy_logging_obj"
1048+
) as mock_logging, patch(
1049+
"litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin"
1050+
), patch(
1051+
"litellm.proxy.auth.auth_checks._cache_team_object"
1052+
) as mock_cache_team, patch(
1053+
"litellm.proxy.management_endpoints.team_endpoints._upsert_team_member_budget_table"
1054+
) as mock_upsert_budget:
1055+
1056+
# Setup mock prisma client
1057+
mock_existing_team = MagicMock()
1058+
mock_existing_team.model_dump.return_value = {
1059+
"team_id": "test_team_id",
1060+
"team_alias": "test_team",
1061+
"metadata": {"team_member_budget_id": "budget_123"},
1062+
}
1063+
mock_prisma_client.db.litellm_teamtable.find_unique = AsyncMock(
1064+
return_value=mock_existing_team
1065+
)
1066+
1067+
# Mock the update return value
1068+
mock_updated_team = MagicMock()
1069+
mock_updated_team.team_id = "test_team_id"
1070+
mock_updated_team.model_dump.return_value = {"team_id": "test_team_id"}
1071+
mock_prisma_client.db.litellm_teamtable.update = AsyncMock(
1072+
return_value=mock_updated_team
1073+
)
1074+
mock_prisma_client.jsonify_team_object = MagicMock(
1075+
side_effect=lambda db_data: db_data
1076+
)
1077+
1078+
# Mock budget upsert to return updated_kv without team_member_budget
1079+
def mock_upsert_side_effect(
1080+
team_table, updated_kv, team_member_budget, user_api_key_dict
1081+
):
1082+
# Remove team_member_budget from updated_kv as the real function does
1083+
result_kv = updated_kv.copy()
1084+
result_kv.pop("team_member_budget", None)
1085+
return result_kv
1086+
1087+
mock_upsert_budget.side_effect = mock_upsert_side_effect
1088+
1089+
# Test Case 1: team_member_budget is set (not None)
1090+
update_request_with_budget = UpdateTeamRequest(
1091+
team_id="test_team_id", team_member_budget=100.0, team_alias="updated_alias"
1092+
)
1093+
1094+
result = await update_team(
1095+
data=update_request_with_budget,
1096+
http_request=mock_request,
1097+
user_api_key_dict=mock_user_api_key_dict,
1098+
)
1099+
1100+
# Verify update was called
1101+
assert mock_prisma_client.db.litellm_teamtable.update.called
1102+
1103+
# Get the call arguments
1104+
call_args = mock_prisma_client.db.litellm_teamtable.update.call_args
1105+
update_data = call_args[1]["data"] # data parameter from the update call
1106+
1107+
# Verify team_member_budget is NOT in the update data
1108+
assert (
1109+
"team_member_budget" not in update_data
1110+
), f"team_member_budget should not be in update data, but found: {update_data}"
1111+
1112+
# Verify other fields are present (team_alias should be there)
1113+
assert "team_alias" in update_data or "team_id" in str(
1114+
call_args
1115+
), "Expected team update fields should be present"
1116+
1117+
# Reset mock for second test
1118+
mock_prisma_client.db.litellm_teamtable.update.reset_mock()
1119+
1120+
# Test Case 2: team_member_budget is None
1121+
update_request_without_budget = UpdateTeamRequest(
1122+
team_id="test_team_id",
1123+
team_member_budget=None,
1124+
team_alias="updated_alias_2",
1125+
)
1126+
1127+
result = await update_team(
1128+
data=update_request_without_budget,
1129+
http_request=mock_request,
1130+
user_api_key_dict=mock_user_api_key_dict,
1131+
)
1132+
1133+
# Verify update was called again
1134+
assert mock_prisma_client.db.litellm_teamtable.update.called
1135+
1136+
# Get the call arguments for second call
1137+
call_args = mock_prisma_client.db.litellm_teamtable.update.call_args
1138+
update_data = call_args[1]["data"] # data parameter from the update call
1139+
1140+
# Verify team_member_budget is NOT in the update data
1141+
assert (
1142+
"team_member_budget" not in update_data
1143+
), f"team_member_budget should not be in update data, but found: {update_data}"
1144+
1145+
# Test Case 3: No team_member_budget field at all (excluded from request)
1146+
mock_prisma_client.db.litellm_teamtable.update.reset_mock()
1147+
1148+
update_request_no_budget_field = UpdateTeamRequest(
1149+
team_id="test_team_id",
1150+
team_alias="updated_alias_3",
1151+
# team_member_budget not specified at all
1152+
)
1153+
1154+
result = await update_team(
1155+
data=update_request_no_budget_field,
1156+
http_request=mock_request,
1157+
user_api_key_dict=mock_user_api_key_dict,
1158+
)
1159+
1160+
# Verify update was called again
1161+
assert mock_prisma_client.db.litellm_teamtable.update.called
1162+
1163+
# Get the call arguments for third call
1164+
call_args = mock_prisma_client.db.litellm_teamtable.update.call_args
1165+
update_data = call_args[1]["data"] # data parameter from the update call
1166+
1167+
# Verify team_member_budget is NOT in the update data
1168+
assert (
1169+
"team_member_budget" not in update_data
1170+
), f"team_member_budget should not be in update data, but found: {update_data}"
1171+
1172+
print(
1173+
"✅ All test cases passed: team_member_budget is properly excluded from database update operations"
1174+
)

0 commit comments

Comments
 (0)