Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { screen, waitFor, within } from "@testing-library/react";
import { renderWithProviders } from "../../../../../tests/test-utils";
import CacheDashboard from "./cache_dashboard";

const { adminGlobalCacheActivity, cachingHealthCheckCall } = vi.hoisted(() => ({
adminGlobalCacheActivity: vi.fn(),
cachingHealthCheckCall: vi.fn(),
}));

vi.mock("@/components/networking", () => ({
adminGlobalCacheActivity,
cachingHealthCheckCall,
}));

const cacheActivity = [
{
api_key: "sk-1",
model: "gpt-5.1",
call_type: "acompletion",
total_rows: 1500,
cache_hit_true_rows: 300,
cached_completion_tokens: 12000,
generated_completion_tokens: 48000,
},
{
api_key: "sk-2",
model: "text-embedding-3-large",
call_type: "aembedding",
total_rows: 700,
cache_hit_true_rows: 100,
cached_completion_tokens: 2000,
generated_completion_tokens: 9000,
},
];

const renderDashboard = () =>
renderWithProviders(
<CacheDashboard accessToken="sk-test" token="tok" userRole="Admin" userID="u1" premiumUser={false} />,
);

const findChartCards = async () => {
await screen.findByText("Cache Hits vs API Requests");
await waitFor(() => {
expect(document.querySelectorAll("path.recharts-rectangle").length).toBeGreaterThan(0);
});
const cards = Array.from(document.querySelectorAll('[data-slot="card"]'));
expect(cards).toHaveLength(2);
return { requestsCard: cards[0] as HTMLElement, tokensCard: cards[1] as HTMLElement };
};

const barFills = (card: HTMLElement) =>
Array.from(card.querySelectorAll(".recharts-bar")).map((bar) =>
bar.querySelector("path.recharts-rectangle")?.getAttribute("fill"),
);

const legendFillByCategory = (card: HTMLElement) =>
Object.fromEntries(
Array.from(card.querySelectorAll('.recharts-legend-wrapper [style*="background-color"]')).map((swatch) => [
swatch.parentElement?.textContent,
swatch.getAttribute("style")?.match(/background-color:\s*([^;]+);?/)?.[1],
]),
);

describe("CacheDashboard cache analytics charts", () => {
beforeEach(() => {
vi.clearAllMocks();
adminGlobalCacheActivity.mockResolvedValue(cacheActivity);
});

it("renders both chart card titles", async () => {
renderDashboard();

expect(await screen.findByText("Cache Hits vs API Requests")).toBeInTheDocument();
expect(screen.getByText("Cached Completion Tokens vs Generated Completion Tokens")).toBeInTheDocument();
});

it("renders the requests chart with each category legend-bound to its fill and stacked in order", async () => {
renderDashboard();
const { requestsCard } = await findChartCards();

expect(legendFillByCategory(requestsCard)).toEqual({
"LLM API requests": "var(--color-sky-500, #0ea5e9)",
"Cache hit": "var(--color-teal-500, #14b8a6)",
});
expect(barFills(requestsCard)).toEqual(["var(--color-sky-500, #0ea5e9)", "var(--color-teal-500, #14b8a6)"]);
});

it("renders the tokens chart with each category legend-bound to its fill and stacked in order", async () => {
renderDashboard();
const { tokensCard } = await findChartCards();

expect(legendFillByCategory(tokensCard)).toEqual({
"Generated Completion Tokens": "var(--color-sky-500, #0ea5e9)",
"Cached Completion Tokens": "var(--color-teal-500, #14b8a6)",
});
expect(barFills(tokensCard)).toEqual(["var(--color-sky-500, #0ea5e9)", "var(--color-teal-500, #14b8a6)"]);
});

it("indexes bars by call_type name on the x axis", async () => {
renderDashboard();
const { requestsCard, tokensCard } = await findChartCards();

for (const card of [requestsCard, tokensCard]) {
expect(within(card).getAllByText("acompletion").length).toBeGreaterThan(0);
expect(within(card).getAllByText("aembedding").length).toBeGreaterThan(0);
}
});

it("stacks the two categories into one column per call_type", async () => {
renderDashboard();
const { requestsCard, tokensCard } = await findChartCards();

for (const card of [requestsCard, tokensCard]) {
const rects = Array.from(card.querySelectorAll("path.recharts-rectangle"));
expect(rects).toHaveLength(4);
const xPositions = rects.map((rect) => rect.getAttribute("d")?.split(",")[0]);
expect(new Set(xPositions).size).toBe(2);
}
Comment thread
ryan-crabbe-berri marked this conversation as resolved.
});

it("formats y-axis ticks with compact notation", async () => {
renderDashboard();
const { requestsCard, tokensCard } = await findChartCards();

const compactTicks = (card: HTMLElement) =>
within(card)
.getAllByText(/^\d+(\.\d+)?K$/)
.map((tick) => tick.textContent);
Comment thread
ryan-crabbe-berri marked this conversation as resolved.

expect(compactTicks(requestsCard).length).toBeGreaterThan(0);
expect(compactTicks(tokensCard)).toContain("60K");
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import {
BarChart,
Card,
Col,
DateRangePickerValue,
Grid,
Icon,
MultiSelect,
MultiSelectItem,
Subtitle,
Tab,
TabGroup,
TabList,
Expand All @@ -18,6 +16,8 @@
import React, { useEffect, useState } from "react";
import NotificationsManager from "@/components/molecules/notifications_manager";
import UsageDatePicker from "@/components/shared/usage_date_picker";
import { BarChart } from "@/components/shared/charts";
import { Card as ChartCard, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

import { RefreshIcon } from "@heroicons/react/outline";
import { adminGlobalCacheActivity, cachingHealthCheckCall } from "@/components/networking";
Expand Down Expand Up @@ -61,13 +61,13 @@
// Add other properties as needed
}

interface uiData {
type uiData = {
name: string;
"LLM API requests": number;
"Cache hit": number;
"Cached Completion Tokens": number;
"Generated Completion Tokens": number;
}
};

interface CacheHealthResponse {
status?: string;
Expand All @@ -84,7 +84,7 @@
}

// Helper function to deep-parse a JSON string if possible
const deepParse = (input: any) => {

Check warning on line 87 in ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
let parsed = input;
if (typeof parsed === "string") {
try {
Expand All @@ -111,7 +111,7 @@
});

const [lastRefreshed, setLastRefreshed] = useState("");
const [healthCheckResponse, setHealthCheckResponse] = useState<any>("");

Check warning on line 114 in ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

useEffect(() => {
if (!accessToken || !dateValue) {
Expand All @@ -129,7 +129,7 @@

const currentDate = new Date();
setLastRefreshed(currentDate.toLocaleString());
}, [accessToken]);

Check warning on line 132 in ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

React Hook useEffect has a missing dependency: 'dateValue'. Either include it or remove the dependency array

const uniqueApiKeys = Array.from(new Set(data.map((item) => item?.api_key ?? "")));
const uniqueModels = Array.from(new Set(data.map((item) => item?.model ?? "")));
Expand Down Expand Up @@ -198,7 +198,7 @@
existingItem["Cached Completion Tokens"] += item.cached_completion_tokens || 0;
existingItem["Generated Completion Tokens"] += item.generated_completion_tokens || 0;
} else {
acc.push({

Check warning on line 201 in ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Object literal with 5 properties passed inline as an argument; assign it to a named variable first
name: item.call_type,
"LLM API requests": (item.total_rows || 0) - (item.cache_hit_true_rows || 0),
"Cache hit": item.cache_hit_true_rows || 0,
Expand Down Expand Up @@ -235,7 +235,7 @@
setHealthCheckResponse("");
const response = await cachingHealthCheckCall(accessToken !== null ? accessToken : "");
setHealthCheckResponse(response);
} catch (error: any) {

Check warning on line 238 in ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
console.error("Error running health check:", error);
let errorData;
if (error && error.message) {
Expand Down Expand Up @@ -348,29 +348,41 @@
</Card>
</div>

<Subtitle className="mt-4">Cache Hits vs API Requests</Subtitle>
<BarChart
title="Cache Hits vs API Requests"
data={filteredData}
stack={true}
index="name"
valueFormatter={valueFormatterNumbers}
categories={["LLM API requests", "Cache hit"]}
colors={["sky", "teal"]}
yAxisWidth={48}
/>

<Subtitle className="mt-4">Cached Completion Tokens vs Generated Completion Tokens</Subtitle>
<BarChart
className="mt-6"
data={filteredData}
stack={true}
index="name"
valueFormatter={valueFormatterNumbers}
categories={["Generated Completion Tokens", "Cached Completion Tokens"]}
colors={["sky", "teal"]}
yAxisWidth={48}
/>
<ChartCard className="mt-4">
<CardHeader>
<CardTitle className="text-base font-semibold">Cache Hits vs API Requests</CardTitle>
</CardHeader>
<CardContent>
<BarChart
data={filteredData}
stack={true}
index="name"
valueFormatter={valueFormatterNumbers}
categories={["LLM API requests", "Cache hit"]}
colors={["sky", "teal"]}
yAxisWidth={48}
/>
</CardContent>
</ChartCard>

<ChartCard className="mt-6">
<CardHeader>
<CardTitle className="text-base font-semibold">
Cached Completion Tokens vs Generated Completion Tokens
</CardTitle>
</CardHeader>
<CardContent>
<BarChart
data={filteredData}
stack={true}
index="name"
valueFormatter={valueFormatterNumbers}
categories={["Generated Completion Tokens", "Cached Completion Tokens"]}
colors={["sky", "teal"]}
yAxisWidth={48}
/>
</CardContent>
</ChartCard>
</Card>
</TabPanel>
<TabPanel>
Expand Down
Loading