Skip to content

feat(proxy): let proxy admins choose which team fields team admins may edit - #39996

Merged
ryan-crabbe-berri merged 16 commits into
mainfrom
litellm_team_admin_editable_fields
Sep 17, 2026
Merged

ryan-crabbe-berri merged 16 commits into
mainfrom
litellm_team_admin_editable_fields

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Team admins can't change any team setting, /team/update 401s them
  • No proxy-wide way to pick which team fields team admins may touch
  • The dashboard would have to guess edit rights from props and the org list
  • /team/update skips the org's budget, TPM and RPM caps for every caller
  • UI settings are read once at boot, so other replicas serve stale values

How it solves it:

  • /team/update becomes self-managed; the handler resolves proxy, org or team admin
  • New UI setting team_admin_editable_team_fields: an allow-list of team fields
  • tpm_limit is the first field it accepts, the rest follow one PR each
  • Empty list: team admins get a 403 that points them to the proxy admin
  • Changing a field outside the list: 403 naming that field
  • Only values that differ from what is stored count, so resending the form is fine
  • Only those changed values get written, so a resend leaves budget resets alone
  • Live e2e tests cover the refusal, the allow-list and the budget resend
  • /team/info reports caller_edit_access, and the team page renders from it
  • Team admins get a settings form with only the enabled fields
  • That form sends only changed values, and Save stays off until one changes
  • The UI Settings tab gets its own "Team admin editable fields" card: tick fields, then click Save
  • That Save button and the User Banner card's "Save banner" button now sit at the bottom right
  • /team/update now loads the org's budget, so org caps apply there too
  • Every replica re-reads the UI settings on its config reload, no restart needed

User Flow

Before: a team admin who needs a higher TPM limit for their team cannot change it, and nothing the proxy admin configures helps

  1. A proxy admin creates a user with POST http://localhost:4000/user/new and {"user_role": "internal_user", "auto_create_key": true} and gets a key back
  2. The proxy admin creates a team with POST http://localhost:4000/team/new, "tpm_limit": 1000, and that user in members_with_roles with "role": "admin"
  3. The team admin sends POST http://localhost:4000/team/update with {"team_id": "<team>", "tpm_limit": 5000} using their key and gets 401 Authentication Error, Only proxy admin can be used to generate, delete, update info for new keys/users/teams
  4. GET http://localhost:4000/team/info?team_id=<team> with their key returns the team but nothing about what they may change
  5. The proxy admin sends PATCH http://localhost:4000/update/ui_settings with {"team_admin_editable_team_fields": ["tpm_limit"]} and gets 200, but the key is silently dropped
  6. The team admin retries the TPM update and gets the same 401
  7. They try {"team_id": "<team>", "team_alias": "renamed-by-team-admin"} and get the same 401
  8. On http://localhost:4000/ui/teams the team admin opens the team, clicks Edit Settings, gets the full form, and saving it fails with that 401 in a toast
  9. A proxy admin sends POST http://localhost:4000/team/update with "tpm_limit": 20000 for a team whose organization is capped at 10000 TPM, gets 200, and /team/info shows 20000
  10. An org admin who is also listed as a team admin on their organization's team gets the full form and can rename the team
  11. A UI setting changed through one replica stays stale on the others until they restart
  12. An unrelated user sending any team update gets the same 401

After: the proxy admin ticks TPM for team admins, the team admin changes it from the dashboard, and the organization's cap holds for everyone

  1. A proxy admin creates a user with POST http://localhost:4000/user/new and {"user_role": "internal_user", "auto_create_key": true} and gets a key back
  2. The proxy admin creates a team with POST http://localhost:4000/team/new, "tpm_limit": 1000, and that user in members_with_roles with "role": "admin"
  3. The team admin sends POST http://localhost:4000/team/update with {"team_id": "<team>", "tpm_limit": 5000} using their key and gets 403 Team admins on this proxy cannot edit team settings. Ask a proxy admin to enable fields under Settings > UI > Team admin editable fields.
  4. GET http://localhost:4000/team/info?team_id=<team> with their key carries "caller_edit_access": {"kind": "team_admin_disabled"}
  5. The proxy admin opens http://localhost:4000/ui/admin-panel, UI Settings tab, ticks "Tokens per minute Limit (TPM)" in the "Team admin editable fields" card and clicks Save, which sends the same PATCH http://localhost:4000/update/ui_settings and gets 200. Ticking alone stores nothing
  6. /team/info now says {"kind": "team_admin", "editable_fields": ["tpm_limit"]}, and the TPM update returns 200 with "tpm_limit": 5000
  7. The rename returns 403 Team admins on this proxy do not have permission to update 'team_alias'. Ask a proxy admin to add it under Settings > UI > Team admin editable fields.
  8. On http://localhost:4000/ui/teams the team admin opens the team and clicks Edit Settings; the form shows only the TPM field, and saving 7000 shows "Team settings updated successfully"
  9. The team admin, or a proxy admin, sends "tpm_limit": 20000 for the team in the 10000 TPM organization and gets 400 Team tpm_limit (20000) exceeds organization's tpm_limit (10000); 8000 still saves
  10. The org admin who is also listed as a team admin still gets the full form and can rename the team, because the proxy reports them as unrestricted
  11. A UI setting changed through one replica reaches the others within one config reload, about 30 seconds, with no restart
  12. An unrelated user asking for a team that does not exist, or one they do not administer, gets a 403 that never echoes the team id

Relevant issues

Linear ticket

Refs LIT-5722

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Both runs use litellm/proxy/dev_config.yaml with STORE_MODEL_IN_DB=True, master key sk-1234, and two proxies on the same Postgres so the cross-pod case has a second replica to read from. Before ran on ports 4811 and 4812 from a checkout of the merge base, After on 4831 and 4832 from this branch. $TEAM_ADMIN_KEY and $ORG_ADMIN_KEY are the keys /user/new returned in the team admin and org admin cases. The proxy admin also adds the team admin to the capped organization with POST /organization/member_add before creating its team. The dashboard screenshots come from the bundle the proxy serves at /ui for Before and from npm run dev in ui/litellm-dashboard pointed at the After proxy; each user logged in with their email after POST /user/update gave them a password

Before (9357491)

The proxy-wide allow-list setting

  1. curl -s http://localhost:4811/get/ui_settings -H 'Authorization: Bearer sk-1234' | jq -c '{value: .values.team_admin_editable_team_fields, supported: .field_schema.properties.team_admin_editable_team_fields.items.enum}'

  2. Output: {"value":null,"supported":null}

  3. curl -s -X PATCH http://localhost:4811/update/ui_settings -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_admin_editable_team_fields": ["team_alias"]}' -w '\nHTTP %{http_code}\n'

  4. Output:

    {"message":"UI settings updated successfully","status":"success","settings":{"allow_public_health_readiness_details":false}}
    HTTP 200
    

A team admin before any field is enabled

  1. curl -s -X POST http://localhost:4811/user/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"user_id": "lit5722-team-admin-1789598553", "user_email": "lit5722-team-admin-1789598553@example.com", "user_role": "internal_user", "auto_create_key": true}' | jq -r .key

  2. Output: sk-...Xd5Q

  3. curl -s -X POST http://localhost:4811/team/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_alias": "lit5722-team-1789598553", "tpm_limit": 1000, "members_with_roles": [{"user_id": "lit5722-team-admin-1789598553", "role": "admin"}]}' | jq -r .team_id

  4. Output: 993a5f94-659c-4f0a-ae13-782eb2286114

  5. curl -s 'http://localhost:4811/team/info?team_id=993a5f94-659c-4f0a-ae13-782eb2286114' -H "Authorization: Bearer $TEAM_ADMIN_KEY" | jq -c '.team_info | {tpm_limit, caller_edit_access}'

  6. Output: {"tpm_limit":1000,"caller_edit_access":null}

  7. curl -s -X POST http://localhost:4811/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "993a5f94-659c-4f0a-ae13-782eb2286114", "tpm_limit": 5000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  8. Output:

    Authentication Error, Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route=/team/update. Your role=internal_user. Your user_id=lit572*********************53
    HTTP 401
    

The proxy admin lets team admins edit tpm_limit

  1. curl -s -X PATCH http://localhost:4811/update/ui_settings -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_admin_editable_team_fields": ["tpm_limit"]}' -w '\nHTTP %{http_code}\n'

  2. Output:

    {"message":"UI settings updated successfully","status":"success","settings":{"allow_public_health_readiness_details":false}}
    HTTP 200
    
  3. curl -s http://localhost:4811/get/ui_settings -H 'Authorization: Bearer sk-1234' | jq -c '{value: .values.team_admin_editable_team_fields}'

  4. Output: {"value":null}

  5. curl -s 'http://localhost:4811/team/info?team_id=993a5f94-659c-4f0a-ae13-782eb2286114' -H "Authorization: Bearer $TEAM_ADMIN_KEY" | jq -c '.team_info | {tpm_limit, caller_edit_access}'

  6. Output: {"tpm_limit":1000,"caller_edit_access":null}

The team admin changes the team's TPM limit

  1. curl -s -X POST http://localhost:4811/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "993a5f94-659c-4f0a-ae13-782eb2286114", "tpm_limit": 5000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  2. Output:

    Authentication Error, Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route=/team/update. Your role=internal_user. Your user_id=lit572*********************53
    HTTP 401
    
  3. curl -s -X POST http://localhost:4811/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "993a5f94-659c-4f0a-ae13-782eb2286114", "team_alias": "renamed-by-team-admin"}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  4. Output:

    Authentication Error, Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route=/team/update. Your role=internal_user. Your user_id=lit572*********************53
    HTTP 401
    
  5. curl -s -X POST http://localhost:4811/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "993a5f94-659c-4f0a-ae13-782eb2286114", "team_alias": "lit5722-team-1789598553", "tpm_limit": 6000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  6. Output:

    Authentication Error, Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route=/team/update. Your role=internal_user. Your user_id=lit572*********************53
    HTTP 401
    

The organization's TPM limit still caps a team admin

  1. curl -s -X POST http://localhost:4811/organization/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"organization_alias": "lit5722-limited-org-1789598553", "tpm_limit": 10000}' | jq -r .organization_id

  2. Output: 893f38c4-d887-4a1a-85e3-50058ec924de

  3. curl -s -X POST http://localhost:4811/team/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_alias": "lit5722-limited-team-1789598553", "organization_id": "893f38c4-d887-4a1a-85e3-50058ec924de", "tpm_limit": 1000, "members_with_roles": [{"user_id": "lit5722-team-admin-1789598553", "role": "admin"}]}' | jq -r .team_id

  4. Output: 84fb1c88-257b-42d9-8932-fdaf38074a7e

  5. curl -s -X POST http://localhost:4811/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "84fb1c88-257b-42d9-8932-fdaf38074a7e", "tpm_limit": 20000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  6. Output:

    Authentication Error, Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route=/team/update. Your role=internal_user. Your user_id=lit572*********************53
    HTTP 401
    
  7. curl -s -X POST http://localhost:4811/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "84fb1c88-257b-42d9-8932-fdaf38074a7e", "tpm_limit": 8000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  8. Output:

    Authentication Error, Only proxy admin can be used to generate, delete, update info for new keys/users/teams. Route=/team/update. Your role=internal_user. Your user_id=lit572*********************53
    HTTP 401
    

A proxy admin raising the same team past its organization's TPM limit

  1. curl -s -X POST http://localhost:4811/team/update -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_id": "84fb1c88-257b-42d9-8932-fdaf38074a7e", "tpm_limit": 20000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  2. Output:

    {"team_alias":"lit5722-limited-team-1789598553","tpm_limit":20000}
    HTTP 200
    
  3. curl -s 'http://localhost:4811/team/info?team_id=84fb1c88-257b-42d9-8932-fdaf38074a7e' -H 'Authorization: Bearer sk-1234' | jq -c '.team_info | {tpm_limit}'

  4. Output: {"tpm_limit":20000}

A proxy admin editing the same team

  1. curl -s -X POST http://localhost:4811/team/update -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_id": "993a5f94-659c-4f0a-ae13-782eb2286114", "team_alias": "renamed-by-proxy-admin"}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  2. Output:

    {"team_alias":"renamed-by-proxy-admin","tpm_limit":1000}
    HTTP 200
    

An org admin who is also a team admin editing a team in their organization

  1. curl -s -X POST http://localhost:4811/user/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"user_id": "lit5722-org-admin-1789598553", "user_email": "lit5722-org-admin-1789598553@example.com", "user_role": "internal_user", "auto_create_key": true}' | jq -r .key
  2. Output: sk-...jPmQ
  3. curl -s -X POST http://localhost:4811/organization/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"organization_alias": "lit5722-org-1789598553"}' | jq -r .organization_id
  4. Output: 0eebe7d9-c47a-4190-b60d-729c2a88b470
  5. curl -s -X POST http://localhost:4811/organization/member_add -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"organization_id": "0eebe7d9-c47a-4190-b60d-729c2a88b470", "member": {"user_id": "lit5722-org-admin-1789598553", "role": "org_admin"}}' | jq -c '{updated_users: [.updated_users[]?.user_id]}'
  6. Output: {"updated_users":["lit5722-org-admin-1789598553"]}
  7. curl -s -X POST http://localhost:4811/team/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_alias": "lit5722-org-team-1789598553", "organization_id": "0eebe7d9-c47a-4190-b60d-729c2a88b470", "members_with_roles": [{"user_id": "lit5722-org-admin-1789598553", "role": "admin"}]}' | jq -r .team_id
  8. Output: 23d6fac4-755a-4827-b3e9-66dde18c4fea
  9. curl -s 'http://localhost:4811/team/info?team_id=23d6fac4-755a-4827-b3e9-66dde18c4fea' -H "Authorization: Bearer $ORG_ADMIN_KEY" | jq -c '.team_info | {organization_id, caller_edit_access}'
  10. Output:
{"organization_id":"0eebe7d9-c47a-4190-b60d-729c2a88b470","caller_edit_access":null}
  1. curl -s -X POST http://localhost:4811/team/update -H "Authorization: Bearer $ORG_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "23d6fac4-755a-4827-b3e9-66dde18c4fea", "team_alias": "renamed-by-org-admin"}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'
  2. Output:
{"team_alias":"renamed-by-org-admin","tpm_limit":null}
HTTP 200

A UI setting changed on one pod, read on another

  1. curl -s http://localhost:4811/health/readiness; echo; curl -s http://localhost:4812/health/readiness

  2. Output:

    {"status":"healthy","db":"connected"}
    {"status":"healthy","db":"connected"}
    
  3. curl -s -X PATCH http://localhost:4811/update/ui_settings -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"allow_public_health_readiness_details": true}'

  4. Output:

    {"message":"UI settings updated successfully","status":"success","settings":{"allow_public_health_readiness_details":true}}
    
  5. curl -s http://localhost:4811/health/readiness | jq -c 'keys'

  6. Output:

    ["cache","db","is_detailed_debug","litellm_version","log_level","show_env_credential_login_warning","show_no_redis_warning","status","success_callbacks","use_aiohttp_transport"]
    
  7. start=$(date +%s); until curl -s http://localhost:4812/health/readiness | grep -q litellm_version; do sleep 2; [ $(( $(date +%s) - start )) -gt 90 ] && break; done; echo "$(( $(date +%s) - start ))s"; curl -s http://localhost:4812/health/readiness | jq -c 'if .litellm_version then keys else . end'

  8. Output:

    91s
    {"status":"healthy","db":"connected"}
    

Admin UI: the UI Settings tab as the proxy admin

  1. Log in at http://localhost:4000/ui as a proxy admin
  2. Open http://localhost:4000/ui/admin-panel, pick the UI Settings tab and scroll to the end of the toggles
  3. The list ends at "Internal User Page Visibility", nothing mentions team admins

Before: UI Settings tab ends at Internal User Page Visibility

Admin UI: Edit Settings as the team admin

  1. Log in at http://localhost:4000/ui as a team admin set up like the curl cases
  2. Open http://localhost:4000/ui/teams, open the team, pick the Settings tab and click Edit Settings
  3. The full form opens; click Save Changes
  4. An error toast shows the same 401 the API returns

Before: the team admin gets the full form and a 401 toast on save

Admin UI: Edit Settings as the org admin

  1. Log in at http://localhost:4000/ui as an org admin set up like the curl cases
  2. Open http://localhost:4000/ui/teams, open the team that belongs to their organization, pick the Settings tab and click Edit Settings
  3. The full form opens

Before: the org admin gets the full form

After (3ee8d43)

The proxy-wide allow-list setting

  1. curl -s http://localhost:4831/get/ui_settings -H 'Authorization: Bearer sk-1234' | jq -c '{value: .values.team_admin_editable_team_fields, supported: .field_schema.properties.team_admin_editable_team_fields.items.enum}'

  2. Output: {"value":[],"supported":["tpm_limit"]}

  3. curl -s -X PATCH http://localhost:4831/update/ui_settings -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_admin_editable_team_fields": ["team_alias"]}' -w '\nHTTP %{http_code}\n'

  4. Output:

    {"detail":{"error":"team_admin_editable_team_fields does not support ['team_alias']. Supported fields: ['tpm_limit']."}}
    HTTP 400
    

A team admin before any field is enabled

  1. curl -s -X POST http://localhost:4831/user/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"user_id": "lit5722-team-admin-1789601272", "user_email": "lit5722-team-admin-1789601272@example.com", "user_role": "internal_user", "auto_create_key": true}' | jq -r .key

  2. Output: sk-...i9qw

  3. curl -s -X POST http://localhost:4831/team/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_alias": "lit5722-team-1789601272", "tpm_limit": 1000, "members_with_roles": [{"user_id": "lit5722-team-admin-1789601272", "role": "admin"}]}' | jq -r .team_id

  4. Output: 4285c114-8ed1-4e51-99bc-5cca050804ee

  5. curl -s 'http://localhost:4831/team/info?team_id=4285c114-8ed1-4e51-99bc-5cca050804ee' -H "Authorization: Bearer $TEAM_ADMIN_KEY" | jq -c '.team_info | {tpm_limit, caller_edit_access}'

  6. Output:

    {"tpm_limit":1000,"caller_edit_access":{"kind":"team_admin_disabled"}}
    
  7. curl -s -X POST http://localhost:4831/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "4285c114-8ed1-4e51-99bc-5cca050804ee", "tpm_limit": 5000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  8. Output:

    Team admins on this proxy cannot edit team settings. Ask a proxy admin to enable fields under Settings > UI > Team admin editable fields.
    HTTP 403
    

The proxy admin lets team admins edit tpm_limit

  1. curl -s -X PATCH http://localhost:4831/update/ui_settings -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_admin_editable_team_fields": ["tpm_limit"]}' -w '\nHTTP %{http_code}\n'

  2. Output:

    {"message":"UI settings updated successfully","status":"success","settings":{"team_admin_editable_team_fields":["tpm_limit"],"allow_public_health_readiness_details":false}}
    HTTP 200
    
  3. curl -s http://localhost:4831/get/ui_settings -H 'Authorization: Bearer sk-1234' | jq -c '{value: .values.team_admin_editable_team_fields}'

  4. Output: {"value":["tpm_limit"]}

  5. curl -s 'http://localhost:4831/team/info?team_id=4285c114-8ed1-4e51-99bc-5cca050804ee' -H "Authorization: Bearer $TEAM_ADMIN_KEY" | jq -c '.team_info | {tpm_limit, caller_edit_access}'

  6. Output:

    {"tpm_limit":1000,"caller_edit_access":{"kind":"team_admin","editable_fields":["tpm_limit"]}}
    

The team admin changes the team's TPM limit

  1. curl -s -X POST http://localhost:4831/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "4285c114-8ed1-4e51-99bc-5cca050804ee", "tpm_limit": 5000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  2. Output:

    {"team_alias":"lit5722-team-1789601272","tpm_limit":5000}
    HTTP 200
    
  3. curl -s -X POST http://localhost:4831/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "4285c114-8ed1-4e51-99bc-5cca050804ee", "team_alias": "renamed-by-team-admin"}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  4. Output:

    Team admins on this proxy do not have permission to update 'team_alias'. Ask a proxy admin to add it under Settings > UI > Team admin editable fields.
    HTTP 403
    
  5. curl -s -X POST http://localhost:4831/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "4285c114-8ed1-4e51-99bc-5cca050804ee", "team_alias": "lit5722-team-1789601272", "tpm_limit": 6000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  6. Output:

    {"team_alias":"lit5722-team-1789601272","tpm_limit":6000}
    HTTP 200
    

The organization's TPM limit still caps a team admin

  1. curl -s -X POST http://localhost:4831/organization/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"organization_alias": "lit5722-limited-org-1789601272", "tpm_limit": 10000}' | jq -r .organization_id

  2. Output: 37bc02ca-de10-4b4a-8b21-90a28d8e8d99

  3. curl -s -X POST http://localhost:4831/team/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_alias": "lit5722-limited-team-1789601272", "organization_id": "37bc02ca-de10-4b4a-8b21-90a28d8e8d99", "tpm_limit": 1000, "members_with_roles": [{"user_id": "lit5722-team-admin-1789601272", "role": "admin"}]}' | jq -r .team_id

  4. Output: 6fd001a7-2987-4a75-ac3e-3022df83ecbf

  5. curl -s -X POST http://localhost:4831/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "6fd001a7-2987-4a75-ac3e-3022df83ecbf", "tpm_limit": 20000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  6. Output:

    {'error': "Team tpm_limit (20000) exceeds organization's tpm_limit (10000). Organization: 37bc02ca-de10-4b4a-8b21-90a28d8e8d99"}
    HTTP 400
    
  7. curl -s -X POST http://localhost:4831/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "6fd001a7-2987-4a75-ac3e-3022df83ecbf", "tpm_limit": 8000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  8. Output:

    {"team_alias":"lit5722-limited-team-1789601272","tpm_limit":8000}
    HTTP 200
    

A proxy admin raising the same team past its organization's TPM limit

  1. curl -s -X POST http://localhost:4831/team/update -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_id": "6fd001a7-2987-4a75-ac3e-3022df83ecbf", "tpm_limit": 20000}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  2. Output:

    {'error': "Team tpm_limit (20000) exceeds organization's tpm_limit (10000). Organization: 37bc02ca-de10-4b4a-8b21-90a28d8e8d99"}
    HTTP 400
    
  3. curl -s 'http://localhost:4831/team/info?team_id=6fd001a7-2987-4a75-ac3e-3022df83ecbf' -H 'Authorization: Bearer sk-1234' | jq -c '.team_info | {tpm_limit}'

  4. Output: {"tpm_limit":8000}

A proxy admin editing the same team

  1. curl -s -X POST http://localhost:4831/team/update -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_id": "4285c114-8ed1-4e51-99bc-5cca050804ee", "team_alias": "renamed-by-proxy-admin"}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'

  2. Output:

    {"team_alias":"renamed-by-proxy-admin","tpm_limit":6000}
    HTTP 200
    

An org admin who is also a team admin editing a team in their organization

  1. curl -s -X POST http://localhost:4831/user/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"user_id": "lit5722-org-admin-1789601272", "user_email": "lit5722-org-admin-1789601272@example.com", "user_role": "internal_user", "auto_create_key": true}' | jq -r .key
  2. Output: sk-...GLlg
  3. curl -s -X POST http://localhost:4831/organization/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"organization_alias": "lit5722-org-1789601272"}' | jq -r .organization_id
  4. Output: e7db396b-c568-4b61-ac05-7e255b164349
  5. curl -s -X POST http://localhost:4831/organization/member_add -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"organization_id": "e7db396b-c568-4b61-ac05-7e255b164349", "member": {"user_id": "lit5722-org-admin-1789601272", "role": "org_admin"}}' | jq -c '{updated_users: [.updated_users[]?.user_id]}'
  6. Output: {"updated_users":["lit5722-org-admin-1789601272"]}
  7. curl -s -X POST http://localhost:4831/team/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_alias": "lit5722-org-team-1789601272", "organization_id": "e7db396b-c568-4b61-ac05-7e255b164349", "members_with_roles": [{"user_id": "lit5722-org-admin-1789601272", "role": "admin"}]}' | jq -r .team_id
  8. Output: cc34a267-b8d3-4a50-91fd-193a2584c58a
  9. curl -s 'http://localhost:4831/team/info?team_id=cc34a267-b8d3-4a50-91fd-193a2584c58a' -H "Authorization: Bearer $ORG_ADMIN_KEY" | jq -c '.team_info | {organization_id, caller_edit_access}'
  10. Output:
{"organization_id":"e7db396b-c568-4b61-ac05-7e255b164349","caller_edit_access":{"kind":"unrestricted"}}
  1. curl -s -X POST http://localhost:4831/team/update -H "Authorization: Bearer $ORG_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "cc34a267-b8d3-4a50-91fd-193a2584c58a", "team_alias": "renamed-by-org-admin"}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .error?.message? // . | if type == "object" then {team_alias, tpm_limit} else . end'
  2. Output:
{"team_alias":"renamed-by-org-admin","tpm_limit":null}
HTTP 200

A UI setting changed on one pod, read on another

  1. curl -s http://localhost:4831/health/readiness; echo; curl -s http://localhost:4832/health/readiness

  2. Output:

    {"status":"healthy","db":"connected"}
    {"status":"healthy","db":"connected"}
    
  3. curl -s -X PATCH http://localhost:4831/update/ui_settings -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"allow_public_health_readiness_details": true}'

  4. Output:

    {"message":"UI settings updated successfully","status":"success","settings":{"team_admin_editable_team_fields":["tpm_limit"],"allow_public_health_readiness_details":true}}
    
  5. curl -s http://localhost:4831/health/readiness | jq -c 'keys'

  6. Output:

    ["cache","db","is_detailed_debug","litellm_version","log_level","show_env_credential_login_warning","show_no_redis_warning","status","success_callbacks","use_aiohttp_transport"]
    
  7. start=$(date +%s); until curl -s http://localhost:4832/health/readiness | grep -q litellm_version; do sleep 2; [ $(( $(date +%s) - start )) -gt 90 ] && break; done; echo "$(( $(date +%s) - start ))s"; curl -s http://localhost:4832/health/readiness | jq -c 'if .litellm_version then keys else . end'

  8. Output:

    20s
    ["cache","db","is_detailed_debug","litellm_version","log_level","show_env_credential_login_warning","show_no_redis_warning","status","success_callbacks","use_aiohttp_transport"]
    

A team admin resending the team's budget settings leaves its budget reset alone

  1. curl -s -X POST http://localhost:4831/team/update -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"team_id": "4285c114-8ed1-4e51-99bc-5cca050804ee", "budget_duration": "30s"}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .team_info? // .error?.message? // . | if type == "object" then {tpm_limit, budget_duration, budget_reset_at} else . end'

  2. Output:

    {"tpm_limit":6000,"budget_duration":"30s","budget_reset_at":"2026-09-16T23:28:30Z"}
    HTTP 200
    
  3. sleep 31; curl -s -X POST http://localhost:4831/team/update -H "Authorization: Bearer $TEAM_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"team_id": "4285c114-8ed1-4e51-99bc-5cca050804ee", "tpm_limit": 7000, "budget_duration": "30s"}' -w '\nHTTP %{http_code}\n' | jq -Rrc 'fromjson? // . | .data? // .team_info? // .error?.message? // . | if type == "object" then {tpm_limit, budget_duration, budget_reset_at} else . end'

  4. Output:

    {"tpm_limit":7000,"budget_duration":"30s","budget_reset_at":"2026-09-16T23:28:30Z"}
    HTTP 200
    
  5. curl -s 'http://localhost:4831/team/info?team_id=4285c114-8ed1-4e51-99bc-5cca050804ee' -H 'Authorization: Bearer sk-1234' | jq -Rrc 'fromjson? // . | .data? // .team_info? // .error?.message? // . | if type == "object" then {tpm_limit, budget_duration, budget_reset_at} else . end'

  6. Output:

    {"tpm_limit":7000,"budget_duration":"30s","budget_reset_at":"2026-09-16T23:28:30Z"}
    

Admin UI: the UI Settings tab as the proxy admin

  1. Log in at http://localhost:3000 as a proxy admin
  2. Open http://localhost:3000/admin-panel, pick the UI Settings tab and scroll to the end of the toggles
  3. A separate "Team admin editable fields" card follows the UI Settings card, with the badge "Team admins cannot edit team settings", an unticked "Tokens per minute Limit (TPM)" checkbox and a disabled Save button
  4. Tick "Tokens per minute Limit (TPM)". Save turns on, the badge does not change, and GET http://localhost:4000/get/ui_settings still returns "team_admin_editable_team_fields": []
  5. Click Save. A "Team admin editable fields updated successfully" toast appears, the badge reads "1 field enabled", Save is disabled again, and /get/ui_settings now returns ["tpm_limit"]

After: TPM is ticked but not saved yet, so Save is enabled and the badge is unchanged

After: Save stores the list, the badge reads 1 field enabled and Save is disabled again

Admin UI: Edit Settings as the team admin

  1. Log in at http://localhost:3000 as a team admin set up like the curl cases, with tpm_limit enabled
  2. Open http://localhost:3000/teams, open the team, pick the Settings tab and click Edit Settings
  3. The form reads "A proxy admin chose which settings team admins can change. Ask a proxy admin to change anything else." and shows only "Tokens per minute Limit (TPM)". Save Changes stays disabled while the value matches the team's
  4. Change it to 7000; Save Changes turns on, click it
  5. A "Team settings updated successfully" toast appears and the Overview tab shows "TPM: 7000"

After: the team admin's form shows only the TPM field

After: the save succeeds and the Overview tab shows TPM 7000

Admin UI: Edit Settings as the org admin

  1. Log in at http://localhost:3000 as an org admin set up like the curl cases
  2. Open http://localhost:3000/teams, open the team that belongs to their organization, pick the Settings tab and click Edit Settings
  3. The full form opens with Team Name, Models and the rest, because the proxy reports the org admin as unrestricted

After: the org admin gets the full form

Type

🆕 New Feature
🐛 Bug Fix

Caveats (if any)

Severe

  • /team/update now enforces the organization's max_budget, tpm_limit and rpm_limit for every caller
    • It used to skip them, so even a proxy admin could set a team above its org's cap
    • Scripts that did that now get a 400, the same answer /team/new already gives
  • /team/update now authorizes in the handler instead of the route check
    • Non-admin callers get a 403 from the handler where they used to get a 401

Medium

  • Only tpm_limit can be enabled so far
    • rpm_limit, max_budget, models, team_alias and soft_budget follow, one PR each

Low

  • Unknown or foreign team ids answer 403 without echoing the id, proxy admins still get 404
  • The team admin 403 comes back with "type": "internal_server_error", the wrapper the handler's other 403s already use
  • /organization/member_add does not refresh the cached user row, so a user just promoted to org admin can be treated as a plain team admin until that cache entry expires; this predates the PR and every org admin check shares it
  • caller_edit_access is additive on /team/info and defaults to {"kind": "none"}, so older dashboards ignore it and the new dashboard treats a proxy that omits it as read-only for non-admins
  • A UI settings change reaches other replicas on their next config reload, not instantly
    • proxy_config_reload_interval_seconds sets that interval and defaults to 30 seconds
    • The reload only runs when STORE_MODEL_IN_DB is on, which the settings endpoint already requires
  • An API update from a team admin that changes nothing still writes and audits, as it does for any caller; the dashboard no longer sends one
  • Docs for the setting follow in the separate docs repo

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Note

Medium Risk
Changes authorization for /team/update (401→403 semantics), enforces org budget/TPM/RPM limits on update for all callers, and introduces field-level team-admin permissions that must stay aligned with the dashboard and multi-pod UI settings sync.

Overview
Adds a proxy-wide UI setting team_admin_editable_team_fields so proxy admins can choose which team settings team admins may change (initially only tpm_limit). /team/update moves to self-managed routes; the handler resolves proxy/org/team admin roles, applies the allow-list for team admins (403 with clear messages), compares submitted vs stored values so unchanged form fields are ignored, and strips unchanged budget fields to avoid resetting budget timers.

/team/info now returns caller_edit_access so the dashboard can show a limited team-admin settings form or block editing without guessing from roles. Admin UI gets a Team admin editable fields card; team settings editing uses the proxy verdict.

UI settings that affect runtime (including the new allow-list) sync into general_settings on startup and on every periodic add_deployment reload so replicas converge without restart. /team/update also loads the org budget relation so org max budget / TPM / RPM caps apply on update (not only on create).

Reviewed by Cursor Bugbot for commit 3ee8d43. Bugbot is set up for automated code reviews on this repo. Configure here.

@codspeed

codspeed Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_team_admin_editable_fields (3ee8d43) with main (390ab45)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds configurable, server-enforced team-admin permissions for editing selected team fields and exposes those permissions to the dashboard.

  • Makes /team/update resolve proxy, organization, and team-admin access within the handler.
  • Restricts team admins to the proxy-wide editable-field allow-list, initially supporting tpm_limit.
  • Applies organization budget, TPM, and RPM limits during team updates.
  • Reports caller edit access through /team/info and renders the corresponding dashboard form.
  • Refreshes persisted runtime UI settings during periodic configuration reloads across replicas.
  • Updates the team-admin form to submit only values changed from its initial state.

Confidence Score: 5/5

The PR appears safe to merge, with no newly established blocking issue.

Cross-replica UI settings are now refreshed during periodic configuration reloads, resolving the earlier stale-permission concern. The prior unchanged-update finding remains outstanding for direct API callers because the backend still permits an empty filtered request to reach persistence; the new dashboard-side change only avoids that request for this client, so the non-blocking database-write and audit-noise concern remains.

Important Files Changed
Filename Overview
litellm/proxy/management_endpoints/team_admin_field_permissions.py Defines the editable-field allow-list, detects logical field changes, and filters team-admin update requests before persistence.
litellm/proxy/management_endpoints/team_endpoints.py Resolves caller access for team updates and information responses while enforcing organization limits.
litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py Persists, validates, and synchronizes the new runtime UI setting across proxy replicas.
ui/litellm-dashboard/src/components/team/TeamAdminSettingsForm.tsx Disables saving until an editable value changes and submits only the changed value.
ui/litellm-dashboard/src/components/team/teamAdminEditAccess.ts Normalizes editable TPM values and excludes values unchanged from the stored team state.

Reviews (8): Last reviewed commit: "fix(ui): send only a changed TPM limit f..." | Re-trigger Greptile

@@ -360,6 +380,7 @@ def _derived_ui_setting_value(key: str) -> object:
"disable_vector_stores_for_internal_users",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Settings Stay Stale Across Pods

In a multi-pod deployment, changing this setting updates only the pod handling the request. Other pods continue using their process-local general_settings, so new grants or revocations do not take effect there until restart once fields become configurable

Knowledge Base Used: Management APIs and resources

@codecov

codecov Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.68508% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...nagement_endpoints/team_admin_field_permissions.py 97.93% 2 Missing ⚠️
...tellm/proxy/management_endpoints/team_endpoints.py 95.12% 2 Missing ⚠️
litellm/proxy/proxy_server.py 71.42% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ryan-crabbe-berri
ryan-crabbe-berri force-pushed the litellm_team_admin_editable_fields branch from 98a99d2 to 7743c3b Compare September 9, 2026 16:11
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptile re review

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@yuneng-berri
yuneng-berri deleted the branch main September 13, 2026 04:49
@yuneng-berri yuneng-berri reopened this Sep 13, 2026
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@ryan-crabbe-berri
ryan-crabbe-berri force-pushed the litellm_team_admin_editable_fields branch from 25933b7 to 8c86c24 Compare September 15, 2026 22:43
@CLAassistant

CLAassistant commented Sep 15, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@ryan-crabbe-berri
ryan-crabbe-berri changed the base branch from litellm_internal_staging to main September 15, 2026 22:44

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread ui/litellm-dashboard/src/components/team/TeamInfo.tsx
return _GENERAL_SETTINGS.validate_python(cast(object, proxy_server.general_settings))


def _caller_edit_access(role: TeamAccessRole | None, general_settings: Mapping[str, object]) -> TeamEditAccess:
…y edit

Team admins could never reach POST /team/update: the route gate answered
401 before the handler's team-admin branch ran. This moves /team/update
into the self-managed routes, resolves proxy admin, org admin or team
admin inside the handler, and filters team admins through a new
proxy-wide UI setting, team_admin_editable_team_fields. The setting is
an allow-list of team fields. Empty means team admins cannot edit team
settings and get a 403 pointing at the proxy admin, and changing a field
outside the list fails 403 naming that field. Only values that differ
from what is stored count, since the dashboard resends the whole form.

The registry of fields the setting accepts ships empty on purpose. Each
field lands in its own follow-up PR with its value diff and dashboard
wiring. The Admin UI gains a "Team admin editable fields" section under
Settings > UI and a toast on the team page while editing is disabled.

Refs LIT-5722

Claude-Session: https://claude.ai/code/session_01A6SkwJdfZUmkzfUkrEkqX8
The persisted UI settings were read once at startup, so a proxy admin
flipping a runtime flag through PATCH /update/ui_settings only changed the
pod that served the request. Every other pod kept serving the old value
until it restarted.

add_deployment, the reload the scheduler runs every 30s, now re-reads the
row and applies the runtime flags before it takes the model reconcile lock,
so a change made through one pod reaches the rest within one reload
interval. The startup hook and the two settings endpoints share that helper
instead of each repeating the flag copy.

Claude-Session: https://claude.ai/code/session_018PUCupsaarVLJy4iDFx256
The five-condition chain pushed local/no-long-condition-chain one over its
ceiling and failed the dashboard lint gate.

Claude-Session: https://claude.ai/code/session_018PUCupsaarVLJy4iDFx256
… 3.10

assert_never landed in typing in 3.11, so importing it from typing broke the
3.10 import smoke job and the py310 typing gate. Take it from typing_extensions
like the rest of the proxy does.

Also drop /team/update from test_neighbouring_team_routes_stay_closed. The route
is self-managed now, so the coarse gate admits the caller and update_team decides,
which the sibling test in the same file already asserts.

Claude-Session: https://claude.ai/code/session_018PUCupsaarVLJy4iDFx256
The dashboard gated the team settings form on a role it guessed from the
is_* props, the members list and the org list. The org list is premium
gated and empty while loading, so a team admin who is also an org admin
was told team admins cannot edit, although /team/update accepts them as
an org admin

/team/info now returns caller_edit_access, resolved by the same helper
/team/update uses, and TeamInfo keys the form and the toast off that
field. The org list is only read for the organization dropdown now, and
general_settings is read through one validated accessor in both handlers
max_budget is not a team-admin editable field yet, so the behavior suite now
pins the 403 in both directions instead of the old lower-is-allowed rule

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread litellm/proxy/management_endpoints/team_admin_field_permissions.py Outdated
@veria-ai

veria-ai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10


def team_admin_request_or_raise(verdict: TeamAdminEditVerdict) -> UpdateTeamRequest:
match verdict:
case TeamAdminEditAllowed(request=request):
return TeamAdminEditAllowed(request=_only_changes(data, changed))


def team_admin_request_or_raise(verdict: TeamAdminEditVerdict) -> UpdateTeamRequest:
Team admins are refused until a proxy admin enables a field, then limited to the enabled fields, and resending unchanged budget settings keeps the team's budget reset times
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment on lines +147 to +152
def _only_changes(data: UpdateTeamRequest, changed: frozenset[str]) -> UpdateTeamRequest:
"""The request without the values it resends unchanged, which would otherwise still trigger derived writes
such as a resent budget_duration pushing budget_reset_at back."""
sent: Final = frozenset(data.model_fields_set)
via_metadata: Final = frozenset({"metadata"}) if changed - sent else frozenset()
kept: Final = frozenset({"team_id"}) | (changed & sent) | via_metadata

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unchanged Updates Still Write

An unchanged team-admin request still writes to the database, refreshes caches, and creates avoidable audit noise

Knowledge Base Used: Proxy control plane and persistence

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3ee8d43 on the client: the team admin form sends only changed values and keeps Save disabled until something changes

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Save stays disabled until the value differs from the team's, so an unchanged form never reaches /team/update
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 3ee8d43. Configure here.

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

Two caveats worth a look

  • UX footgun: /team/update now enforces the org's max_budget, tpm_limit and rpm_limit for every caller
    • A proxy admin raising a team past its org's cap now gets a 400, where it used to save silently
  • Semantics: /team/update now authorizes in the handler instead of the route check
    • Non-admin callers get a 403 where they used to get a 401

@ryan-crabbe-berri
ryan-crabbe-berri merged commit 43713f7 into main Sep 17, 2026
88 of 90 checks passed
@ryan-crabbe-berri
ryan-crabbe-berri deleted the litellm_team_admin_editable_fields branch September 17, 2026 00:07
yuneng-berri added a commit that referenced this pull request Sep 17, 2026
leilei3167 pushed a commit to leilei3167/litellm that referenced this pull request Sep 17, 2026
…iAI#39996 and BerriAI#41310

Every one of these fails on main's own scheduled CircleCI run with the same
assertion as on any PR, and each traces to a merged behavior change that
never updated the test that pinned the old behavior

- tests/integration/_support/client.py: BerriAI#41311 made /key/info serve deleted
  keys from the archive with status deleted, so the scenario teardown asserts
  the live row is gone and the readback reports deleted instead of a 404.
  This alone accounts for nine integration-management and one
  integration-providers failure
- tests/integration/authorization/test_warmed_policy.py: BerriAI#39996 made team
  admins unable to edit any team field unless a proxy admin allow-lists it,
  and tpm_limit is the only field it accepts today. The demotion test now
  enables tpm_limit for the scenario and edits that instead of team_alias
- tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py:
  BerriAI#41337 reads usage off the terminal response and copies the event when it
  is missing, which a Mock(spec=ResponsesAPIResponse) cannot survive. The
  four mocks now carry a usage object
- tests/test_openai_endpoints.py: BerriAI#41310 lengthened the access-denied
  message, and the test matched against the ExceptionInfo repr, which
  saferepr truncates in the middle. It now matches the exception text
- tests/local_testing/test_text_completion.py: Together no longer serves
  Qwen2-1.5B serverless, the cheapest cost-map row. The test mocks the
  completions call and asserts the request litellm builds, so a vendor
  catalog rotation cannot fail it again

test_router_fallbacks_with_cooldowns_and_dynamic_credentials is deliberately
untouched: it passes and fails on main with identical code, and the failing
path is a product question about whether dynamic-credential 429s cool down

This branch is waiting to be deployed

1 waiting deployment
e2e-changed — 3ee8d43f Waiting Sep 16, 2026 by ryan-crabbe-berri via Run changed e2e tests against the stage-mirror stack #5954
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants