Skip to content

Commit af61132

Browse files
yuneng-jiangclaude
andcommitted
refactor: use DualCache for UI settings reads and get_team_object for team lookup
- Add get_ui_settings_cached() helper that reads from DualCache first, falls back to DB, and populates cache on miss. - Update update_ui_settings() to set cache after DB write so subsequent reads see new values immediately. - Replace raw prisma_client.db.litellm_teamtable.find_unique with the existing get_team_object helper which uses DualCache. - Update all tests to mock get_ui_settings_cached and get_team_object instead of raw DB calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c631708 commit af61132

File tree

3 files changed

+142
-108
lines changed

3 files changed

+142
-108
lines changed

litellm/proxy/management_endpoints/internal_user_endpoints.py

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
get_daily_activity,
3131
get_daily_activity_aggregated,
3232
)
33-
from litellm.proxy.auth.auth_checks import get_user_object
33+
from litellm.proxy.auth.auth_checks import get_team_object, get_user_object
3434
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
3535
from litellm.proxy.management_endpoints.key_management_endpoints import (
3636
generate_key_helper_fn,
@@ -1869,21 +1869,17 @@ async def ui_view_users(
18691869
proxy_logging_obj,
18701870
user_api_key_cache,
18711871
)
1872+
from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import (
1873+
get_ui_settings_cached,
1874+
)
18721875

18731876
if prisma_client is None:
18741877
raise HTTPException(status_code=500, detail={"error": "No db connected"})
18751878

18761879
try:
1877-
# Read the scope_user_search_to_org flag from the DB
1878-
ui_settings_row = (
1879-
await prisma_client.db.litellm_uisettings.find_unique(
1880-
where={"id": "ui_settings"}
1881-
)
1882-
)
1883-
scope_flag = False
1884-
if ui_settings_row is not None:
1885-
settings_json = ui_settings_row.settings or {} # type: ignore[union-attr]
1886-
scope_flag = bool(settings_json.get("scope_user_search_to_org", False))
1880+
# Read the scope_user_search_to_org flag (cached)
1881+
ui_settings = await get_ui_settings_cached()
1882+
scope_flag = bool(ui_settings.get("scope_user_search_to_org", False))
18871883

18881884
org_filter_ids: Optional[List[str]] = None
18891885

@@ -1915,27 +1911,29 @@ async def ui_view_users(
19151911
if org_admin_org_ids:
19161912
org_filter_ids = org_admin_org_ids
19171913
elif team_id is not None:
1918-
# Look up the team to check if it belongs to an org
1919-
team_row = await prisma_client.db.litellm_teamtable.find_unique(
1920-
where={"team_id": team_id}
1921-
)
1922-
if team_row is not None:
1923-
team_obj = LiteLLM_TeamTable(**team_row.model_dump())
1924-
if _is_user_team_admin(user_api_key_dict, team_obj):
1925-
if team_obj.organization_id:
1926-
org_filter_ids = [team_obj.organization_id]
1927-
else:
1928-
raise HTTPException(
1929-
status_code=403,
1930-
detail={
1931-
"error": "scope_user_search_to_org is enabled and this team is not part of an organization. Contact your proxy admin to adjust this setting."
1932-
},
1933-
)
1914+
# Look up the team via cached helper
1915+
try:
1916+
team_obj = await get_team_object(
1917+
team_id=team_id,
1918+
prisma_client=prisma_client,
1919+
user_api_key_cache=user_api_key_cache,
1920+
proxy_logging_obj=proxy_logging_obj,
1921+
)
1922+
except HTTPException:
1923+
raise HTTPException(
1924+
status_code=403,
1925+
detail={
1926+
"error": "scope_user_search_to_org is enabled. Only proxy admins, organization admins, or team admins can search users."
1927+
},
1928+
)
1929+
if _is_user_team_admin(user_api_key_dict, team_obj):
1930+
if team_obj.organization_id:
1931+
org_filter_ids = [team_obj.organization_id]
19341932
else:
19351933
raise HTTPException(
19361934
status_code=403,
19371935
detail={
1938-
"error": "scope_user_search_to_org is enabled. Only proxy admins, organization admins, or team admins can search users."
1936+
"error": "scope_user_search_to_org is enabled and this team is not part of an organization. Contact your proxy admin to adjust this setting."
19391937
},
19401938
)
19411939
else:

litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,48 @@ async def get_in_product_nudges():
980980
return InProductNudgeResponse(is_claude_code_enabled=False)
981981

982982

983+
UI_SETTINGS_CACHE_KEY = "ui_settings:settings_dict"
984+
985+
986+
async def get_ui_settings_cached() -> Dict[str, Any]:
987+
"""
988+
Return the persisted UI settings dict, using DualCache for reads.
989+
990+
Cache hit → return cached dict immediately.
991+
Cache miss → read from DB, populate cache, return dict.
992+
"""
993+
from litellm.proxy.proxy_server import prisma_client, user_api_key_cache
994+
995+
# 1. Try cache
996+
cached = await user_api_key_cache.async_get_cache(key=UI_SETTINGS_CACHE_KEY)
997+
if cached is not None and isinstance(cached, dict):
998+
return cached
999+
1000+
# 2. Fallback to DB
1001+
if prisma_client is None:
1002+
return {}
1003+
1004+
db_record = await prisma_client.db.litellm_uisettings.find_unique(
1005+
where={"id": "ui_settings"}
1006+
)
1007+
ui_settings: Dict[str, Any] = {}
1008+
if db_record and db_record.ui_settings:
1009+
raw = db_record.ui_settings
1010+
ui_settings = json.loads(raw) if isinstance(raw, str) else dict(raw)
1011+
1012+
# Sanitize
1013+
ui_settings = {
1014+
k: v for k, v in ui_settings.items() if k in ALLOWED_UI_SETTINGS_FIELDS
1015+
}
1016+
1017+
# 3. Populate cache
1018+
await user_api_key_cache.async_set_cache(
1019+
key=UI_SETTINGS_CACHE_KEY, value=ui_settings
1020+
)
1021+
1022+
return ui_settings
1023+
1024+
9831025
@router.get(
9841026
"/get/ui_settings",
9851027
tags=["UI Settings"],
@@ -1108,6 +1150,16 @@ async def update_ui_settings(
11081150

11091151
general_settings.update(_flags_to_sync)
11101152

1153+
# Invalidate + set DualCache so subsequent reads see the new values immediately
1154+
from litellm.proxy.proxy_server import user_api_key_cache
1155+
1156+
sanitized = {
1157+
k: v for k, v in ui_settings.items() if k in ALLOWED_UI_SETTINGS_FIELDS
1158+
}
1159+
await user_api_key_cache.async_set_cache(
1160+
key=UI_SETTINGS_CACHE_KEY, value=sanitized
1161+
)
1162+
11111163
return {
11121164
"message": "UI settings updated successfully",
11131165
"status": "success",

tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py

Lines changed: 64 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ async def mock_find_many(*args, **kwargs):
5454

5555
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
5656

57-
# Flag OFF by default — no settings row
58-
async def mock_find_unique_settings(*args, **kwargs):
59-
return None
60-
61-
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
57+
# Flag OFF by default
58+
mocker.patch(
59+
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
60+
return_value={},
61+
)
6262

6363
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
6464

@@ -92,10 +92,10 @@ async def mock_find_many(*args, **kwargs):
9292
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
9393

9494
# Flag OFF by default
95-
async def mock_find_unique_settings(*args, **kwargs):
96-
return None
97-
98-
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
95+
mocker.patch(
96+
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
97+
return_value={},
98+
)
9999
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
100100

101101
await ui_view_users(
@@ -132,13 +132,10 @@ async def mock_find_many(*args, **kwargs):
132132
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
133133

134134
# Flag ON
135-
mock_settings_row = mocker.MagicMock()
136-
mock_settings_row.settings = {"scope_user_search_to_org": True}
137-
138-
async def mock_find_unique_settings(*args, **kwargs):
139-
return mock_settings_row
140-
141-
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
135+
mocker.patch(
136+
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
137+
return_value={"scope_user_search_to_org": True},
138+
)
142139

143140
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
144141
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
@@ -185,13 +182,10 @@ async def test_ui_view_users_non_org_admin_returns_403(mocker):
185182
mock_prisma_client = mocker.MagicMock()
186183

187184
# Flag ON
188-
mock_settings_row = mocker.MagicMock()
189-
mock_settings_row.settings = {"scope_user_search_to_org": True}
190-
191-
async def mock_find_unique_settings(*args, **kwargs):
192-
return mock_settings_row
193-
194-
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
185+
mocker.patch(
186+
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
187+
return_value={"scope_user_search_to_org": True},
188+
)
195189

196190
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
197191
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
@@ -237,11 +231,11 @@ async def mock_find_many(*args, **kwargs):
237231

238232
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
239233

240-
# Flag OFF — no settings row
241-
async def mock_find_unique_settings(*args, **kwargs):
242-
return None
243-
244-
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
234+
# Flag OFF
235+
mocker.patch(
236+
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
237+
return_value={},
238+
)
245239
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
246240

247241
response = await ui_view_users(
@@ -261,7 +255,7 @@ async def test_ui_view_users_flag_on_team_admin_org_team(mocker):
261255
"""
262256
Flag ON, team admin for org-bound team: org filter is applied using team's org.
263257
"""
264-
from litellm.proxy._types import LiteLLM_TeamTable, Member
258+
from litellm.proxy._types import LiteLLM_TeamTableCachedObj
265259

266260
mock_prisma_client = mocker.MagicMock()
267261
org_id = "org-456"
@@ -278,30 +272,26 @@ async def mock_find_many(*args, **kwargs):
278272
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
279273

280274
# Flag ON
281-
mock_settings_row = mocker.MagicMock()
282-
mock_settings_row.settings = {"scope_user_search_to_org": True}
283-
284-
async def mock_find_unique_settings(*args, **kwargs):
285-
return mock_settings_row
286-
287-
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
288-
289-
# Team lookup
290-
mock_team_row = mocker.MagicMock()
291-
mock_team_row.model_dump.return_value = {
292-
"team_id": tid,
293-
"team_alias": "test-team",
294-
"organization_id": org_id,
295-
"members_with_roles": [{"user_id": "team-admin-user", "role": "admin"}],
296-
"admins": [],
297-
"members": [],
298-
"blocked": False,
299-
}
275+
mocker.patch(
276+
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
277+
return_value={"scope_user_search_to_org": True},
278+
)
279+
280+
# Mock get_team_object
281+
team_obj = LiteLLM_TeamTableCachedObj(
282+
team_id=tid,
283+
team_alias="test-team",
284+
organization_id=org_id,
285+
members_with_roles=[{"user_id": "team-admin-user", "role": "admin"}],
286+
)
300287

301-
async def mock_find_unique_team(*args, **kwargs):
302-
return mock_team_row
288+
async def mock_get_team_object(*args, **kwargs):
289+
return team_obj
303290

304-
mock_prisma_client.db.litellm_teamtable.find_unique = mock_find_unique_team
291+
mocker.patch(
292+
"litellm.proxy.management_endpoints.internal_user_endpoints.get_team_object",
293+
side_effect=mock_get_team_object,
294+
)
305295

306296
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
307297
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
@@ -337,35 +327,32 @@ async def test_ui_view_users_flag_on_team_admin_non_org_team_403(mocker):
337327
Flag ON, team admin for non-org team: returns 403.
338328
"""
339329
from fastapi import HTTPException
330+
from litellm.proxy._types import LiteLLM_TeamTableCachedObj
340331

341332
mock_prisma_client = mocker.MagicMock()
342333
tid = "team-no-org"
343334

344335
# Flag ON
345-
mock_settings_row = mocker.MagicMock()
346-
mock_settings_row.settings = {"scope_user_search_to_org": True}
347-
348-
async def mock_find_unique_settings(*args, **kwargs):
349-
return mock_settings_row
350-
351-
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
352-
353-
# Team lookup — no organization_id
354-
mock_team_row = mocker.MagicMock()
355-
mock_team_row.model_dump.return_value = {
356-
"team_id": tid,
357-
"team_alias": "no-org-team",
358-
"organization_id": None,
359-
"members_with_roles": [{"user_id": "team-admin-user", "role": "admin"}],
360-
"admins": [],
361-
"members": [],
362-
"blocked": False,
363-
}
336+
mocker.patch(
337+
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
338+
return_value={"scope_user_search_to_org": True},
339+
)
364340

365-
async def mock_find_unique_team(*args, **kwargs):
366-
return mock_team_row
341+
# Mock get_team_object — team has no organization_id
342+
team_obj = LiteLLM_TeamTableCachedObj(
343+
team_id=tid,
344+
team_alias="no-org-team",
345+
organization_id=None,
346+
members_with_roles=[{"user_id": "team-admin-user", "role": "admin"}],
347+
)
367348

368-
mock_prisma_client.db.litellm_teamtable.find_unique = mock_find_unique_team
349+
async def mock_get_team_object(*args, **kwargs):
350+
return team_obj
351+
352+
mocker.patch(
353+
"litellm.proxy.management_endpoints.internal_user_endpoints.get_team_object",
354+
side_effect=mock_get_team_object,
355+
)
369356

370357
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
371358
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
@@ -409,13 +396,10 @@ async def test_ui_view_users_flag_on_non_admin_no_team_id_403(mocker):
409396
mock_prisma_client = mocker.MagicMock()
410397

411398
# Flag ON
412-
mock_settings_row = mocker.MagicMock()
413-
mock_settings_row.settings = {"scope_user_search_to_org": True}
414-
415-
async def mock_find_unique_settings(*args, **kwargs):
416-
return mock_settings_row
417-
418-
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
399+
mocker.patch(
400+
"litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints.get_ui_settings_cached",
401+
return_value={"scope_user_search_to_org": True},
402+
)
419403

420404
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
421405
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())

0 commit comments

Comments
 (0)