In this section, we're going to investigate how to persist messages to a database. The way this works is we'll wait for our stream to finish and then take the created messages and upload them to a database.
However, waiting for messages to finish is slightly non-trivial in the AI SDK. There are several properties that look similar but do subtly different things. Let's demystify them.
Our setup is that inside our POST route, we're doing some streamText processing:
export const POST = async (req: Request): Promise<Response> => {const body: { messages: UIMessage[] } = await req.json()const { messages } = bodyconst result = streamText({
We're taking the UI messages from the request body and passing them to the streamText function by converting them to model messages.
Then we have multiple onFinish callbacks:
streamText, which logs the response.messagestoUIMessageStreamResponse, which logs both messages and responseMessagereturn result.toUIMessageStreamResponse({originalMessages: messages,onFinish: ({ messages, responseMessage }) => {console.log("toUIMessageStreamResponse.onFinish")console.log(" messages")console.dir(messages, { depth: null })console.
When we interact with our UI and ask "Write me a poem about a fish called Grant", we get several logs in the terminal.
Let's examine the differences between the three types of responses:
| Response Type | Description | Content | Suitability for Persistence |
|---|---|---|---|
streamText > onFinish > response.messages | Model messages (AssistantModelMessage or ToolModelMessage) | Minimal information, no UI data | Not suitable for UI applications |
toUIMessageStreamResponse > onFinish > messages | Full message history | Includes original user message and assistant's response with all parts | Ideal for persisting entire conversations |
toUIMessageStreamResponse > onFinish > responseMessage | Single message | Just the newly generated assistant message | Good for persisting only the latest response |
Depending on how you want to manage persistence, you might use either the entire message history or just the final generated message.
The full message history in toUIMessageStreamResponse.onFinish.messages contains all parts, including state information (start, done), making it suitable for persisting the entire conversation.
To summarize:
streamText.onFinish has response.messages (model messages) - not suitable for persisting UI datatoUIMessageStreamResponse.onFinish has the full messages history (especially if you pass in originalMessages)toUIMessageStreamResponse.onFinish also has responseMessage which is just the newly generated message