import type { Api, Context, Model } from "@earendil-works/pi-ai"; import { isImageGenerationCallBlock, isWebSearchCallBlock, type ImageGenerationCallBlock, type WebSearchCallBlock } from "./native-items.ts"; type Message = Context["messages"][number]; type InternalAssistantContent = Extract["content"][number] | ImageGenerationCallBlock | WebSearchCallBlock; const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)"; const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)"; function replaceImagesWithPlaceholder( content: Extract extends { content: infer T } ? Exclude : never, placeholder: string, ) { const result: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> = []; let previousWasPlaceholder = false; for (const block of content) { if (block.type === "image") { if (!previousWasPlaceholder) { result.push({ type: "text", text: placeholder }); } previousWasPlaceholder = true; continue; } result.push(block); previousWasPlaceholder = block.text === placeholder; } return result; } function downgradeUnsupportedImages(messages: Context["messages"], model: Model): Context["messages"] { if (model.input.includes("image")) return messages; return messages.map((msg) => { if (msg.role === "user" && Array.isArray(msg.content)) { return { ...msg, content: replaceImagesWithPlaceholder(msg.content, NON_VISION_USER_IMAGE_PLACEHOLDER) }; } if (msg.role === "toolResult") { return { ...msg, content: replaceImagesWithPlaceholder(msg.content, NON_VISION_TOOL_IMAGE_PLACEHOLDER) }; } return msg; }); } export function normalizeResponsesMessageHistory( messages: Context["messages"], model: Model, normalizeToolCallId?: (id: string, targetModel: Model, source: Extract) => string, ): Context["messages"] { const toolCallIdMap = new Map(); const imageAwareMessages = downgradeUnsupportedImages(messages, model); const transformed = imageAwareMessages.map((msg) => { if (msg.role === "user") return msg; if (msg.role === "toolResult") { const normalizedId = toolCallIdMap.get(msg.toolCallId); return normalizedId && normalizedId !== msg.toolCallId ? { ...msg, toolCallId: normalizedId } : msg; } if (msg.role === "assistant") { const assistantMsg = msg; const isSameModel = assistantMsg.provider === model.provider && assistantMsg.api === model.api && assistantMsg.model === model.id; const transformedContent = (assistantMsg.content as InternalAssistantContent[]).flatMap((block) => { if (isImageGenerationCallBlock(block)) return block; if (isWebSearchCallBlock(block)) return block; if (block.type === "thinking") { if (block.redacted) return isSameModel ? block : []; if (isSameModel && block.thinkingSignature) return block; if (!block.thinking || block.thinking.trim() === "") return []; return isSameModel ? block : { type: "text" as const, text: block.thinking }; } if (block.type === "text") return isSameModel ? block : { type: "text" as const, text: block.text }; if (block.type === "toolCall") { let normalizedToolCall = block; if (!isSameModel && block.thoughtSignature) { normalizedToolCall = { ...block }; delete normalizedToolCall.thoughtSignature; } if (!isSameModel && normalizeToolCallId) { const normalizedId = normalizeToolCallId(block.id, model, assistantMsg); if (normalizedId !== block.id) { toolCallIdMap.set(block.id, normalizedId); normalizedToolCall = { ...normalizedToolCall, id: normalizedId }; } } return normalizedToolCall; } return block; }); return { ...assistantMsg, content: transformedContent as Extract["content"] }; } return msg; }); const result: Context["messages"] = []; let pendingToolCalls: Array["content"][number], { type: "toolCall" }>> = []; let existingToolResultIds = new Set(); const insertSyntheticToolResults = () => { if (pendingToolCalls.length === 0) return; for (const toolCall of pendingToolCalls) { if (!existingToolResultIds.has(toolCall.id)) { result.push({ role: "toolResult", toolCallId: toolCall.id, toolName: toolCall.name, content: [{ type: "text", text: "aborted" }], isError: true, timestamp: Date.now(), }); existingToolResultIds.add(toolCall.id); } } pendingToolCalls = []; existingToolResultIds = new Set(); }; for (const msg of transformed) { if (msg.role === "assistant") { insertSyntheticToolResults(); if (msg.stopReason === "error" || msg.stopReason === "aborted") continue; const toolCalls = msg.content.filter((block) => block.type === "toolCall"); if (toolCalls.length > 0) { const seen = new Set(); pendingToolCalls = toolCalls.filter((toolCall) => { if (seen.has(toolCall.id)) return false; seen.add(toolCall.id); return true; }); existingToolResultIds = new Set(); } result.push(msg); continue; } if (msg.role === "toolResult") { if (!pendingToolCalls.some((toolCall) => toolCall.id === msg.toolCallId) || existingToolResultIds.has(msg.toolCallId)) continue; existingToolResultIds.add(msg.toolCallId); result.push(msg); continue; } if (msg.role === "user") { insertSyntheticToolResults(); result.push(msg); continue; } result.push(msg); } insertSyntheticToolResults(); return result; }