π File detail
utils/imageValidation.ts
π§© .tsπ 105 linesπΎ 3,619 bytesπ text
β Back to All Filesπ― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes OversizedImage, ImageSizeError, and validateImagesForAPI β mainly types, interfaces, or factory objects. It composes internal code from constants, services, and format (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { API_IMAGE_MAX_BASE64_SIZE } from '../constants/apiLimits.js' import { logEvent } from '../services/analytics/index.js' import { formatFileSize } from './format.js' /**
π€ Exports (heuristic)
OversizedImageImageSizeErrorvalidateImagesForAPI
π₯οΈ Source preview
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)
}
}