mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-11 08:43:48 +00:00
* 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
353 lines
10 KiB
TypeScript
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 };
|
|
}
|