diff --git a/ui/litellm-dashboard/src/components/logs/detail/LogDetailContent.tsx b/ui/litellm-dashboard/src/components/logs/detail/LogDetailContent.tsx index f740b6c586a..6a5122d997d 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/LogDetailContent.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/LogDetailContent.tsx @@ -10,6 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { LogEntry } from "../types"; +import { getResponseCacheStatus } from "../responseCache"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { PROMPT_CACHE_CREATION_TOOLTIP, PROMPT_CACHE_READ_TOOLTIP } from "@/utils/promptCacheUsage"; import GuardrailViewer from "./sections/GuardrailViewer/GuardrailViewer"; @@ -481,10 +482,10 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: ? new Date(completionStartTime).getTime() - new Date(logEntry.startTime).getTime() : null; - const responseCacheValue = String(logEntry.cache_hit ?? "").toLowerCase(); + const responseCacheStatus = getResponseCacheStatus(logEntry); const responseCacheKey = logEntry.cache_key && logEntry.cache_key !== "Cache OFF" ? logEntry.cache_key : undefined; - const isResponseCacheHit = responseCacheValue === "true"; - const showResponseCache = isResponseCacheHit || responseCacheValue === "false" || responseCacheKey != null; + const isResponseCacheHit = responseCacheStatus === "hit"; + const showResponseCache = responseCacheStatus !== undefined; const promptCacheReadTokens = Number(metadata?.additional_usage_values?.cache_read_input_tokens) || 0; const promptCacheCreationTokens = Number(metadata?.additional_usage_values?.cache_creation_input_tokens) || 0; diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.integration.test.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.integration.test.tsx new file mode 100644 index 00000000000..73c5a13848e --- /dev/null +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.integration.test.tsx @@ -0,0 +1,83 @@ +import { render, screen, within } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { DataTable } from "@/components/shared/DataTable"; + +import type { LogEntry } from "../types"; +import { getRequestLogsTableColumns } from "./RequestLogsTableColumns"; + +const logEntry = (overrides: Partial): LogEntry => ({ + request_id: "request", + api_key: "key", + team_id: "team", + model: "test-model", + model_id: "model", + call_type: "acompletion", + spend: 0, + total_tokens: 10, + prompt_tokens: 5, + completion_tokens: 5, + startTime: "2026-01-01T00:00:00Z", + endTime: "2026-01-01T00:00:01Z", + cache_hit: "None", + messages: [], + response: {}, + ...overrides, +}); + +function renderCacheCells(logs: LogEntry[]) { + render( + row.request_id} + />, + ); + const header = screen.getByRole("columnheader", { name: "Cache Hit" }); + const columnIndex = screen.getAllByRole("columnheader").indexOf(header); + return screen + .getAllByRole("row") + .slice(1) + .map((row) => within(row).getAllByRole("cell")[columnIndex]); +} + +describe("Request log cache hits", () => { + it("distinguishes a response cache hit, an enabled cache miss and caching disabled", () => { + const cells = renderCacheCells([ + logEntry({ request_id: "hit", cache_hit: "True" }), + logEntry({ request_id: "miss", cache_key: "response-cache-key" }), + logEntry({ request_id: "off", cache_key: "Cache OFF" }), + ]); + + expect(cells[0]).toHaveTextContent(/^Yes$/); + expect(cells[1]).toHaveTextContent(/^No$/); + expect(cells[2]).toHaveTextContent(/^-$/); + }); + + it("shows session hit counts instead of the representative request's cache status", () => { + const sessions: Partial[] = [ + { request_id: "mixed", cache_hit: "False", session_total_count: 3, session_cache_hit_count: 2 }, + { request_id: "one-hit", cache_hit: "False", session_total_count: 2, session_cache_hit_count: 1 }, + { request_id: "no-hits", session_total_count: 2, session_cache_hit_count: 0 }, + { request_id: "unknown", cache_hit: "True", session_total_count: 2 }, + ]; + const cells = renderCacheCells(sessions.map(logEntry)); + + expect(cells[0]).toHaveTextContent(/^2 hits$/); + expect(cells[1]).toHaveTextContent(/^1 hit$/); + expect(cells[2]).toHaveTextContent(/^0 hits$/); + expect(cells[3]).toHaveTextContent(/^-$/); + }); + + it("does not report provider prompt caching as a response cache hit", () => { + const promptCached: Partial = { + cache_key: "Cache OFF", + session_total_count: 1, + session_cache_hit_count: 0, + metadata: { additional_usage_values: { cache_read_input_tokens: 100 } }, + }; + const [cell] = renderCacheCells([logEntry(promptCached)]); + + expect(cell).toHaveTextContent(/^-$/); + }); +}); diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.tsx index b3d44361d0a..4c67b0b18f1 100644 --- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.tsx @@ -10,6 +10,7 @@ import { copyToClipboard, getSpendString } from "@/utils/dataUtils"; import { getProviderLogoAndName } from "../../provider_info_helpers"; import { getBatchIdFromRequestId, getBatchRequestCounts, isBatchCallType } from "../batchLogUtils"; import type { LogEntry } from "../types"; +import { getResponseCacheStatus } from "../responseCache"; import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "../constants"; import { AgentBadge, AgentIcon, BatchBadge, LlmBadge, McpBadge, SparkleIcon, WrenchIcon } from "./TypeBadges"; @@ -159,6 +160,25 @@ export const getRequestLogsTableColumns = ({ return ; }, }, + { + id: "cache_hit", + accessorKey: "cache_hit", + header: "Cache Hit", + size: 100, + enableSorting: false, + cell: ({ row }) => { + const log = row.original; + if ((log.session_total_count ?? 1) > 1) { + const hits = log.session_cache_hit_count; + return ( + {hits == null ? "-" : `${hits} ${hits === 1 ? "hit" : "hits"}`} + ); + } + const status = getResponseCacheStatus(log); + if (status === "hit") return ; + return {status === "miss" ? "No" : "-"}; + }, + }, { id: "session_id", accessorKey: "session_id", diff --git a/ui/litellm-dashboard/src/components/logs/responseCache.test.ts b/ui/litellm-dashboard/src/components/logs/responseCache.test.ts new file mode 100644 index 00000000000..ed915ea0569 --- /dev/null +++ b/ui/litellm-dashboard/src/components/logs/responseCache.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; + +import { getResponseCacheStatus } from "./responseCache"; + +describe("getResponseCacheStatus", () => { + it.each([ + { cache_hit: "True", cache_key: undefined, expected: "hit" }, + { cache_hit: "true", cache_key: "response-key", expected: "hit" }, + { cache_hit: "TRUE", cache_key: "Cache OFF", expected: "hit" }, + { cache_hit: "False", cache_key: undefined, expected: "miss" }, + { cache_hit: "false", cache_key: "response-key", expected: "miss" }, + { cache_hit: "FALSE", cache_key: "Cache OFF", expected: "miss" }, + { cache_hit: "None", cache_key: "response-key", expected: "miss" }, + { cache_hit: "", cache_key: "response-key", expected: "miss" }, + { cache_hit: "None", cache_key: "Cache OFF", expected: undefined }, + { cache_hit: "None", cache_key: undefined, expected: undefined }, + { cache_hit: "None", cache_key: "", expected: undefined }, + { cache_hit: "", cache_key: undefined, expected: undefined }, + ])("returns $expected for cache_hit=$cache_hit and cache_key=$cache_key", ({ cache_hit, cache_key, expected }) => { + expect(getResponseCacheStatus({ cache_hit, cache_key })).toBe(expected); + }); +}); diff --git a/ui/litellm-dashboard/src/components/logs/responseCache.ts b/ui/litellm-dashboard/src/components/logs/responseCache.ts new file mode 100644 index 00000000000..c602d4ded9b --- /dev/null +++ b/ui/litellm-dashboard/src/components/logs/responseCache.ts @@ -0,0 +1,8 @@ +import type { LogEntry } from "./types"; + +export function getResponseCacheStatus(log: Pick): "hit" | "miss" | undefined { + const value = String(log.cache_hit ?? "").toLowerCase(); + if (value === "true") return "hit"; + if (value === "false" || (log.cache_key && log.cache_key !== "Cache OFF")) return "miss"; + return undefined; +}