Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
Neon
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
Persistence (Advanced)
Embeddings
Reranking
Evaluate
Sandboxes (Advanced)
Class References
Function References
Interface References
Type Alias References
Variable References
Protocol

Event metadata

Your server speaks AG-UI. TanStack useChat still misses finishReason, the model id, or leftover usage. Put those extras in metadata.tanstack on the event. Spec fields stay at the top.

Copy this RUN_FINISHED event first:

json
{
  "type": "RUN_FINISHED",
  "threadId": "thread-1",
  "runId": "run-1",
  "usage": [
    {
      "inputTokens": 12,
      "outputTokens": 34,
      "totalTokens": 46
    }
  ],
  "metadata": {
    "tanstack": {
      "finishReason": "stop",
      "model": "gpt-5.5"
    }
  }
}
{
  "type": "RUN_FINISHED",
  "threadId": "thread-1",
  "runId": "run-1",
  "usage": [
    {
      "inputTokens": 12,
      "outputTokens": 34,
      "totalTokens": 46
    }
  ],
  "metadata": {
    "tanstack": {
      "finishReason": "stop",
      "model": "gpt-5.5"
    }
  }
}

The client copies metadata.tanstack onto the chunk. After chunk.type === "RUN_FINISHED", read chunk.metadata?.tanstack?.finishReason. In-process usage is TanStack TokenUsage (promptTokens). The wire uses the spec array (inputTokens).

Do now

Send these on every successful run:

FieldWhereWhy the client needs it
type, threadId, runIdSpec top levelFrame the run. Missing ids break resume and correlation.
usage[]Spec top level on RUN_FINISHED / RUN_ERRORToken counts. Use inputTokens, outputTokens, totalTokens.
finishReasonmetadata.tanstack on RUN_FINISHEDAfter a tool result, the client continues only when this is not "stop".
modelmetadata.tanstackSSE [DONE] fallback and generation reconstruct.

finishReason is one of "stop", "length", "content_filter", "tool_calls", or null.

If you omit finishReason after a tool result, the client treats it as not "stop" and can send another turn.

Read it on the client

typescript
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";

const { messages } = useChat({
  connection: fetchServerSentEvents("/api/chat"),
  onChunk: (chunk) => {
    if (chunk.type === "RUN_FINISHED") {
      console.log(chunk.usage);
      console.log(chunk.metadata?.tanstack?.finishReason);
      console.log(chunk.metadata?.tanstack?.model);
    }
  },
});
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";

const { messages } = useChat({
  connection: fetchServerSentEvents("/api/chat"),
  onChunk: (chunk) => {
    if (chunk.type === "RUN_FINISHED") {
      console.log(chunk.usage);
      console.log(chunk.metadata?.tanstack?.finishReason);
      console.log(chunk.metadata?.tanstack?.model);
    }
  },
});

Do not import a helper. Check chunk.type, then read chunk.metadata?.tanstack.

RUN_ERROR

AG-UI RUN_ERROR has message and optional code at the top. Put correlation ids in metadata.tanstack:

json
{
  "type": "RUN_ERROR",
  "message": "Provider timeout",
  "code": "TIMEOUT",
  "metadata": {
    "tanstack": {
      "threadId": "thread-1",
      "runId": "run-1",
      "model": "gpt-5.5"
    }
  }
}
{
  "type": "RUN_ERROR",
  "message": "Provider timeout",
  "code": "TIMEOUT",
  "metadata": {
    "tanstack": {
      "threadId": "thread-1",
      "runId": "run-1",
      "model": "gpt-5.5"
    }
  }
}

Rate limit? Put the wait in metadata.tanstack.retryAfterMs, in milliseconds. Clients read it from there. See Rate limits.

Later

Add these when you use the matching feature:

  • Leftover usage. Spec usage[] also accepts cachedInputTokens and reasoningTokens. Put cost and other leftover fields in metadata.tanstack.usage. The client rebuilds TanStack TokenUsage (promptTokens) from the array plus that leftover.
  • Interrupt errors. On RUN_ERROR, set metadata.tanstack.interruptErrors so ChatClient can match a failed interrupt submit.
  • Tool output error. On TOOL_CALL_RESULT, set metadata.tanstack.state to "output-error" when the tool result is an error payload.
  • Message stamps. On wire messages, metadata.tanstack.createdAt is an ISO-8601 string.
  • Signatures. Stream thinking signatures on REASONING_ENCRYPTED_VALUE. On the next-turn body, set spec encryptedValue on role: "reasoning" messages and on toolCalls. Other tool-call provider fields stay in metadata.tanstack.toolCallMetadata. See Thinking & Reasoning.

A TanStack chat() server already writes this bag. Use this page when you emit AG-UI events yourself.

See Stream Events for the event table, and AG-UI Client Compliance for the request body.