import { API_IMAGE_MAX_BASE64_SIZE } from '../constants/apiLimits.js' import { logEvent } from '../services/analytics/index.js' import { formatFileSize } from './format.js' /** * Information about an oversized image. */ export type OversizedImage = { index: number size: number } /** * Error thrown when one or more images exceed the API size limit. */ export class ImageSizeError extends Error { constructor(oversizedImages: OversizedImage[], maxSize: number) { let message: string const firstImage = oversizedImages[0] if (oversizedImages.length === 1 && firstImage) { message = `Image base64 size (${formatFileSize(firstImage.size)}) exceeds API limit (${formatFileSize(maxSize)}). ` + `Please resize the image before sending.` } else { message = `${oversizedImages.length} images exceed the API limit (${formatFileSize(maxSize)}): ` + oversizedImages .map(img => `Image ${img.index}: ${formatFileSize(img.size)}`) .join(', ') + `. Please resize these images before sending.` } super(message) this.name = 'ImageSizeError' } } /** * Type guard to check if a block is a base64 image block */ function isBase64ImageBlock( block: unknown, ): block is { type: 'image'; source: { type: 'base64'; data: string } } { if (typeof block !== 'object' || block === null) return false const b = block as Record if (b.type !== 'image') return false if (typeof b.source !== 'object' || b.source === null) return false const source = b.source as Record return source.type === 'base64' && typeof source.data === 'string' } /** * Validates that all images in messages are within the API size limit. * This is a safety net at the API boundary to catch any oversized images * that may have slipped through upstream processing. * * Note: The API's 5MB limit applies to the base64-encoded string length, * not the decoded raw bytes. * * Works with both UserMessage/AssistantMessage types (which have { type, message }) * and raw MessageParam types (which have { role, content }). * * @param messages - Array of messages to validate * @throws ImageSizeError if any image exceeds the API limit */ export function validateImagesForAPI(messages: unknown[]): void { const oversizedImages: OversizedImage[] = [] let imageIndex = 0 for (const msg of messages) { if (typeof msg !== 'object' || msg === null) continue const m = msg as Record // Handle wrapped message format { type: 'user', message: { role, content } } // Only check user messages if (m.type !== 'user') continue const innerMessage = m.message as Record | undefined if (!innerMessage) continue const content = innerMessage.content if (typeof content === 'string' || !Array.isArray(content)) continue for (const block of content) { if (isBase64ImageBlock(block)) { imageIndex++ // Check the base64-encoded string length directly (not decoded bytes) // The API limit applies to the base64 payload size const base64Size = block.source.data.length if (base64Size > API_IMAGE_MAX_BASE64_SIZE) { logEvent('tengu_image_api_validation_failed', { base64_size_bytes: base64Size, max_bytes: API_IMAGE_MAX_BASE64_SIZE, }) oversizedImages.push({ index: imageIndex, size: base64Size }) } } } } if (oversizedImages.length > 0) { throw new ImageSizeError(oversizedImages, API_IMAGE_MAX_BASE64_SIZE) } }