LibreChat/packages/api/src/files/validation.ts
Danny Avila 937563f645
🖼️ feat: File Size and MIME Type Filtering at Agent level (#10446)
* refactor: add image file size validation as part of payload build

* feat: implement file size and MIME type filtering in endpoint configuration

* chore: import order
2025-11-10 21:36:48 -05:00

286 lines
8.4 KiB
TypeScript

import { Providers } from '@librechat/agents';
import { mbToBytes, isOpenAILikeProvider } from 'librechat-data-provider';
export interface PDFValidationResult {
isValid: boolean;
error?: string;
}
export interface VideoValidationResult {
isValid: boolean;
error?: string;
}
export interface AudioValidationResult {
isValid: boolean;
error?: string;
}
export interface ImageValidationResult {
isValid: boolean;
error?: string;
}
export async function validatePdf(
pdfBuffer: Buffer,
fileSize: number,
provider: Providers,
configuredFileSizeLimit?: number,
): Promise<PDFValidationResult> {
if (provider === Providers.ANTHROPIC) {
return validateAnthropicPdf(pdfBuffer, fileSize, configuredFileSizeLimit);
}
if (isOpenAILikeProvider(provider)) {
return validateOpenAIPdf(fileSize, configuredFileSizeLimit);
}
if (provider === Providers.GOOGLE || provider === Providers.VERTEXAI) {
return validateGooglePdf(fileSize, configuredFileSizeLimit);
}
return { isValid: true };
}
/**
* Validates if a PDF meets Anthropic's requirements
* @param pdfBuffer - The PDF file as a buffer
* @param fileSize - The file size in bytes
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
async function validateAnthropicPdf(
pdfBuffer: Buffer,
fileSize: number,
configuredFileSizeLimit?: number,
): Promise<PDFValidationResult> {
try {
const providerLimit = mbToBytes(32);
const effectiveLimit = configuredFileSizeLimit ?? providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `PDF file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
if (!pdfBuffer || pdfBuffer.length < 5) {
return {
isValid: false,
error: 'Invalid PDF file: too small or corrupted',
};
}
const pdfHeader = pdfBuffer.subarray(0, 5).toString();
if (!pdfHeader.startsWith('%PDF-')) {
return {
isValid: false,
error: 'Invalid PDF file: missing PDF header',
};
}
const pdfContent = pdfBuffer.toString('binary');
if (
pdfContent.includes('/Encrypt ') ||
pdfContent.includes('/U (') ||
pdfContent.includes('/O (')
) {
return {
isValid: false,
error: 'PDF is password-protected or encrypted. Anthropic requires unencrypted PDFs.',
};
}
const pageMatches = pdfContent.match(/\/Type[\s]*\/Page[^s]/g);
const estimatedPages = pageMatches ? pageMatches.length : 1;
if (estimatedPages > 100) {
return {
isValid: false,
error: `PDF has approximately ${estimatedPages} pages, exceeding Anthropic's 100-page limit`,
};
}
return { isValid: true };
} catch (error) {
console.error('PDF validation error:', error);
return {
isValid: false,
error: 'Failed to validate PDF file',
};
}
}
/**
* Validates if a PDF meets OpenAI's requirements
* @param fileSize - The file size in bytes
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
async function validateOpenAIPdf(
fileSize: number,
configuredFileSizeLimit?: number,
): Promise<PDFValidationResult> {
const providerLimit = mbToBytes(10);
const effectiveLimit = configuredFileSizeLimit ?? providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `PDF file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
return { isValid: true };
}
/**
* Validates if a PDF meets Google's requirements
* @param fileSize - The file size in bytes
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
async function validateGooglePdf(
fileSize: number,
configuredFileSizeLimit?: number,
): Promise<PDFValidationResult> {
const providerLimit = mbToBytes(20);
const effectiveLimit = configuredFileSizeLimit ?? providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `PDF file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
return { isValid: true };
}
/**
* Validates video files for different providers
* @param videoBuffer - The video file as a buffer
* @param fileSize - The file size in bytes
* @param provider - The provider to validate for
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
export async function validateVideo(
videoBuffer: Buffer,
fileSize: number,
provider: Providers,
configuredFileSizeLimit?: number,
): Promise<VideoValidationResult> {
if (provider === Providers.GOOGLE || provider === Providers.VERTEXAI) {
const providerLimit = mbToBytes(20);
const effectiveLimit = configuredFileSizeLimit ?? providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `Video file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
}
if (!videoBuffer || videoBuffer.length < 10) {
return {
isValid: false,
error: 'Invalid video file: too small or corrupted',
};
}
return { isValid: true };
}
/**
* Validates audio files for different providers
* @param audioBuffer - The audio file as a buffer
* @param fileSize - The file size in bytes
* @param provider - The provider to validate for
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
export async function validateAudio(
audioBuffer: Buffer,
fileSize: number,
provider: Providers,
configuredFileSizeLimit?: number,
): Promise<AudioValidationResult> {
if (provider === Providers.GOOGLE || provider === Providers.VERTEXAI) {
const providerLimit = mbToBytes(20);
const effectiveLimit = configuredFileSizeLimit ?? providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `Audio file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
}
if (!audioBuffer || audioBuffer.length < 10) {
return {
isValid: false,
error: 'Invalid audio file: too small or corrupted',
};
}
return { isValid: true };
}
/**
* Validates image files for different providers
* @param imageBuffer - The image file as a buffer
* @param fileSize - The file size in bytes
* @param provider - The provider to validate for
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
export async function validateImage(
imageBuffer: Buffer,
fileSize: number,
provider: Providers | string,
configuredFileSizeLimit?: number,
): Promise<ImageValidationResult> {
if (provider === Providers.GOOGLE || provider === Providers.VERTEXAI) {
const providerLimit = mbToBytes(20);
const effectiveLimit = configuredFileSizeLimit ?? providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `Image file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
}
if (provider === Providers.ANTHROPIC) {
const providerLimit = mbToBytes(5);
const effectiveLimit = configuredFileSizeLimit ?? providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = Math.round(effectiveLimit / (1024 * 1024));
return {
isValid: false,
error: `Image file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds the ${limitMB}MB limit`,
};
}
}
if (!imageBuffer || imageBuffer.length < 10) {
return {
isValid: false,
error: 'Invalid image file: too small or corrupted',
};
}
return { isValid: true };
}