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.
This commit is contained in:
Ryan Crabbe 2026-05-19 10:42:19 -07:00
parent cff3e0b75e
commit 6b1ad046c8
No known key found for this signature in database
2 changed files with 100 additions and 13 deletions

View file

@ -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<TokenMetricKind, React.ReactNode> = {
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&apos;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 / &ldquo;thinking&rdquo; tokens. Billed at the
model&apos;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<TokenMetricLabelProps> = ({ kind, label, trailing }) => (
<div className="flex items-center gap-2">
<Title>{label}</Title>
<Tooltip title={TOOLTIP_COPY[kind]}>
<InfoCircleOutlined className="text-gray-400 hover:text-gray-600" />
</Tooltip>
{trailing}
</div>
);
export default TokenMetricLabel;

View file

@ -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<UsagePageProps> = ({ teams, organizations }) => {
className="cursor-pointer hover:bg-gray-50 transition-colors"
onClick={() => setShowTokenBreakdown(!showTokenBreakdown)}
>
<div className="flex items-center gap-2">
<Title>Total Tokens</Title>
{showTokenBreakdown ? (
<DownOutlined className="text-gray-400 text-xs" />
) : (
<RightOutlined className="text-gray-400 text-xs" />
)}
</div>
<TokenMetricLabel
kind="total"
label="Total Tokens"
trailing={
showTokenBreakdown ? (
<DownOutlined className="text-gray-400 text-xs" />
) : (
<RightOutlined className="text-gray-400 text-xs" />
)
}
/>
<Text className="text-2xl font-bold mt-2">
{userSpendData.metadata?.total_tokens?.toLocaleString() || 0}
</Text>
@ -652,25 +656,30 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
{showTokenBreakdown && (
<Grid numItems={4} className="gap-4 mt-4">
<Card>
<Title>Input Tokens</Title>
<TokenMetricLabel kind="input" label="Input Tokens" />
<Text className="text-2xl font-bold mt-2 text-blue-600">
{(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()}
</Text>
</Card>
<Card>
<Title>Output Tokens</Title>
<TokenMetricLabel kind="output" label="Output Tokens" />
<Text className="text-2xl font-bold mt-2 text-cyan-600">
{userSpendData.metadata?.total_completion_tokens?.toLocaleString() || 0}
</Text>
</Card>
<Card>
<Title>Cache Read Tokens</Title>
<TokenMetricLabel kind="cache_read" label="Cache Read Tokens" />
<Text className="text-2xl font-bold mt-2 text-green-600">
{userSpendData.metadata?.total_cache_read_input_tokens?.toLocaleString() || 0}
</Text>
</Card>
<Card>
<Title>Cache Write Tokens</Title>
<TokenMetricLabel kind="cache_write" label="Cache Write Tokens" />
<Text className="text-2xl font-bold mt-2 text-purple-600">
{userSpendData.metadata?.total_cache_creation_input_tokens?.toLocaleString() || 0}
</Text>