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.
Put headers in headers. Put extra JSON in body:
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.
Read the header from the request. Read the body data from forwardedProps:
// 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.
If the token never changes, pass a plain object:
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.
If the URL depends on per-request state (for example, the current user), pass a function for the URL too:
import { fetchServerSentEvents } from "@tanstack/ai-react";
import { currentUserId, getToken } from "./auth";
export const connection = fetchServerSentEvents(
() => `/api/chat?user=${currentUserId}`,
() => ({
headers: { Authorization: `Bearer ${getToken()}` },
}),
);Pass extra JSON for one send in the second argument of sendMessage:
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:
// 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);
}body on the transport and forwardedProps fill the same wire field. Later sources win on the same key:
Two more rules:
To keep SSE or HTTP streaming but wrap fetch (auth refresh, retries, logs, or an edge proxy), pass a fetchClient:
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.