Cost Model Viz

Per-request cost distribution for an AI system. ~50 requests from the last hour are plotted on a log-scale Y-axis ($0.001$50); most cluster below $0.05, but a small handful of runaway agent loops with quadratic scratchpad growth blow past a configurable alert threshold and dominate the bill. Click any dot to drill into the full token-times-price breakdown — model, input / output / cached counts, line-by-line cost math, model-call count, and (for anomaly-tier requests) the scratchpad-growth note.

The insight: per-request cost alerting is not redundant with monthly bill caps. A single 15-call agent loop on a quadratically-growing scratchpad can spend more than 40 normal requests combined — and it'll do it in the next minute, not at end-of-month.

Total cost (1h)$4.04
Avg / request$0.081
P95 cost$0.688
Anomalies3above $0.150
Request Cost Distribution/ last 60 min
$0.001$0.01$0.1$1$10$50now-15m-30m-45m-60mtimecost (log scale)$0.150normalelevatedanomaly
Request Detail
Click a request dot
Cost by tier
normal: 40 reqs, $0.242 (6%)
elevated: 7 reqs, $0.646 (16%)
anomaly: 3 reqs, $3.15 (78%)
Cost distribution overview. 50 requests, total cost $4.04, 3 anomalies.

Each dot is one API request over the last hour. The Y-axis is cost on a log scale — from fractions of a cent to tens of dollars. Notice how most requests cluster below $0.05, but a few outliers above the dashed line dominate the total bill. Click any dot to see where the money went.

Customize
Alerting
$0.15

Installation

npx shadcn@latest add https://craftbits.dev/r/cost-model-viz.json

Usage

import { CostModelViz } from "@craft-bits/viz/cost-model-viz";
 
<CostModelViz />

Drive the selection from outside (controlled mode):

const [selectedId, setSelectedId] = useState<number | null>(null);
 
<CostModelViz
  selectedRequestId={selectedId}
  onSelectionChange={setSelectedId}
/>;

Bring your own dataset and threshold:

<CostModelViz
  requests={myRequests}
  alertThreshold={0.5}
  onPhaseChange={(phase) => {
    if (phase === "insight") trackAnomalyInspected();
  }}
/>

Understanding the component

  1. The scatter plot. Each request becomes one <motion.circle>. Dot radius scales with tier (normal: 3, elevated: 4, anomaly: 5.5) — the eye picks out the outliers before the color does. Selected dots inflate +2, hovered dots inflate +1, and unselected dots dim to 0.2 opacity while a selection is active.
  2. Log-scale Y-axis. Cost compresses across four orders of magnitude (sub-cent to tens of dollars). The grid lines mark each decade; the dashed line at alertThreshold separates "normal cluster" from "fix this loop".
  3. Detail panel. A mode="wait" AnimatePresence swaps the panel on every selection. Rows enter on a tight STAGGER cadence (SPRINGS.snap), the cost-breakdown card uses SPRINGS.smooth, and the anomaly scratchpad-note appears only for tier: "anomaly" requests.
  4. Phase derivation. Selection drives a derived CostModelVizPhaseoverview when nothing's selected, inspecting for normal/elevated, insight for anomaly. The narration paragraph and the phase indicator dot both follow.
  5. Tier breakdown bar. A 3-segment bar under the chart shows where the dollars went (often anomaly is <10% of count but >50% of cost). The width animates in once on mount via the override-able transition prop.
  6. Reduced motion. Under prefers-reduced-motion: reduce, every entrance — stat row, dot pop-in, detail panel, tier bar, scratchpad note — collapses to duration: 0.

Props

PropTypeDefaultDescription
requestsreadonly CostModelVizRequest[]curated 50-request datasetRequests to plot.
alertThresholdnumber0.15Cost above which a request is flagged as an anomaly.
selectedRequestIdnumber | nullControlled selection id. Pair with onSelectionChange.
defaultSelectedRequestIdnumber | nullnullUncontrolled initial selection.
onSelectionChange(id) => voidFires after the selection changes.
onPhaseChange(phase) => voidFires on every overview → inspecting → insight transition.
transitionTransitionSPRINGS.smoothOverride the entrance spring for stats / detail / tier bar.
classNamestringMerged onto the root via cn().

Accessibility

  • The root is role="figure" with an aria-label describing the dataset; the inner <svg> repeats role="img" with a color-legend description.
  • Every dot is a role="button" with an aria-label carrying the query preview, cost, and tier, and is reachable with Tab + activated with Enter / Space.
  • A polite live region announces the current selection (with cost + tier + model-call count) or, when nothing is selected, the dataset-level summary (total requests, total cost, anomaly count).
  • Escape clears the selection and returns focus behaviour to the overview phase.
  • Color is never the only signal — dot radius scales with tier, the narration paragraph names the phase, the detail panel labels every value, and the tier-breakdown legend prints "normal / elevated / anomaly" alongside the swatches.
  • Motion respects prefers-reduced-motion: reduce — every entrance collapses to instant.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/systems/CostModelViz.tsx). The source was a lesson-scope viz that imported raw --color-ink-* / --color-accent-400 / --color-surface-raised tokens, depended on the project's SPRINGS.gentle / SPRINGS.snappy / STAGGER.tight re-exports, and pinned tier colors to literal oklch(…) values. The viz extract remaps the palette to var(--cb-success) / var(--cb-warning) / var(--cb-error) / var(--cb-fg-*) semantic tokens, re-keys the springs to the canonical SPRINGS.snap / SPRINGS.smooth / SPRINGS.bouncy and STAGGER constant from @craft-bits/core/motion, hoists the 50-request dataset and alertThreshold to props (with the original curated dataset shipped as COST_MODEL_VIZ_DEFAULT_REQUESTS), wraps the component in forwardRef, accepts className via cn(), spreads ...props on the root, exposes selectedRequestId via the controlled / uncontrolled Radix pattern with an onSelectionChange event, and adds an onPhaseChange event so consumers can wire the viz to external scoreboards or tracking.