Components
RunMeter
Tokens, estimated cost from your rates, time to first token and the prompt-cache hit rate.
Example
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
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 aiimport { RunMeter, useRunTiming } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/run-meter.jsonimport { RunMeter } from '@/components/signoff-ui/run-meter';
import { useRunTiming } from '@/components/signoff-ui/lib/hooks';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
usageRunUsagepricingModelPricingUsed to estimate cost when
costis not given.costnumberExplicit cost in USD, e.g. reported by your gateway.
ttftMsnumberTime to first token, ms.
durationMsnumberTotal (active) run time, ms.
livebooleanDefaultfalseThe run is in progress: values are live.
modelstringvariant'compact' | 'expanded'Default'compact'titleReactNodeHeading for the expanded variant. Default "Run" (
labels.runMeter.title).headingLevelHeadingLevelDefault3Heading level for the expanded variant's title.
labelsSignoffLabelsInputWords 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): RunTimingReturns: RunTiming
activeMsrequirednumberMilliseconds the agent spent working, excluding time waiting on the user.
isRunningrequiredbooleanstartedAtnumberWhen the run was first seen in flight: its first request, or later if it was already running.
firstTokenAtnumberWhen the run's first response arrived. Undefined while unknown, as
ttftMs.ttftMsnumberTime to first token of the run: from submitting its first request to the first streamed status. A first request that ends without a
streamingstatus (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.
inputrequirednumberoutputrequirednumbercachedInputnumberPrice for cache-read input tokens. Defaults to
input.cacheWritenumberPrice 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, atheadingLevel) 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
/* 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);
}