API

@tanstack/ai

The core AI library for TanStack AI.

Installation

sh
npm install @tanstack/ai
npm install @tanstack/ai

chat(options)

Creates a streaming chat response.

typescript
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const stream = chat({
  adapter: openaiText("gpt-5.2"),
  messages: [{ role: "user", content: "Hello!" }],
  tools: [myTool],
  systemPrompts: ["You are a helpful assistant"],
  agentLoopStrategy: maxIterations(20),
});
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const stream = chat({
  adapter: openaiText("gpt-5.2"),
  messages: [{ role: "user", content: "Hello!" }],
  tools: [myTool],
  systemPrompts: ["You are a helpful assistant"],
  agentLoopStrategy: maxIterations(20),
});

Parameters

  • adapter - An AI adapter instance with model (e.g., openaiText('gpt-5.2'), anthropicText('claude-sonnet-4-5'))
  • messages - Array of chat messages. Accepts mixed UIMessage | ModelMessage arrays — internal conversion handles AG-UI fan-out dedup, drops reasoning/activity, and collapses developersystem
  • tools? - Array of tools for function calling
  • context? - Typed runtime context passed to server tools and middleware. If a tool or middleware declares a concrete context type, chat() requires a compatible value here
  • systemPrompts? - System prompts to prepend to messages
  • agentLoopStrategy? - Strategy for agent loops (default: maxIterations(5))
  • abortController? - AbortController for cancellation
  • modelOptions? - Provider-native model options. This is where sampling parameters live — temperature, top_p/topP, and the provider's token-limit key (max_output_tokens, max_tokens, maxOutputTokens, …) — under each provider's canonical name, rather than as generic root-level props. See Moving Sampling Options into modelOptions. (Renamed from providerOptions.)
  • threadId? - AG-UI thread identifier propagated into RUN_STARTED events for run correlation
  • runId? - AG-UI run identifier (auto-generated if omitted)
  • parentRunId? - AG-UI parent run identifier for nested runs

Returns

An async iterable of StreamChunk.

summarize(options)

Creates a text summarization.

typescript
import { summarize } from "@tanstack/ai";
import { openaiSummarize } from "@tanstack/ai-openai";

const result = await summarize({
  adapter: openaiSummarize("gpt-5.2"),
  text: "Long text to summarize...",
  maxLength: 100,
  style: "concise",
});
import { summarize } from "@tanstack/ai";
import { openaiSummarize } from "@tanstack/ai-openai";

const result = await summarize({
  adapter: openaiSummarize("gpt-5.2"),
  text: "Long text to summarize...",
  maxLength: 100,
  style: "concise",
});

Parameters

  • adapter - An AI adapter instance with model
  • text - Text to summarize
  • maxLength? - Maximum length of summary
  • style? - Summary style ("concise" | "detailed")
  • modelOptions? - Model-specific options

Returns

A SummarizationResult with the summary text.

toolDefinition(config)

Creates an isomorphic tool definition that can be instantiated for server or client execution.

typescript
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";

const myToolDef = toolDefinition({
  name: "my_tool",
  description: "Tool description",
  inputSchema: z.object({
    param: z.string(),
  }),
  outputSchema: z.object({
    result: z.string(),
  }),
  needsApproval: false, // Optional
});

// Or create client implementation
const myClientTool = myToolDef.client(async ({ param }) => {
  // Client-side implementation
  return { result: "..." };
});

// Use directly in chat() (server-side, no execute)
chat({
  adapter: openaiText("gpt-5.2"),
  tools: [myToolDef],
  messages: [{ role: "user", content: "..." }],
});

// Or create server implementation
const myServerTool = myToolDef.server(async ({ param }) => {
  // Server-side implementation
  return { result: "..." };
});

// Use directly in chat() (server-side, no execute)
chat({
  adapter: openaiText("gpt-5.2"),
  tools: [myServerTool],
  messages: [{ role: "user", content: "..." }],
});
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";

const myToolDef = toolDefinition({
  name: "my_tool",
  description: "Tool description",
  inputSchema: z.object({
    param: z.string(),
  }),
  outputSchema: z.object({
    result: z.string(),
  }),
  needsApproval: false, // Optional
});

// Or create client implementation
const myClientTool = myToolDef.client(async ({ param }) => {
  // Client-side implementation
  return { result: "..." };
});

// Use directly in chat() (server-side, no execute)
chat({
  adapter: openaiText("gpt-5.2"),
  tools: [myToolDef],
  messages: [{ role: "user", content: "..." }],
});

// Or create server implementation
const myServerTool = myToolDef.server(async ({ param }) => {
  // Server-side implementation
  return { result: "..." };
});

// Use directly in chat() (server-side, no execute)
chat({
  adapter: openaiText("gpt-5.2"),
  tools: [myServerTool],
  messages: [{ role: "user", content: "..." }],
});

Tools can declare typed runtime context for request-scoped dependencies:

typescript
type AppContext = {
  userId: string;
  db: { users: { findName(id: string): Promise<string> } };
};

const currentUser = toolDefinition({
  name: "current_user",
  description: "Get the current user",
}).server<AppContext>(async (_input, ctx) => {
  return { name: await ctx.context.db.users.findName(ctx.context.userId) };
});

chat({
  adapter: openaiText("gpt-5.2"),
  messages,
  tools: [currentUser],
  context: { userId: session.user.id, db },
});
type AppContext = {
  userId: string;
  db: { users: { findName(id: string): Promise<string> } };
};

const currentUser = toolDefinition({
  name: "current_user",
  description: "Get the current user",
}).server<AppContext>(async (_input, ctx) => {
  return { name: await ctx.context.db.users.findName(ctx.context.userId) };
});

chat({
  adapter: openaiText("gpt-5.2"),
  messages,
  tools: [currentUser],
  context: { userId: session.user.id, db },
});

Parameters

  • name - Tool name (must be unique)
  • description - Tool description for the model
  • inputSchema - Zod schema for input validation
  • outputSchema? - Zod schema for output validation
  • needsApproval? - Whether tool requires user approval
  • metadata? - Additional metadata

Returns

A ToolDefinition object with .server() and .client() methods for creating concrete implementations.

toServerSentEventsStream(stream, abortController?)

Converts a stream to a ReadableStream in Server-Sent Events format.

typescript
import { chat, toServerSentEventsStream } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const stream = chat({
  adapter: openaiText("gpt-5.2"),
  messages: [...],
});
const readableStream = toServerSentEventsStream(stream);
import { chat, toServerSentEventsStream } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const stream = chat({
  adapter: openaiText("gpt-5.2"),
  messages: [...],
});
const readableStream = toServerSentEventsStream(stream);

Parameters

  • stream - Async iterable of StreamChunk
  • abortController? - Optional AbortController to abort when stream is cancelled

Returns

A ReadableStream<Uint8Array> in Server-Sent Events format. Each chunk is:

  • Prefixed with "data: "
  • Followed by "\n\n"
  • Stream ends with "data: [DONE]\n\n"

toServerSentEventsResponse(stream, init?)

Converts a stream to an HTTP Response with proper SSE headers.

typescript
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const stream = chat({
  adapter: openaiText("gpt-5.2"),
  messages: [...],
});
return toServerSentEventsResponse(stream);
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const stream = chat({
  adapter: openaiText("gpt-5.2"),
  messages: [...],
});
return toServerSentEventsResponse(stream);

Parameters

  • stream - Async iterable of StreamChunk
  • init? - Optional ResponseInit options (including abortController)

Returns

A Response object suitable for HTTP endpoints with SSE headers (Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive).

chatParamsFromRequest(req)

Reads an HTTP Request, parses its JSON body, and validates it against AG-UI RunAgentInputSchema. Returns parsed chat parameters ready to spread into chat(). On a malformed body, throws a 400 Response that frameworks like TanStack Start, SolidStart, Remix, and React Router 7 return to the client automatically.

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

export async function POST(req: Request) {
  const params = await chatParamsFromRequest(req);
  const stream = chat({
    adapter: openaiText("gpt-4o"),
    messages: params.messages,
    tools: serverTools,
  });
  return toServerSentEventsResponse(stream);
}
import { chat, chatParamsFromRequest, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

export async function POST(req: Request) {
  const params = await chatParamsFromRequest(req);
  const stream = chat({
    adapter: openaiText("gpt-4o"),
    messages: params.messages,
    tools: serverTools,
  });
  return toServerSentEventsResponse(stream);
}

Parameters

  • req - An incoming Request whose JSON body conforms to AG-UI RunAgentInput

Returns

A promise resolving to { messages, threadId, runId, parentRunId?, tools, forwardedProps, state, aguiContext, context }.

The returned aguiContext is the AG-UI protocol RunAgentInput.context field. It is not the same as TanStack AI runtime chat({ context }); validate and map it explicitly if you want those values available to tools or middleware.

The returned context field is a deprecated alias of aguiContext kept for backward compatibility. Prefer aguiContext in new code.

Framework note. Next.js Route Handlers, SvelteKit, Hono, and raw Node do not auto-handle thrown Response objects. In those, wrap with try/catch or use chatParamsFromRequestBody(await req.json()) directly.

chatParamsFromRequestBody(body)

Lower-level variant of chatParamsFromRequest that validates an already-parsed body. Rejects with an AGUIError on malformed input. Use this when you need explicit error handling control.

typescript
const body = await req.json();
try {
  const params = await chatParamsFromRequestBody(body);
  // ...
} catch (error) {
  return new Response(error.message, { status: 400 });
}
const body = await req.json();
try {
  const params = await chatParamsFromRequestBody(body);
  // ...
} catch (error) {
  return new Response(error.message, { status: 400 });
}

mergeAgentTools(serverTools, clientTools)

Merges a server-side tool registry with the AG-UI client-declared tools received in the request payload. Server tools win on name collision; client-only tools become no-execute stubs that the runtime dispatches via ClientToolRequest events.

typescript
import { chat, chatParamsFromRequest, mergeAgentTools } from "@tanstack/ai";

const params = await chatParamsFromRequest(req);
const stream = chat({
  adapter: openaiText("gpt-4o"),
  messages: params.messages,
  tools: mergeAgentTools(serverTools, params.tools),
});
import { chat, chatParamsFromRequest, mergeAgentTools } from "@tanstack/ai";

const params = await chatParamsFromRequest(req);
const stream = chat({
  adapter: openaiText("gpt-4o"),
  messages: params.messages,
  tools: mergeAgentTools(serverTools, params.tools),
});

Parameters

  • serverTools - The server's toolDefinition().server(...) registry, keyed by tool name
  • clientTools - The tools array from chatParamsFromRequest's return value

Returns

A merged tool record suitable for chat({ tools }).

maxIterations(count)

Creates an agent loop strategy that limits iterations.

typescript
import { chat, maxIterations } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const stream = chat({
  adapter: openaiText("gpt-5.2"),
  messages: [...],
  agentLoopStrategy: maxIterations(20),
});
import { chat, maxIterations } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const stream = chat({
  adapter: openaiText("gpt-5.2"),
  messages: [...],
  agentLoopStrategy: maxIterations(20),
});

Parameters

  • count - Maximum number of tool execution iterations

Returns

An AgentLoopStrategy object.

Types

ModelMessage

typescript
interface ModelMessage {
  role: "user" | "assistant" | "system" | "tool";
  content: string;
  toolCallId?: string;
}
interface ModelMessage {
  role: "user" | "assistant" | "system" | "tool";
  content: string;
  toolCallId?: string;
}

StreamChunk

typescript
type StreamChunk =
  | ContentStreamChunk
  | ThinkingStreamChunk
  | ToolCallStreamChunk
  | ToolResultStreamChunk
  | DoneStreamChunk
  | ErrorStreamChunk;

interface ThinkingStreamChunk {
  type: "thinking";
  id: string;
  model: string;
  timestamp: number;
  delta?: string; // Incremental thinking token
  content: string; // Accumulated thinking content
}
type StreamChunk =
  | ContentStreamChunk
  | ThinkingStreamChunk
  | ToolCallStreamChunk
  | ToolResultStreamChunk
  | DoneStreamChunk
  | ErrorStreamChunk;

interface ThinkingStreamChunk {
  type: "thinking";
  id: string;
  model: string;
  timestamp: number;
  delta?: string; // Incremental thinking token
  content: string; // Accumulated thinking content
}

Stream chunks represent different types of data in the stream:

  • Content chunks - Text content being generated
  • Thinking chunks - Model's reasoning process (when supported by the model)
  • Tool call chunks - When the model calls a tool
  • Tool result chunks - Results from tool execution
  • Done chunks - Stream completion
  • Error chunks - Stream errors

Tool

typescript
interface Tool<TContext = unknown> {
  name: string;
  description: string;
  inputSchema?: SchemaInput;
  outputSchema?: SchemaInput;
  execute?: (
    args: any,
    context?: ToolExecutionContext<TContext>
  ) => Promise<any> | any;
  needsApproval?: boolean;
  lazy?: boolean;
  metadata?: Record<string, any>;
}
interface Tool<TContext = unknown> {
  name: string;
  description: string;
  inputSchema?: SchemaInput;
  outputSchema?: SchemaInput;
  execute?: (
    args: any,
    context?: ToolExecutionContext<TContext>
  ) => Promise<any> | any;
  needsApproval?: boolean;
  lazy?: boolean;
  metadata?: Record<string, any>;
}

ToolExecutionContext<TContext>

typescript
type ToolExecutionContext<TContext = unknown> = {
  toolCallId?: string;
  emitCustomEvent: (eventName: string, value: Record<string, any>) => void;
} & (unknown extends TContext ? { context?: TContext } : { context: TContext });
type ToolExecutionContext<TContext = unknown> = {
  toolCallId?: string;
  emitCustomEvent: (eventName: string, value: Record<string, any>) => void;
} & (unknown extends TContext ? { context?: TContext } : { context: TContext });

context is the runtime value from chat({ context }) for server tools, or from ChatClient / framework hook options for client tools. It is required when a tool declares a concrete TContext and optional for untyped tools where the context type is unknown.

ChatMiddleware<TContext>

typescript
interface ChatMiddleware<TContext = unknown> {
  name?: string;
  onStart?: (ctx: ChatMiddlewareContext<TContext>) => void | Promise<void>;
  onChunk?: (
    ctx: ChatMiddlewareContext<TContext>,
    chunk: StreamChunk
  ) => void | StreamChunk | StreamChunk[] | null | Promise<void | StreamChunk | StreamChunk[] | null>;
  onBeforeToolCall?: (
    ctx: ChatMiddlewareContext<TContext>,
    hookCtx: ToolCallHookContext
  ) => BeforeToolCallDecision | Promise<BeforeToolCallDecision>;
  onAfterToolCall?: (
    ctx: ChatMiddlewareContext<TContext>,
    info: AfterToolCallInfo
  ) => void | Promise<void>;
  onFinish?: (
    ctx: ChatMiddlewareContext<TContext>,
    info: FinishInfo
  ) => void | Promise<void>;
  onAbort?: (
    ctx: ChatMiddlewareContext<TContext>,
    info: AbortInfo
  ) => void | Promise<void>;
  onError?: (
    ctx: ChatMiddlewareContext<TContext>,
    info: ErrorInfo
  ) => void | Promise<void>;
}

interface ChatMiddlewareContext<TContext = unknown> {
  requestId: string;
  streamId: string;
  threadId: string;
  phase: ChatMiddlewarePhase;
  iteration: number;
  context: TContext;
  abort(reason?: string): void;
  defer(promise: Promise<unknown>): void;
}
interface ChatMiddleware<TContext = unknown> {
  name?: string;
  onStart?: (ctx: ChatMiddlewareContext<TContext>) => void | Promise<void>;
  onChunk?: (
    ctx: ChatMiddlewareContext<TContext>,
    chunk: StreamChunk
  ) => void | StreamChunk | StreamChunk[] | null | Promise<void | StreamChunk | StreamChunk[] | null>;
  onBeforeToolCall?: (
    ctx: ChatMiddlewareContext<TContext>,
    hookCtx: ToolCallHookContext
  ) => BeforeToolCallDecision | Promise<BeforeToolCallDecision>;
  onAfterToolCall?: (
    ctx: ChatMiddlewareContext<TContext>,
    info: AfterToolCallInfo
  ) => void | Promise<void>;
  onFinish?: (
    ctx: ChatMiddlewareContext<TContext>,
    info: FinishInfo
  ) => void | Promise<void>;
  onAbort?: (
    ctx: ChatMiddlewareContext<TContext>,
    info: AbortInfo
  ) => void | Promise<void>;
  onError?: (
    ctx: ChatMiddlewareContext<TContext>,
    info: ErrorInfo
  ) => void | Promise<void>;
}

interface ChatMiddlewareContext<TContext = unknown> {
  requestId: string;
  streamId: string;
  threadId: string;
  phase: ChatMiddlewarePhase;
  iteration: number;
  context: TContext;
  abort(reason?: string): void;
  defer(promise: Promise<unknown>): void;
}

See Runtime Context for the recommended context patterns.

Usage Examples

typescript
import { chat, summarize, generateImage } from "@tanstack/ai";
import {
  openaiText,
  openaiSummarize,
  openaiImage,
} from "@tanstack/ai-openai";

// --- Streaming chat
const stream = chat({
  adapter: openaiText("gpt-5.2"),
  messages: [{ role: "user", content: "Hello!" }],
});

// --- One-shot chat response (stream: false)
const response = await chat({
  adapter: openaiText("gpt-5.2"),
  messages: [{ role: "user", content: "What's the capital of France?" }],
  stream: false, // Returns a Promise<string> instead of AsyncIterable
});

// --- Structured response with outputSchema
import { z } from "zod";
const parsed = await chat({
  adapter: openaiText("gpt-5.2"),
  messages: [{ role: "user", content: "Summarize this text in JSON with keys 'summary' and 'keywords': ... " }],
  outputSchema: z.object({
    summary: z.string(),
    keywords: z.array(z.string()),
  }),
});

// --- Structured response with tools
import { toolDefinition } from "@tanstack/ai";
const weatherTool = toolDefinition({
  name: "getWeather",
  description: "Get the current weather for a city",
  inputSchema: z.object({
    city: z.string().meta({ description: "City name" }),
  }),
}).server(async ({ city }) => {
  // Implementation that fetches weather info
  return JSON.stringify({ temperature: 72, condition: "Sunny" });
});

const toolResult = await chat({
  adapter: openaiText("gpt-5.2"),
  messages: [
    { role: "user", content: "What's the weather in Paris?" }
  ],
  tools: [weatherTool],
  outputSchema: z.object({
    answer: z.string(),
    weather: z.object({
      temperature: z.number(),
      condition: z.string(),
    }),
  }),
});

// --- Summarization
const summary = await summarize({
  adapter: openaiSummarize("gpt-5.2"),
  text: "Long text to summarize...",
  maxLength: 100,
});

// --- Image generation
const image = await generateImage({
  adapter: openaiImage("dall-e-3"),
  prompt: "A futuristic city skyline at sunset",
  numberOfImages: 1,
  size: "1024x1024",
});
import { chat, summarize, generateImage } from "@tanstack/ai";
import {
  openaiText,
  openaiSummarize,
  openaiImage,
} from "@tanstack/ai-openai";

// --- Streaming chat
const stream = chat({
  adapter: openaiText("gpt-5.2"),
  messages: [{ role: "user", content: "Hello!" }],
});

// --- One-shot chat response (stream: false)
const response = await chat({
  adapter: openaiText("gpt-5.2"),
  messages: [{ role: "user", content: "What's the capital of France?" }],
  stream: false, // Returns a Promise<string> instead of AsyncIterable
});

// --- Structured response with outputSchema
import { z } from "zod";
const parsed = await chat({
  adapter: openaiText("gpt-5.2"),
  messages: [{ role: "user", content: "Summarize this text in JSON with keys 'summary' and 'keywords': ... " }],
  outputSchema: z.object({
    summary: z.string(),
    keywords: z.array(z.string()),
  }),
});

// --- Structured response with tools
import { toolDefinition } from "@tanstack/ai";
const weatherTool = toolDefinition({
  name: "getWeather",
  description: "Get the current weather for a city",
  inputSchema: z.object({
    city: z.string().meta({ description: "City name" }),
  }),
}).server(async ({ city }) => {
  // Implementation that fetches weather info
  return JSON.stringify({ temperature: 72, condition: "Sunny" });
});

const toolResult = await chat({
  adapter: openaiText("gpt-5.2"),
  messages: [
    { role: "user", content: "What's the weather in Paris?" }
  ],
  tools: [weatherTool],
  outputSchema: z.object({
    answer: z.string(),
    weather: z.object({
      temperature: z.number(),
      condition: z.string(),
    }),
  }),
});

// --- Summarization
const summary = await summarize({
  adapter: openaiSummarize("gpt-5.2"),
  text: "Long text to summarize...",
  maxLength: 100,
});

// --- Image generation
const image = await generateImage({
  adapter: openaiImage("dall-e-3"),
  prompt: "A futuristic city skyline at sunset",
  numberOfImages: 1,
  size: "1024x1024",
});

Next Steps