diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx index 8a2ca8e5a05..7820e19cd60 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx @@ -1,10 +1,11 @@ -import { render } from "@testing-library/react"; +import { fireEvent, render } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import type { DailyData, SpendMetrics } from "@/components/UsagePage/types"; const mockUsePaginatedDailyActivity = vi.fn(); const mockUseQuery = vi.fn(); +const mockUiSpendLogsCall = vi.fn(); vi.mock("@tanstack/react-query", () => ({ useQuery: (args: unknown) => mockUseQuery(args), @@ -17,6 +18,12 @@ vi.mock("@/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity", ( vi.mock("@/components/networking", () => ({ userDailyActivityCall: vi.fn(), getCostOptimizationUsageLogs: vi.fn(), + uiSpendLogsCall: (args: unknown) => mockUiSpendLogsCall(args), +})); + +vi.mock("@/components/view_logs/LogDetailsDrawer", () => ({ + LogDetailsDrawer: ({ open, logEntry }: { open: boolean; logEntry: { request_id: string } | null }) => + open && logEntry ?
{logEntry.request_id}
: null, })); vi.mock("@/components/shared/advanced_date_picker", () => ({ @@ -61,31 +68,63 @@ const day = (date: string, metrics: Partial): DailyData => ({ }, }); +const detailLog = { + request_id: "req-123456789", + api_key: "key", + team_id: "team", + model: "test-model", + model_id: "model-id", + call_type: "completion", + spend: 0.02, + total_tokens: 150, + prompt_tokens: 100, + completion_tokens: 50, + startTime: "2026-07-13T12:00:00Z", + endTime: "2026-07-13T12:00:01Z", + messages: [], + response: {}, + cache_hit: "", + metadata: {}, +}; + const renderWith = (results: DailyData[]) => { mockUsePaginatedDailyActivity.mockReturnValue({ data: { results }, loading: false, isFetchingMore: false }); - mockUseQuery.mockReturnValue({ - data: { - logs: [ - { - request_id: "req-123456789", - timestamp: "2026-07-13T12:00:00Z", - model: "test-model", - total_tokens: 150, - optimization_type: "both", - spend: 0.02, - savings: 0.14, - original_cost: 0.16, + mockUseQuery.mockImplementation((args: { queryKey: string[] }) => + args.queryKey[0] === "cost-optimization-usage-logs" + ? { + data: { + logs: [ + { + request_id: "req-123456789", + timestamp: "2026-07-13T12:00:00Z", + model: "test-model", + total_tokens: 150, + optimization_type: "both", + spend: 0.02, + savings: 0.14, + original_cost: 0.16, + compression_savings_spend: 0.1, + prompt_caching_savings_spend: 0.04, + tokens_saved: 100, + cache_read_tokens: 50, + }, + ], + total: 1, + page: 1, + page_size: 50, + total_pages: 1, + }, + isLoading: false, + isFetching: false, + error: null, + } + : { + data: { data: [detailLog], total: 1 }, + isLoading: false, + isFetching: false, + error: null, }, - ], - total: 1, - page: 1, - page_size: 50, - total_pages: 1, - }, - isLoading: false, - isFetching: false, - error: null, - }); + ); return render(); }; @@ -146,4 +185,23 @@ describe("CostOptimizationView", () => { expect(getByText("$0.0200")).toBeInTheDocument(); expect(getByText("$0.1400")).toBeInTheDocument(); }); + + it("fetches and opens request details when an optimized request is clicked", async () => { + const { getByText, getByTestId } = renderWith([day("2026-07-12", { compression_savings_spend: 0.04 })]); + + fireEvent.click(getByText("req-123456789")); + + const detailQuery = mockUseQuery.mock.calls + .map(([args]) => args as { queryKey: string[]; queryFn: () => Promise }) + .find((args) => args.queryKey[0] === "cost-optimization-spend-log"); + expect(detailQuery).toBeDefined(); + await detailQuery?.queryFn(); + expect(mockUiSpendLogsCall).toHaveBeenCalledWith( + expect.objectContaining({ + accessToken: "test-token", + params: { request_id: "req-123456789" }, + }), + ); + expect(getByTestId("log-details-drawer")).toHaveTextContent("req-123456789"); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx index 358113dcb2b..b4be35f2fdc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx @@ -3,12 +3,14 @@ import React, { useMemo, useState } from "react"; import { PiggyBank } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; +import moment from "moment"; import { AreaChart, DonutChart } from "@/components/shared/charts"; import AdvancedDatePicker from "@/components/shared/advanced_date_picker"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { getCostOptimizationUsageLogs, + uiSpendLogsCall, type OptimizedRequestLog, type OptimizedRequestLogsResponse, userDailyActivityCall, @@ -17,6 +19,8 @@ import { DailyData, SpendMetrics } from "@/components/UsagePage/types"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { all_admin_roles } from "@/utils/roles"; import { usePaginatedDailyActivity } from "@/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity"; +import { LogDetailsDrawer } from "@/components/view_logs/LogDetailsDrawer"; +import type { LogEntry as ViewLogsLogEntry } from "@/components/view_logs/columns"; interface CostOptimizationViewProps { accessToken: string | null; @@ -81,11 +85,13 @@ const OptimizedRequestsTable = ({ logsPage, onPrevious, onNext, + onRowClick, }: { data: OptimizedRequestLogsResponse; logsPage: number; onPrevious: () => void; onNext: () => void; + onRowClick: (log: OptimizedRequestLog) => void; }) => ( <>
@@ -104,7 +110,19 @@ const OptimizedRequestsTable = ({ {data.logs.map((log) => ( - + onRowClick(log)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onRowClick(log); + } + }} + role="button" + tabIndex={0} + > {log.request_id} @@ -183,6 +201,8 @@ const CostOptimizationView: React.FC = ({ accessToken const [logsPage, setLogsPage] = useState(1); const startDate = startTime ? startTime.toISOString().slice(0, 10) : ""; const endDate = endTime ? endTime.toISOString().slice(0, 10) : ""; + const [selectedRequestId, setSelectedRequestId] = useState(null); + const [drawerOpen, setDrawerOpen] = useState(false); const { data: optimizedLogsData, isLoading: optimizedLogsLoading, @@ -206,6 +226,29 @@ const CostOptimizationView: React.FC = ({ accessToken optimizedLogsError, optimizedLogsData, ); + const drawerStartTime = startTime + ? moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss") + : moment().subtract(24, "hours").utc().format("YYYY-MM-DD HH:mm:ss"); + const drawerEndTime = endTime + ? moment(endTime).utc().endOf("day").format("YYYY-MM-DD HH:mm:ss") + : moment().utc().format("YYYY-MM-DD HH:mm:ss"); + const { data: fullLogResponse } = useQuery({ + queryKey: ["cost-optimization-spend-log", selectedRequestId, drawerStartTime, drawerEndTime], + queryFn: async () => { + if (!accessToken || !selectedRequestId) return null; + const response = await uiSpendLogsCall({ + accessToken, + start_date: drawerStartTime, + end_date: drawerEndTime, + page: 1, + page_size: 10, + params: { request_id: selectedRequestId }, + }); + return response as { data: ViewLogsLogEntry[]; total: number }; + }, + enabled: Boolean(accessToken && selectedRequestId && drawerOpen), + }); + const selectedLog: ViewLogsLogEntry | null = fullLogResponse?.data?.[0] ?? null; const overTime = useMemo( () => @@ -313,6 +356,10 @@ const CostOptimizationView: React.FC = ({ accessToken logsPage={logsPage} onPrevious={() => setLogsPage((page) => Math.max(1, page - 1))} onNext={() => setLogsPage((page) => Math.min(optimizedLogsData.total_pages, page + 1))} + onRowClick={(log) => { + setSelectedRequestId(log.request_id); + setDrawerOpen(true); + }} /> ) : null} {!optimizedLogsLoading && !optimizedLogsError && !optimizedLogsData?.logs.length && ( @@ -320,6 +367,17 @@ const CostOptimizationView: React.FC = ({ accessToken )} + { + setDrawerOpen(false); + setSelectedRequestId(null); + }} + logEntry={selectedLog} + accessToken={accessToken} + allLogs={selectedLog ? [selectedLog] : []} + startTime={drawerStartTime} + />
); };