init
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user