Stream chat from a route
The terminal keeps the provider loop from Days 6 and 7. For the web, the AI SDK's streamText runs the model with the Day 7 tool registry. It produces a UI message stream, the format that the AI SDK's useChat hook reads in the browser.
Study first
- Read the AI SDK's chatbot guide for
useChat, UI messages and their parts, and the shape of the route handler. - Read the streamText reference for
toolsandstopWhen, and learnconvertToModelMessages, which turns UI messages into the messages the model takes.
Build
- Run
pnpm add ai @ai-sdk/react @ai-sdk/anthropic @ai-sdk/openai, which adds the SDK, its React hooks and one adapter per provider. - Write
src/lib/chat.ts, wherechatModel()returns the configured provider's default model from the Day 6 list, or the id inCHAT_MODEL, through that provider's AI SDK adapter. - In the same file, write
chatTools, which maps the registry's tools into the SDK'stool()shape. Each tool's schema andexecutecome from the same Day 7 definition, so the terminal and the web share one set of tools. - Write
src/app/api/chat/route.ts. Validate the body, then check the messages withsafeValidateUIMessagesagainst the tools. Stream with the system prompt, the tools andstopWhen: isStepCount(8), and answer withcreateUIMessageStreamResponse. - Write a unit test with the SDK's
MockLanguageModelV4, a fake model from its testing helpers, to check the shape of the stream. Also check that a body without an id and a message without parts both get a 400. - Write a live test that asks the real small model for a calculation and finds the calculator call in the stream.
Done when
curl -Nagainst/api/chatwith one user message prints a UI message stream, andpnpm test:liveshows the tool call.
Technical notes
- Validate the envelope, the body's
idandmessages, with Zod, and the messages themselves withsafeValidateUIMessages, which knows the shapes of the tool parts. A bad body then gets a 400 that names the failure, not a stream that fails halfway. isStepCount(8)matches the 8 rounds of the Day 7 loop.- The model id comes from the provider list in
src/lib/providers.ts, so the web and the terminal agree on the default.CHAT_MODELoverrides it, which is how the live suite runs on the small model.

