refactor(ui): give each Caching tab its own route

Split the Caching page's four tabs (Cache Analytics, Cache Health, Cache
Settings, Coordination Redis) into their own prerendered paths under
/caching, mirroring the Models + Endpoints per-tab routing. A shared
layout renders the tab bar and derives the active tab from the pathname;
each tab is its own page.tsx that owns only its state, so deep links and
hard-loads to /caching/health, /caching/settings and
/caching/coordination-redis resolve to real static HTML with no nginx
change.

The former 430-line cache_dashboard god-component is gone. Its analytics
panel moved into CacheAnalyticsTab, which now derives the chart data and
summary stats with useMemo during render instead of writing them back to
state inside an effect, and the health-check state moved into the health
route. The tab bar is rebuilt on the shadcn Tabs primitive rather than
adding new antd/tremor usage.

The old tab-bar refresh control only reset a "Last Refreshed" timestamp
without refetching, so it is dropped.
This commit is contained in:
ryan-crabbe-berri 2026-07-23 11:44:32 -07:00
parent 7aaaa055b7
commit 8d80f8cba9
12 changed files with 537 additions and 472 deletions

View file

@ -157,21 +157,9 @@
"count": 2
}
},
"src/app/(dashboard)/caching/_components/cache_dashboard.tsx": {
"local/filename-pascal-case": {
"count": 1
},
"src/app/(dashboard)/caching/_components/CacheAnalyticsTab.tsx": {
"no-restricted-imports": {
"count": 1
},
"prefer-const": {
"count": 3
},
"react-hooks/purity": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/app/(dashboard)/caching/_components/cache_health.tsx": {
@ -4529,4 +4517,4 @@
"count": 1
}
}
}
}

View file

@ -2,16 +2,14 @@ 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";
import CacheAnalyticsTab from "./CacheAnalyticsTab";
const { adminGlobalCacheActivity, cachingHealthCheckCall } = vi.hoisted(() => ({
const { adminGlobalCacheActivity } = vi.hoisted(() => ({
adminGlobalCacheActivity: vi.fn(),
cachingHealthCheckCall: vi.fn(),
}));
vi.mock("@/components/networking", () => ({
adminGlobalCacheActivity,
cachingHealthCheckCall,
}));
const cacheActivity = [
@ -35,10 +33,7 @@ const cacheActivity = [
},
];
const renderDashboard = () =>
renderWithProviders(
<CacheDashboard accessToken="sk-test" token="tok" userRole="Admin" userID="u1" premiumUser={false} />,
);
const renderAnalytics = () => renderWithProviders(<CacheAnalyticsTab accessToken="sk-test" />);
const findChartCards = async () => {
await screen.findByText("Cache Hits vs API Requests");
@ -63,21 +58,21 @@ const legendFillByCategory = (card: HTMLElement) =>
]),
);
describe("CacheDashboard cache analytics charts", () => {
describe("CacheAnalyticsTab charts", () => {
beforeEach(() => {
vi.clearAllMocks();
adminGlobalCacheActivity.mockResolvedValue(cacheActivity);
});
it("renders both chart card titles", async () => {
renderDashboard();
renderAnalytics();
expect(await screen.findByText("Cache Hits vs API Requests")).toBeInTheDocument();
expect(screen.getByText("Cached Completion Tokens vs Generated Completion Tokens")).toBeInTheDocument();
});
it("scopes the analytics tab to the response cache, not provider prompt caching", async () => {
renderDashboard();
renderAnalytics();
expect(await screen.findByText(/is not shown here/)).toBeInTheDocument();
expect(screen.getByRole("link", { name: "response cache" })).toHaveAttribute(
@ -93,7 +88,7 @@ describe("CacheDashboard cache analytics charts", () => {
});
it("renders the requests chart with each category legend-bound to its fill and stacked in order", async () => {
renderDashboard();
renderAnalytics();
const { requestsCard } = await findChartCards();
expect(legendFillByCategory(requestsCard)).toEqual({
@ -104,7 +99,7 @@ describe("CacheDashboard cache analytics charts", () => {
});
it("renders the tokens chart with each category legend-bound to its fill and stacked in order", async () => {
renderDashboard();
renderAnalytics();
const { tokensCard } = await findChartCards();
expect(legendFillByCategory(tokensCard)).toEqual({
@ -115,7 +110,7 @@ describe("CacheDashboard cache analytics charts", () => {
});
it("indexes bars by call_type name on the x axis", async () => {
renderDashboard();
renderAnalytics();
const { requestsCard, tokensCard } = await findChartCards();
for (const card of [requestsCard, tokensCard]) {
@ -125,7 +120,7 @@ describe("CacheDashboard cache analytics charts", () => {
});
it("stacks the two categories into one column per call_type", async () => {
renderDashboard();
renderAnalytics();
const { requestsCard, tokensCard } = await findChartCards();
for (const card of [requestsCard, tokensCard]) {
@ -137,7 +132,7 @@ describe("CacheDashboard cache analytics charts", () => {
});
it("formats y-axis ticks with compact notation", async () => {
renderDashboard();
renderAnalytics();
const { requestsCard, tokensCard } = await findChartCards();
const compactTicks = (card: HTMLElement) =>

View file

@ -0,0 +1,258 @@
import { Card, Col, DateRangePickerValue, Grid, MultiSelect, MultiSelectItem, Text } from "@tremor/react";
import React, { useEffect, useMemo, useState } from "react";
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 { adminGlobalCacheActivity } from "@/components/networking";
const formatDateWithoutTZ = (date: Date | undefined) => {
if (!date) return undefined;
return date.toISOString().split("T")[0];
};
function valueFormatterNumbers(number: number) {
const formatter = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0,
notation: "compact",
compactDisplay: "short",
});
return formatter.format(number);
}
interface cacheDataItem {
api_key: string;
model: string;
cache_hit_true_rows: number;
cached_completion_tokens: number;
total_rows: number;
generated_completion_tokens: number;
call_type: string;
}
type uiData = {
name: string;
"LLM API requests": number;
"Cache hit": number;
"Cached Completion Tokens": number;
"Generated Completion Tokens": number;
};
const aggregateCacheData = (rows: cacheDataItem[]) => {
const chartData = rows.reduce<uiData[]>((acc, item) => {
const name = item.call_type || "Unknown";
const apiRequests = (item.total_rows || 0) - (item.cache_hit_true_rows || 0);
const existing = acc.find((row) => row.name === name);
if (existing) {
existing["LLM API requests"] += apiRequests;
existing["Cache hit"] += item.cache_hit_true_rows || 0;
existing["Cached Completion Tokens"] += item.cached_completion_tokens || 0;
existing["Generated Completion Tokens"] += item.generated_completion_tokens || 0;
return acc;
}
return [
...acc,
{
name,
"LLM API requests": apiRequests,
"Cache hit": item.cache_hit_true_rows || 0,
"Cached Completion Tokens": item.cached_completion_tokens || 0,
"Generated Completion Tokens": item.generated_completion_tokens || 0,
},
];
}, []);
const totals = rows.reduce(
(acc, item) => ({
cacheHits: acc.cacheHits + (item.cache_hit_true_rows || 0),
cachedTokens: acc.cachedTokens + (item.cached_completion_tokens || 0),
apiRequests: acc.apiRequests + ((item.total_rows || 0) - (item.cache_hit_true_rows || 0)),
}),
{ cacheHits: 0, cachedTokens: 0, apiRequests: 0 },
);
const allRequests = totals.cacheHits + totals.apiRequests;
const cacheHitRatio = allRequests > 0 ? ((totals.cacheHits / allRequests) * 100).toFixed(2) : "0";
return {
chartData,
cacheHitRatio,
cachedResponses: valueFormatterNumbers(totals.cacheHits),
cachedTokens: valueFormatterNumbers(totals.cachedTokens),
};
};
const CacheAnalyticsTab: React.FC<{ accessToken: string | null }> = ({ accessToken }) => {
const [selectedApiKeys, setSelectedApiKeys] = useState<string[]>([]);
const [selectedModels, setSelectedModels] = useState<string[]>([]);
const [data, setData] = useState<cacheDataItem[]>([]);
const [dateValue, setDateValue] = useState<DateRangePickerValue>(() => {
const now = new Date();
return { from: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), to: now };
});
useEffect(() => {
if (!accessToken || !dateValue) {
return;
}
const fetchData = async () => {
const response = await adminGlobalCacheActivity(
accessToken,
formatDateWithoutTZ(dateValue.from),
formatDateWithoutTZ(dateValue.to),
);
setData(response);
};
fetchData();
}, [accessToken]);
const uniqueApiKeys = Array.from(new Set(data.map((item) => item?.api_key ?? "")));
const uniqueModels = Array.from(new Set(data.map((item) => item?.model ?? "")));
const updateCachingData = async (startTime: Date | undefined, endTime: Date | undefined) => {
if (!startTime || !endTime || !accessToken) {
return;
}
const newCacheData = await adminGlobalCacheActivity(
accessToken,
formatDateWithoutTZ(startTime),
formatDateWithoutTZ(endTime),
);
setData(newCacheData);
};
const { chartData, cacheHitRatio, cachedResponses, cachedTokens } = useMemo(() => {
const matchesFilters = (item: cacheDataItem) => {
const keyMatch = selectedApiKeys.length === 0 || selectedApiKeys.includes(item.api_key);
const modelMatch = selectedModels.length === 0 || selectedModels.includes(item.model);
return keyMatch && modelMatch;
};
return aggregateCacheData(data.filter(matchesFilters));
}, [data, selectedApiKeys, selectedModels]);
return (
<Card>
<Text className="text-tremor-content dark:text-dark-tremor-content">
Analytics for LiteLLM&apos;s{" "}
<a href="https://docs.litellm.ai/docs/proxy/caching" target="_blank" rel="noreferrer" className="underline">
response cache
</a>{" "}
(e.g. Redis / in-memory): requests answered from cache without calling the LLM provider. Provider-side{" "}
<a
href="https://docs.litellm.ai/docs/completion/prompt_caching"
target="_blank"
rel="noreferrer"
className="underline"
>
prompt caching
</a>{" "}
(cached input tokens from Anthropic, OpenAI, etc.) is not shown here; see &quot;Prompt Caching Metrics&quot; on
the Usage page or individual requests in the Logs page.
</Text>
<Grid numItems={3} className="gap-4 mt-4">
<Col>
<MultiSelect placeholder="Select Virtual Keys" value={selectedApiKeys} onValueChange={setSelectedApiKeys}>
{uniqueApiKeys.map((key) => (
<MultiSelectItem key={key} value={key}>
{key}
</MultiSelectItem>
))}
</MultiSelect>
</Col>
<Col>
<MultiSelect placeholder="Select Models" value={selectedModels} onValueChange={setSelectedModels}>
{uniqueModels.map((model) => (
<MultiSelectItem key={model} value={model}>
{model}
</MultiSelectItem>
))}
</MultiSelect>
</Col>
<Col>
<UsageDatePicker
value={dateValue}
onValueChange={(value) => {
setDateValue(value);
updateCachingData(value.from, value.to);
}}
/>
</Col>
</Grid>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mt-4">
<Card>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Cache Hit Ratio
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
{cacheHitRatio}%
</p>
</div>
</Card>
<Card>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Cache Hits
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
{cachedResponses}
</p>
</div>
</Card>
<Card>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Cached Completion Tokens
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
{cachedTokens}
</p>
</div>
</Card>
</div>
<ChartCard className="mt-4">
<CardHeader>
<CardTitle className="text-base font-semibold">Cache Hits vs API Requests</CardTitle>
</CardHeader>
<CardContent>
<BarChart
data={chartData}
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={chartData}
stack={true}
index="name"
valueFormatter={valueFormatterNumbers}
categories={["Generated Completion Tokens", "Cached Completion Tokens"]}
colors={["sky", "teal"]}
yAxisWidth={48}
/>
</CardContent>
</ChartCard>
</Card>
);
};
export default CacheAnalyticsTab;

View file

@ -1,430 +0,0 @@
import {
Card,
Col,
DateRangePickerValue,
Grid,
Icon,
MultiSelect,
MultiSelectItem,
Tab,
TabGroup,
TabList,
TabPanel,
TabPanels,
Text,
} from "@tremor/react";
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";
// Import the new component
import { CacheHealthTab } from "./cache_health";
import CacheSettings from "./cache_settings";
import CoordinationRedisSettings from "./coordination_redis_settings";
const formatDateWithoutTZ = (date: Date | undefined) => {
if (!date) return undefined;
return date.toISOString().split("T")[0];
};
function valueFormatterNumbers(number: number) {
const formatter = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0,
notation: "compact",
compactDisplay: "short",
});
return formatter.format(number);
}
interface CachePageProps {
accessToken: string | null;
token: string | null;
userRole: string | null;
userID: string | null;
premiumUser: boolean;
}
interface cacheDataItem {
api_key: string;
model: string;
cache_hit_true_rows: number;
cached_completion_tokens: number;
total_rows: number;
generated_completion_tokens: number;
call_type: string;
// Add other properties as needed
}
type uiData = {
name: string;
"LLM API requests": number;
"Cache hit": number;
"Cached Completion Tokens": number;
"Generated Completion Tokens": number;
};
interface CacheHealthResponse {
status?: string;
cache_type?: string;
ping_response?: boolean;
set_cache_response?: string;
litellm_cache_params?: string;
error?: {
message: string;
type: string;
param: string;
code: string;
};
}
// Helper function to deep-parse a JSON string if possible
const deepParse = (input: any) => {
let parsed = input;
if (typeof parsed === "string") {
try {
parsed = JSON.parse(parsed);
} catch {
return parsed;
}
}
return parsed;
};
const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole, userID, premiumUser }) => {
const [filteredData, setFilteredData] = useState<uiData[]>([]);
const [selectedApiKeys, setSelectedApiKeys] = useState<string[]>([]);
const [selectedModels, setSelectedModels] = useState<string[]>([]);
const [data, setData] = useState<cacheDataItem[]>([]);
const [cachedResponses, setCachedResponses] = useState("0");
const [cachedTokens, setCachedTokens] = useState("0");
const [cacheHitRatio, setCacheHitRatio] = useState("0");
const [dateValue, setDateValue] = useState<DateRangePickerValue>({
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
to: new Date(),
});
const [lastRefreshed, setLastRefreshed] = useState("");
const [healthCheckResponse, setHealthCheckResponse] = useState<any>("");
useEffect(() => {
if (!accessToken || !dateValue) {
return;
}
const fetchData = async () => {
const response = await adminGlobalCacheActivity(
accessToken,
formatDateWithoutTZ(dateValue.from),
formatDateWithoutTZ(dateValue.to),
);
setData(response);
};
fetchData();
const currentDate = new Date();
setLastRefreshed(currentDate.toLocaleString());
}, [accessToken]);
const uniqueApiKeys = Array.from(new Set(data.map((item) => item?.api_key ?? "")));
const uniqueModels = Array.from(new Set(data.map((item) => item?.model ?? "")));
const uniqueCallTypes = Array.from(new Set(data.map((item) => item?.call_type ?? "")));
const updateCachingData = async (startTime: Date | undefined, endTime: Date | undefined) => {
if (!startTime || !endTime || !accessToken) {
return;
}
let new_cache_data = await adminGlobalCacheActivity(
accessToken,
formatDateWithoutTZ(startTime),
formatDateWithoutTZ(endTime),
);
setData(new_cache_data);
};
useEffect(() => {
let newData: cacheDataItem[] = data;
if (selectedApiKeys.length > 0) {
newData = newData.filter((item) => selectedApiKeys.includes(item.api_key));
}
if (selectedModels.length > 0) {
newData = newData.filter((item) => selectedModels.includes(item.model));
}
/*
Data looks like this
[{"api_key":"sk-test-mock-key-001","call_type":"acompletion","model":"llama3-8b-8192","total_rows":13,"cache_hit_true_rows":0},
{"api_key":"sk-test-mock-key-002","call_type":"None","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0},
{"api_key":"sk-test-mock-key-123","call_type":"acompletion","model":"gpt-3.5-turbo","total_rows":19,"cache_hit_true_rows":0},
{"api_key":"sk-test-mock-key-123","call_type":"aimage_generation","model":"","total_rows":3,"cache_hit_true_rows":0},
{"api_key":"sk-test-mock-key-003","call_type":"None","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0},
{"api_key":"sk-test-mock-key-004","call_type":"","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0},
{"api_key":"sk-test-mock-key-005","call_type":"","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0},
*/
// What data we need for bar chat
// ui_data = [
// {
// name: "Call Type",
// Cache hit: 20,
// LLM API requests: 10,
// }
// ]
let llm_api_requests = 0;
let cache_hits = 0;
let cached_tokens = 0;
const processedData = newData.reduce((acc: uiData[], item) => {
if (!item.call_type) {
item.call_type = "Unknown";
}
llm_api_requests += (item.total_rows || 0) - (item.cache_hit_true_rows || 0);
cache_hits += item.cache_hit_true_rows || 0;
cached_tokens += item.cached_completion_tokens || 0;
const existingItem = acc.find((i) => i.name === item.call_type);
if (existingItem) {
existingItem["LLM API requests"] += (item.total_rows || 0) - (item.cache_hit_true_rows || 0);
existingItem["Cache hit"] += item.cache_hit_true_rows || 0;
existingItem["Cached Completion Tokens"] += item.cached_completion_tokens || 0;
existingItem["Generated Completion Tokens"] += item.generated_completion_tokens || 0;
} else {
acc.push({
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,
"Cached Completion Tokens": item.cached_completion_tokens || 0,
"Generated Completion Tokens": item.generated_completion_tokens || 0,
});
}
return acc;
}, []);
// set header cache statistics
setCachedResponses(valueFormatterNumbers(cache_hits));
setCachedTokens(valueFormatterNumbers(cached_tokens));
let allRequests = cache_hits + llm_api_requests;
if (allRequests > 0) {
let cache_hit_ratio = ((cache_hits / allRequests) * 100).toFixed(2);
setCacheHitRatio(cache_hit_ratio);
} else {
setCacheHitRatio("0");
}
setFilteredData(processedData);
}, [selectedApiKeys, selectedModels, dateValue, data]);
const handleRefreshClick = () => {
// Update the 'lastRefreshed' state to the current date and time
const currentDate = new Date();
setLastRefreshed(currentDate.toLocaleString());
};
const runCachingHealthCheck = async () => {
try {
NotificationsManager.info("Running cache health check...");
setHealthCheckResponse("");
const response = await cachingHealthCheckCall(accessToken !== null ? accessToken : "");
setHealthCheckResponse(response);
} catch (error: any) {
console.error("Error running health check:", error);
let errorData;
if (error && error.message) {
try {
// Parse the error message which may contain a nested error layer.
let parsedData = JSON.parse(error.message);
// If the parsed object is wrapped (e.g. { error: { ... } }), unwrap it.
if (parsedData.error) {
parsedData = parsedData.error;
}
errorData = parsedData;
} catch (e) {
errorData = { message: error.message };
}
} else {
errorData = { message: "Unknown error occurred" };
}
setHealthCheckResponse({ error: errorData });
}
};
return (
<TabGroup className="gap-2 p-8 h-full w-full mt-2 mb-8">
<TabList className="flex justify-between mt-2 w-full items-center">
<div className="flex">
<Tab>Cache Analytics</Tab>
<Tab>Cache Health</Tab>
<Tab>Cache Settings</Tab>
<Tab>Coordination Redis</Tab>
</div>
<div className="flex items-center space-x-2">
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
<Icon
icon={RefreshIcon} // Modify as necessary for correct icon name
variant="shadow"
size="xs"
className="self-center"
onClick={handleRefreshClick}
/>
</div>
</TabList>
<TabPanels>
<TabPanel>
<Card>
<Text className="text-tremor-content dark:text-dark-tremor-content">
Analytics for LiteLLM&apos;s{" "}
<a
href="https://docs.litellm.ai/docs/proxy/caching"
target="_blank"
rel="noreferrer"
className="underline"
>
response cache
</a>{" "}
(e.g. Redis / in-memory): requests answered from cache without calling the LLM provider. Provider-side{" "}
<a
href="https://docs.litellm.ai/docs/completion/prompt_caching"
target="_blank"
rel="noreferrer"
className="underline"
>
prompt caching
</a>{" "}
(cached input tokens from Anthropic, OpenAI, etc.) is not shown here; see &quot;Prompt Caching
Metrics&quot; on the Usage page or individual requests in the Logs page.
</Text>
<Grid numItems={3} className="gap-4 mt-4">
<Col>
<MultiSelect
placeholder="Select Virtual Keys"
value={selectedApiKeys}
onValueChange={setSelectedApiKeys}
>
{uniqueApiKeys.map((key) => (
<MultiSelectItem key={key} value={key}>
{key}
</MultiSelectItem>
))}
</MultiSelect>
</Col>
<Col>
<MultiSelect placeholder="Select Models" value={selectedModels} onValueChange={setSelectedModels}>
{uniqueModels.map((model) => (
<MultiSelectItem key={model} value={model}>
{model}
</MultiSelectItem>
))}
</MultiSelect>
</Col>
<Col>
<UsageDatePicker
value={dateValue}
onValueChange={(value) => {
setDateValue(value);
updateCachingData(value.from, value.to);
}}
/>
</Col>
</Grid>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mt-4">
<Card>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Cache Hit Ratio
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
{cacheHitRatio}%
</p>
</div>
</Card>
<Card>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Cache Hits
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
{cachedResponses}
</p>
</div>
</Card>
<Card>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Cached Completion Tokens
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
{cachedTokens}
</p>
</div>
</Card>
</div>
<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>
<CacheHealthTab
accessToken={accessToken}
healthCheckResponse={healthCheckResponse}
runCachingHealthCheck={runCachingHealthCheck}
/>
</TabPanel>
<TabPanel>
<CacheSettings accessToken={accessToken} userRole={userRole} userID={userID} />
</TabPanel>
<TabPanel>
<CoordinationRedisSettings />
</TabPanel>
</TabPanels>
</TabGroup>
);
};
export default CacheDashboard;

View file

@ -0,0 +1,7 @@
"use client";
import CoordinationRedisSettings from "@/app/(dashboard)/caching/_components/coordination_redis_settings";
export default function CoordinationRedisPage() {
return <CoordinationRedisSettings />;
}

View file

@ -0,0 +1,62 @@
"use client";
import { useState } from "react";
import { CacheHealthTab } from "@/app/(dashboard)/caching/_components/cache_health";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { cachingHealthCheckCall } from "@/components/networking";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
interface CacheHealthResponse {
status?: string;
cache_type?: string;
ping_response?: boolean;
set_cache_response?: string;
litellm_cache_params?: string;
error?: { message?: string; type?: string; param?: string; code?: string };
}
const parseHealthCheckError = (message: string): CacheHealthResponse["error"] => {
try {
const parsed: unknown = JSON.parse(message);
if (parsed && typeof parsed === "object") {
const outer = parsed as Record<string, unknown>;
const inner = outer.error;
const source = inner && typeof inner === "object" ? (inner as Record<string, unknown>) : outer;
const read = (field: string) => (typeof source[field] === "string" ? (source[field] as string) : undefined);
return {
message: read("message") ?? message,
type: read("type"),
param: read("param"),
code: read("code"),
};
}
} catch {
// message is not JSON; fall through to the raw string
}
return { message };
};
export default function CacheHealthPage() {
const { accessToken } = useAuthorized();
const [healthCheckResponse, setHealthCheckResponse] = useState<CacheHealthResponse | string>("");
const runCachingHealthCheck = async () => {
try {
NotificationsManager.info("Running cache health check...");
setHealthCheckResponse("");
const response = await cachingHealthCheckCall(accessToken ?? "");
setHealthCheckResponse(response);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error occurred";
setHealthCheckResponse({ error: parseHealthCheckError(message) });
}
};
return (
<CacheHealthTab
accessToken={accessToken}
healthCheckResponse={healthCheckResponse}
runCachingHealthCheck={runCachingHealthCheck}
/>
);
}

View file

@ -0,0 +1,76 @@
/* @vitest-environment jsdom */
import { act, render } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import CachingLayout from "./layout";
const { mockPush, navState } = vi.hoisted(() => ({
mockPush: vi.fn(),
navState: { pathname: "/caching" },
}));
vi.mock("next/navigation", () => ({
usePathname: () => navState.pathname,
useRouter: () => ({ push: mockPush }),
}));
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
const renderLayout = () =>
render(
<CachingLayout>
<div data-testid="tab-content">CHILD</div>
</CachingLayout>,
);
describe("CachingLayout", () => {
beforeEach(() => {
navState.pathname = "/caching";
mockPush.mockClear();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(global as any).ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
});
it("renders the tab bar and the active tab's page content", () => {
const { getByRole, getByTestId } = renderLayout();
expect(getByRole("tab", { name: "Cache Analytics" })).toBeInTheDocument();
expect(getByRole("tab", { name: "Cache Health" })).toBeInTheDocument();
expect(getByRole("tab", { name: "Cache Settings" })).toBeInTheDocument();
expect(getByRole("tab", { name: "Coordination Redis" })).toBeInTheDocument();
expect(getByTestId("tab-content")).toHaveTextContent("CHILD");
});
it("navigates to a tab's path when its tab is clicked", async () => {
const { getByRole } = renderLayout();
await act(async () => {
getByRole("tab", { name: "Cache Health" }).click();
});
expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/caching\/health\/$/));
});
it("routes the base tab back to the caching root (no slug)", async () => {
navState.pathname = "/caching/health";
const { getByRole } = renderLayout();
await act(async () => {
getByRole("tab", { name: "Cache Analytics" }).click();
});
expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/caching\/$/));
});
it("redirects to the base caching path when the tab slug is unknown", async () => {
const replaceMock = vi.fn();
const originalLocation = window.location;
Object.defineProperty(window, "location", {
configurable: true,
value: { replace: replaceMock, assign: vi.fn(), href: "http://localhost/", pathname: "/", search: "" },
});
navState.pathname = "/caching/bogus";
await act(async () => {
renderLayout();
});
expect(replaceMock).toHaveBeenCalledWith(expect.stringMatching(/\/caching\/$/));
Object.defineProperty(window, "location", { configurable: true, value: originalLocation });
});
});

View file

@ -0,0 +1,49 @@
"use client";
import type { ReactNode } from "react";
import { useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cacheTabHref, slugFromPathname, type CacheTabSlug } from "@/app/(dashboard)/caching/tabRoutes";
const BASE_TAB_KEY = "cache-analytics";
const ORDERED_KEYS: Array<"" | CacheTabSlug> = ["", "health", "settings", "coordination-redis"];
const TAB_LABELS: Record<"" | CacheTabSlug, string> = {
"": "Cache Analytics",
health: "Cache Health",
settings: "Cache Settings",
"coordination-redis": "Coordination Redis",
};
export default function CachingLayout({ children }: { children: ReactNode }) {
const pathname = usePathname();
const router = useRouter();
const activeSlug = slugFromPathname(pathname);
const isKnownSlug = ORDERED_KEYS.some((slug) => slug === activeSlug);
const activeKey = isKnownSlug ? activeSlug || BASE_TAB_KEY : BASE_TAB_KEY;
useEffect(() => {
if (activeSlug !== "" && !isKnownSlug) {
window.location.replace(cacheTabHref(""));
}
}, [activeSlug, isKnownSlug]);
return (
<div className="p-8 w-full mt-2 mb-8">
<Tabs value={activeKey} onValueChange={(key) => router.push(cacheTabHref(key === BASE_TAB_KEY ? "" : key))}>
<TabsList variant="line">
{ORDERED_KEYS.map((slug) => (
<TabsTrigger key={slug || BASE_TAB_KEY} value={slug || BASE_TAB_KEY}>
{TAB_LABELS[slug]}
</TabsTrigger>
))}
</TabsList>
</Tabs>
<div className="mt-4">{children}</div>
</div>
);
}

View file

@ -1,17 +1,9 @@
"use client";
import CacheDashboard from "./_components/cache_dashboard";
import CacheAnalyticsTab from "./_components/CacheAnalyticsTab";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function Caching() {
const { accessToken, userRole, userId, token, premiumUser } = useAuthorized();
return (
<CacheDashboard
userID={userId}
userRole={userRole}
token={token}
accessToken={accessToken}
premiumUser={premiumUser}
/>
);
export default function CachingPage() {
const { accessToken } = useAuthorized();
return <CacheAnalyticsTab accessToken={accessToken} />;
}

View file

@ -0,0 +1,9 @@
"use client";
import CacheSettings from "@/app/(dashboard)/caching/_components/cache_settings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function CacheSettingsPage() {
const { accessToken, userRole, userId } = useAuthorized();
return <CacheSettings accessToken={accessToken} userRole={userRole} userID={userId} />;
}

View file

@ -0,0 +1,38 @@
/* @vitest-environment jsdom */
import { describe, expect, it, vi } from "vitest";
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
import { CACHE_TAB_SLUGS, cacheTabHref, slugFromPathname } from "./tabRoutes";
describe("slugFromPathname", () => {
it("returns empty string for the base path with or without a trailing slash", () => {
expect(slugFromPathname("/caching")).toBe("");
expect(slugFromPathname("/caching/")).toBe("");
});
it("extracts the tab slug from dev and proxy-mounted (/ui) paths", () => {
expect(slugFromPathname("/caching/health")).toBe("health");
expect(slugFromPathname("/ui/caching/coordination-redis/")).toBe("coordination-redis");
});
it("returns the raw segment for an unknown tab so the layout can redirect to base", () => {
expect(slugFromPathname("/ui/caching/bogus")).toBe("bogus");
});
it("returns empty string when the caching base segment is not in the path", () => {
expect(slugFromPathname("/teams")).toBe("");
});
});
describe("cacheTabHref", () => {
it("builds the trailing-slash base href for the empty slug", () => {
expect(cacheTabHref("")).toBe("/ui/caching/");
});
it("builds a trailing-slash href for every tab slug (required by static export)", () => {
for (const slug of CACHE_TAB_SLUGS) {
expect(cacheTabHref(slug)).toBe(`/ui/caching/${slug}/`);
}
});
});

View file

@ -0,0 +1,21 @@
import { migratedHref } from "@/utils/migratedPages";
export const CACHING_BASE_SEGMENT = "caching";
export const CACHE_TAB_SLUGS = ["health", "settings", "coordination-redis"] as const;
export type CacheTabSlug = (typeof CACHE_TAB_SLUGS)[number];
export function cacheTabHref(slug: string): string {
const base = migratedHref(CACHING_BASE_SEGMENT);
return slug ? `${base}/${slug}/` : `${base}/`;
}
export function slugFromPathname(pathname: string): string {
const parts = pathname.split("/").filter(Boolean);
const idx = parts.indexOf(CACHING_BASE_SEGMENT);
if (idx === -1) {
return "";
}
return parts[idx + 1] ?? "";
}