mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
feat(ui): optimization type filter scoping cost savings KPIs, charts, and recent requests
This commit is contained in:
parent
1eaad862f0
commit
a6474f7354
2 changed files with 272 additions and 61 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as networking from "@/components/networking";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
|
@ -16,6 +17,12 @@ function renderView() {
|
|||
);
|
||||
}
|
||||
|
||||
async function clickFilter(name: string) {
|
||||
const radio = screen.getByRole("radio", { name });
|
||||
const label = radio.closest("label");
|
||||
await userEvent.click(label ?? radio);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
if (typeof window !== "undefined" && !window.ResizeObserver) {
|
||||
window.ResizeObserver = class ResizeObserver {
|
||||
|
|
@ -86,6 +93,42 @@ const RECENT_RESPONSE: networking.RecentOptimizedRequestsResponse = {
|
|||
scanned_requests: 42,
|
||||
};
|
||||
|
||||
const MIXED_RECENT_RESPONSE: networking.RecentOptimizedRequestsResponse = {
|
||||
requests: [
|
||||
{
|
||||
request_id: "req_cache_only",
|
||||
start_time: "2026-07-15T12:00:00+00:00",
|
||||
model: "claude-x",
|
||||
total_tokens: 1000,
|
||||
optimizations: ["caching"],
|
||||
original_cost: 0.04,
|
||||
optimized_cost: 0.03,
|
||||
savings: 0.01,
|
||||
},
|
||||
{
|
||||
request_id: "req_compress_only",
|
||||
start_time: "2026-07-15T12:01:00+00:00",
|
||||
model: "claude-y",
|
||||
total_tokens: 2000,
|
||||
optimizations: ["compression"],
|
||||
original_cost: 0.08,
|
||||
optimized_cost: 0.05,
|
||||
savings: 0.03,
|
||||
},
|
||||
{
|
||||
request_id: "req_both",
|
||||
start_time: "2026-07-15T12:02:00+00:00",
|
||||
model: "claude-z",
|
||||
total_tokens: 3000,
|
||||
optimizations: ["caching", "compression"],
|
||||
original_cost: 0.12,
|
||||
optimized_cost: 0.07,
|
||||
savings: 0.05,
|
||||
},
|
||||
],
|
||||
scanned_requests: 99,
|
||||
};
|
||||
|
||||
describe("formatUsd", () => {
|
||||
it("formats zero, cents, and sub-cent values", () => {
|
||||
expect(formatUsd(0)).toBe("$0");
|
||||
|
|
@ -158,4 +201,83 @@ describe("CostSavingsView", () => {
|
|||
expect(screen.getByText(/No optimized requests in this window/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("filters the recent requests table by optimization type", async () => {
|
||||
vi.mocked(networking.costSavingsRecentRequestsCall).mockResolvedValue(MIXED_RECENT_RESPONSE);
|
||||
renderView();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("req_both")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("req_cache_only")).toBeInTheDocument();
|
||||
expect(screen.getByText("req_compress_only")).toBeInTheDocument();
|
||||
|
||||
await clickFilter("Compression");
|
||||
|
||||
expect(screen.queryByText("req_cache_only")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("req_compress_only")).toBeInTheDocument();
|
||||
expect(screen.getByText("req_both")).toBeInTheDocument();
|
||||
|
||||
await clickFilter("Caching");
|
||||
|
||||
expect(screen.getByText("req_cache_only")).toBeInTheDocument();
|
||||
expect(screen.queryByText("req_compress_only")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("req_both")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a type-specific empty state when the filter excludes every request", async () => {
|
||||
vi.mocked(networking.costSavingsRecentRequestsCall).mockResolvedValue({
|
||||
requests: [
|
||||
{
|
||||
request_id: "req_cache_only",
|
||||
start_time: "2026-07-15T12:00:00+00:00",
|
||||
model: "claude-x",
|
||||
total_tokens: 1000,
|
||||
optimizations: ["caching"],
|
||||
original_cost: 0.04,
|
||||
optimized_cost: 0.03,
|
||||
savings: 0.01,
|
||||
},
|
||||
],
|
||||
scanned_requests: 7,
|
||||
});
|
||||
renderView();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("req_cache_only")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await clickFilter("Compression");
|
||||
|
||||
expect(screen.queryByText("req_cache_only")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("No compression requests in this window.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("scopes the KPI cards and charts to the selected optimization type", async () => {
|
||||
renderView();
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("$2.00").length).toBeGreaterThan(0);
|
||||
});
|
||||
expect(screen.getByText("Savings Distribution")).toBeInTheDocument();
|
||||
|
||||
await clickFilter("Compression");
|
||||
|
||||
expect(screen.queryByText("Total Savings")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Caching Savings")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Compression Savings")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("$0.50").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("Tokens Compressed Away")).toBeInTheDocument();
|
||||
expect(screen.getByText("250")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Savings Distribution")).not.toBeInTheDocument();
|
||||
|
||||
await clickFilter("Caching");
|
||||
|
||||
expect(screen.getByText("Caching Savings")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Compression Savings")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Cached Tokens Read")).toBeInTheDocument();
|
||||
expect(screen.getByText("1,000")).toBeInTheDocument();
|
||||
|
||||
await clickFilter("All");
|
||||
|
||||
expect(screen.getByText("Total Savings")).toBeInTheDocument();
|
||||
expect(screen.getByText("Savings Distribution")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Alert, Card, Table, Tag, Typography } from "antd";
|
||||
import { Alert, Card, Segmented, Table, Tag, Typography } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { ComponentProps, useState } from "react";
|
||||
import { ComponentProps, useMemo, useState } from "react";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { MetricCard } from "@/components/GuardrailsMonitor/MetricCard";
|
||||
import AdvancedDatePicker from "@/components/shared/advanced_date_picker";
|
||||
|
|
@ -15,18 +15,32 @@ import {
|
|||
costSavingsActivityCall,
|
||||
costSavingsRecentRequestsCall,
|
||||
OptimizedRequestSummary,
|
||||
RecentOptimizedRequestsResponse,
|
||||
} from "@/components/networking";
|
||||
|
||||
type DateRangeValue = ComponentProps<typeof AdvancedDatePicker>["value"];
|
||||
|
||||
const SERIES_CATEGORIES = ["Caching", "Compression"] as const;
|
||||
const SERIES_COLORS: readonly ChartColor[] = ["emerald", "blue"];
|
||||
const SERIES = [
|
||||
{ key: "caching", name: "Caching", color: "emerald" },
|
||||
{ key: "compression", name: "Compression", color: "blue" },
|
||||
] as const;
|
||||
|
||||
const SERIES_CATEGORIES = SERIES.map((series) => series.name);
|
||||
const SERIES_COLORS: readonly ChartColor[] = SERIES.map((series) => series.color);
|
||||
|
||||
const OPTIMIZATION_TAG_COLOR: Record<CostOptimizationType, string> = {
|
||||
caching: "green",
|
||||
compression: "blue",
|
||||
};
|
||||
|
||||
type OptimizationFilter = "all" | CostOptimizationType;
|
||||
|
||||
const OPTIMIZATION_FILTER_OPTIONS: readonly { label: string; value: OptimizationFilter }[] = [
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Caching", value: "caching" },
|
||||
{ label: "Compression", value: "compression" },
|
||||
];
|
||||
|
||||
export function formatUsd(value: number): string {
|
||||
if (value === 0) return "$0";
|
||||
const sign = value < 0 ? "-" : "";
|
||||
|
|
@ -97,9 +111,41 @@ const RECENT_REQUEST_COLUMNS: ColumnsType<OptimizedRequestSummary> = [
|
|||
|
||||
interface SavingsKpiGridProps {
|
||||
totals: CostSavingsMetrics | undefined;
|
||||
filter: OptimizationFilter;
|
||||
}
|
||||
|
||||
function SavingsKpiGrid({ totals }: SavingsKpiGridProps) {
|
||||
const FILTERED_KPI_CONTENT = {
|
||||
caching: {
|
||||
savingsLabel: "Caching Savings",
|
||||
savingsColor: "text-emerald-600",
|
||||
tokensLabel: "Cached Tokens Read",
|
||||
savingsOf: (totals: CostSavingsMetrics | undefined) => totals?.cache_savings ?? 0,
|
||||
tokensOf: (totals: CostSavingsMetrics | undefined) => totals?.cache_read_input_tokens ?? 0,
|
||||
},
|
||||
compression: {
|
||||
savingsLabel: "Compression Savings",
|
||||
savingsColor: "text-blue-600",
|
||||
tokensLabel: "Tokens Compressed Away",
|
||||
savingsOf: (totals: CostSavingsMetrics | undefined) => totals?.compression_savings ?? 0,
|
||||
tokensOf: (totals: CostSavingsMetrics | undefined) => totals?.compression_saved_tokens ?? 0,
|
||||
},
|
||||
} as const;
|
||||
|
||||
function SavingsKpiGrid({ totals, filter }: SavingsKpiGridProps) {
|
||||
if (filter !== "all") {
|
||||
const content = FILTERED_KPI_CONTENT[filter];
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
<MetricCard
|
||||
label={content.savingsLabel}
|
||||
value={formatUsd(content.savingsOf(totals))}
|
||||
valueColor={content.savingsColor}
|
||||
/>
|
||||
<MetricCard label={content.tokensLabel} value={content.tokensOf(totals).toLocaleString()} />
|
||||
<MetricCard label="Total Spend" value={formatUsd(totals?.spend ?? 0)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<MetricCard label="Total Savings" value={formatUsd(totals?.total_savings ?? 0)} valueColor="text-green-600" />
|
||||
|
|
@ -120,9 +166,58 @@ function SavingsKpiGrid({ totals }: SavingsKpiGridProps) {
|
|||
);
|
||||
}
|
||||
|
||||
interface RecentOptimizedRequestsCardProps {
|
||||
data: RecentOptimizedRequestsResponse | undefined;
|
||||
isLoading: boolean;
|
||||
optimizationFilter: OptimizationFilter;
|
||||
}
|
||||
|
||||
function RecentOptimizedRequestsCard({ data, isLoading, optimizationFilter }: RecentOptimizedRequestsCardProps) {
|
||||
const requests = data?.requests;
|
||||
const filteredRequests = useMemo(() => {
|
||||
if (!requests) return [];
|
||||
if (optimizationFilter === "all") return requests;
|
||||
return requests.filter((request) => request.optimizations.includes(optimizationFilter));
|
||||
}, [requests, optimizationFilter]);
|
||||
|
||||
return (
|
||||
<Card className="border border-gray-200 rounded-lg" styles={{ body: { padding: 0 } }}>
|
||||
<div className="p-6 pb-4">
|
||||
<Typography.Title level={5} className="mb-0!">
|
||||
Recent Optimized Requests
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
Latest requests that benefited from caching or compression
|
||||
{data ? ` (scanned last ${data.scanned_requests} requests in range)` : ""}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="p-6">
|
||||
<ChartLoader />
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
columns={RECENT_REQUEST_COLUMNS}
|
||||
dataSource={filteredRequests}
|
||||
rowKey="request_id"
|
||||
pagination={false}
|
||||
size="middle"
|
||||
locale={{
|
||||
emptyText:
|
||||
optimizationFilter === "all"
|
||||
? "No optimized requests in this window. Savings appear here once prompt caching or prompt compression kicks in."
|
||||
: `No ${optimizationFilter} requests in this window.`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CostSavingsView() {
|
||||
const { accessToken } = useAuthorized();
|
||||
const [dateValue, setDateValue] = useState<DateRangeValue>(defaultDateRange);
|
||||
const [optimizationFilter, setOptimizationFilter] = useState<OptimizationFilter>("all");
|
||||
|
||||
const startTime = dateValue.from;
|
||||
const endTime = dateValue.to;
|
||||
|
|
@ -142,6 +237,9 @@ export default function CostSavingsView() {
|
|||
|
||||
const totals = activityQuery.data?.totals;
|
||||
const unpricedModels = activityQuery.data?.unpriced_models ?? [];
|
||||
const visibleSeries = SERIES.filter((series) => optimizationFilter === "all" || series.key === optimizationFilter);
|
||||
const visibleCategories = visibleSeries.map((series) => series.name);
|
||||
const visibleColors: readonly ChartColor[] = visibleSeries.map((series) => series.color);
|
||||
const chartData =
|
||||
activityQuery.data?.results.map((day) => ({
|
||||
date: day.date,
|
||||
|
|
@ -154,7 +252,6 @@ export default function CostSavingsView() {
|
|||
{ name: "Compression", value: totals.compression_savings },
|
||||
]
|
||||
: [];
|
||||
const recentRequests = recentQuery.data?.requests ?? [];
|
||||
|
||||
return (
|
||||
<div className="w-full p-8">
|
||||
|
|
@ -165,7 +262,14 @@ export default function CostSavingsView() {
|
|||
</Typography.Title>
|
||||
<Typography.Text type="secondary">Savings from prompt caching and prompt compression</Typography.Text>
|
||||
</div>
|
||||
<AdvancedDatePicker value={dateValue} onValueChange={setDateValue} label="" showTimeRange={false} />
|
||||
<div className="flex items-end gap-4">
|
||||
<Segmented<OptimizationFilter>
|
||||
options={[...OPTIMIZATION_FILTER_OPTIONS]}
|
||||
value={optimizationFilter}
|
||||
onChange={setOptimizationFilter}
|
||||
/>
|
||||
<AdvancedDatePicker value={dateValue} onValueChange={setDateValue} label="" showTimeRange={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{unpricedModels.length > 0 && (
|
||||
|
|
@ -178,10 +282,16 @@ export default function CostSavingsView() {
|
|||
/>
|
||||
)}
|
||||
|
||||
<SavingsKpiGrid totals={totals} />
|
||||
<SavingsKpiGrid totals={totals} filter={optimizationFilter} />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
|
||||
<Card className="lg:col-span-2 border border-gray-200 rounded-lg">
|
||||
<Card
|
||||
className={
|
||||
optimizationFilter === "all"
|
||||
? "lg:col-span-2 border border-gray-200 rounded-lg"
|
||||
: "lg:col-span-3 border border-gray-200 rounded-lg"
|
||||
}
|
||||
>
|
||||
<Typography.Title level={5} className="mb-0!">
|
||||
Savings Over Time
|
||||
</Typography.Title>
|
||||
|
|
@ -193,65 +303,44 @@ export default function CostSavingsView() {
|
|||
className="mt-4"
|
||||
data={chartData}
|
||||
index="date"
|
||||
categories={SERIES_CATEGORIES}
|
||||
colors={SERIES_COLORS}
|
||||
categories={visibleCategories}
|
||||
colors={visibleColors}
|
||||
valueFormatter={formatUsd}
|
||||
yAxisWidth={80}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
<Card className="border border-gray-200 rounded-lg">
|
||||
<Typography.Title level={5} className="mb-0!">
|
||||
Savings Distribution
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">By optimization type</Typography.Text>
|
||||
{activityQuery.isLoading ? (
|
||||
<ChartLoader />
|
||||
) : (
|
||||
<>
|
||||
<DonutChart
|
||||
className="mt-4 h-60"
|
||||
data={donutData}
|
||||
index="name"
|
||||
category="value"
|
||||
colors={SERIES_COLORS}
|
||||
valueFormatter={formatUsd}
|
||||
showLabel
|
||||
/>
|
||||
<CustomLegend categories={SERIES_CATEGORIES} colors={SERIES_COLORS} />
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
{optimizationFilter === "all" && (
|
||||
<Card className="border border-gray-200 rounded-lg">
|
||||
<Typography.Title level={5} className="mb-0!">
|
||||
Savings Distribution
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">By optimization type</Typography.Text>
|
||||
{activityQuery.isLoading ? (
|
||||
<ChartLoader />
|
||||
) : (
|
||||
<>
|
||||
<DonutChart
|
||||
className="mt-4 h-60"
|
||||
data={donutData}
|
||||
index="name"
|
||||
category="value"
|
||||
colors={SERIES_COLORS}
|
||||
valueFormatter={formatUsd}
|
||||
showLabel
|
||||
/>
|
||||
<CustomLegend categories={SERIES_CATEGORIES} colors={SERIES_COLORS} />
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className="border border-gray-200 rounded-lg" styles={{ body: { padding: 0 } }}>
|
||||
<div className="p-6 pb-4">
|
||||
<Typography.Title level={5} className="mb-0!">
|
||||
Recent Optimized Requests
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
Latest requests that benefited from caching or compression
|
||||
{recentQuery.data ? ` (scanned last ${recentQuery.data.scanned_requests} requests in range)` : ""}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{recentQuery.isLoading ? (
|
||||
<div className="p-6">
|
||||
<ChartLoader />
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
columns={RECENT_REQUEST_COLUMNS}
|
||||
dataSource={recentRequests}
|
||||
rowKey="request_id"
|
||||
pagination={false}
|
||||
size="middle"
|
||||
locale={{
|
||||
emptyText:
|
||||
"No optimized requests in this window. Savings appear here once prompt caching or prompt compression kicks in.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
<RecentOptimizedRequestsCard
|
||||
data={recentQuery.data}
|
||||
isLoading={recentQuery.isLoading}
|
||||
optimizationFilter={optimizationFilter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue