| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- 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<string, unknown>
- if (b.type !== 'image') return false
- if (typeof b.source !== 'object' || b.source === null) return false
- const source = b.source as Record<string, unknown>
- 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<string, unknown>
- // Handle wrapped message format { type: 'user', message: { role, content } }
- // Only check user messages
- if (m.type !== 'user') continue
- const innerMessage = m.message as Record<string, unknown> | 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)
- }
- }
|