Skip to content

AI Chat & Tool Calls

Most apps will want AI for some feature. This explains common patterns you’ll run into and how we solved them in Every Chef using the Vercel AI SDK.

Streaming Endpoint

The chat endpoint uses TanStack Router’s server handler with streamText:

const result = streamText({
model: openaiProvider("gpt-5.1"),
messages,
tools: recipeTools,
onFinish: async ({ text }) => {
// Persist assistant message after streaming completes
await MessageService.saveAssistantMessage(chatId, userId, text);
},
});
return result.toUIMessageStreamResponse();

User messages are saved immediately when sent. Assistant messages are saved in onFinish after streaming completes.

Human-in-the-Loop Tools

When the AI wants to save a recipe, it shouldn’t just do it - it should ask the user first. Define tools WITHOUT an execute function to forward them to the client:

const promptUserWithRecipeUpdate = tool({
description: "Prompt the user to decide whether to save a recipe",
inputSchema: z.object({
title: z.string(),
content: z.string(),
}),
// No execute function - forwards to client
});

When a tool has no execute function, the AI SDK forwards the call to the client, which can show UI for the user to respond.

┌──────────┐ ┌──────────┐ ┌──────────┐
│ AI │ │ Client │ │ User │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ tool call │ │
│───────────────►│ │
│ │ show UI │
│ │───────────────►│
│ │ │
│ │ user decides │
│ │◄───────────────│
│ │ │
│ tool result │ │
│◄───────────────│ │
│ │ │
│ continues... │ │
│───────────────►│ │

Rendering Tool Calls

The client renders tool calls as interactive UI:

{
toolParts.map((part) => {
if (part.toolInvocation.toolName === "promptUserWithRecipeUpdate") {
return (
<RecipeUpdatePrompt
recipe={part.toolInvocation.args}
onRespond={handleToolResponse}
/>
);
}
});
}

Sending Results Back

When the user makes a choice, send it back to the AI:

const handleToolResponse = async (toolCallId: string, result: ToolResult) => {
// Tell the AI about the user's choice
addToolOutput({ toolCallId, output: JSON.stringify(result) });
// If they chose to save, create the recipe
if (result.action === "create") {
await recipesCollection.insert({ ... });
}
};