feat(chat-ui): add personal Logs view scoped to the current user - #33829
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
Greptile SummaryThis PR adds a "Logs" page to the pre-v0 chat UI that shows the signed-in user their own request logs, reusing the existing
Confidence Score: 4/5Safe to merge with minor follow-ups; the two issues in LogsPanel are polish-level and do not affect data correctness or security. The detail-dialog query key uses a moment()-derived startDate that re-computes on every render, so any state update while the dialog is open can produce a new key and trigger an extra network round-trip. Separately, fetch failures in the main logs query silently fall through to the empty-state UI, giving users no signal that something went wrong. Both are visible UX issues but neither corrupts data or breaks routing. ui/litellm-dashboard/src/components/chat/LogsPanel.tsx — detail query key and missing error branch
|
| Filename | Overview |
|---|---|
| ui/litellm-dashboard/src/components/chat/LogsPanel.tsx | New component for user-scoped logs. Has two issues: the detail-query key includes a volatile moment()-derived startDate that can trigger unnecessary refetches when the dialog is open, and there is no error-state branch in renderBody() — fetch failures silently render as the empty state. |
| ui/litellm-dashboard/src/app/chat/logs/page.tsx | Thin page wrapper that reads accessToken/userId from ChatShellContext and renders LogsPanel — follows the same pattern as the existing usage page, no issues. |
| ui/litellm-dashboard/src/components/chat/ChatShell.tsx | Adds logs route and NavItem for Logs between API Keys and Usage. Clean, follows existing sidebar-nav pattern. |
| ui/litellm-dashboard/src/components/chat/LogsPanel.test.tsx | New test file covering scoping assertion, row rendering, empty state, and lazy detail-load on row click — good coverage of the key behaviors. |
| ui/litellm-dashboard/src/components/chat/ChatShell.test.tsx | Adds tests for Logs nav-item click routing and active-state marking — appropriate additions that follow existing test patterns. |
| ui/litellm-dashboard/src/components/chat/ChatShell.serverRootPath.test.ts | Single assertion added to verify server_root_path is prepended to the logs route — correct and consistent with the existing test structure. |
Reviews (1): Last reviewed commit: "feat(chat-ui): add personal Logs view sc..." | Re-trigger Greptile
| const { data: detailData, isLoading: isDetailLoading } = useQuery({ | ||
| queryKey: [LOGS_QUERY_KEY, "detail", accessToken, selectedLog?.request_id, startDate], | ||
| queryFn: () => uiSpendLogDetailsCall(accessToken, selectedLog!.request_id, startDate), | ||
| enabled: !!accessToken && !!selectedLog, | ||
| }); |
There was a problem hiding this comment.
The detail query key includes
startDate, which is re-derived from moment() on every render. When the dialog is open and any state update causes a re-render (e.g., the main logs query resolving), startDate can advance by a second, producing a new query key and triggering a redundant re-fetch of the detail payload. Using selectedLog?.startTime instead gives a stable, per-log anchor that doesn't drift — and is semantically more correct as a hint to the backend about where to find the record.
| const { data: detailData, isLoading: isDetailLoading } = useQuery({ | |
| queryKey: [LOGS_QUERY_KEY, "detail", accessToken, selectedLog?.request_id, startDate], | |
| queryFn: () => uiSpendLogDetailsCall(accessToken, selectedLog!.request_id, startDate), | |
| enabled: !!accessToken && !!selectedLog, | |
| }); | |
| const { data: detailData, isLoading: isDetailLoading } = useQuery({ | |
| queryKey: [LOGS_QUERY_KEY, "detail", accessToken, selectedLog?.request_id], | |
| queryFn: () => uiSpendLogDetailsCall(accessToken, selectedLog!.request_id, selectedLog!.startTime), | |
| enabled: !!accessToken && !!selectedLog, | |
| }); |
| enabled: !!accessToken && !!userId, | ||
| placeholderData: keepPreviousData, | ||
| }; | ||
| const { data, isLoading } = useQuery(logsQueryOptions); |
There was a problem hiding this comment.
When the logs query fails,
isLoading is false and rows is [] (because data is undefined), so renderBody falls through to LogsEmpty and shows "No logs for this period" to the user. A network error or a 4xx/5xx response is indistinguishable from a genuinely empty result set — add a branch for the isError case.
| const { data, isLoading } = useQuery(logsQueryOptions); | |
| const { data, isLoading, isError } = useQuery(logsQueryOptions); |
| <> | ||
| <LogsTable rows={rows} onRowClick={setSelectedLog} /> | ||
| <div className="mt-3 flex items-center justify-between"> | ||
| <p className="m-0 text-xs text-muted-foreground"> | ||
| {total.toLocaleString()} request{total === 1 ? "" : "s"} | ||
| {totalPages > 1 ? ` · Page ${page} of ${totalPages}` : ""} | ||
| </p> | ||
| {totalPages > 1 && ( | ||
| <div className="flex gap-1"> |
There was a problem hiding this comment.
…detail Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…error state) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| return moment().subtract(30, "days"); | ||
| } | ||
|
|
||
| interface LogRow { |
There was a problem hiding this comment.
how big of a lift would this be to have this use shared type from schema.d.ts so that we dont have to write the type by hand here
There was a problem hiding this comment.
Good call. Small lift for the pragmatic version, done in follow-up #33858: it drops the hand-rolled LogRow and reuses the shared LogEntry type that the main Spend Logs UI already defines in view_logs/columns.tsx, so the row shape lives in one place.
On pulling straight from schema.d.ts: components["schemas"]["LiteLLM_SpendLogs"] exists, but it doesn't fully fit this table today. It omits status, request_duration_ms, and custom_llm_provider (which the endpoint returns and the table renders), and it types the /spend/logs/ui response as a bare LiteLLM_SpendLogs[] rather than the paginated { data, total, page, page_size, total_pages } wrapper the endpoint actually returns. That mismatch is why the main log UI keeps its own LogEntry. Making schema.d.ts the single source of truth would additionally need the backend Pydantic response model fixed (add the missing fields, model the pagination wrapper) and types regenerated, which is a larger, separate change I'm happy to pick up if you'd like it.
Relevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
UI-only change, verified end-to-end against a live proxy + dashboard hitting real Anthropic APIs (chat UI enabled with
enable_chat_ui: true). Sent live chats, opened the Logs sidebar item, confirmed the table is scoped to only the signed-in user (other users' logs excluded), toggled 24h / 7d / 30d, paged through results, and opened the row details dialogSteps to reproduce by hand:
store_prompts_in_spend_logsis enabled, same as the main Spend Logs page)Type
🆕 New Feature
Changes
Adds a "Logs" section to the pre-v0 chat UI that shows the signed-in user their own request logs, mirroring the spend-logs table but scoped to the caller so nobody else's logs are visible.
The panel reuses the existing
/spend/logs/uiendpoint, always passinguser_idequal to the current user. That endpoint already enforces non-admin scoping server-side (a non-admin caller is constrained touser = self, optionally OR'd with teams they administer), and for an admin the explicituser_idfilter narrows the result to just their own rows, so the "only my logs" guarantee does not depend on the client. No backend changes were needed.New
LogsPanelfetches a page at a time with TanStack Query:Wiring:
ChatShell.getChatRoutes()gains alogsroute and a sidebar nav item (ScrollTexticon) following the documented sidebar-nav patternsrc/app/chat/logs/page.tsxrendersLogsPanelwithaccessToken/userIdfromChatShellContext, matching the existingusagerouteuiSpendLogDetailsCall(request_id)to show the request/response payloadDetails dialog renders
details.proxy_server_request ?? details.messagesfor the Request section, matching the main Spend Logs drawer (the backend stores the prompt inproxy_server_requestfor standard chat completions and leavesmessagesas{}). As with the main page, both payloads only populate whenstore_prompts_in_spend_logsis enabled on the proxy.Review feedback addressed:
startTimeinstead of amoment()-derived range start, so an open dialog no longer refetches when the value drifts by a second on re-renderTests:
LogsPanel.test.tsxasserts the query is scoped to the currentuser_id, that rows and the empty state render, that the detail call fires only on row click, that the request payload falls back toproxy_server_request, and that a failed query renders the error state rather than the empty state;ChatShelltests cover the new route and active state.Final Attestation
Link to Devin session: https://app.devin.ai/sessions/d8f28b85a07e42fba149e7e51169bf0a
Requested by: @krrish-berri-2