/** * Converts OpenAI ChatCompletionMessageParam format to Vercel AI SDK CoreMessage format */ import type { ChatCompletionMessageParam } from "openai/resources/index.js"; export interface VercelCoreMessage { role: "system" | "user" | "assistant" | "tool"; content: string | Array; } /** * Converts OpenAI messages to Vercel AI SDK CoreMessage format. * * Key differences: * - OpenAI tool results: { role: "tool", tool_call_id: "...", content: "string" } * - Vercel tool results: { role: "tool", content: [{ type: "tool-result", toolCallId: "...", toolName: "...", result: any }] } * * IMPORTANT: For multi-turn conversations with tools: * - We EXCLUDE assistant messages that have tool_calls because Vercel AI SDK manages tool call state internally * - We only include tool results, and Vercel will associate them with its internal tool call tracking */ export function convertOpenAIMessagesToVercel( messages: ChatCompletionMessageParam[], ): VercelCoreMessage[] { const vercelMessages: VercelCoreMessage[] = []; // Build a map of tool_call_id => toolName from assistant messages const toolCallMap = new Map(); // First pass: collect tool call names for (const msg of messages) { if (msg.role === "assistant" && msg.tool_calls) { for (const tc of msg.tool_calls) { if (tc.type === "function") { toolCallMap.set(tc.id, tc.function.name); } } } } // Second pass: convert messages for (const msg of messages) { switch (msg.role) { case "system": vercelMessages.push({ role: "system", content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content), }); break; case "user": vercelMessages.push({ role: "user", content: msg.content, }); break; case "assistant": // CRITICAL: Skip assistant messages with tool_calls in subsequent turns // Vercel AI SDK manages tool call state internally and doesn't expect us to re-send them // Only include assistant messages WITHOUT tool calls (normal responses) if (!msg.tool_calls || msg.tool_calls.length === 0) { vercelMessages.push({ role: "assistant", content: msg.content || "", }); } // Note: We skip assistant messages WITH tool_calls because: // 1. They were generated by Vercel AI SDK in a previous turn // 2. Vercel tracks tool call state internally // 3. Re-sending them causes Anthropic API errors about missing tool_use blocks break; case "tool": // Convert OpenAI tool result to Vercel format const toolName = toolCallMap.get(msg.tool_call_id) || "unknown_tool"; vercelMessages.push({ role: "tool", content: [ { type: "tool-result", toolCallId: msg.tool_call_id, toolName, result: msg.content, }, ], }); break; } } return vercelMessages; }