Skip to content
Transports

Request Options

Transport options send an auth token, a user id, a model choice, or per-message data with the messages.

The examples use fetchServerSentEvents. fetchHttpStream and fetchJson take the same options. The XHR transports take headers and body too, and use withCredentials for cookies.

Quick setup

1. Send a header and extra data from the client

Put headers in headers. Put extra JSON in body:

tsx
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import { getToken } from "./auth";

export function Chat() {
  const { messages, sendMessage } = useChat({
    connection: fetchServerSentEvents("/api/chat", () => ({
      headers: { Authorization: `Bearer ${getToken()}` },
      body: { provider: "openai", model: "gpt-6.1-sol" },
    })),
    threadId: "support-chat",
  });

  return (
    <div>
      {messages.map((message) => (
        <p key={message.id}>
          {message.parts.map((part) =>
            part.type === "text" ? part.content : "",
          )}
        </p>
      ))}
      <button onClick={() => void sendMessage("Hello")}>Send</button>
    </div>
  );
}

The options are a function, so the transport calls it on every send. The header always has the latest token.

2. Read them on the server

Read the header from the request. Read the body data from forwardedProps:

ts
// api/chat.ts
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

export async function POST(request: Request) {
  const authorization = request.headers.get("Authorization");
  if (!authorization) {
    return new Response("Unauthorized", { status: 401 });
  }

  const { messages, threadId, runId, forwardedProps } =
    await chatParamsFromRequest(request);
  console.log("Client asked for", forwardedProps.model);

  const stream = chat({
    adapter: openaiText("gpt-6.1-sol"),
    messages,
    threadId,
    runId,
  });
  return toServerSentEventsResponse(stream);
}

Click Send. Your server gets the header, the body data, and the messages in one request.

How it works

  • headers go on the POST request.
  • body goes into the AG-UI forwardedProps field of the request body. The model does not see it.
  • Cookies go with the request when credentials is "same-origin" (the default) or "include".

Advanced

Static headers

If the token never changes, pass a plain object:

ts
import { fetchServerSentEvents } from "@tanstack/ai-react";
import { token } from "./auth";

export const connection = fetchServerSentEvents("/api/chat", {
  headers: { Authorization: `Bearer ${token}` },
});

Pass this connection to useChat.

Change the URL per request

If the URL depends on per-request state (for example, the current user), pass a function for the URL too:

ts
import { fetchServerSentEvents } from "@tanstack/ai-react";
import { currentUserId, getToken } from "./auth";

export const connection = fetchServerSentEvents(
  () => `/api/chat?user=${currentUserId}`,
  () => ({
    headers: { Authorization: `Bearer ${getToken()}` },
  }),
);

Send data with one message

Pass extra JSON for one send in the second argument of sendMessage:

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

export function SummarizeButton() {
  const { sendMessage } = useChat({
    connection: fetchServerSentEvents("/api/chat"),
    threadId: "support-chat",
    forwardedProps: { provider: "openai" },
  });

  return (
    <button
      onClick={() =>
        void sendMessage("Summarize the attached files", {
          body: { attachmentIds: ["att_1", "att_2"] },
        })
      }
    >
      Summarize
    </button>
  );
}

The client does a shallow merge of this body into the forwardedProps option. If a key is in both, the sendMessage value wins. The merge applies to this request only.

Read the merged object on the server. If the model must not see those keys, do not copy them into messages:

ts
// api/chat.ts
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

export async function POST(request: Request) {
  const { messages, forwardedProps, threadId } = await chatParamsFromRequest(request);
  const { attachmentIds } = forwardedProps;
  if (Array.isArray(attachmentIds) && attachmentIds.length > 0) {
    // Look up the uploads. Do not add them to `messages`.
  }
  const stream = chat({
    adapter: openaiText("gpt-6.1-sol"),
    messages,
    threadId,
  });
  return toServerSentEventsResponse(stream);
}

Merge order

body on the transport and forwardedProps fill the same wire field. Later sources win on the same key:

  1. The body option of the transport: static defaults.
  2. The forwardedProps option of useChat.
  3. The body of one sendMessage call.

Two more rules:

  • ChatClient: sendMessage(content, body, sendOptions) takes the extra JSON as its second argument. The client does a shallow merge of it with sendOptions.body. If a key is in both, sendOptions.body wins.
  • reload(): it starts a new request with the transport body and the forwardedProps option. It does not send the per-call body of the previous send again.

Wrap fetch

To keep SSE or HTTP streaming but wrap fetch (auth refresh, retries, logs, or an edge proxy), pass a fetchClient:

ts
import { fetchServerSentEvents } from "@tanstack/ai-react";
import { refreshToken } from "./auth";

async function authedFetch(input: RequestInfo | URL, init?: RequestInit) {
  let response = await fetch(input, init);
  if (response.status === 401) {
    await refreshToken();
    response = await fetch(input, init);
  }
  return response;
}

export const connection = fetchServerSentEvents("/api/chat", {
  fetchClient: authedFetch,
});

The fetchClient must have the standard fetch signature. fetchHttpStream accepts the same option. For a production example, see the Cloudflare Adapter.