Skip to content

Commit 3f90fe9

Browse files
Merge pull request #14514 from iabhi4/fix-13581
feat(proxy): Assign default budget to auto-generated JWT teams
2 parents f4e2870 + dc27bcc commit 3f90fe9

File tree

4 files changed

+105
-3
lines changed

4 files changed

+105
-3
lines changed

docs/my-website/docs/proxy/team_budgets.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,30 @@ import TabItem from '@theme/TabItem';
1010
- You must set up a Postgres database (e.g. Supabase, Neon, etc.)
1111
- To enable team member rate limits, set the environment variable `EXPERIMENTAL_MULTI_INSTANCE_RATE_LIMITING=true` **before starting the proxy server**. Without this, team member rate limits will not be enforced.
1212

13+
14+
## Default Budget for Auto-Generated JWT Teams
15+
16+
When using JWT authentication with `team_id_upsert: true`, you can automatically assign a default budget to any newly created team.
17+
18+
This is configured in `default_team_settings` in your `config.yaml`.
19+
20+
**Example:**
21+
```yaml
22+
# in your config.yaml
23+
24+
litellm_jwtauth:
25+
team_id_upsert: true
26+
team_id_jwt_field: "team_id"
27+
# ... other jwt settings
28+
29+
litellm_settings:
30+
default_team_settings:
31+
- team_id: "default-settings"
32+
max_budget: 100.0
33+
```
1334
Track spend, set budgets for your Internal Team
1435
36+
1537
## Setting Monthly Team Budgets
1638
1739
### 1. Create a team

litellm/proxy/auth/auth_checks.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
RoleBasedPermissions,
4747
SpecialModelNames,
4848
UserAPIKeyAuth,
49+
NewTeamRequest,
4950
)
5051
from litellm.proxy.auth.route_checks import RouteChecks
5152
from litellm.proxy.route_llm_request import route_request
@@ -889,10 +890,17 @@ async def _get_team_db_check(
889890
)
890891

891892
if response is None and team_id_upsert:
892-
response = await prisma_client.db.litellm_teamtable.create(
893-
data={"team_id": team_id}
894-
)
893+
from litellm.proxy.management_endpoints.team_endpoints import new_team
894+
895+
new_team_data = NewTeamRequest(team_id=team_id)
895896

897+
mock_request = Request(scope={"type": "http"})
898+
system_admin_user = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
899+
900+
created_team_dict = await new_team(
901+
data=new_team_data, http_request=mock_request, user_api_key_dict=system_admin_user
902+
)
903+
response = LiteLLM_TeamTable(**created_team_dict)
896904
return response
897905

898906

litellm/proxy/management_endpoints/team_endpoints.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,19 @@ async def new_team( # noqa: PLR0915
383383
"error": f"Team id = {data.team_id} already exists. Please use a different team id."
384384
},
385385
)
386+
387+
# If max_budget is not explicitly provided in the request,
388+
# check for a default value in the proxy configuration.
389+
if data.max_budget is None:
390+
if (
391+
isinstance(litellm.default_team_settings, list)
392+
and len(litellm.default_team_settings) > 0
393+
and isinstance(litellm.default_team_settings[0], dict)
394+
):
395+
default_settings = litellm.default_team_settings[0]
396+
default_budget = default_settings.get("max_budget")
397+
if default_budget is not None:
398+
data.max_budget = default_budget
386399

387400
if (
388401
user_api_key_dict.user_role is None

tests/test_litellm/proxy/auth/test_auth_checks.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
_can_object_call_vector_stores,
2929
get_user_object,
3030
vector_store_access_check,
31+
_get_team_db_check,
3132
)
3233
from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
3334
from litellm.utils import get_utc_datetime
@@ -192,6 +193,64 @@ async def test_default_internal_user_params_with_get_user_object(monkeypatch):
192193
assert creation_args["user_role"] == "internal_user"
193194

194195

196+
@pytest.mark.asyncio
197+
@patch("litellm.proxy.management_endpoints.team_endpoints.new_team", new_callable=AsyncMock)
198+
async def test_get_team_db_check_calls_new_team_on_upsert(mock_new_team, monkeypatch):
199+
"""
200+
Test that _get_team_db_check correctly calls the `new_team` function
201+
when a team does not exist and upsert is enabled.
202+
"""
203+
mock_prisma_client = MagicMock()
204+
mock_db = AsyncMock()
205+
mock_prisma_client.db = mock_db
206+
mock_prisma_client.db.litellm_teamtable.find_unique.return_value = None
207+
208+
# Define what our mocked `new_team` function should return
209+
team_id_to_create = "new-jwt-team"
210+
mock_new_team.return_value = {"team_id": team_id_to_create, "max_budget": 123.45}
211+
212+
await _get_team_db_check(
213+
team_id=team_id_to_create,
214+
prisma_client=mock_prisma_client,
215+
team_id_upsert=True,
216+
)
217+
218+
# Verify that our mocked `new_team` function was called exactly once
219+
mock_new_team.assert_called_once()
220+
221+
call_args = mock_new_team.call_args[1]
222+
data_arg = call_args["data"]
223+
224+
# Verify that `new_team` was called with the correct team_id and that
225+
# `max_budget` was None, as our function's job is to delegate, not to set defaults.
226+
assert data_arg.team_id == team_id_to_create
227+
assert data_arg.max_budget is None
228+
229+
230+
@pytest.mark.asyncio
231+
@patch("litellm.proxy.management_endpoints.team_endpoints.new_team", new_callable=AsyncMock)
232+
async def test_get_team_db_check_does_not_call_new_team_if_exists(mock_new_team, monkeypatch):
233+
"""
234+
Test that _get_team_db_check does NOT call the `new_team` function
235+
if the team already exists in the database.
236+
"""
237+
mock_prisma_client = MagicMock()
238+
mock_db = AsyncMock()
239+
mock_prisma_client.db = mock_db
240+
mock_prisma_client.db.litellm_teamtable.find_unique.return_value = MagicMock()
241+
242+
team_id_to_find = "existing-jwt-team"
243+
244+
await _get_team_db_check(
245+
team_id=team_id_to_find,
246+
prisma_client=mock_prisma_client,
247+
team_id_upsert=True,
248+
)
249+
250+
# Verify that `new_team` was NEVER called, because the team was found.
251+
mock_new_team.assert_not_called()
252+
253+
195254
# Vector Store Auth Check Tests
196255

197256

0 commit comments

Comments
 (0)