source dump of claude code
at main 104 lines 3.6 kB view raw
1import { API_IMAGE_MAX_BASE64_SIZE } from '../constants/apiLimits.js' 2import { logEvent } from '../services/analytics/index.js' 3import { formatFileSize } from './format.js' 4 5/** 6 * Information about an oversized image. 7 */ 8export type OversizedImage = { 9 index: number 10 size: number 11} 12 13/** 14 * Error thrown when one or more images exceed the API size limit. 15 */ 16export class ImageSizeError extends Error { 17 constructor(oversizedImages: OversizedImage[], maxSize: number) { 18 let message: string 19 const firstImage = oversizedImages[0] 20 if (oversizedImages.length === 1 && firstImage) { 21 message = 22 `Image base64 size (${formatFileSize(firstImage.size)}) exceeds API limit (${formatFileSize(maxSize)}). ` + 23 `Please resize the image before sending.` 24 } else { 25 message = 26 `${oversizedImages.length} images exceed the API limit (${formatFileSize(maxSize)}): ` + 27 oversizedImages 28 .map(img => `Image ${img.index}: ${formatFileSize(img.size)}`) 29 .join(', ') + 30 `. Please resize these images before sending.` 31 } 32 super(message) 33 this.name = 'ImageSizeError' 34 } 35} 36 37/** 38 * Type guard to check if a block is a base64 image block 39 */ 40function isBase64ImageBlock( 41 block: unknown, 42): block is { type: 'image'; source: { type: 'base64'; data: string } } { 43 if (typeof block !== 'object' || block === null) return false 44 const b = block as Record<string, unknown> 45 if (b.type !== 'image') return false 46 if (typeof b.source !== 'object' || b.source === null) return false 47 const source = b.source as Record<string, unknown> 48 return source.type === 'base64' && typeof source.data === 'string' 49} 50 51/** 52 * Validates that all images in messages are within the API size limit. 53 * This is a safety net at the API boundary to catch any oversized images 54 * that may have slipped through upstream processing. 55 * 56 * Note: The API's 5MB limit applies to the base64-encoded string length, 57 * not the decoded raw bytes. 58 * 59 * Works with both UserMessage/AssistantMessage types (which have { type, message }) 60 * and raw MessageParam types (which have { role, content }). 61 * 62 * @param messages - Array of messages to validate 63 * @throws ImageSizeError if any image exceeds the API limit 64 */ 65export function validateImagesForAPI(messages: unknown[]): void { 66 const oversizedImages: OversizedImage[] = [] 67 let imageIndex = 0 68 69 for (const msg of messages) { 70 if (typeof msg !== 'object' || msg === null) continue 71 72 const m = msg as Record<string, unknown> 73 74 // Handle wrapped message format { type: 'user', message: { role, content } } 75 // Only check user messages 76 if (m.type !== 'user') continue 77 78 const innerMessage = m.message as Record<string, unknown> | undefined 79 if (!innerMessage) continue 80 81 const content = innerMessage.content 82 if (typeof content === 'string' || !Array.isArray(content)) continue 83 84 for (const block of content) { 85 if (isBase64ImageBlock(block)) { 86 imageIndex++ 87 // Check the base64-encoded string length directly (not decoded bytes) 88 // The API limit applies to the base64 payload size 89 const base64Size = block.source.data.length 90 if (base64Size > API_IMAGE_MAX_BASE64_SIZE) { 91 logEvent('tengu_image_api_validation_failed', { 92 base64_size_bytes: base64Size, 93 max_bytes: API_IMAGE_MAX_BASE64_SIZE, 94 }) 95 oversizedImages.push({ index: imageIndex, size: base64Size }) 96 } 97 } 98 } 99 } 100 101 if (oversizedImages.length > 0) { 102 throw new ImageSizeError(oversizedImages, API_IMAGE_MAX_BASE64_SIZE) 103 } 104}