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 { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import type { DailyData, SpendMetrics } from "@/components/UsagePage/types";
|
import type { DailyData, SpendMetrics } from "@/components/UsagePage/types";
|
||||||
|
|
||||||
const mockUsePaginatedDailyActivity = vi.fn();
|
const mockUsePaginatedDailyActivity = vi.fn();
|
||||||
const mockUseQuery = vi.fn();
|
const mockUseQuery = vi.fn();
|
||||||
|
const mockUiSpendLogsCall = vi.fn();
|
||||||
|
|
||||||
vi.mock("@tanstack/react-query", () => ({
|
vi.mock("@tanstack/react-query", () => ({
|
||||||
useQuery: (args: unknown) => mockUseQuery(args),
|
useQuery: (args: unknown) => mockUseQuery(args),
|
||||||
|
|
@ -17,6 +18,12 @@ vi.mock("@/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity", (
|
||||||
vi.mock("@/components/networking", () => ({
|
vi.mock("@/components/networking", () => ({
|
||||||
userDailyActivityCall: vi.fn(),
|
userDailyActivityCall: vi.fn(),
|
||||||
getCostOptimizationUsageLogs: 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", () => ({
|
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[]) => {
|
const renderWith = (results: DailyData[]) => {
|
||||||
mockUsePaginatedDailyActivity.mockReturnValue({ data: { results }, loading: false, isFetchingMore: false });
|
mockUsePaginatedDailyActivity.mockReturnValue({ data: { results }, loading: false, isFetchingMore: false });
|
||||||
mockUseQuery.mockReturnValue({
|
mockUseQuery.mockImplementation((args: { queryKey: string[] }) =>
|
||||||
data: {
|
args.queryKey[0] === "cost-optimization-usage-logs"
|
||||||
logs: [
|
? {
|
||||||
{
|
data: {
|
||||||
request_id: "req-123456789",
|
logs: [
|
||||||
timestamp: "2026-07-13T12:00:00Z",
|
{
|
||||||
model: "test-model",
|
request_id: "req-123456789",
|
||||||
total_tokens: 150,
|
timestamp: "2026-07-13T12:00:00Z",
|
||||||
optimization_type: "both",
|
model: "test-model",
|
||||||
spend: 0.02,
|
total_tokens: 150,
|
||||||
savings: 0.14,
|
optimization_type: "both",
|
||||||
original_cost: 0.16,
|
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" />);
|
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.0200")).toBeInTheDocument();
|
||||||
expect(getByText("$0.1400")).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 React, { useMemo, useState } from "react";
|
||||||
import { PiggyBank } from "lucide-react";
|
import { PiggyBank } from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
import { AreaChart, DonutChart } from "@/components/shared/charts";
|
import { AreaChart, DonutChart } from "@/components/shared/charts";
|
||||||
import AdvancedDatePicker from "@/components/shared/advanced_date_picker";
|
import AdvancedDatePicker from "@/components/shared/advanced_date_picker";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
getCostOptimizationUsageLogs,
|
getCostOptimizationUsageLogs,
|
||||||
|
uiSpendLogsCall,
|
||||||
type OptimizedRequestLog,
|
type OptimizedRequestLog,
|
||||||
type OptimizedRequestLogsResponse,
|
type OptimizedRequestLogsResponse,
|
||||||
userDailyActivityCall,
|
userDailyActivityCall,
|
||||||
|
|
@ -17,6 +19,8 @@ import { DailyData, SpendMetrics } from "@/components/UsagePage/types";
|
||||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||||
import { all_admin_roles } from "@/utils/roles";
|
import { all_admin_roles } from "@/utils/roles";
|
||||||
import { usePaginatedDailyActivity } from "@/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity";
|
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 {
|
interface CostOptimizationViewProps {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
|
|
@ -81,11 +85,13 @@ const OptimizedRequestsTable = ({
|
||||||
logsPage,
|
logsPage,
|
||||||
onPrevious,
|
onPrevious,
|
||||||
onNext,
|
onNext,
|
||||||
|
onRowClick,
|
||||||
}: {
|
}: {
|
||||||
data: OptimizedRequestLogsResponse;
|
data: OptimizedRequestLogsResponse;
|
||||||
logsPage: number;
|
logsPage: number;
|
||||||
onPrevious: () => void;
|
onPrevious: () => void;
|
||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
|
onRowClick: (log: OptimizedRequestLog) => void;
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
|
@ -104,7 +110,19 @@ const OptimizedRequestsTable = ({
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.logs.map((log) => (
|
{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}>
|
<td className="max-w-40 truncate px-3 py-3 font-mono text-xs" title={log.request_id}>
|
||||||
{log.request_id}
|
{log.request_id}
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -183,6 +201,8 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
|
||||||
const [logsPage, setLogsPage] = useState(1);
|
const [logsPage, setLogsPage] = useState(1);
|
||||||
const startDate = startTime ? startTime.toISOString().slice(0, 10) : "";
|
const startDate = startTime ? startTime.toISOString().slice(0, 10) : "";
|
||||||
const endDate = endTime ? endTime.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 {
|
const {
|
||||||
data: optimizedLogsData,
|
data: optimizedLogsData,
|
||||||
isLoading: optimizedLogsLoading,
|
isLoading: optimizedLogsLoading,
|
||||||
|
|
@ -206,6 +226,29 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
|
||||||
optimizedLogsError,
|
optimizedLogsError,
|
||||||
optimizedLogsData,
|
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(
|
const overTime = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -313,6 +356,10 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
|
||||||
logsPage={logsPage}
|
logsPage={logsPage}
|
||||||
onPrevious={() => setLogsPage((page) => Math.max(1, page - 1))}
|
onPrevious={() => setLogsPage((page) => Math.max(1, page - 1))}
|
||||||
onNext={() => setLogsPage((page) => Math.min(optimizedLogsData.total_pages, page + 1))}
|
onNext={() => setLogsPage((page) => Math.min(optimizedLogsData.total_pages, page + 1))}
|
||||||
|
onRowClick={(log) => {
|
||||||
|
setSelectedRequestId(log.request_id);
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!optimizedLogsLoading && !optimizedLogsError && !optimizedLogsData?.logs.length && (
|
{!optimizedLogsLoading && !optimizedLogsError && !optimizedLogsData?.logs.length && (
|
||||||
|
|
@ -320,6 +367,17 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<LogDetailsDrawer
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
setSelectedRequestId(null);
|
||||||
|
}}
|
||||||
|
logEntry={selectedLog}
|
||||||
|
accessToken={accessToken}
|
||||||
|
allLogs={selectedLog ? [selectedLog] : []}
|
||||||
|
startTime={drawerStartTime}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue