Deep-dive topics for stronger typing and larger integrations.
With InferUITools in AI SDK 5, you can infer tool input/output types from your tool set and get fully typed message.parts in the UI.
import { tool } from 'ai';import { z } from 'zod';import { serializableMediaCardSchema } from '@/components/tool-ui/media-card/schema';export const tools = { previewLink: tool({ description: 'Return a simple link preview', inputSchema: z.object({ url: z.string().url() }), outputSchema: serializableMediaCardSchema, async execute({ url }) { return { id: 'link-1', kind: 'link', href: url, title: 'React Server Components', thumb: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=1200', }; }, }),} as const;// Export a type only; the client should import types, not server code.export type Tools = typeof tools;import { streamText, convertToModelMessages } from 'ai';import { openai } from '@ai-sdk/openai';import { tools } from '@/lib/tools';export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), tools, }); return result.toUIMessageStreamResponse();}'use client';import { useChat } from '@ai-sdk/react';import { InferUITools } from 'ai';import type { Tools } from '@/lib/tools';import { MediaCard, parseSerializableMediaCard } from '@/components/tool-ui/media-card';type MyUITools = InferUITools<Tools>;export default function Chat() { const { messages } = useChat<MyUITools>({ api: '/api/chat' }); return ( <div> {messages.map((m) => ( <div key={m.id}> {m.parts.map((part, i) => { // Fully typed: 'tool-previewLink' with correct output shape if (part.type === 'tool-previewLink' && part.state === 'output-available') { const card = parseSerializableMediaCard(part.output); return <MediaCard key={i} {...card} />; } return null; })} </div> ))} </div> );}Keep runtime boundaries clean
Export only types from lib/tools.ts to the client; never import server code into the browser.
Single-tool helpers
For a single tool, InferUITool works the same way as InferUITools but for one tool definition.
End-to-end validation
Use the component's outputSchema on the server and parseSerializableX on the client to validate at both ends.
Lifecycle-aware rendering
Use part.state to respect the Tool UI lifecycle from UI Guidelines:
state === 'invocation' → show intent / shellstate === 'output-pending' → show skeleton / loading shellstate === 'output-available' → render the full componentstate === 'errored' → show an error surface instead of silently failingimport { tool } from 'ai';
import { z } from 'zod';
import { serializableMediaCardSchema } from '@/components/tool-ui/media-card/schema';
export const tools = {
previewLink: tool({
description: 'Return a simple link preview',
inputSchema: z.object({ url: z.string().url() }),
outputSchema: serializableMediaCardSchema,
async execute({ url }) {
return {
id: 'link-1',
kind: 'link',
href: url,
title: 'React Server Components',
thumb:
'https://images.unsplash.com/photo-1633356122544-f134324a6cee?auto=format&fit=crop&q=80&w=1200',
};
},
}),
} as const;
// Export a type only; the client should import types, not server code.
export type Tools = typeof tools;import { streamText, convertToModelMessages } from 'ai';
import { openai } from '@ai-sdk/openai';
import { tools } from '@/lib/tools';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
tools,
});
return result.toUIMessageStreamResponse();
}'use client';
import { useChat } from '@ai-sdk/react';
import { InferUITools } from 'ai';
import type { Tools } from '@/lib/tools';
import { MediaCard, parseSerializableMediaCard } from '@/components/tool-ui/media-card';
type MyUITools = InferUITools<Tools>;
export default function Chat() {
const { messages } = useChat<MyUITools>({ api: '/api/chat' });
return (
<div>
{messages.map((m) => (
<div key={m.id}>
{m.parts.map((part, i) => {
// Fully typed: 'tool-previewLink' with correct output shape
if (part.type === 'tool-previewLink' && part.state === 'output-available') {
const card = parseSerializableMediaCard(part.output);
return <MediaCard key={i} {...card} />;
}
return null;
})}
</div>
))}
</div>
);
}