From 6b1ad046c830b29236b36760b0751bbf485ba9fe Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Tue, 19 May 2026 10:42:19 -0700 Subject: [PATCH] feat(ui): explain input/output/cache token accounting on usage Cost tab Customers were confused about what input vs output tokens mean and how cache tokens relate to the total. Adds a reusable TokenMetricLabel with info tooltips on the Cost tab's Total/Input/Output/Cache Read/Cache Write cards, documenting that input already includes cache tokens (so the breakdown sums to total) and that output includes reasoning tokens. --- .../UsagePage/components/TokenMetricLabel.tsx | 78 +++++++++++++++++++ .../UsagePage/components/UsagePageView.tsx | 35 +++++---- 2 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/UsagePage/components/TokenMetricLabel.tsx diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/TokenMetricLabel.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/TokenMetricLabel.tsx new file mode 100644 index 00000000000..004c7111087 --- /dev/null +++ b/ui/litellm-dashboard/src/components/UsagePage/components/TokenMetricLabel.tsx @@ -0,0 +1,78 @@ +/** + * Labeled token metric heading with an info tooltip. + * + * Centralizes the wording for what "input", "output", "cache read", and + * "cache write" tokens actually mean so the Cost tab (and, later, the other + * usage views) communicate token accounting consistently. + * + * Backend semantics these definitions reflect: + * - `prompt_tokens` is the *total* input, already including cache read + + * cache creation tokens (LiteLLM normalizes Anthropic's split usage to + * match OpenAI's convention — see anthropic/chat/transformation.py). + * - `completion_tokens` is all output, including reasoning/thinking tokens. + * - `total_tokens` = prompt_tokens + completion_tokens, so cache tokens are a + * subset of input, never added on top. + * - The "Input Tokens" card shows the non-cached portion + * (prompt_tokens − cache_read − cache_write), so Input + Output + Cache + * Read + Cache Write sum exactly to Total Tokens. + */ + +import { InfoCircleOutlined } from "@ant-design/icons"; +import { Title } from "@tremor/react"; +import { Tooltip } from "antd"; +import React from "react"; + +export type TokenMetricKind = "total" | "input" | "output" | "cache_read" | "cache_write"; + +const TOOLTIP_COPY: Record = { + total: ( + <> + Total = Input + Output. Cached tokens are counted as part of Input (not added on top), so the Input, Output, Cache + Read, and Cache Write cards sum exactly to this number. Reflects successful requests only. + + ), + input: ( + <> + Non-cached prompt tokens sent to the model — the prompt minus any tokens served from or written to the prompt + cache. Billed at the model's standard input rate. Cached portions are shown separately as Cache Read / Cache + Write. + + ), + output: ( + <> + Tokens the model generated in its response, including any reasoning / “thinking” tokens. Billed at the + model's output rate. + + ), + cache_read: ( + <> + Input tokens served from the prompt cache instead of being reprocessed. Billed at a steep discount versus standard + input (e.g. ~0.1× on Anthropic / OpenAI). Already included in total input. + + ), + cache_write: ( + <> + Input tokens written into the prompt cache so later requests can reuse them. Billed at a premium over standard + input on some providers (e.g. ~1.25× on Anthropic). Already included in total input. + + ), +}; + +interface TokenMetricLabelProps { + kind: TokenMetricKind; + label: string; + /** Optional trailing node (e.g. an expand/collapse chevron) rendered after the icon. */ + trailing?: React.ReactNode; +} + +const TokenMetricLabel: React.FC = ({ kind, label, trailing }) => ( +
+ {label} + + + + {trailing} +
+); + +export default TokenMetricLabel; diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx index 41dfdb21d1b..32bad96c8b0 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx @@ -49,6 +49,7 @@ import EndpointUsage from "./EndpointUsage/EndpointUsage"; import EntityUsage, { EntityList } from "./EntityUsage/EntityUsage"; import SpendByProvider from "./EntityUsage/SpendByProvider"; import TopKeyView from "./EntityUsage/TopKeyView"; +import TokenMetricLabel from "./TokenMetricLabel"; import UsageAIChatPanel from "./UsageAIChatPanel"; import { UsageOption, UsageViewSelect } from "./UsageViewSelect/UsageViewSelect"; @@ -636,14 +637,17 @@ const UsagePage: React.FC = ({ teams, organizations }) => { className="cursor-pointer hover:bg-gray-50 transition-colors" onClick={() => setShowTokenBreakdown(!showTokenBreakdown)} > -
- Total Tokens - {showTokenBreakdown ? ( - - ) : ( - - )} -
+ + ) : ( + + ) + } + /> {userSpendData.metadata?.total_tokens?.toLocaleString() || 0} @@ -652,25 +656,30 @@ const UsagePage: React.FC = ({ teams, organizations }) => { {showTokenBreakdown && ( - Input Tokens + - {(userSpendData.metadata?.total_prompt_tokens || 0).toLocaleString()} + {Math.max( + 0, + (userSpendData.metadata?.total_prompt_tokens || 0) - + (userSpendData.metadata?.total_cache_read_input_tokens || 0) - + (userSpendData.metadata?.total_cache_creation_input_tokens || 0) + ).toLocaleString()} - Output Tokens + {userSpendData.metadata?.total_completion_tokens?.toLocaleString() || 0} - Cache Read Tokens + {userSpendData.metadata?.total_cache_read_input_tokens?.toLocaleString() || 0} - Cache Write Tokens + {userSpendData.metadata?.total_cache_creation_input_tokens?.toLocaleString() || 0}