diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.test.tsx
new file mode 100644
index 000000000000..17d14cd7fac8
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.test.tsx
@@ -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(
+ ,
+ );
+
+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);
+ }
+ });
+
+ 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);
+
+ expect(compactTicks(requestsCard).length).toBeGreaterThan(0);
+ expect(compactTicks(tokensCard)).toContain("60K");
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx
index 944983da0e14..246f358a0f41 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_dashboard.tsx
@@ -1,5 +1,4 @@
import {
- BarChart,
Card,
Col,
DateRangePickerValue,
@@ -7,7 +6,6 @@ import {
Icon,
MultiSelect,
MultiSelectItem,
- Subtitle,
Tab,
TabGroup,
TabList,
@@ -18,6 +16,8 @@ import {
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";
@@ -61,13 +61,13 @@ interface cacheDataItem {
// 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;
@@ -348,29 +348,41 @@ const CacheDashboard: React.FC = ({ accessToken, token, userRole
- Cache Hits vs API Requests
-
-
- Cached Completion Tokens vs Generated Completion Tokens
-
+
+
+ Cache Hits vs API Requests
+
+
+
+
+
+
+
+
+
+ Cached Completion Tokens vs Generated Completion Tokens
+
+
+
+
+
+