Cost Breakdown
An LLM cost receipt — every metered line as quantity × rate = amount in micro-unit money that keeps sub-cent charges, grouping by model or kind, a highlighted prompt-caching savings line, and four contract-driven states.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/cost-breakdown.jsonPrompt
Build a React + TypeScript + Tailwind "CostBreakdown" component with zod and
lucide-react. It is the itemized receipt for ONE billed LLM request or run.
Contract
- A zod schema in a sibling contract file is the single source of truth.
- MONEY RULE: every amount is an integer in MICRO-UNITS of the currency's major
unit — 1_000_000 = $1.00 = EUR 1.00 = JPY 1. Not cents, because a real line is
12,431 tokens at $3.00 per 1M = $0.037293, and cents render that as $0.04 — a
7% lie on one row, and a receipt built out of them stops matching the charge.
Not floats, because 0.1 + 0.2 !== 0.3. Micro-units of the MAJOR unit (not the
currency's minor unit) keep the exponent out of the arithmetic entirely: only
the display has to know that JPY draws no decimals.
- Line item: { id, kind, label, model?, quantity, unit? ("tokens"), rateMicros,
per? (1_000_000), amountMicros?, note? }. `rateMicros` is the price for every
`per` units, so $3.00 per 1M tokens is rateMicros 3_000_000 with per 1_000_000,
and a $0.01 tool call is rateMicros 10_000 with per 1 and unit "calls".
- kind is one of six: "input" | "output" | "cache-read" | "cache-write" | "tool"
| "other". Cache reads and cache writes are their own kinds, never a footnote
on input: they are priced differently (a read is typically a tenth of input, a
write a quarter more) and collapsing them hides the biggest lever the reader
has over the bill.
- `amountMicros` is what was CHARGED and is authoritative. Omit it and the
component derives round(quantity * rateMicros / per); supply it and the derived
value becomes an audit instead of the answer.
- Totals: { currency (ISO 4217), subtotalMicros, discounts?: [{label,
amountMicros}], totalMicros, cacheSavingsMicros?, cacheSavingsNote?,
estimated? }. A discount carries a POSITIVE magnitude and renders as a
subtraction. subtotal and total are never recomputed — proration and
per-invoice rounding live upstream.
- Envelope status — "loading" | "empty" | "error" | "ready" — is the component's
own render state, independent of the receipt's contents.
- Props: receipt: Receipt | null; status; groupBy? + defaultGroupBy? +
onGroupByChange? (controlled or uncontrolled); maxRows? (6); locale?
("en-US"); maxFractionDigits? (4); significantDigits? (2); toleranceMicros?
(1); showShareBars? (true); onRetry?; errorMessage?; emptyState?; label?
("Cost breakdown"); className plus the rest of the element props, ref
forwarded to the <article>.
- Every numeric prop is clamped (decimals 0..6, significant digits 1..6, maxRows
>= 1, tolerance >= 0) so a 0 or a NaN can never blank the receipt.
Behavior
- DECIMALS ARE EARNED, NOT FIXED. Each amount is printed with the fewest
decimals that tell the truth: start at what the currency itself asks for (2 for
USD, 0 for JPY, 3 for KWD), grow up to `maxFractionDigits` when the amount
needs them to be exact, and grow PAST that cap only when the cap alone would
round a real charge down to zero — $0.0000 is not a rounding, it is a wrong
number the reader cannot tell apart from a free line. So $3.42, $0.0373 and
$0.000037 sit in one column, each honest, and none of them padded.
- The arithmetic is always on screen: every row reads "12,431 tokens x $3.00 /
1M tokens" under its amount. A receipt you cannot recompute is a number you
have to take on faith. A zero rate reads "41 calls - free", never "x free".
- RECONCILIATION IS SURFACED, NEVER APPLIED. Three checks run, each with the
same `toleranceMicros` slack: charged vs quantity x rate per line (the row
gains a marker saying what the rate math implies), the sum of the lines vs
`subtotalMicros`, and subtotal minus discounts vs `totalMicros`. Each mismatch
becomes a note under the totals. The component NEVER edits a number it was
given: the charged amount is what the customer pays, and a UI that silently
"fixes" it is a UI that will eventually disagree with the invoice while looking
confident.
- Grouping has three axes — as billed / by model / by kind — and an axis with
nothing to say is NOT OFFERED: "by model" disappears when no line carries a
model, "by kind" when every line is the same kind, and the whole control
disappears when only the flat list remains. A toggle that produces one bucket
containing everything is a control that lies about being useful. The requested
axis is reconciled with the available ones by DERIVATION, not by correcting
state, so a receipt with no models falls back to the flat list without
destroying the reader's preference for the next one.
- Groups are ranked by spend, biggest first, with ties broken by arrival order,
and the "Unattributed" bucket (lines with no model) always sinks to the bottom
however much it costs — it is a residue, not a competitor.
- Rows never truncate silently. Each group draws at most `maxRows` lines and then
an explicit control that names both the count and the money it is holding back:
"Show 15 more lines ($0.24)". Reveal state is per group and resets when the
receipt id changes (adjust state during render, no effect), so a group key that
repeats across receipts — the same model name — cannot inherit a "show all" the
reader never asked for.
- The grouping control is a real radiogroup: role="radiogroup" with role="radio"
buttons, aria-checked, a roving tabindex (one tab stop for the whole group),
Arrow/Home/End keys, and selection following focus. Focus is MOVED with the
selection, not merely re-marked — the roving tabindex alone would leave the
caret on a button that is now tabIndex -1.
- Empty is a first-class branch, and `status="ready"` with zero lines takes it
too: drawing a $0.00 total over an empty list reads as "this is free", which is
a different claim from "nothing was metered yet".
- The component owns NO CLOCK, no timer, no observer and no effect: everything is
derived from props during render, so there is nothing to clean up on unmount
and the server, a replayed stream and a screenshot fixture all produce the same
pixels. All Intl formatters are memoized on locale + currency and are built
inside try/catch — a bad BCP 47 tag or a non-ISO currency code makes the
constructor throw, and a receipt must not take the page down over a typo in a
prop.
- Derivation survives scale: quantity * rateMicros is computed first (exact for
realistic token counts) but falls back to quantity * (rateMicros / per) when
the product would leave the safe-integer range, which JPY prices per million
reach at ordinary volumes.
Rendering & styling
- Semantic tokens only: bg-card / border / text-muted-foreground for chrome,
bg-primary + text-primary-foreground for the checked grouping option, bg-muted
for the share-bar track and the group headers, text-destructive for the error
envelope. The six kinds are coloured from var(--chart-1..5) with
var(--muted-foreground) for "other", declared as one editable lookup table, and
every kind ALSO has its own icon so the receipt survives a monochrome theme and
a colour-blind reader.
- Rows are a list, not a table: label + amount on the first line, the arithmetic
on the second, an optional share bar on the third. A four-column table squeezes
its rate column into unreadable wrapping at panel width, and the numbers here
are read as prose per row rather than ranked down a column.
- Long model names and labels use `wrap-anywhere` plus `min-w-0`, NOT
`break-words`: only the former lowers the element's min-content width, which is
what actually stops a 60-character model id from making the card wider than the
panel it sits in. Amounts are tabular-nums so the column stays a column.
- The root is an <article aria-label> holding ONE persistent sr-only
role="status" line that spells out the line count, the total, the current
grouping and how many lines disagree with their rate math. It is mounted for
the whole ready branch, so a regrouping is announced from an element the screen
reader is already watching.
- The share bar is decorative (aria-hidden) because the percentage is already in
the text; its width transition is motion-reduce-guarded and the loading pulse
stops entirely under prefers-reduced-motion. A sub-percent line keeps a
hairline of bar instead of vanishing.
- cn() merges the consumer's className into the root, which also spreads the
remaining element props and forwards its ref.
Customization levers
- Precision: `maxFractionDigits` sets the usual decimal budget (4 reads well for
per-request receipts, 2 for a monthly bill) and `significantDigits` decides how
hard the floor fights back for tiny charges. Neither can make a real charge
render as zero.
- Density: `maxRows` decides how much of each group is drawn before the announced
cap; `showShareBars` off gives a dense numeric receipt; drop the group header
percentage for an even quieter one.
- Grouping: ship it uncontrolled with `defaultGroupBy` for a self-contained card,
or lift it with `groupBy` + `onGroupByChange` to persist the reader's axis
across receipts or mirror it into the URL.
- Taxonomy: the six kinds and their labels are a lookup table — rename "Cache
reads", or route thinking tokens and image surcharges into "other" with their
own labels. The colour map is a second table; point all six at your brand ramp
and the bars follow.
- Money: `locale` drives every number and `currency` comes from the contract, so
the same component renders a JPY receipt with no decimals and a EUR receipt
with the symbol trailing, without a branch.
- Audit strictness: `toleranceMicros` decides how loud the receipt is about
disagreement — 1 micro flags any drift at all, a larger slack ignores a
gateway that rounds each line its own way.
- Chrome: `label` names the region, the receipt's own `label` titles it,
`emptyState` replaces the empty body, `errorMessage` + `onRetry` own the
envelope failure, and `cacheSavingsNote` is where you spell out what the
savings figure is measured against.Concepts
- Quantity times rate equals amount — the whole point of a cost receipt is that it can be recomputed on screen. Every row carries all three numbers, so "why was this run expensive" is answered by reading, not by opening a pricing page in another tab and doing long division.
- Micro-unit money — amounts are integers at one millionth of the currency's major unit, so a $0.037293 line and a $3.42 total live in the same arithmetic without floats and without a per-currency exponent. Decimals are then earned per amount: enough to be exact, never so few that a real charge renders as zero.
- Reconciliation surfaced, never applied — the charged amount is authoritative and the rate math is an audit. When they disagree, the row says what the rate implies and the total says how far the lines are from the subtotal; nothing is quietly corrected, because a UI that edits a charge will eventually contradict the invoice while looking confident.
- An axis with nothing to say is not offered — grouping by model vanishes when no line carries a model, grouping by kind vanishes when every line is one kind, and the control itself vanishes when only the flat list survives. The reader's requested axis is reconciled by derivation, so their preference survives a receipt that cannot honour it.
- Announced row cap — each group draws a fixed number of lines and then names both what it is hiding and what that costs. A silent cut on a cost breakdown is worse than no cut: it makes the visible rows look like the whole bill.
- The savings line is the reward — prompt caching is the one thing on this receipt the reader controls, so what it took off the bill gets its own highlighted line with the share it represents, instead of being inferable only by comparing two rates.
Model Card
One model's spec sheet — context window, max output and cutoff, tri-state capability chips, per-million-token pricing with the cached-read discount, benchmark bars that switch to a signed comparison against a baseline, and a deprecation notice that names the successor.
Eval Scorecard
One evaluation run at a glance — an overall dial marked with the pass gate, per-criterion bars carrying their own gate ticks and baseline deltas, regressions called out by name, and the worst failing cases disclosed on the row that owns them.