Display stream metadata in AI chat — generation duration, tokens per second, and time to first token, rendered via assistant-ui's React components.
Display stream performance metrics (duration, tokens per second, time to first token) on assistant messages.
Warning
This feature is experimental. The useMessageTiming() API and the set of tracked fields may change in future versions.
Info
The MessageTiming registry component provides a ready-made badge and tooltip UI. This guide covers the underlying useMessageTiming() hook for custom implementations and runtime-specific setup.
Reading Timing Data
Use useMessageTiming() inside a message component to access timing data:
import type { FC } from "react";
import { useMessageTiming } from "@assistant-ui/react";
const MessageTimingDisplay: FC = () => {
const timing = useMessageTiming();
if (!timing?.totalStreamTime) return null;
const formatMs = (ms: number) =>
ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(2)}s`;
return (
<span className="text-xs text-muted-foreground">
{formatMs(timing.totalStreamTime)}
{timing.tokensPerSecond !== undefined &&
` · ${timing.tokensPerSecond.toFixed(1)} tok/s`}
</span>
);
};Place it inside MessagePrimitive.Root, typically near the action bar:
const AssistantMessage: FC = () => {
return (
<MessagePrimitive.Root>
<MessagePrimitive.Parts>{...}</MessagePrimitive.Parts>
<ActionBarPrimitive.Root>
<ActionBarPrimitive.Copy />
<ActionBarPrimitive.Reload />
<MessageTimingDisplay />
</ActionBarPrimitive.Root>
</MessagePrimitive.Root>
);
};useMessageTiming() Return Fields
| Field | Type | Description |
|---|---|---|
streamStartTime | number | Unix timestamp when stream started |
firstTokenTime | number? | Time to first text token (ms) |
totalStreamTime | number? | Total stream duration (ms) |
tokenCount | number? | Output token count. Data Stream uses reported usage when present; client-side tracking estimates it from text length |
tokensPerSecond | number? | Throughput (tokens/sec), when tokenCount is available |
totalChunks | number | Total stream chunks received |
toolCallCount | number | Number of tool calls |
Runtime Support
| Runtime | Supported | Notes |
|---|---|---|
| Data Stream | Yes | Automatic via AssistantMessageAccumulator |
AI SDK (useChatRuntime) | Yes | Automatic via client-side tracking |
Local (useLocalRuntime) | Yes | Pass timing in ChatModelRunResult.metadata |
| ExternalStore | Yes | Pass timing in ThreadMessageLike.metadata |
| LangGraph | Yes | Automatic via client-side tracking |
| AG-UI | Yes | Automatic via client-side tracking |
| OpenCode | Yes | Automatic via client-side tracking |
Data Stream
Timing is tracked automatically inside AssistantMessageAccumulator. No setup required.
import { useDataStreamRuntime } from "@assistant-ui/react-data-stream";
const runtime = useDataStreamRuntime({ api: "/api/chat" });
// useMessageTiming() works out of the boxAI SDK (useChatRuntime)
Timing is tracked automatically on the client side by observing streaming state transitions and content changes. Timing is finalized when each stream completes.
Warning
tokenCount and tokensPerSecond are estimates. The client-side tracker derives the token count from the streamed text length (one token per four characters) and does not read the usage metadata from your AI SDK route.
import { useChatRuntime } from "@assistant-ui/ai-sdk";
const runtime = useChatRuntime();
// useMessageTiming() works out of the boxLocal (useLocalRuntime)
Pass timing in the metadata field of your ChatModelRunResult:
import type { ChatModelAdapter } from "@assistant-ui/react";
const myAdapter: ChatModelAdapter = {
async run({ messages, abortSignal }) {
const startTime = Date.now();
const result = await callMyAPI(messages, abortSignal);
const totalStreamTime = Date.now() - startTime;
return {
content: [{ type: "text", text: result.text }],
metadata: {
timing: {
streamStartTime: startTime,
totalStreamTime,
tokenCount: result.usage?.completionTokens,
tokensPerSecond:
result.usage?.completionTokens
? result.usage.completionTokens / (totalStreamTime / 1000)
: undefined,
totalChunks: 1,
toolCallCount: 0,
},
},
};
},
};ExternalStore (useExternalStoreRuntime)
Pass timing in the metadata.timing field of your ThreadMessageLike messages:
import type { ThreadMessageLike } from "@assistant-ui/react";
const message: ThreadMessageLike = {
role: "assistant",
content: [{ type: "text", text: fullText }],
metadata: {
timing: {
streamStartTime: startTime,
firstTokenTime,
totalStreamTime,
tokenCount,
tokensPerSecond,
totalChunks: chunks,
toolCallCount: 0,
},
},
};LangGraph (useLangGraphRuntime)
Timing is tracked automatically on the client side by observing streaming state transitions and LangChainMessage content changes. No setup required.
import { useLangGraphRuntime } from "@assistant-ui/react-langgraph";
const runtime = useLangGraphRuntime({ stream: myStream });
// useMessageTiming() works out of the boxAG-UI (useAgUiRuntime)
Timing is tracked automatically on the client side by the AG-UI run aggregator. Each emitted message includes timing metadata computed from stream chunk observations.
import { useMemo } from "react";
import { useAgUiRuntime } from "@assistant-ui/react-ag-ui";
import { HttpAgent } from "@ag-ui/client";
const agent = useMemo(
() => new HttpAgent({ url: "http://localhost:8000/agent" }),
[],
);
const runtime = useAgUiRuntime({ agent });
// useMessageTiming() works out of the boxOpenCode (useOpenCodeRuntime)
Timing is tracked automatically on the client side by observing OpenCodeThreadState transitions and assistant message content deltas. No setup required.
import { useOpenCodeRuntime } from "@assistant-ui/react-opencode";
const runtime = useOpenCodeRuntime();
// useMessageTiming() works out of the boxAPI Reference
useMessageTiming()
const timing: MessageTiming | undefined = useMessageTiming();Returns timing metadata for the current assistant message, or undefined for non-assistant messages or when no timing data is available.
Must be used inside a MessagePrimitive.Root context.