[Feature] User Info V2 Endpoint - #23437
Conversation
- Add UserInfoV2Response type in _types.py (returns only user object, no keys/teams) - Add /v2/user/info endpoint handler with proper access control: - Proxy admins can query any user - Team admins can query users in their teams - Internal users can query themselves only - Returns 404 for unauthorized/not-found (not 403) - Add /v2/user/info to info_routes in LiteLLMRoutes - Add route check passthrough in route_checks.py - Add get_user_v2() method to Python client Co-authored-by: yuneng-jiang <yuneng-jiang@users.noreply.github.com>
- 9 tests for the endpoint: admin access, self-lookup, unauthorized access, default to self, nonexistent user, response shape, team admin access, team admin denied, URL encoding - 2 tests for route checks: route in info_routes, route access control Co-authored-by: yuneng-jiang <yuneng-jiang@users.noreply.github.com>
…tUser hook - Add UserInfoV2Response type and userGetInfoV2() function in networking.tsx - Migrate useCurrentUser hook from userInfoCall to userGetInfoV2 - Update useCurrentUser.test.ts to test new v2 API integration - The hook no longer needs userRole since the endpoint handles auth itself Co-authored-by: yuneng-jiang <yuneng-jiang@users.noreply.github.com>
…er/info
- user_dashboard.tsx: Replace userInfoCall with userGetInfoV2 for spend data,
remove keys/teams logic (keys come from props/useKeys hook, teams from fetchTeams)
- user_info_view.tsx: Replace userInfoCall with userGetInfoV2, flatten data
structure from nested {user_info: {...}} to flat response, fetch team details
separately using teamInfoCall, remove keys display (Virtual Keys section)
- Update user_dashboard.test.tsx and user_info_view.test.tsx mocks
Co-authored-by: yuneng-jiang <yuneng-jiang@users.noreply.github.com>
|
Cursor Agent can help with this pull request. Just |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
Greptile SummaryThis PR introduces a new Key changes:
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| litellm/proxy/management_endpoints/internal_user_endpoints.py | Adds _check_user_info_v2_access helper and user_info_v2 endpoint. RBAC logic is correct; minor issue: prisma_client is None guard returns None instead of raising, which could produce a misleading 404 if the helper is called without the outer guard. |
| litellm/proxy/_types.py | Adds UserInfoV2Response Pydantic model and registers /v2/user/info in info_routes. Clean, well-structured addition with correct field types and defaults. |
| litellm/proxy/auth/route_checks.py | Adds a pass-through case for /v2/user/info in non_proxy_admin_allowed_routes_check, deferring all access control to the endpoint handler. The comment clearly explains the rationale. |
| litellm/proxy/client/users.py | Adds get_user_v2 method to the Python client. Clean implementation with appropriate error handling for 401 and 404 status codes. |
| ui/litellm-dashboard/src/components/networking.tsx | Adds UserInfoV2Response interface and userGetInfoV2 function. Follows existing patterns in the file for error handling and URL construction. |
| ui/litellm-dashboard/src/components/view_users/user_info_view.tsx | Migrated to userGetInfoV2 with separate teamInfoCall per team ID. The N+1 pattern (one teamInfoCall per team, parallelised) could cause a large number of concurrent requests for users in many teams. |
| ui/litellm-dashboard/src/components/user_dashboard.tsx | Replaces userInfoCall with userGetInfoV2 for spend data; keys now sourced from useKeys hook. The !keys guard and its sessionStorage write were both removed cleanly (no dangling reads remain). |
| ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useCurrentUser.ts | Switched from userInfoCall to userGetInfoV2 for self-lookup. Removes userRole from the enabled condition (now only requires accessToken && userId), which is appropriate for a self-lookup endpoint. |
| tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py | Adds 9 well-structured async tests covering proxy admin, self-lookup, cross-user access denial, team admin access, non-team-member denial, URL encoding, and response shape. All use mocks; no real network calls. |
| tests/test_litellm/proxy/auth/test_info_routes.py | Adds 2 tests verifying /v2/user/info is in info_routes and that route-level access control passes through to the endpoint handler. Clean and correct. |
Sequence Diagram
sequenceDiagram
participant Client
participant FastAPI
participant user_info_v2
participant _check_user_info_v2_access
participant DB as Prisma DB
Client->>+FastAPI: GET /v2/user/info?user_id=X
FastAPI->>+user_info_v2: user_api_key_dict, user_id
user_info_v2->>+_check_user_info_v2_access: (caller, target_user_id)
alt Proxy admin / admin viewer
_check_user_info_v2_access->>DB: find_unique(target_user)
DB-->>_check_user_info_v2_access: user_row
else Self-lookup (caller == target)
_check_user_info_v2_access->>DB: find_unique(caller == target)
DB-->>_check_user_info_v2_access: user_row
else Team admin check
_check_user_info_v2_access->>DB: find_unique(caller_user)
DB-->>_check_user_info_v2_access: caller_row
_check_user_info_v2_access->>DB: find_unique(target_user)
DB-->>_check_user_info_v2_access: target_row
_check_user_info_v2_access->>DB: find_many(caller's teams)
DB-->>_check_user_info_v2_access: teams[]
Note over _check_user_info_v2_access: Check if caller is admin<br/>of any team containing target
end
_check_user_info_v2_access-->>-user_info_v2: user_row or None
alt user_row is None
user_info_v2-->>Client: 404 Not Found
else user_row found
user_info_v2-->>-FastAPI: UserInfoV2Response (flat: no keys, team IDs only)
FastAPI-->>Client: 200 OK
end
Last reviewed commit: cd1b31b
| except Exception: | ||
| verbose_proxy_logger.debug( | ||
| f"Error checking team admin access for user {user_api_key_dict.user_id}" | ||
| ) |
There was a problem hiding this comment.
Silent exception swallows real DB errors
The bare except Exception: pass with only a debug-level log means that genuine DB failures (e.g. connection timeouts, query errors) are silently converted into a False return. From the caller's perspective this is indistinguishable from "access denied", so the endpoint will return 404 even when the real cause is a transient infrastructure issue — making it extremely hard to diagnose in production.
Consider logging at warning or error level so that operator-level failures surface in observability tooling:
| except Exception: | |
| verbose_proxy_logger.debug( | |
| f"Error checking team admin access for user {user_api_key_dict.user_id}" | |
| ) | |
| except Exception as e: | |
| verbose_proxy_logger.warning( | |
| f"Error checking team admin access for user {user_api_key_dict.user_id}: {e}" | |
| ) |
| # Check if target user is in this team | ||
| target_user = await prisma_client.db.litellm_usertable.find_unique( | ||
| where={"user_id": target_user_id} | ||
| ) | ||
| if ( | ||
| target_user is not None | ||
| and team.team_id in (target_user.teams or []) | ||
| ): | ||
| return True |
There was a problem hiding this comment.
N+1 DB query inside team loop
The litellm_usertable.find_unique call for target_user_id is executed inside the for team in teams loop. If the caller is an admin of N teams, the same database row is fetched N times despite the result being identical on every iteration.
The fix is to move the target_user fetch to just before the loop begins, then reference the already-fetched object inside the loop when checking team.team_id in (target_user.teams or []). This reduces the query count from O(N teams) to a single round-trip.
…_user_info_v2_access Three bugs fixed in the RBAC helper for /v2/user/info: 1. N+1 query: target user was fetched inside the team loop, causing one redundant DB round-trip per team the caller administers. Now hoisted before the loop — single fetch regardless of team count. 2. Double fetch: the handler performed a second find_unique on the target user after the access check already fetched it. The helper now returns the user row directly (Optional[row] instead of bool), so the handler reuses it. 3. Silent error swallowing: a bare except Exception caught real DB errors (connection failures, timeouts) and returned False, surfacing them as mysterious 404s. Removed the try/except so real errors propagate as 500s. Co-authored-by: yuneng-jiang <yuneng-jiang@users.noreply.github.com>
| const teamPromises = data.teams.map(async (teamId: string) => { | ||
| try { | ||
| const teamData = await teamInfoCall(accessToken, teamId); | ||
| return { | ||
| team_id: teamId, | ||
| team_alias: teamData?.team_alias || null, | ||
| }; | ||
| } catch { | ||
| return { team_id: teamId, team_alias: null }; | ||
| } | ||
| }); | ||
| const teams = await Promise.all(teamPromises); | ||
| setTeamDetails(teams); | ||
| } catch { | ||
| // Fall back to just team IDs | ||
| setTeamDetails(data.teams.map((id: string) => ({ team_id: id, team_alias: null }))); | ||
| } | ||
| } |
There was a problem hiding this comment.
N+1 teamInfoCall requests per team
The component fires one teamInfoCall per team ID in parallel. For users that belong to many teams (e.g., 50+), this results in 50+ simultaneous HTTP requests to /team/info, each returning a full team object (including members_with_roles) when only team_alias is needed.
Consider caching team aliases or batching by adding a multi-team-alias endpoint, or at minimum limit the parallel calls:
// Only fetch aliases for the first 20 teams shown, fetch more lazily
const visibleTeamIds = data.teams.slice(0, 20);
const teamPromises = visibleTeamIds.map(async (teamId: string) => {
try {
const teamData = await teamInfoCall(accessToken, teamId);
return { team_id: teamId, team_alias: teamData?.team_alias || null };
} catch {
return { team_id: teamId, team_alias: null };
}
});At a minimum, the rest of the teams beyond the initial visible 20 could be fetched on demand when the user clicks "show more".
| from litellm.proxy.proxy_server import prisma_client | ||
|
|
||
| if prisma_client is None: | ||
| return None |
There was a problem hiding this comment.
prisma_client is None returns None, causing misleading 404
When prisma_client is None, _check_user_info_v2_access returns None. The caller (user_info_v2) treats None as "access denied / user not found" and raises a 404. However, user_info_v2 already guards against this with its own if prisma_client is None: raise HTTPException(500) check before calling this function.
The issue is that _check_user_info_v2_access is a standalone helper function. If it is ever called from a context that does not first check prisma_client, a DB-unavailable condition would silently produce a None return indistinguishable from a legitimate "access denied" result.
Consider raising an exception here to make the failure loud:
if prisma_client is None:
raise HTTPException(
status_code=500,
detail=CommonProxyErrors.db_not_connected_error.value,
)beb521b
into
litellm_internal_dev_03_12_2026
…int-v2-24cc [Feature] User Info V2 Endpoint
Relevant issues
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/test_litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewCI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Type
🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test
Changes
This PR introduces a new
/v2/user/infoendpoint and migrates relevant UI flows to use it, addressing the stability and memory issues caused by the existing/user/info"god endpoint".Key Changes:
GET /v2/user/infoEndpoint:LiteLLM_UserTableobject, excluding keys and team objects, to reduce data transfer and memory footprint.user_idor passing their own).404 Not Foundstatus for unauthorized access or non-existent users, instead of403 Forbidden, to prevent user enumeration.UserInfoV2ResponsePydantic model, route registration, and route check.user_get_info_v2method to the Python client.useCurrentUserhook now utilizesuserGetInfoV2for self-lookup.user_dashboard.tsxis updated to fetch spend data from/v2/user/info; keys are now sourced from the dedicateduseKeyshook, removing redundancy.user_info_view.tsxis refactored to fetch core user details from/v2/user/info, with team details fetched separately viateamInfoCall./v2/user/infoendpoint, covering various access scenarios and response types.useCurrentUser,user_dashboard, anduser_info_viewto reflect the new data fetching patterns.The original
/user/infoendpoint remains untouched for backward compatibility. Organization admin access was intentionally skipped as per requirements.