- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
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:
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:
// 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:
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:
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:
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);
}Merge order#
body on the transport and forwardedProps fill the same wire field. Later sources win on the same key:
- The body option of the transport: static defaults.
- The forwardedProps option of useChat.
- 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:
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.