fix(ui): clarify response cache vs provider prompt caching in logs and caching dashboard

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yuneng 2026-07-21 19:04:48 +00:00
parent 212a9213c4
commit 1224ad9b0c
7 changed files with 102 additions and 16 deletions

View file

@ -2207,7 +2207,7 @@
},
"src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx": {
"no-nested-ternary": {
"count": 4
"count": 3
}
},
"src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx": {

View file

@ -69,6 +69,13 @@ describe("CacheDashboard cache analytics charts", () => {
adminGlobalCacheActivity.mockResolvedValue(cacheActivity);
});
it("labels the dashboard as response caching and distinguishes it from prompt caching", async () => {
renderDashboard();
expect(await screen.findByRole("heading", { name: "Response Caching" })).toBeInTheDocument();
expect(screen.getByText(/separate from provider prompt caching/i)).toBeInTheDocument();
});
it("renders both chart card titles", async () => {
renderDashboard();

View file

@ -260,6 +260,16 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
return (
<TabGroup className="gap-2 p-8 h-full w-full mt-2 mb-8">
<div className="mb-2">
<h1 className="text-2xl font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
Response Caching
</h1>
<Text className="mt-1">
Analytics and settings for LiteLLM&apos;s response cache, which returns a stored response for repeated
requests instead of calling the provider again. This is separate from provider prompt caching; prompt caching
is configured under Settings and its per-request activity shows up as prompt cache tokens on the Logs page
</Text>
</div>
<TabList className="flex justify-between mt-2 w-full items-center">
<div className="flex">
<Tab>Cache Analytics</Tab>

View file

@ -244,7 +244,13 @@ const menuGroups: MenuGroup[] = [
icon: <BookOpen {...ICON} />,
external_url: "https://models.litellm.ai/cookbook",
},
{ key: "caching", page: "caching", label: "Caching", icon: <Database {...ICON} />, roles: all_admin_roles },
{
key: "caching",
page: "caching",
label: "Response Caching",
icon: <Database {...ICON} />,
roles: all_admin_roles,
},
{
key: "experimental",
page: "experimental",

View file

@ -30,7 +30,8 @@ export const pageDescriptions: Record<string, string> = {
api_ref: "Browse API documentation and endpoints",
"model-hub-table": "Explore available AI models and providers",
"learning-resources": "Access tutorials and documentation",
caching: "Configure response caching and coordination Redis settings",
caching:
"Configure LiteLLM response caching (exact-match / semantic) and coordination Redis settings; separate from provider prompt caching",
"transform-request": "Set up request transformation rules",
"cost-tracking": "Track and analyze API costs",
"ui-theme": "Customize dashboard appearance",

View file

@ -255,6 +255,8 @@ describe("LogDetailContent", () => {
expect(screen.getByText("2 masked")).toBeInTheDocument();
});
const cacheHitTag = () => screen.getByText("Response Cache Hit").closest(".ant-descriptions-item") as HTMLElement;
it("should display cache hit information when cache_hit is true", () => {
render(
<LogDetailContent
@ -271,12 +273,37 @@ describe("LogDetailContent", () => {
/>,
);
expect(screen.getByText("Cache Hit")).toBeInTheDocument();
expect(screen.getByText("true")).toBeInTheDocument();
expect(screen.getByText("Cache Read Tokens")).toBeInTheDocument();
expect(screen.getByText("Response Cache Hit")).toBeInTheDocument();
expect(within(cacheHitTag()).getByText("true").closest(".ant-tag")).toHaveClass("ant-tag-green");
expect(screen.getByText("Prompt Cache Read Tokens")).toBeInTheDocument();
expect(screen.getByText("100")).toBeInTheDocument();
});
it("should not render a red 'false' tag when only provider prompt caching is active", () => {
render(
<LogDetailContent
logEntry={createLogEntry({
cache_hit: "false",
metadata: {
status: "success",
additional_usage_values: {
cache_read_input_tokens: 512,
cache_creation_input_tokens: 128,
},
},
})}
/>,
);
const falseTag = within(cacheHitTag()).getByText("false").closest(".ant-tag") as HTMLElement;
expect(falseTag).not.toHaveClass("ant-tag-red");
expect(screen.getByText("Prompt Cache Read Tokens")).toBeInTheDocument();
expect(screen.getByText("512")).toBeInTheDocument();
expect(screen.getByText("Prompt Cache Creation Tokens")).toBeInTheDocument();
expect(screen.getByText("128")).toBeInTheDocument();
});
it("should display LiteLLM Overhead when litellm_overhead_time_ms is in metadata", () => {
render(
<LogDetailContent

View file

@ -1,5 +1,6 @@
import { useState } from "react";
import { Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space, Spin } from "antd";
import { Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space, Spin, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import moment from "moment";
import { LogEntry } from "../columns";
import { formatNumberWithCommas } from "@/utils/dataUtils";
@ -232,6 +233,17 @@ function ErrorDescription({ errorInfo }: { errorInfo: any }) {
);
}
function LabelWithInfo({ label, tooltip }: { label: string; tooltip: string }) {
return (
<Space size={4}>
{label}
<Tooltip title={tooltip}>
<InfoCircleOutlined style={{ color: "#8c8c8c", cursor: "help" }} />
</Tooltip>
</Space>
);
}
function TagsSection({ tags }: { tags: Record<string, any> }) {
return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden p-4 mb-6">
@ -291,8 +303,10 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
metadata.additional_usage_values.cache_read_input_tokens > 0);
const cacheHitValue = String(logEntry.cache_hit ?? "None");
const cacheHitColor =
cacheHitValue.toLowerCase() === "true" ? "green" : cacheHitValue.toLowerCase() === "false" ? "red" : "default";
const cacheHitColor = cacheHitValue.toLowerCase() === "true" ? "green" : "default";
const cacheReadTokens = metadata?.additional_usage_values?.cache_read_input_tokens;
const cacheCreationTokens = metadata?.additional_usage_values?.cache_creation_input_tokens;
const uncachedInputTokens = getUncachedInputTextTokens(metadata);
const showAnthropicMessagesInputOutput =
@ -328,17 +342,38 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
{hasCacheActivity && (
<>
<Descriptions.Item label="Cache Hit">
<Descriptions.Item
label={
<LabelWithInfo
label="Response Cache Hit"
tooltip="Whether LiteLLM served this entire response from its response cache (the exact-match or semantic cache configured under Response Caching). This is separate from provider prompt caching; a value of false does not mean prompt caching failed. Prompt caching is reflected by the Prompt Cache Read / Creation Tokens below."
/>
}
>
<Tag color={cacheHitColor}>{cacheHitValue}</Tag>
</Descriptions.Item>
{metadata?.additional_usage_values?.cache_read_input_tokens > 0 && (
<Descriptions.Item label="Cache Read Tokens">
{formatNumberWithCommas(metadata.additional_usage_values.cache_read_input_tokens)}
{cacheReadTokens > 0 && (
<Descriptions.Item
label={
<LabelWithInfo
label="Prompt Cache Read Tokens"
tooltip="Input tokens read from the provider's prompt cache. A non-zero value means provider prompt caching is working for this request, even when Response Cache Hit is false."
/>
}
>
{formatNumberWithCommas(cacheReadTokens)}
</Descriptions.Item>
)}
{metadata?.additional_usage_values?.cache_creation_input_tokens > 0 && (
<Descriptions.Item label="Cache Creation Tokens">
{formatNumberWithCommas(metadata.additional_usage_values.cache_creation_input_tokens)}
{cacheCreationTokens > 0 && (
<Descriptions.Item
label={
<LabelWithInfo
label="Prompt Cache Creation Tokens"
tooltip="Input tokens written to the provider's prompt cache on this request so that subsequent requests can read them back."
/>
}
>
{formatNumberWithCommas(cacheCreationTokens)}
</Descriptions.Item>
)}
</>