import type { ImageContent, UserMessage } from "@earendil-works/pi-ai"; export const MAX_IMAGE_BYTES = 5 * 1024 * 1024; export const MAX_IMAGES = 4; export const MAX_MESSAGE_BYTES = 30 * 1024 * 1024; export const IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp"] as const; /** Check an image's declared format against its file signature and bounded size. */ export function validateImage(bytes: Uint8Array, mimeType: string, size = bytes.length): void { if (size === 0 || size > MAX_IMAGE_BYTES) { throw new Error("Images must be between 1 byte and 5 MiB"); } if (!IMAGE_MIME_TYPES.some((type) => type === mimeType)) { throw new Error("Supported image formats are PNG, JPEG, and WebP"); } const matches = (offset: number, signature: readonly number[]) => signature.every((byte, index) => bytes[offset + index] === byte); const valid = mimeType === "image/png" ? matches(0, [137, 80, 78, 71, 13, 10, 26, 10]) : mimeType === "image/jpeg" ? matches(0, [255, 216, 255]) : matches(0, [82, 73, 70, 70]) && matches(8, [87, 69, 66, 80]); if (!valid) throw new Error("Image bytes do not match their declared format"); } /** Text-only submissions retain their original shape; attachments become image content blocks. */ export function userContent(text: string, images: readonly ImageContent[]): UserMessage["content"] { return images.length === 0 ? text : [...(text ? [{ type: "text" as const, text }] : []), ...images]; } /** A compact label for images in places without thumbnails. */ export function imageSummary(content: UserMessage["content"]): string { const count = typeof content === "string" ? 0 : content.filter((block) => block.type === "image").length; return count === 0 ? "" : `[${count} image${count === 1 ? "" : "s"}]`; }