Skip to content

Docs

Components

RunMeter

Tokens, estimated cost from your rates, time to first token and the prompt-cache hit rate.

Example

variant="expanded"

Run

mock-agent-1
Tokens
41.1k
Est. cost
$0.056
  • Input 38.7k(28.8k cached)
  • Output 2.41k(96 reasoning)
TTFT
684ms
Total
21.8s
Cache hit
74%

$0.032 input · $0.024 output · $2.5/$10 per 1M

variant="compact"
38.7k input tokens, 2.41k output tokens, estimated cost $0.056, time to first token 684ms, total 21.8s

Pass totalUsage from streamText through message metadata, and timing from useRunTiming(status, messages). Cost is an estimate from the pricing you supply.

Interactive, and the same on the components page. Try the keyboard below on it.

Installation

npm i signoff-ui ai
import { RunMeter, useRunTiming } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';

The styles are once per app; Getting started has the Tailwind v4 and plain CSS options.

Usage

// A run starts with each user message and spans its approval round trips.
const timing = useRunTiming(status, messages);

<RunMeter
  variant="expanded"
  usage={last?.metadata?.usage}
  pricing={{ input: 2.5, cachedInput: 0.25, output: 10 }} // USD per million tokens
  ttftMs={timing.ttftMs}
  durationMs={timing.activeMs}
  live={status === 'streaming'}
/>

How it works

Tokens in and out, estimated cost, time to first token, active run time and prompt-cache hit rate. It takes the AI SDK LanguageModelUsage shape directly; to cover a whole run across review and approval round trips, sum it on the server with addUsage, as the quickstart's route does.

useRunTiming(status, messages) measures time to first token and active time per turn: a run starts with each user message and spans its round trips, leaving out the time spent waiting on you. Without messages, each request is timed on its own. Time to first token is left unknown, not 0, for a run that was already streaming when the hook first saw it.

pricing is in USD per million tokens: input and output, plus optional cachedInput for cache reads and cacheWrite for cache writes (1.25× input on Anthropic, for example), which both default to input.

API reference

Generated from the types the package ships, so it matches the version you install.

RunMeter

Token usage, estimated cost and latency for an agent run. compact is a single inline strip for headers; expanded is a card with a token breakdown bar and the prompt-cache hit rate.

Props

usage
RunUsage
pricing
ModelPricing

Used to estimate cost when cost is not given.

cost
number

Explicit cost in USD, e.g. reported by your gateway.

ttftMs
number

Time to first token, ms.

durationMs
number

Total (active) run time, ms.

live
booleanDefault false

The run is in progress: values are live.

model
string
variant
'compact' | 'expanded'Default 'compact'
title
ReactNode

Heading for the expanded variant. Default "Run" (labels.runMeter.title).

headingLevel
HeadingLevelDefault 3

Heading level for the expanded variant's title.

labels
SignoffLabelsInput

Words to use instead of the English defaults: see SignoffLabelsProvider.

Other props go to the root <div>: className (merged with tailwind-merge), id, aria-*, data-* and event handlers.

useRunTiming

Derives run latency from useChat status transitions: time to first token, and active time. A run can span several requests (e.g. continuing after an approval); time spent waiting on the human between requests is excluded from activeMs.

Pass the chat's messages (useChat().messages, or useAgUiAgent's): a run then starts with each user message (or a regenerated reply) and spans its approval round trips. Without them, each request submitted after the last one ended starts a new run, so a turn that pauses for approval is timed per request. A key of your own instead (any value but an array) starts a new run only when it changes.

useRunTiming(status: ChatStatus, messages?: readonly RunTimingMessage[] | undefined): RunTiming

Returns: RunTiming

activeMsrequired
number

Milliseconds the agent spent working, excluding time waiting on the user.

isRunningrequired
boolean
startedAt
number

When the run was first seen in flight: its first request, or later if it was already running.

firstTokenAt
number

When the run's first response arrived. Undefined while unknown, as ttftMs.

ttftMs
number

Time to first token of the run: from submitting its first request to the first streamed status. A first request that ends without a streaming status (its response arrived in one burst) counts its end as the first token. Undefined until then, and when it cannot be known: the run was already streaming when first seen, its first request failed, or (given the messages) it ended with no reply.

ModelPricing

USD per million tokens.

inputrequired
number
outputrequired
number
cachedInput
number

Price for cache-read input tokens. Defaults to input.

cacheWrite
number

Price for input tokens written to the prompt cache (e.g. 1.25x input on Anthropic). Defaults to input.

RunUsage

Token usage, compatible with the AI SDK LanguageModelUsage shape.

type RunUsage = {
  inputTokens?: number | undefined;
  outputTokens?: number | undefined;
  totalTokens?: number | undefined;
  inputTokenDetails?: Partial<LanguageModelUsage['inputTokenDetails']> | undefined;
  outputTokenDetails?: Partial<LanguageModelUsage['outputTokenDetails']> | undefined;
};

Accessibility

  • The compact strip is a group named "Run metrics" with one visually hidden sentence, such as "20.9k input tokens, 255 output tokens, estimated cost $0.025, …", instead of a run of loose numbers.
  • The expanded card has a heading (title, at headingLevel) and a labelled token breakdown list.
  • Numbers tween as they change, except with prefers-reduced-motion.

Theming

Every color, radius and font is a CSS variable, so the overrides in Theming apply, on :root or scoped to any element. These hooks are read from the component's markup.

Slots (data-slot)
signoff-run-meter
State attributes
data-varianton signoff-run-meter'compact' | 'expanded'
Tokens it uses
--signoff-accent--signoff-accent-fg--signoff-border--signoff-chart-input--signoff-chart-output--signoff-fg--signoff-fg-muted--signoff-fg-subtle--signoff-font-mono--signoff-font-sans--signoff-radius--signoff-ring--signoff-surface--signoff-surface-2
app/globals.css
/* Only this component, and only inside .settings-panel */
.settings-panel [data-slot='signoff-run-meter'] {
  --signoff-radius: 4px;
}

[data-slot='signoff-run-meter'][data-variant='compact'] {
  outline: 1px solid var(--signoff-accent);
}