Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/content/docs/api/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Chat, root, paragraph, text, Card, Button, emoji } from "chat";

| Export | Description |
|--------|-------------|
| [`toAiMessages`](/docs/streaming#toaimessagesmessages-options) | Convert `Message[]` to AI SDK `{ role, content }[]` format |
| [`toAiMessages`](/docs/api/to-ai-messages) | Convert `Message[]` to AI SDK `{ role, content }[]` format |

## Message formats

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/api/message.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Links found in incoming messages are extracted and exposed as `LinkPreview` obje
/>

<Callout type="info">
When using [`toAiMessages()`](/docs/streaming#toaimessagesmessages-options), link metadata is automatically appended to the message content. Embedded message links are labeled as `[Embedded message: ...]` so the AI model understands the context.
When using [`toAiMessages()`](/docs/api/to-ai-messages), link metadata is automatically appended to the message content. Embedded message links are labeled as `[Embedded message: ...]` so the AI model understands the context.
</Callout>

### Platform support
Expand Down
190 changes: 190 additions & 0 deletions apps/docs/content/docs/api/to-ai-messages.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
title: toAiMessages
description: Convert Chat SDK messages to AI SDK conversation format.
type: reference
---

Convert an array of `Message` objects into the `{ role, content }[]` format expected by AI SDKs. The output is structurally compatible with AI SDK's `ModelMessage[]`.

```typescript
import { toAiMessages } from "chat";
```

## Usage

```typescript title="lib/bot.ts" lineNumbers
import { toAiMessages } from "chat";

bot.onSubscribedMessage(async (thread, message) => {
const result = await thread.adapter.fetchMessages(thread.id, { limit: 20 });
const history = await toAiMessages(result.messages);
const response = await agent.stream({ prompt: history });
await thread.post(response.fullStream);
});
```

## Signature

```typescript
function toAiMessages(
messages: Message[],
options?: ToAiMessagesOptions
): Promise<AiMessage[]>
```

### Parameters

<TypeTable
type={{
messages: {
description: 'Array of Chat SDK Message objects. Works with FetchResult.messages, thread.recentMessages, or any collected iterable.',
type: 'Message[]',
},
options: {
description: 'Optional configuration.',
type: 'ToAiMessagesOptions',
default: '{}',
},
}}
/>

### Options

<TypeTable
type={{
includeNames: {
description: 'Prefix user messages with [username]: for multi-user context.',
type: 'boolean',
default: 'false',
},
transformMessage: {
description: 'Transform or filter each message after default processing. Return null to skip the message.',
type: '(aiMessage: AiMessage, source: Message) => AiMessage | null | Promise<AiMessage | null>',
},
onUnsupportedAttachment: {
description: 'Called when an attachment type is not supported (video, audio).',
type: '(attachment: Attachment, message: Message) => void',
default: 'console.warn',
},
}}
/>

### Returns

`Promise<AiMessage[]>` — an array of messages with `role` and `content` fields, directly assignable to AI SDK's `ModelMessage[]`.

## Behavior

- **Role mapping** — `author.isMe === true` maps to `"assistant"`, all others to `"user"`
- **Filtering** — Messages with empty or whitespace-only text are removed
- **Sorting** — Messages are sorted chronologically (oldest first) by `metadata.dateSent`
- **Links** — Link metadata (URL, title, description, site name) is appended to message content. Embedded message links are labeled as `[Embedded message: ...]`
- **Attachments** — Images and text files (JSON, XML, YAML, etc.) are included as multipart content using `fetchData()`. Video and audio attachments trigger `onUnsupportedAttachment`

## Return types

```typescript
type AiMessage = AiUserMessage | AiAssistantMessage;

interface AiUserMessage {
role: "user";
content: string | AiMessagePart[];
}

interface AiAssistantMessage {
role: "assistant";
content: string;
}
```

User messages have multipart `content` when attachments are present:

```typescript
type AiMessagePart = AiTextPart | AiImagePart | AiFilePart;

interface AiTextPart {
type: "text";
text: string;
}

interface AiImagePart {
type: "image";
image: DataContent | URL;
mediaType?: string;
}

interface AiFilePart {
type: "file";
data: DataContent | URL;
filename?: string;
mediaType: string;
}
```

## Examples

### Multi-user context

Prefix each user message with their username so the AI model can distinguish speakers:

```typescript
const history = await toAiMessages(result.messages, { includeNames: true });
// [{ role: "user", content: "[alice]: Hello" },
// { role: "assistant", content: "Hi there!" },
// { role: "user", content: "[bob]: Thanks" }]
```

### Transforming messages

Replace raw user IDs with readable names:

```typescript
const history = await toAiMessages(result.messages, {
transformMessage: (aiMessage) => {
if (typeof aiMessage.content === "string") {
return {
...aiMessage,
content: aiMessage.content.replace(/<@U123>/g, "@VercelBot"),
};
}
return aiMessage;
},
});
```

### Filtering messages

Skip messages from a specific user:

```typescript
const history = await toAiMessages(result.messages, {
transformMessage: (aiMessage, source) => {
if (source.author.userId === "U_NOISY_BOT") return null;
return aiMessage;
},
});
```

### Handling unsupported attachments

```typescript
const history = await toAiMessages(result.messages, {
onUnsupportedAttachment: (attachment, message) => {
logger.warn(`Skipped ${attachment.type} attachment in message ${message.id}`);
},
});
```

## Supported attachment types

| Type | MIME types | Included as |
|------|-----------|-------------|
| `image` | Any image MIME type | `FilePart` with base64 data |
| `file` | `text/*`, `application/json`, `application/xml`, `application/javascript`, `application/typescript`, `application/yaml`, `application/toml` | `FilePart` with base64 data |
| `video` | Any | Skipped (triggers `onUnsupportedAttachment`) |
| `audio` | Any | Skipped (triggers `onUnsupportedAttachment`) |
| `file` | Other (e.g. `application/pdf`) | Silently skipped |

<Callout type="info">
Attachments require `fetchData()` to be available on the attachment object. Attachments without `fetchData()` are silently skipped.
</Callout>
2 changes: 2 additions & 0 deletions apps/docs/content/docs/handling-events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ bot.onSubscribedMessage(async (thread, message) => {
});
```

See [`toAiMessages`](/docs/api/to-ai-messages) for all options including multi-user name prefixing, message transforms, and attachment handling.

### Example: Unsubscribe on keyword

```typescript title="lib/bot.ts" lineNumbers
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/content/docs/posting-messages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ await thread.post(result.fullStream);

Both `fullStream` and `textStream` are supported. Use `fullStream` with multi-step agents — it preserves paragraph breaks between steps. Any `AsyncIterable<string>` also works for custom streams.

For multi-turn conversations, use [`toAiMessages()`](/docs/api/to-ai-messages) to convert thread history into the `{ role, content }[]` format expected by AI SDKs.

See the [Streaming](/docs/streaming) page for details on platform behavior and configuration.

## Attachments and files
Expand Down
51 changes: 2 additions & 49 deletions apps/docs/content/docs/streaming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ await thread.stream(textStream, {
## Streaming with conversation history

Combine message history with streaming for multi-turn AI conversations.
Use `toAiMessages()` to convert chat messages into the `{ role, content }` format expected by AI SDKs:
Use [`toAiMessages()`](/docs/api/to-ai-messages) to convert chat messages into the `{ role, content }` format expected by AI SDKs:

```typescript title="lib/bot.ts" lineNumbers
import { toAiMessages } from "chat";
Expand All @@ -200,51 +200,4 @@ bot.onSubscribedMessage(async (thread, message) => {
});
```

### `toAiMessages(messages, options?)`

Converts an array of `Message` objects into AI SDK conversation format:

- Maps `author.isMe` to `"assistant"` role, all others to `"user"`
- Filters out empty messages
- Sorts chronologically (oldest first)
- Appends link metadata (URLs, titles, descriptions) when present
- Labels embedded message links (e.g. shared Slack messages) as `[Embedded message: ...]`

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `includeNames` | `boolean` | `false` | Prefix user messages with `[username]: ` for multi-user context |
| `transformMessage` | `(aiMessage, source) => AiMessage \| Promise<AiMessage \| null> \| null` | — | Transform or filter each message after default processing. Return `null` to skip. |
| `onUnsupportedAttachment` | `(attachment, message) => void` | `console.warn` | Called when an attachment type is not supported |

### Customizing messages with `transformMessage`

Use `transformMessage` to modify, enrich, or filter messages after default processing:

```typescript title="lib/bot.ts" lineNumbers
import { toAiMessages } from "chat";

const history = await toAiMessages(result.messages, {
transformMessage: (aiMessage, source) => {
// Replace bot user IDs with readable names
if (typeof aiMessage.content === "string") {
return {
...aiMessage,
content: aiMessage.content.replace(/<@U123>/g, "@VercelBot"),
};
}
return aiMessage;
},
});
```

Return `null` to skip a message entirely:

```typescript
const history = await toAiMessages(result.messages, {
transformMessage: (aiMessage, source) => {
// Skip messages from a specific user
if (source.author.userId === "U_NOISY_BOT") return null;
return aiMessage;
},
});
```
See the [`toAiMessages` API reference](/docs/api/to-ai-messages) for all options including `includeNames`, `transformMessage`, and attachment handling.
Loading