📁 feat: Send Attachments Directly to Provider (OpenAI) (#9098)

* refactor: change references from direct upload to direct attach to better reflect functionality

since we are just using base64 encoding strategy now rather than Files/File API for sending our attachments directly to the provider, the upload nomenclature no longer makes sense. direct_attach better describes the different methods of sending attachments to providers anyways even if we later introduce direct upload support

* feat: add upload to provider option for openai (and agent) ui

* chore: move anthropic pdf validator over to packages/api

* feat: simple pdf validation according to openai docs

* feat: add provider agnostic validatePdf logic to start handling multiple endpoints

* feat: add handling for openai specific documentPart formatting

* refactor: move require statement to proper place at top of file

* chore: add in openAI endpoint for the rest of the document handling logic

* feat: add direct attach support for azureOpenAI endpoint and agents

* feat: add pdf validation for azureOpenAI endpoint

* refactor: unify all the endpoint checks with isDocumentSupportedEndpoint

* refactor: consolidate Upload to Provider vs Upload image logic for clarity

* refactor: remove anthropic from anthropic_multimodal fileType since we support multiple providers now
This commit is contained in:
Dustin Healy 2025-08-17 02:14:25 -07:00 committed by Dustin Healy
parent 89843262b2
commit b5aadf1302
10 changed files with 122 additions and 64 deletions

View file

@ -2,3 +2,4 @@ export * from './mistral/crud';
export * from './audio';
export * from './text';
export * from './parse';
export * from './validation';

View file

@ -0,0 +1,98 @@
import { anthropicPdfSizeLimit, EModelEndpoint } from 'librechat-data-provider';
export interface PDFValidationResult {
isValid: boolean;
error?: string;
}
export async function validatePdf(
pdfBuffer: Buffer,
fileSize: number,
endpoint: EModelEndpoint,
): Promise<PDFValidationResult> {
if (endpoint === EModelEndpoint.anthropic) {
return validateAnthropicPdf(pdfBuffer, fileSize);
}
if (endpoint === EModelEndpoint.openAI || endpoint === EModelEndpoint.azureOpenAI) {
return validateOpenAIPdf(fileSize);
}
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
* @returns Promise that resolves to validation result
*/
async function validateAnthropicPdf(
pdfBuffer: Buffer,
fileSize: number,
): Promise<PDFValidationResult> {
try {
if (fileSize > anthropicPdfSizeLimit) {
return {
isValid: false,
error: `PDF file size (${Math.round(fileSize / (1024 * 1024))}MB) exceeds Anthropic's 32MB 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',
};
}
}
async function validateOpenAIPdf(fileSize: number): Promise<PDFValidationResult> {
if (fileSize > 10 * 1024 * 1024) {
return {
isValid: false,
error: "PDF file size exceeds OpenAI's 10MB limit",
};
}
return { isValid: true };
}