import { $env } from "@oh-my-pi/pi-utils"; import type { Api, ImageContent, Model, TextContent } from "../types"; export const NON_VISION_IMAGE_PLACEHOLDER = "[image omitted: model does not support vision]"; export function partitionVisionContent( content: ReadonlyArray, supportsImages: boolean, ): { textBlocks: TextContent[]; imageBlocks: ImageContent[]; omittedImages: boolean; } { const textBlocks = content.filter((block): block is TextContent => block.type === "text"); const imageBlocks = content.filter((block): block is ImageContent => block.type === "image"); return { textBlocks, imageBlocks: supportsImages ? imageBlocks : [], omittedImages: !supportsImages && imageBlocks.length > 0, }; } export function joinTextWithImagePlaceholder(text: string, omittedImages: boolean): string { const parts: string[] = []; if (text.length > 0) { parts.push(text); } if (omittedImages) { parts.push(NON_VISION_IMAGE_PLACEHOLDER); } return parts.join("\n"); } /** * Evaluates whether an OpenAI-compatible Chat Completions model genuinely * supports multimodal image inputs on the wire. Defensive guards override * misconfigured provider descriptors or user model entries (e.g. text-only * DashScope Qwen SKUs, DeepSeek models) whose endpoints reject `image_url`. */ export function isOpenAICompletionsVisionSupported(model: Model<"openai-completions" | "openrouter">): boolean { if (!model.input.includes("image")) return false; if (model.compat.stripImageInput) return false; return true; } /** * Whether the transport that will carry `model` sends image content on the wire. * * The `pi-native` transport forwards the original context (images included) to * the gateway, which resolves its own model server-side, so the Chat * Completions guard below never runs client-side and the declared input * applies. Otherwise the OpenAI Chat Completions path applies the text-only * guard, as does the OpenRouter chat fallback (`PI_OPENROUTER_RESPONSES=0`, * which dispatches `openrouter` models through `streamOpenAICompletions`); * every other API ships the modalities the model declares. Callers that report * or gate on the wire (for example the `omp models` table) read this * predicate; declared capability reads `model.input`. */ export function sendsImageInputOnWire(model: Model): boolean { if (model.transport === "pi-native") return model.input.includes("image"); if (isGuardedCompletionsTransport(model)) return isOpenAICompletionsVisionSupported(model); return model.input.includes("image"); } /** True for the transports that encode through the Chat Completions guard. */ function isGuardedCompletionsTransport(model: Model): model is Model<"openai-completions" | "openrouter"> { if (model.api === "openai-completions") return true; return model.api === "openrouter" && $env.PI_OPENROUTER_RESPONSES === "0"; }