Looking for the chatbot template? It's now here.
Vercel

toAiMessages

Convert Chat SDK messages to AI SDK conversation format.

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[].

import { toAiMessages } from "chat";

Usage

lib/bot.ts
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

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

Parameters

Prop

Type

Options

Prop

Type

Returns

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

Behavior

  • Role mappingauthor.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

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:

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:

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:

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:

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

Handling unsupported attachments

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

Supported attachment types

TypeMIME typesIncluded as
imageAny image MIME typeFilePart with base64 data
filetext/*, application/json, application/xml, application/javascript, application/typescript, application/yaml, application/tomlFilePart with base64 data
videoAnySkipped (triggers onUnsupportedAttachment)
audioAnySkipped (triggers onUnsupportedAttachment)
fileOther (e.g. application/pdf)Silently skipped

Attachments require fetchData() to be available on the attachment object. Attachments without fetchData() are silently skipped.