📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
# Chat Persistence
|
||||
|
||||
Strategies for persisting chat messages with AI SDK v6.
|
||||
|
||||
## Core Principle
|
||||
|
||||
**Persist `UIMessage[]`, convert to `ModelMessage[]` only at call sites.**
|
||||
|
||||
```typescript
|
||||
// Database stores UIMessage format
|
||||
const chat = await loadChat(chatId); // Returns UIMessage[]
|
||||
|
||||
// Convert only when calling the model
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o'),
|
||||
messages: convertToModelMessages(chat.messages),
|
||||
});
|
||||
```
|
||||
|
||||
## Server-Side Persistence
|
||||
|
||||
### onFinish Callback
|
||||
|
||||
```typescript
|
||||
import { streamText, createIdGenerator } from 'ai';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages, chatId } = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o'),
|
||||
messages: convertToModelMessages(messages),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse({
|
||||
originalMessages: messages,
|
||||
|
||||
// Generate stable, server-side message IDs
|
||||
generateMessageId: createIdGenerator({ prefix: 'msg', size: 16 }),
|
||||
|
||||
// Persist after stream completes
|
||||
onFinish: async ({ messages: completeMessages }) => {
|
||||
await db.chat.upsert({
|
||||
where: { id: chatId },
|
||||
update: { messages: completeMessages, updatedAt: new Date() },
|
||||
create: { id: chatId, messages: completeMessages },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Survive Client Disconnects
|
||||
|
||||
Call `consumeStream()` to ensure the stream completes even if the client disconnects:
|
||||
|
||||
```typescript
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o'),
|
||||
messages: convertToModelMessages(messages),
|
||||
});
|
||||
|
||||
// Start consuming immediately (runs in background)
|
||||
result.consumeStream();
|
||||
|
||||
return result.toUIMessageStreamResponse({
|
||||
originalMessages: messages,
|
||||
onFinish: async ({ messages }) => {
|
||||
await saveChat(chatId, messages);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Drizzle Example
|
||||
|
||||
```typescript
|
||||
import { pgTable, text, jsonb, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const chats = pgTable('chats', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id').notNull(),
|
||||
title: text('title'),
|
||||
messages: jsonb('messages').$type<UIMessage[]>().notNull().default([]),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
### Prisma Example
|
||||
|
||||
```prisma
|
||||
model Chat {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
title String?
|
||||
messages Json @default("[]")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
```
|
||||
|
||||
## Loading Chats
|
||||
|
||||
```typescript
|
||||
// API route to load chat
|
||||
export async function GET(req: Request, { params }: { params: { id: string } }) {
|
||||
const chat = await db.chat.findUnique({
|
||||
where: { id: params.id },
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
id: chat.id,
|
||||
messages: chat.messages as UIMessage[],
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Client-side loading
|
||||
'use client';
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
function Chat({ chatId }: { chatId: string }) {
|
||||
const [initialMessages, setInitialMessages] = useState<UIMessage[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/chats/${chatId}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setInitialMessages(data.messages);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [chatId]);
|
||||
|
||||
const { messages, sendMessage } = useChat({
|
||||
id: chatId,
|
||||
messages: initialMessages,
|
||||
transport: new DefaultChatTransport({ api: '/api/chat' }),
|
||||
});
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
|
||||
return <ChatUI messages={messages} onSend={sendMessage} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Bandwidth Optimization
|
||||
|
||||
Send only the last message, load history server-side:
|
||||
|
||||
```typescript
|
||||
// Client: Send only new message
|
||||
const { sendMessage } = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: '/api/chat',
|
||||
prepareSendMessagesRequest: ({ id, messages }) => ({
|
||||
body: {
|
||||
chatId: id,
|
||||
message: messages.at(-1) // Only send last message
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
// Server: Load history from database
|
||||
export async function POST(req: Request) {
|
||||
const { chatId, message } = await req.json();
|
||||
|
||||
// Load existing messages from database
|
||||
const chat = await db.chat.findUnique({ where: { id: chatId } });
|
||||
const messages = [...(chat?.messages ?? []), message];
|
||||
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o'),
|
||||
messages: convertToModelMessages(messages),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse({
|
||||
originalMessages: messages,
|
||||
onFinish: async ({ messages }) => {
|
||||
await db.chat.update({
|
||||
where: { id: chatId },
|
||||
data: { messages },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Resumable Streams
|
||||
|
||||
Resume interrupted streams on page reload:
|
||||
|
||||
```tsx
|
||||
function Chat({ chatId }: { chatId: string }) {
|
||||
const { messages, resumeStream, status } = useChat({
|
||||
id: chatId,
|
||||
messages: initialMessages,
|
||||
transport: new DefaultChatTransport({ api: '/api/chat' }),
|
||||
});
|
||||
|
||||
// Resume on mount if there's an incomplete stream
|
||||
useEffect(() => {
|
||||
const lastMessage = messages.at(-1);
|
||||
if (lastMessage?.role === 'assistant' && status === 'ready') {
|
||||
// Check if message seems incomplete
|
||||
resumeStream();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* ... */}
|
||||
{status === 'streaming' && <div>AI is typing...</div>}
|
||||
<button onClick={() => resumeStream()}>Resume</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from v4/v5
|
||||
|
||||
If migrating existing data, use the dual-write pattern:
|
||||
|
||||
1. Create new `messages_v6` table
|
||||
2. Dual-write to both tables
|
||||
3. Run background migration
|
||||
4. Switch reads to v6 schema
|
||||
5. Remove dual-write
|
||||
6. Drop old table
|
||||
|
||||
See [AI SDK Migration Guide](https://sdk.vercel.ai/docs/migration-guides) for detailed steps.
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Persistence:**
|
||||
- Always use `onFinish` for reliable persistence
|
||||
- Generate server-side message IDs for consistency
|
||||
- Use `consumeStream()` to complete streams even on disconnect
|
||||
|
||||
**Performance:**
|
||||
- Index by userId for user-specific queries
|
||||
- Consider pagination for long conversations
|
||||
- Use bandwidth optimization for mobile clients
|
||||
|
||||
**Reliability:**
|
||||
- Handle concurrent updates with optimistic locking
|
||||
- Implement retry logic for database failures
|
||||
- Log persistence errors for debugging
|
||||
Reference in New Issue
Block a user