mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
feat(cost-optimization): open request details from optimized requests
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
cd67a86f96
commit
efc19be8d5
2 changed files with 140 additions and 24 deletions
|
|
@ -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 ? <div data-testid="log-details-drawer">{logEntry.request_id}</div> : null,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shared/advanced_date_picker", () => ({
|
||||
|
|
@ -61,31 +68,63 @@ const day = (date: string, metrics: Partial<SpendMetrics>): 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(<CostOptimizationView accessToken="test-token" userId="u1" userRole="proxy_admin" />);
|
||||
};
|
||||
|
||||
|
|
@ -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<unknown> })
|
||||
.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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}) => (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
|
|
@ -104,7 +110,19 @@ const OptimizedRequestsTable = ({
|
|||
</thead>
|
||||
<tbody>
|
||||
{data.logs.map((log) => (
|
||||
<tr key={log.request_id} className="border-b last:border-0">
|
||||
<tr
|
||||
key={log.request_id}
|
||||
className="cursor-pointer border-b transition-colors hover:bg-muted/50 last:border-0"
|
||||
onClick={() => onRowClick(log)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
onRowClick(log);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<td className="max-w-40 truncate px-3 py-3 font-mono text-xs" title={log.request_id}>
|
||||
{log.request_id}
|
||||
</td>
|
||||
|
|
@ -183,6 +201,8 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ 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<string | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const {
|
||||
data: optimizedLogsData,
|
||||
isLoading: optimizedLogsLoading,
|
||||
|
|
@ -206,6 +226,29 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ 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<CostOptimizationViewProps> = ({ 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<CostOptimizationViewProps> = ({ accessToken
|
|||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<LogDetailsDrawer
|
||||
open={drawerOpen}
|
||||
onClose={() => {
|
||||
setDrawerOpen(false);
|
||||
setSelectedRequestId(null);
|
||||
}}
|
||||
logEntry={selectedLog}
|
||||
accessToken={accessToken}
|
||||
allLogs={selectedLog ? [selectedLog] : []}
|
||||
startTime={drawerStartTime}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue