Skip to content
Elements

Cost meter

What the run spent, split by model, against the session total.

$2.76this run$18.40 session
Opus 548.2k in ยท 12.4k out$1.66
Sonnet 592.8k in ยท 21.1k out$0.86
Haiku 4.5140k in ยท 8.9k out$0.24
fig. 01

Installation

Install manually
npx shadcn@latest add "@assistant-ui/elements-cost-meter"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

CostMeter shows what a run cost as a big number, a segmented bar split by model, and a row per model with its token counts and price. With a runtime you build the numbers from the message's own step usage; standalone you already hold them.

Getting started

assistant-ui tracks token usage per step of a run, but not dollars and not which model produced a given step. Both are your own bookkeeping. The closest data is message.metadata.steps, an array of { usage: { inputTokens, outputTokens } } entries.

Turn a run's steps into a cost line

components/assistant-ui/elements/run-cost.tsx
"use client";

import { useAuiState } from "@assistant-ui/react";
import {
  CostMeter,
  type CostLine,
} from "@/components/assistant-ui/elements/cost-meter";

const PRICE_PER_MILLION = { in: 3, out: 15 };

function useRunCost(model: string): CostLine {
  const inputTokens = useAuiState((s) =>
    s.message.role === "assistant"
      ? s.message.metadata.steps.reduce(
          (total, step) => total + (step.usage?.inputTokens ?? 0),
          0,
        )
      : 0,
  );
  const outputTokens = useAuiState((s) =>
    s.message.role === "assistant"
      ? s.message.metadata.steps.reduce(
          (total, step) => total + (step.usage?.outputTokens ?? 0),
          0,
        )
      : 0,
  );
  const dollars =
    (inputTokens / 1_000_000) * PRICE_PER_MILLION.in +
    (outputTokens / 1_000_000) * PRICE_PER_MILLION.out;
  return { model, inputTokens, outputTokens, cost: `$${dollars.toFixed(2)}`, share: 1 };
}

export function RunCost({ model }: { model: string }) {
  const line = useRunCost(model);
  return <CostMeter runCost={line.cost} sessionCost={line.cost} lines={[line]} />;
}

PRICE_PER_MILLION and the model label are yours; assistant-ui never names a price or a provider.

Add a session total

const sessionTokens = useAuiState((s) =>
  s.thread.messages.reduce(
    (total, message) =>
      message.role === "assistant"
        ? message.metadata.steps.reduce(
            (sum, step) =>
              sum + (step.usage?.inputTokens ?? 0) + (step.usage?.outputTokens ?? 0),
            total,
          )
        : total,
    0,
  ),
);

s.thread.messages is every message on the active branch, so summing its assistant messages' steps gives a session total in tokens. Turning that into sessionCost still takes your own price table. If a thread calls more than one model, you also track which message used which model, because ThreadStep carries no model name.

Anatomy

<div data-slot="cost-meter">
  <div>
    <span>{/* runCost, large */}</span>
    <span>this run</span>
    <span>{/* "{sessionCost} session" */}</span>
  </div>
  <div>{/* segmented bar, one named meter per line, width = share */}</div>
  <div>
    {/* one row per line: model name, token counts, cost */}
  </div>
</div>

The segmented bar's color is assigned by index, not by value: the first line always gets the solid blue tone, the second a faded blue, and every line after that shares one neutral gray, regardless of how large its share is. Each painted segment is a named meter using that line's share as a 0โ€ฆ100 value; a line whose share rounds to no announced width is left out of the bar rather than announced as an empty segment, and the color still follows the line's own index. share isn't validated against the others, so shares that don't sum to 1 leave the bar under or overfull. inputTokens and outputTokens are raw numbers the component prints as-is below a thousand and otherwise to one decimal in the first of k, M, B, or T that keeps the figure under a thousand (950 stays "950 in", 48200 becomes "48.2k in", 999950 becomes "1M in"); runCost, sessionCost, and each line's cost are already-formatted strings the component only displays; it does no currency math.

Examples

Restyle the meter

Both lanes take className on the root. The root uses paper; token counts and cost use mono.

<CostMeter className="max-w-none" /* ... */ />

Computing share from raw costs

Whichever lane supplies the dollar amounts, share is each line's fraction of the total. The bar doesn't compute it.

const raw = [
  { model: "Opus 5", inputTokens: 48_200, outputTokens: 12_400, dollars: 1.66 },
  { model: "Sonnet 5", inputTokens: 92_800, outputTokens: 21_100, dollars: 0.86 },
];
const total = raw.reduce((sum, r) => sum + r.dollars, 0);
const lines: CostLine[] = raw.map((r) => ({
  model: r.model,
  inputTokens: r.inputTokens,
  outputTokens: r.outputTokens,
  cost: `$${r.dollars.toFixed(2)}`,
  share: total > 0 ? r.dollars / total : 0,
}));

API reference

Message and thread state

SelectorTypeDescription
s.message.metadata.stepsreadonly ThreadStep[]Present on assistant messages. Each step's usage gives inputTokens / outputTokens for that step; sum across steps for one message's run cost.
s.thread.messagesreadonly MessageState[]Every message on the active branch. Sum .metadata.steps across its assistant messages for a session total.

assistant-ui tracks token counts, not dollars, and ThreadStep carries no model name. cost, share, and any per-model split are computed by your own price table and your own record of which model ran which message.