Skip to content

[Feature] UI - Usage: Allow Filtering by User#21351

Merged
yuneng-jiang merged 3 commits intomainfrom
litellm_ui_usage_filter_by_user
Feb 17, 2026
Merged

[Feature] UI - Usage: Allow Filtering by User#21351
yuneng-jiang merged 3 commits intomainfrom
litellm_ui_usage_filter_by_user

Conversation

@yuneng-jiang
Copy link
Copy Markdown
Contributor

Relevant issues

Pre-Submission checklist

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

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

✅ Test
🆕 New Feature

Changes

The global Usage page previously showed aggregate spend across all users with no way to drill down by user. This PR adds a user_id query parameter to both /user/daily/activity and /user/daily/activity/aggregated endpoints so admins can filter spend data by a specific user. Non-admin users are now required to provide their own user_id and are forbidden from viewing other users' data or the global view.

On the UI side, admins see a searchable user selector dropdown (with debounced search and infinite-scroll pagination via a new useInfiniteUsers hook) next to the "Project Spend" header in the global usage tab. Selecting a user filters the spend charts to that user; clearing the selection returns to the global view.

Backend:

  • Added user_id optional query param to get_user_daily_activity and get_user_daily_activity_aggregated
  • Admins: user_id=None gives global view, providing a value filters by that user
  • Non-admins: must provide their own user_id; viewing other users or omitting it returns 403

Frontend:

  • New useInfiniteUsers hook for paginated user list fetching
  • Added Select dropdown in global usage view for user filtering (admin-only)
  • userDailyActivityCall and userDailyActivityAggregatedCall now accept and forward userId

Tests:

  • Backend: tests for non-admin permission enforcement and admin global view on both endpoints
  • Frontend: tests for useInfiniteUsers hook (pagination, search, auth gating) and UsagePageView component (user selector rendering, filtering behavior)

Screenshots

image image image

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 17, 2026 1:38am

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 17, 2026

Greptile Summary

This PR adds user-level filtering to the Usage page. Backend endpoints (/user/daily/activity and /user/daily/activity/aggregated) gain a user_id query parameter with admin/non-admin authorization logic. The frontend adds an admin-only searchable user selector dropdown with debounced search and infinite-scroll pagination via a new useInfiniteUsers hook.

  • Backend authorization bug: Both endpoints raise 403 HTTPException inside a try block whose except Exception handler catches it and re-wraps it as a 500 Internal Server Error. Non-admin users will never see the intended 403 response. This needs an except HTTPException: raise clause before the generic handler.
  • Test gap: The backend test acknowledges the 403-to-500 issue in a comment but doesn't assert the HTTP status code, so the bug passes silently.
  • Frontend: The useInfiniteUsers hook, user selector UI, and networking changes are well-implemented. The effectiveUserId is correctly computed at call time (not initialization time), handling async auth loading properly.
  • Test coverage: Both frontend and backend tests are mock-only (no real network calls), which is appropriate for the tests/test_litellm/ directory.

Confidence Score: 2/5

  • The 403-to-500 error wrapping bug means non-admin authorization is functionally broken — the intended access control will not return the correct HTTP status to clients.
  • The core authorization logic (403 for non-admin access) is correct in intent but broken in practice due to the broad exception handler swallowing HTTPExceptions and re-raising them as 500 errors. The frontend and hook code are solid, but the backend bug undermines the feature's access control guarantees.
  • litellm/proxy/management_endpoints/internal_user_endpoints.py — both get_user_daily_activity (line 1998) and get_user_daily_activity_aggregated (line 2096) need except HTTPException: raise before the generic except Exception handler.

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/internal_user_endpoints.py Adds user_id query param to both daily activity endpoints with admin/non-admin authorization logic. However, the new 403 HTTPExceptions are caught by the outer except Exception handler and re-raised as 500 errors, masking the intended authorization response.
tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py Adds tests for non-admin permission enforcement and admin global view. Tests are mock-only (no real network calls). However, they don't assert the HTTP status code, and even acknowledge the 403-to-500 bug in a comment without treating it as a failure.
ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useUsers.ts New useInfiniteUsers hook using @tanstack/react-query infinite query. Clean implementation with proper admin-role gating and pagination support.
ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useUsers.test.ts Comprehensive mock-only tests for the useInfiniteUsers hook covering pagination, search, auth gating, error handling, and all admin roles. No real network calls.
ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx Adds admin-only user selector dropdown with debounced search and infinite-scroll pagination. Correctly computes effectiveUserId at call time for non-admin users. Properly passes userId through to API calls.
ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx Adds extensive tests for user selector rendering, non-admin behavior, aggregated endpoint fallback, deduplication, model view toggles, and more. All mocked, no real network calls.
ui/litellm-dashboard/src/components/networking.tsx Adds userId parameter to userDailyActivityCall and userDailyActivityAggregatedCall. Correctly uses extraQueryParams for the paginated endpoint and direct queryParams.append for the aggregated endpoint, both properly handling null values.

Sequence Diagram

sequenceDiagram
    participant UI as Admin Dashboard
    participant Hook as useInfiniteUsers
    participant Net as networking.tsx
    participant API as /user/daily/activity
    participant Auth as _user_has_admin_view
    participant DB as get_daily_activity

    UI->>Hook: Select user from dropdown
    Hook->>Net: userListCall(page, search)
    Net-->>Hook: UserListResponse (paginated)
    Hook-->>UI: user options

    UI->>Net: userDailyActivityAggregatedCall(dates, userId)
    Net->>API: GET /user/daily/activity/aggregated
    API->>Auth: check admin status
    Auth-->>API: true (admin)
    API->>DB: get_daily_activity_aggregated(entity_id)
    DB-->>API: SpendAnalyticsPaginatedResponse
    API-->>Net: 200 with data
    Net-->>UI: render filtered charts

    Note over UI,API: Non-admin flow
    UI->>Net: userDailyActivityAggregatedCall(dates, ownId)
    Net->>API: GET /user/daily/activity/aggregated
    API->>Auth: check admin status
    Auth-->>API: false (non-admin)
    API->>API: Validate user_id matches caller
    API->>DB: get_daily_activity_aggregated(entity_id)
    DB-->>API: SpendAnalyticsPaginatedResponse
    API-->>Net: 200 with data
Loading

Last reviewed commit: 0d2aac6

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 17, 2026

Additional Comments (2)

litellm/proxy/management_endpoints/internal_user_endpoints.py
403 errors are swallowed and returned as 500

The new HTTPException with a 403 status code raised at lines 1968-1980 is inside a try block whose except Exception as e: handler catches all exceptions — including HTTPException — and re-wraps them as a 500 Internal Server Error. This means non-admin users will never see a 403 Forbidden; they'll receive a 500 with the original message buried in the detail string.

The same issue exists in the aggregated endpoint at lines 2096-2103.

To fix, add an except HTTPException: raise clause before the generic handler:

    except HTTPException:
        raise
    except Exception as e:
        verbose_proxy_logger.exception(
            "/spend/daily/analytics: Exception occured - {}".format(str(e))
        )
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail={"error": f"Failed to fetch analytics: {str(e)}"},
        )

litellm/proxy/management_endpoints/internal_user_endpoints.py
Same 403-swallowed-as-500 bug in aggregated endpoint

Same issue as the paginated endpoint above: the new 403 HTTPException is caught by except Exception and re-raised as a 500. Add except HTTPException: raise before the generic handler here as well.

    except HTTPException:
        raise
    except Exception as e:
        verbose_proxy_logger.exception(
            "/user/daily/activity/aggregated: Exception occured - {}".format(str(e))
        )
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail={"error": f"Failed to fetch analytics: {str(e)}"},
        )

- Fix HTTPException swallowed by broad except block in get_user_daily_activity
  and get_user_daily_activity_aggregated: re-raise HTTPException before the
  generic handler so 403 status codes propagate correctly
- Add status_code assertions in non-admin access tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yuneng-jiang
Copy link
Copy Markdown
Contributor Author

@greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 17, 2026

Greptile Summary

This PR adds user-level filtering to the global Usage page. On the backend, both /user/daily/activity and /user/daily/activity/aggregated endpoints now accept an optional user_id query parameter. Admins can filter by any user or omit it for the global view; non-admins are restricted to their own data. On the frontend, admins see a searchable user selector dropdown (with debounced search and infinite-scroll pagination via a new useInfiniteUsers hook) in the global usage tab.

  • Breaking change: Non-admin users calling these endpoints without user_id now receive a 403, whereas previously they automatically got their own data. This could break existing API consumers.
  • State initialization concern: selectedUserId in UsagePageView uses useState with values derived from async auth state, which may not be settled on first render. This is partially mitigated by the effectiveUserId logic in the fetch callback.
  • The except HTTPException: raise pattern was correctly added to both endpoints to prevent 403s from being swallowed by the generic except Exception handler.
  • Test coverage is thorough with mock-only tests for both backend and frontend, though a backend test for the non-admin happy path (providing their own user_id and getting data) is missing.

Confidence Score: 3/5

  • This PR introduces a backward-incompatible change for non-admin API consumers that should be addressed before merging.
  • The code is well-structured with good test coverage, but the breaking change for non-admin users (403 when omitting user_id, where previously they got their own data automatically) is a significant regression risk for existing API consumers. The frontend state initialization issue is lower severity but could cause subtle bugs.
  • Pay close attention to litellm/proxy/management_endpoints/internal_user_endpoints.py — the non-admin authorization logic introduces a breaking API change. Also review UsagePageView.tsx for the selectedUserId initialization timing issue.

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/internal_user_endpoints.py Adds user_id query param to both daily activity endpoints with admin/non-admin authorization logic. Introduces a backward-incompatible change: non-admins now get 403 when omitting user_id, whereas before they automatically got their own data.
tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py Adds tests for non-admin permission enforcement and admin global view. Tests are mock-only (no real network calls) and properly verify the 403 responses and correct forwarding of parameters. Missing a test for the non-admin happy path (providing own user_id).
ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useUsers.ts New useInfiniteUsers hook using useInfiniteQuery with proper pagination, admin-only gating, and search support. Clean implementation following existing codebase patterns.
ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useUsers.test.ts Thorough test suite covering pagination, search, auth gating, admin role variations, and error handling. All tests are mock-only with no real network calls.
ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx Adds admin-only user selector with debounced search and infinite scroll. The selectedUserId state initialization may be incorrect on first render due to async auth state, though it's partially mitigated by the effectiveUserId logic in the fetch callback.
ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx Extensive test additions covering admin user selector rendering, option formatting, non-admin behavior, aggregated endpoint fallback, pagination, and various UI features. All tests are properly mocked.
ui/litellm-dashboard/src/components/networking.tsx Adds userId parameter to userDailyActivityCall and userDailyActivityAggregatedCall. Both correctly handle null by skipping the query param. Consistent with existing patterns in the file.

Sequence Diagram

sequenceDiagram
    participant UI as UsagePageView
    participant Hook as useInfiniteUsers
    participant Net as networking.tsx
    participant API as Backend Endpoints

    Note over UI: Admin selects a user from dropdown
    UI->>Hook: Fetch paginated user list (debounced search)
    Hook->>Net: userListCall(accessToken, page, pageSize, searchEmail)
    Net->>API: GET /user/list
    API-->>Net: UserListResponse (paginated)
    Net-->>Hook: users[]
    Hook-->>UI: userOptions for Select dropdown

    Note over UI: Admin picks user or clears selection
    UI->>Net: userDailyActivityAggregatedCall(token, start, end, userId)
    Net->>API: GET /user/daily/activity/aggregated?user_id=...
    API->>API: Check is_admin via _user_has_admin_view()
    alt Admin with user_id
        API->>API: entity_id = user_id (filter by user)
    else Admin without user_id
        API->>API: entity_id = None (global view)
    else Non-admin with own user_id
        API->>API: entity_id = user_id
    else Non-admin without user_id or wrong user_id
        API-->>Net: 403 Forbidden
    end
    API-->>Net: SpendAnalyticsPaginatedResponse
    Net-->>UI: Render spend charts
Loading

Last reviewed commit: 310cca1

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

- Default user_id to caller's own ID for non-admins instead of 403 when
  omitted, preserving backward compatibility for API consumers
- Apply same fix to aggregated endpoint
- Update test to verify defaulting behavior instead of expecting 403
- Add useEffect to sync selectedUserId when auth state settles in
  UsagePageView to handle async auth initialization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yuneng-jiang
Copy link
Copy Markdown
Contributor Author

@greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 17, 2026

Greptile Summary

This PR adds user-level filtering to the global Usage page. On the backend, both /user/daily/activity and /user/daily/activity/aggregated endpoints gain an optional user_id query parameter. Admins can filter by any user or omit the parameter for the global view; non-admins default to their own user_id when omitted and receive 403 when attempting to view other users' data. The except HTTPException: raise pattern correctly prevents the generic except Exception handler from swallowing 403 responses.

On the frontend, a new useInfiniteUsers hook provides paginated user fetching with search, and the UsagePageView component adds an admin-only searchable dropdown (using antd Select) with debounced search and infinite scroll pagination. Non-admin users automatically have their user_id set via a useEffect that syncs after auth state settles.

  • Backend permission logic is well-structured and backward-compatible (non-admins default to own user_id when omitted)
  • Frontend correctly separates admin/non-admin behavior with effectiveUserId in the fetch callback
  • New useInfiniteUsers hook is clean and properly gated behind admin role check
  • UsagePageView.test.tsx is missing a mock for @tanstack/react-pacer/debouncer, which is mocked in similar test files and could cause test reliability issues
  • Good test coverage across both backend and frontend, with all tests properly mocked (no real network calls)

Confidence Score: 4/5

  • This PR is safe to merge with minor test mock issue to address
  • The backend changes are well-structured with proper permission enforcement and backward compatibility (non-admins default to own user_id). The frontend adds the feature cleanly using antd Select (not deprecated Tremor). The only concern is a missing test mock for @tanstack/react-pacer/debouncer in UsagePageView.test.tsx which could affect test reliability.
  • ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx — missing mock for @tanstack/react-pacer/debouncer

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/internal_user_endpoints.py Adds user_id query param to both /user/daily/activity and /user/daily/activity/aggregated. Admin logic is correct (None = global, value = filter). Non-admin logic correctly defaults to own user_id when omitted and raises 403 for other users. except HTTPException: raise properly added to prevent 403 being swallowed by the generic handler.
tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py Good test coverage: tests non-admin 403 for viewing other users, non-admin default to own user_id, and admin global view on aggregated endpoint. Tests use mocks properly and don't make real network calls. Missing newline at end of file.
ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useUsers.ts Clean infinite query hook for paginated user list. Properly gates query behind admin role check and accessToken presence. Uses react-query's useInfiniteQuery with correct pagination logic.
ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useUsers.test.ts Thorough test suite covering pagination, search, auth gating for all admin roles, error handling, and edge cases (empty search string). All mocked, no real network calls.
ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx Adds admin-only user selector dropdown with debounced search and infinite scroll. Uses antd Select (not deprecated Tremor). Correctly syncs selectedUserId for non-admins via useEffect. effectiveUserId logic properly separates admin/non-admin behavior.
ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx Extensive new tests for user selector, non-admin behavior, fallback pagination, and other UI features. Missing mock for @tanstack/react-pacer/debouncer which is used by the component but mocked in similar test files (PaginatedModelSelect).
ui/litellm-dashboard/src/components/networking.tsx Minimal changes: adds optional userId parameter to userDailyActivityCall and userDailyActivityAggregatedCall. Null values are properly skipped by existing appendDailyActivityQueryParam logic and the explicit if (userId) guard.

Sequence Diagram

sequenceDiagram
    participant UI as UsagePageView
    participant Hook as useInfiniteUsers
    participant Net as networking.tsx
    participant API as Backend Endpoints

    Note over UI: Admin selects user from dropdown
    UI->>Hook: useInfiniteUsers(50, searchEmail)
    Hook->>Net: userListCall(accessToken, null, page, pageSize, searchEmail)
    Net->>API: GET /user/list?page=1&page_size=50
    API-->>Net: UserListResponse
    Net-->>Hook: paginated users
    Hook-->>UI: user options for dropdown

    Note over UI: Admin picks a user (or clears for global)
    UI->>UI: setSelectedUserId(userId | null)
    UI->>Net: userDailyActivityAggregatedCall(token, start, end, userId)
    Net->>API: GET /user/daily/activity/aggregated?user_id=xxx
    API->>API: _user_has_admin_view() → true
    API->>API: entity_id = user_id (or null for global)
    API-->>Net: SpendAnalyticsPaginatedResponse
    Net-->>UI: spend data

    Note over UI: Non-admin user (no dropdown shown)
    UI->>UI: effectiveUserId = userID (from auth)
    UI->>Net: userDailyActivityAggregatedCall(token, start, end, userID)
    Net->>API: GET /user/daily/activity/aggregated?user_id=own-id
    API->>API: _user_has_admin_view() → false
    API->>API: Verify user_id matches caller
    API-->>Net: SpendAnalyticsPaginatedResponse
    Net-->>UI: own spend data
Loading

Last reviewed commit: df5e8d0

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 17, 2026

Additional Comments (1)

ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx
Missing mock for @tanstack/react-pacer/debouncer

UsagePageView.tsx imports useDebouncedState from @tanstack/react-pacer/debouncer, but this test file doesn't mock it. The similar PaginatedModelSelect.test.tsx explicitly mocks this dependency (see here). Without the mock, the test may behave unpredictably in the jsdom environment depending on how the module resolves.

Add a mock similar to PaginatedModelSelect.test.tsx:

vi.mock("@tanstack/react-pacer/debouncer", async () => {
  const React = await vi.importActual<typeof import("react")>("react");
  return {
    useDebouncedState: (initial: string) => {
      const [value, setValue] = React.useState(initial);
      return [value, setValue];
    },
  };
});

@yuneng-jiang yuneng-jiang merged commit 100a5a1 into main Feb 17, 2026
62 of 81 checks passed
@yuneng-jiang yuneng-jiang deleted the litellm_ui_usage_filter_by_user branch February 19, 2026 18:57
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.

1 participant