Tool Calling and UI Generation with the AI SDK
Generative UI means the model calls tools, and your React tree turns tool results into real components - cards, tables, forms - not only markdown.
This page wires a server tool into streamText, streams typed tool parts to useChat, and maps those parts to components.
Summary
Define tools with description, inputSchema, and optional execute; stream them through a UI message response; on the client, switch on part.type (tool-${name}) and part.state to render loading, result, and error UI.
Recipe
- Create a tools module with
tool()(or plain tool objects) and ZodinputSchema. - Register tools on
streamText(or an agent) and bound steps withstopWhen. - Return a UI message stream response from the App Router route.
- Render
message.partsin the client; map each tool part type to a component. - Handle part states: input streaming/available, output available, output error.
- For client-only tools, omit
executeand useonToolCall+addToolOutput. - Optionally auto-continue with
sendAutomaticallyWhenwhen client tool results are complete.
Working Example
ai/tools.ts:
import { tool } from "ai";
import { z } from "zod";
export const displayWeather = tool({
description: "Show the weather for a location in the chat UI",
inputSchema: z.object({
location: z.string().describe("City or place name"),
}),
execute: async ({ location }) => {
// Replace with a real weather API
return { location, weather: "Sunny", temperature: 22 };
},
});
export const tools = { displayWeather };app/api/chat/route.ts:
import {
convertToModelMessages,
isStepCount,
streamText,
type UIMessage,
} from "ai";
import { tools } from "@/ai/tools";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: "openai/gpt-4o-mini", // verify at build
instructions: "Use displayWeather when the user asks about weather.",
messages: await convertToModelMessages(messages),
tools,
stopWhen: isStepCount(5),
});
return result.toUIMessageStreamResponse();
}components/weather.tsx + client render sketch:
"use client";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";
function Weather(props: {
location: string;
weather: string;
temperature: number;
}) {
return (
<div>
<h3>{props.location}</h3>
<p>
{props.weather}, {props.temperature}°C
</p>
</div>
);
}
export default function Page() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
return (
<div>
{messages.map((m) => (
<div key={m.id}>
{m.parts.map((part, i) => {
if (part.type === "text") return <span key={i}>{part.text}</span>;
if (part.type === "tool-displayWeather") {
if (part.state === "input-available")
return <div key={i}>Loading weather…</div>;
if (part.state === "output-available")
return <Weather key={i} {...part.output} />;
if (part.state === "output-error")
return <div key={i}>Error: {part.errorText}</div>;
}
return null;
})}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
</div>
);
}Deep Dive
Why tools are the UI contract
The model should not invent HTML.
It should call a named tool with structured input; your app owns presentation.
That separation keeps theming, accessibility, and validation under engineering control.
Server tools vs client tools
| Pattern | How it works | Use when |
|---|---|---|
Server execute | Route runs the tool; result streams back | Secrets, DB, privileged APIs |
Client onToolCall + addToolOutput | Browser runs the tool; result sent back into chat | Device APIs, pure UI helpers |
| Interactive client tool (no auto execute) | UI shows confirm/input; addToolOutput after user action | Confirmations, form fills |
| Approval-gated server tool | Pause for approve/deny before execute | Writes, spend, irreversible actions |
Part states drive UX
Treat tool parts as a small state machine:
- input-streaming / input-available - show skeleton or partial args
- output-available - mount the real component with
part.output - output-error - show recoverable error UI
- approval-requested (when using tool approval) - show approve/deny controls
Avoid assuming every tool finishes in one frame; multi-step agents interleave text and tools.
Description quality
Tool selection is only as good as description + schema field .describe() text.
Scope the tool: "weather for a place" beats "general helper."
One tool per UI surface often beats a mega-tool returning arbitrary JSON.
Type safety
Share Zod schemas or inferred types between tool output and React props.
When part types are generated or narrowed, prefer part.type === 'tool-displayWeather' switches over stringly toolName bags.
Multi-step continuation
After server tools run, stopWhen decides whether the model gets another step to narrate results.
After client tools, configure sendAutomaticallyWhen (for example lastAssistantMessageIsCompleteWithToolCalls) so the user does not click "continue" manually.
Generative UI is not free form
Limit which tools exist; do not let the model emit arbitrary component names.
Each tool is an allowlisted UI capability.
Gotchas
- Rendering
contentonly. Tool UIs never appear if you ignoreparts. - Mismatched tool names. Part type
tool-Xmust match the tools object keyX. - Awaiting inside
onToolCallincorrectly. Some guides warn that improperawaitpatterns can deadlock; follow current AI SDK tool-usage docs. - No
stopWhen. Multi-step tools can loop until platform timeout. - Passing secrets to the client. Anything needed for tool
executemust stay server-side. - Treating outputs as trusted HTML. Still sanitize if you ever interpolate strings into
dangerouslySetInnerHTML. - Version drift on response helpers.
toUIMessageStreamResponsevscreateUIMessageStreamResponse- verify at build for your AI SDK major.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| Generative UI via tools | Typed, allowlisted components | More React wiring |
| Markdown-only chat | Simple | Weak for structured data |
| Server-driven RSC streamables | Tight server components story | Different mental model than useChat |
| Custom WebSocket component protocol | Full control | You own compatibility |
| Client tools only | Great for browser APIs | No secret server actions |
FAQs
Is generative UI the same as the model writing JSX?
No. The model calls tools; your code maps tool results to components you wrote.
Can one assistant message contain multiple tool UIs?
Yes. parts is ordered and can mix text with several tool parts.
Should every tool render a component?
No. Some tools are invisible plumbing (search, fetch). Render components when humans benefit from structure.
How do I type `part.output`?
Infer from the tool's execute return type or define a shared type; cast carefully until your SDK version offers stronger narrowing.
What about streaming tool arguments?
Some providers stream partial tool input. Use input-streaming states for progressive UX when available.
Can tools return React elements from the server?
Prefer serializable data in tool results and render components on the client from that data. That keeps the stream protocol stable.
How do client confirmation tools differ from tool approval?
Confirmation tools often use a client tool without execute and addToolOutput. Tool approval (AI SDK 6+) is a first-class pause for server tool execution with approval request/response parts.
Do I need Zod?
Schemas can be Zod or JSON schema depending on version/docs. Zod is the common TypeScript path.
How do I test generative UI?
Unit-test pure components with fixture outputs; integration-test the route with mocked model tool calls if your stack supports it.
Can LangGraph tools drive the same UI?
Yes if the backend emits compatible UI message / data stream events or you map tool events to the AI SDK stream format.
Related
- Vercel AI SDK Basics - chat scaffold
- The Vercel AI SDK Mental Model: Streaming UI Meets Agent Loops - parts and streams
- AI SDK 6's Tool-Approval States for Human-in-the-Loop Agents - approve before execute
- Chaining, Routing, and Evaluator-Optimizer Agent Patterns - multi-step workflows
- Vercel AI SDK Best Practices - habits
- Tool Use and Function Calling Basics - generic tool concepts
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.