Skip to content

Commit 9a22b89

Browse files
committed
Use end user budget instead of key budget when creating new team
1 parent 06449df commit 9a22b89

File tree

2 files changed

+162
-3
lines changed

2 files changed

+162
-3
lines changed

litellm/proxy/management_endpoints/team_endpoints.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -688,13 +688,13 @@ async def new_team( # noqa: PLR0915
688688

689689
if (
690690
data.max_budget is not None
691-
and user_api_key_dict.max_budget is not None
692-
and data.max_budget > user_api_key_dict.max_budget
691+
and user_api_key_dict.end_user_max_budget is not None
692+
and data.max_budget > user_api_key_dict.end_user_max_budget
693693
):
694694
raise HTTPException(
695695
status_code=400,
696696
detail={
697-
"error": f"max budget higher than user max. User max budget={user_api_key_dict.max_budget}. User role={user_api_key_dict.user_role}"
697+
"error": f"max budget higher than user max. User max budget={user_api_key_dict.end_user_max_budget}. User role={user_api_key_dict.user_role}"
698698
},
699699
)
700700

tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,3 +1793,162 @@ async def test_team_member_delete_cleans_membership(mock_db_client, mock_admin_a
17931793
mock_db_client.db.litellm_teammembership.delete_many.assert_awaited_with(
17941794
where={"team_id": test_team_id, "user_id": test_user_id}
17951795
)
1796+
1797+
1798+
@pytest.mark.asyncio
1799+
async def test_new_team_max_budget_exceeds_user_max_budget():
1800+
"""
1801+
Test that /team/new raises ProxyException when max_budget exceeds user's end_user_max_budget.
1802+
1803+
This validates the budget enforcement logic where non-admin users cannot create teams
1804+
with budgets higher than their personal maximum budget limit.
1805+
"""
1806+
from fastapi import Request
1807+
1808+
from litellm.proxy._types import NewTeamRequest, ProxyException, UserAPIKeyAuth
1809+
from litellm.proxy.management_endpoints.team_endpoints import new_team
1810+
1811+
# Create non-admin user with end_user_max_budget set to 100.0
1812+
non_admin_user = UserAPIKeyAuth(
1813+
user_role=LitellmUserRoles.INTERNAL_USER,
1814+
user_id="non-admin-user-123",
1815+
end_user_max_budget=100.0,
1816+
)
1817+
1818+
# Create team request with max_budget (200.0) exceeding user's limit (100.0)
1819+
team_request = NewTeamRequest(
1820+
team_alias="high-budget-team",
1821+
max_budget=200.0, # Exceeds user's end_user_max_budget
1822+
)
1823+
1824+
dummy_request = MagicMock(spec=Request)
1825+
1826+
with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch(
1827+
"litellm.proxy.proxy_server._license_check"
1828+
) as mock_license, patch(
1829+
"litellm.proxy.proxy_server.user_api_key_cache"
1830+
) as mock_cache, patch(
1831+
"litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin"
1832+
), patch(
1833+
"litellm.proxy.proxy_server.create_audit_log_for_update"
1834+
) as mock_audit:
1835+
# Setup basic mocks
1836+
mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0)
1837+
mock_license.is_team_count_over_limit.return_value = False
1838+
mock_prisma.get_data = AsyncMock(return_value=None)
1839+
mock_audit.return_value = AsyncMock(return_value=MagicMock())
1840+
1841+
# The budget validation happens BEFORE trying to add members,
1842+
# so we shouldn't need to mock the user table operations
1843+
1844+
# Should raise ProxyException (HTTPException gets converted by handle_exception_on_proxy)
1845+
with pytest.raises(ProxyException) as exc_info:
1846+
await new_team(
1847+
data=team_request,
1848+
http_request=dummy_request,
1849+
user_api_key_dict=non_admin_user,
1850+
)
1851+
1852+
# Verify exception details
1853+
# ProxyException stores status_code in 'code' attribute
1854+
assert exc_info.value.code == '400'
1855+
assert "max budget higher than user max" in str(exc_info.value.message)
1856+
assert "100.0" in str(exc_info.value.message) # User's max budget should be mentioned
1857+
assert LitellmUserRoles.INTERNAL_USER.value in str(exc_info.value.message)
1858+
1859+
1860+
@pytest.mark.asyncio
1861+
async def test_new_team_max_budget_within_user_limit():
1862+
"""
1863+
Test that /team/new succeeds when max_budget is within user's end_user_max_budget.
1864+
1865+
This ensures that users can create teams with budgets at or below their personal limit.
1866+
"""
1867+
from fastapi import Request
1868+
1869+
from litellm.proxy._types import NewTeamRequest, UserAPIKeyAuth
1870+
from litellm.proxy.management_endpoints.team_endpoints import new_team
1871+
1872+
# Create non-admin user with end_user_max_budget set to 100.0
1873+
non_admin_user = UserAPIKeyAuth(
1874+
user_role=LitellmUserRoles.INTERNAL_USER,
1875+
user_id="non-admin-user-456",
1876+
end_user_max_budget=100.0,
1877+
models=[], # Empty models list to bypass model validation
1878+
)
1879+
1880+
# Create team request with max_budget (50.0) within user's limit (100.0)
1881+
team_request = NewTeamRequest(
1882+
team_alias="within-budget-team",
1883+
max_budget=50.0, # Within user's end_user_max_budget
1884+
)
1885+
1886+
dummy_request = MagicMock(spec=Request)
1887+
1888+
with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch(
1889+
"litellm.proxy.proxy_server.user_api_key_cache"
1890+
) as mock_cache, patch(
1891+
"litellm.proxy.proxy_server._license_check"
1892+
) as mock_license, patch(
1893+
"litellm.proxy.proxy_server.litellm_proxy_admin_name", "admin"
1894+
), patch(
1895+
"litellm.proxy.proxy_server.create_audit_log_for_update"
1896+
) as mock_audit:
1897+
1898+
# Setup mocks
1899+
mock_prisma.db.litellm_teamtable.count = AsyncMock(return_value=0)
1900+
mock_license.is_team_count_over_limit.return_value = False
1901+
mock_prisma.jsonify_team_object = lambda db_data: db_data
1902+
mock_prisma.get_data = AsyncMock(return_value=None)
1903+
mock_prisma.update_data = AsyncMock()
1904+
mock_audit.return_value = AsyncMock(return_value=MagicMock())
1905+
1906+
# Mock team creation
1907+
mock_created_team = MagicMock()
1908+
mock_created_team.team_id = "team-within-budget-789"
1909+
mock_created_team.team_alias = "within-budget-team"
1910+
mock_created_team.max_budget = 50.0
1911+
mock_created_team.members_with_roles = []
1912+
mock_created_team.metadata = None
1913+
mock_created_team.model_dump.return_value = {
1914+
"team_id": "team-within-budget-789",
1915+
"team_alias": "within-budget-team",
1916+
"max_budget": 50.0,
1917+
"members_with_roles": [],
1918+
}
1919+
mock_prisma.db.litellm_teamtable.create = AsyncMock(return_value=mock_created_team)
1920+
mock_prisma.db.litellm_teamtable.update = AsyncMock(return_value=mock_created_team)
1921+
1922+
# Mock model table
1923+
mock_prisma.db.litellm_modeltable = MagicMock()
1924+
mock_prisma.db.litellm_modeltable.create = AsyncMock(return_value=MagicMock(id="model123"))
1925+
1926+
# Mock user table operations for adding the creator as a member
1927+
mock_user = MagicMock()
1928+
mock_user.user_id = "non-admin-user-456"
1929+
mock_user.model_dump.return_value = {"user_id": "non-admin-user-456", "teams": ["team-within-budget-789"]}
1930+
mock_prisma.db.litellm_usertable = MagicMock()
1931+
mock_prisma.db.litellm_usertable.upsert = AsyncMock(return_value=mock_user)
1932+
mock_prisma.db.litellm_usertable.update = AsyncMock(return_value=mock_user)
1933+
1934+
# Mock team membership table
1935+
mock_membership = MagicMock()
1936+
mock_membership.model_dump.return_value = {
1937+
"team_id": "team-within-budget-789",
1938+
"user_id": "non-admin-user-456",
1939+
"budget_id": None,
1940+
}
1941+
mock_prisma.db.litellm_teammembership = MagicMock()
1942+
mock_prisma.db.litellm_teammembership.create = AsyncMock(return_value=mock_membership)
1943+
1944+
# Should NOT raise an exception
1945+
result = await new_team(
1946+
data=team_request,
1947+
http_request=dummy_request,
1948+
user_api_key_dict=non_admin_user,
1949+
)
1950+
1951+
# Verify the team was created successfully
1952+
assert result is not None
1953+
assert result["team_id"] == "team-within-budget-789"
1954+
assert result["max_budget"] == 50.0

0 commit comments

Comments
 (0)