LibreChat/packages/api/src/files/validation.ts
Dustin Healy 1d0a4c501f
🪨 feat: AWS Bedrock Document Uploads (#11912)
* feat: add aws bedrock upload to provider support

* chore: address copilot comments

* feat: add shared Bedrock document format types and MIME mapping

Bedrock Converse API accepts 9 document formats beyond PDF. Add
BedrockDocumentFormat union type, MIME-to-format mapping, and helpers
in data-provider so both client and backend can reference them.

* refactor: generalize Bedrock PDF validation to support all document types

Rename validateBedrockPdf to validateBedrockDocument with MIME-aware
logic: 4.5MB hard limit applies to all types, PDF header check only
runs for application/pdf. Adds test coverage for non-PDF documents.

* feat: support all Bedrock document formats in encoding pipeline

Widen file type gates to accept csv, doc, docx, xls, xlsx, html, txt,
md for Bedrock. Uses shared MIME-to-format map instead of hardcoded
'pdf'. Other providers' PDF-only paths remain unchanged.

* feat: expand Bedrock file upload UI to accept all document types

Add 'image_document_extended' upload type for Bedrock with accept
filters for all 9 supported formats. Update drag-and-drop validation
to use isBedrockDocumentType helper.

* fix: route Bedrock document types through provider pipeline
2026-02-23 22:32:44 -05:00

353 lines
10 KiB
TypeScript

import { Providers } from '@librechat/agents';
import { mbToBytes, isOpenAILikeProvider } from 'librechat-data-provider';
export interface ValidationResult {
isValid: boolean;
error?: string;
}
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 (provider === Providers.BEDROCK) {
return validateBedrockDocument(fileSize, 'application/pdf', pdfBuffer, 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 a document against Bedrock's 4.5MB hard limit. PDF-specific header
* checks run only when the MIME type is `application/pdf`.
* @param fileSize - The file size in bytes
* @param mimeType - The MIME type of the document
* @param fileBuffer - The file buffer (used for PDF header validation)
* @param configuredFileSizeLimit - Optional configured file size limit from fileConfig (in bytes)
* @returns Promise that resolves to validation result
*/
export async function validateBedrockDocument(
fileSize: number,
mimeType: string,
fileBuffer?: Buffer,
configuredFileSizeLimit?: number,
): Promise<ValidationResult> {
try {
/** Bedrock enforces a hard 4.5MB per-document limit at the API level; config can only lower it */
const providerLimit = mbToBytes(4.5);
const effectiveLimit =
configuredFileSizeLimit != null
? Math.min(configuredFileSizeLimit, providerLimit)
: providerLimit;
if (fileSize > effectiveLimit) {
const limitMB = (effectiveLimit / (1024 * 1024)).toFixed(1);
return {
isValid: false,
error: `File size (${(fileSize / (1024 * 1024)).toFixed(1)}MB) exceeds the ${limitMB}MB limit for Bedrock`,
};
}
if (mimeType === 'application/pdf' && fileBuffer) {
if (fileBuffer.length < 5) {
return {
isValid: false,
error: 'Invalid PDF file: too small or corrupted',
};
}
const pdfHeader = fileBuffer.subarray(0, 5).toString();
if (!pdfHeader.startsWith('%PDF-')) {
return {
isValid: false,
error: 'Invalid PDF file: missing PDF header',
};
}
}
return { isValid: true };
} catch (error) {
console.error('Bedrock document validation error:', error);
return {
isValid: false,
error: 'Failed to validate document 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 };
}