# RunMeter

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

Source: https://signoff-ui.vercel.app/docs/components/run-meter

## Installation

From npm:

```bash
npm i signoff-ui ai
```

```tsx
import { RunMeter, useRunTiming } from 'signoff-ui';
import 'signoff-ui/styles.css'; // once per app, or @import 'signoff-ui/tailwind.css' with Tailwind v4
```

Or as source, with the shadcn CLI:

```bash
npx shadcn@latest add https://signoff-ui.vercel.app/r/run-meter.json
```

```tsx
import { RunMeter } from '@/components/signoff-ui/run-meter';
import { useRunTiming } from '@/components/signoff-ui/lib/hooks';
```

## Usage

```tsx
// 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](https://signoff-ui.vercel.app/docs/getting-started#render-a-run) 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

### 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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `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` | `boolean` | `false` | The run is in progress: values are live. |
| `model` | `string` |  |  |
| `variant` | `'compact' \| 'expanded'` | `'compact'` |  |
| `title` | `ReactNode` |  | Heading for the expanded variant. Default "Run" (`labels.runMeter.title`). |
| `headingLevel` | `HeadingLevel` | `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`, `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.

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

Returns `RunTiming`:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `activeMs` (required) | `number` |  | Milliseconds the agent spent working, excluding time waiting on the user. |
| `isRunning` (required) | `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.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `input` (required) | `number` |  |  |
| `output` (required) | `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.

```ts
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

Slots (`data-slot`): `signoff-run-meter`.

- `data-variant` on `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`. See https://signoff-ui.vercel.app/docs/getting-started#theming.
