LibreChat/packages/api/src/files/encode/document.spec.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

714 lines
20 KiB
TypeScript

import { Providers } from '@librechat/agents';
import { mbToBytes } from 'librechat-data-provider';
import type { AppConfig, IMongoFile } from '@librechat/data-schemas';
import type { ServerRequest } from '~/types';
import { encodeAndFormatDocuments } from './document';
/** Mock the validation module */
jest.mock('~/files/validation', () => ({
validatePdf: jest.fn(),
validateBedrockDocument: jest.fn(),
}));
/** Mock the utils module */
jest.mock('./utils', () => ({
getFileStream: jest.fn(),
getConfiguredFileSizeLimit: jest.fn(),
}));
import { validatePdf, validateBedrockDocument } from '~/files/validation';
import { getFileStream, getConfiguredFileSizeLimit } from './utils';
import { Types } from 'mongoose';
const mockedValidatePdf = validatePdf as jest.MockedFunction<typeof validatePdf>;
const mockedValidateBedrockDocument = validateBedrockDocument as jest.MockedFunction<
typeof validateBedrockDocument
>;
const mockedGetFileStream = getFileStream as jest.MockedFunction<typeof getFileStream>;
const mockedGetConfiguredFileSizeLimit = getConfiguredFileSizeLimit as jest.MockedFunction<
typeof getConfiguredFileSizeLimit
>;
describe('encodeAndFormatDocuments - fileConfig integration', () => {
const mockStrategyFunctions = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
/** Default mock implementation for getConfiguredFileSizeLimit */
mockedGetConfiguredFileSizeLimit.mockImplementation((req, params) => {
if (!req.config?.fileConfig) {
return undefined;
}
const { provider, endpoint } = params;
const lookupKey = endpoint ?? provider;
const fileConfig = req.config.fileConfig;
const endpoints = fileConfig.endpoints;
if (endpoints?.[lookupKey]) {
const limit = endpoints[lookupKey].fileSizeLimit;
return limit !== undefined ? mbToBytes(limit) : undefined;
}
if (endpoints?.default) {
const limit = endpoints.default.fileSizeLimit;
return limit !== undefined ? mbToBytes(limit) : undefined;
}
return undefined;
});
});
/** Helper to create a mock request with file config */
const createMockRequest = (fileSizeLimit?: number): Partial<AppConfig> => ({
config:
fileSizeLimit !== undefined
? {
fileConfig: {
endpoints: {
[Providers.OPENAI]: {
fileSizeLimit,
},
},
},
}
: undefined,
});
/** Helper to create a mock PDF file */
const createMockFile = (sizeInMB: number): IMongoFile =>
({
_id: new Types.ObjectId(),
user: new Types.ObjectId(),
file_id: new Types.ObjectId().toString(),
filename: 'test.pdf',
type: 'application/pdf',
bytes: Math.floor(sizeInMB * 1024 * 1024),
object: 'file',
usage: 0,
source: 'test',
filepath: '/test/path.pdf',
createdAt: new Date(),
updatedAt: new Date(),
}) as unknown as IMongoFile;
const createMockDocFile = (
sizeInMB: number,
mimeType: string,
filename: string,
): IMongoFile =>
({
_id: new Types.ObjectId(),
user: new Types.ObjectId(),
file_id: new Types.ObjectId().toString(),
filename,
type: mimeType,
bytes: Math.floor(sizeInMB * 1024 * 1024),
object: 'file',
usage: 0,
source: 'test',
filepath: `/test/path/${filename}`,
createdAt: new Date(),
updatedAt: new Date(),
}) as unknown as IMongoFile;
describe('Configuration extraction and validation', () => {
it('should pass configured file size limit to validatePdf for OpenAI', async () => {
const configuredLimit = mbToBytes(15);
const req = createMockRequest(15) as ServerRequest;
const file = createMockFile(10);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({ isValid: true });
await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.OPENAI },
mockStrategyFunctions,
);
expect(mockedValidatePdf).toHaveBeenCalledWith(
expect.any(Buffer),
expect.any(Number),
Providers.OPENAI,
configuredLimit,
);
});
it('should pass undefined when no fileConfig is provided', async () => {
const req = {} as ServerRequest;
const file = createMockFile(10);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({ isValid: true });
await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.OPENAI },
mockStrategyFunctions,
);
expect(mockedValidatePdf).toHaveBeenCalledWith(
expect.any(Buffer),
expect.any(Number),
Providers.OPENAI,
undefined,
);
});
it('should pass undefined when fileConfig.endpoints is not defined', async () => {
const req = {
config: {
fileConfig: {},
},
} as ServerRequest;
const file = createMockFile(10);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({ isValid: true });
await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.OPENAI },
mockStrategyFunctions,
);
/** When fileConfig has no endpoints, getConfiguredFileSizeLimit returns undefined */
expect(mockedValidatePdf).toHaveBeenCalledWith(
expect.any(Buffer),
expect.any(Number),
Providers.OPENAI,
undefined,
);
});
it('should use endpoint-specific config for Anthropic', async () => {
const configuredLimit = mbToBytes(20);
const req = {
config: {
fileConfig: {
endpoints: {
[Providers.ANTHROPIC]: {
fileSizeLimit: 20,
},
},
},
},
} as unknown as ServerRequest;
const file = createMockFile(15);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({ isValid: true });
await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.ANTHROPIC },
mockStrategyFunctions,
);
expect(mockedValidatePdf).toHaveBeenCalledWith(
expect.any(Buffer),
expect.any(Number),
Providers.ANTHROPIC,
configuredLimit,
);
});
it('should use endpoint-specific config for Google', async () => {
const configuredLimit = mbToBytes(25);
const req = {
config: {
fileConfig: {
endpoints: {
[Providers.GOOGLE]: {
fileSizeLimit: 25,
},
},
},
},
} as unknown as ServerRequest;
const file = createMockFile(18);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({ isValid: true });
await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.GOOGLE },
mockStrategyFunctions,
);
expect(mockedValidatePdf).toHaveBeenCalledWith(
expect.any(Buffer),
expect.any(Number),
Providers.GOOGLE,
configuredLimit,
);
});
it('should pass undefined when provider-specific config not found and no default', async () => {
const req = {
config: {
fileConfig: {
endpoints: {
/** Only configure a different provider, not OpenAI */
[Providers.ANTHROPIC]: {
fileSizeLimit: 25,
},
},
},
},
} as unknown as ServerRequest;
const file = createMockFile(20);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({ isValid: true });
await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.OPENAI },
mockStrategyFunctions,
);
/** When provider-specific config not found and no default, returns undefined */
expect(mockedValidatePdf).toHaveBeenCalledWith(
expect.any(Buffer),
expect.any(Number),
Providers.OPENAI,
undefined,
);
});
});
describe('Validation failure handling', () => {
it('should throw error when validation fails', async () => {
const req = createMockRequest(10) as ServerRequest;
const file = createMockFile(12);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({
isValid: false,
error: 'PDF file size (12MB) exceeds the 10MB limit',
});
await expect(
encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.OPENAI },
mockStrategyFunctions,
),
).rejects.toThrow('PDF validation failed: PDF file size (12MB) exceeds the 10MB limit');
});
it('should not call validatePdf for non-PDF files', async () => {
const req = createMockRequest(10) as ServerRequest;
const file: IMongoFile = {
...createMockFile(5),
type: 'image/jpeg',
filename: 'test.jpg',
};
const mockContent = Buffer.from('test-image-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.OPENAI },
mockStrategyFunctions,
);
expect(mockedValidatePdf).not.toHaveBeenCalled();
});
});
describe('Bug reproduction scenarios', () => {
it('should respect user-configured lower limit (stricter than provider)', async () => {
/**
* Scenario: User sets openAI.fileSizeLimit = 5MB (stricter than 10MB provider limit)
* Uploads 7MB PDF
* Expected: Validation called with 5MB limit
*/
const req = createMockRequest(5) as ServerRequest;
const file = createMockFile(7);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({
isValid: false,
error: 'PDF file size (7MB) exceeds the 5MB limit',
});
await expect(
encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.OPENAI },
mockStrategyFunctions,
),
).rejects.toThrow('PDF validation failed');
expect(mockedValidatePdf).toHaveBeenCalledWith(
expect.any(Buffer),
expect.any(Number),
Providers.OPENAI,
mbToBytes(5),
);
});
it('should respect user-configured higher limit (allows API changes)', async () => {
/**
* Scenario: User sets openAI.fileSizeLimit = 50MB (higher than 10MB provider default)
* Uploads 15MB PDF
* Expected: Validation called with 50MB limit, allowing files between 10-50MB
* This allows users to take advantage of API limit increases
*/
const req = createMockRequest(50) as ServerRequest;
const file = createMockFile(15);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({ isValid: true });
await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.OPENAI },
mockStrategyFunctions,
);
expect(mockedValidatePdf).toHaveBeenCalledWith(
expect.any(Buffer),
expect.any(Number),
Providers.OPENAI,
mbToBytes(50),
);
});
it('should handle multiple files with different sizes', async () => {
const req = createMockRequest(10) as ServerRequest;
const file1 = createMockFile(5);
const file2 = createMockFile(8);
const mockContent1 = Buffer.from('pdf-content-1').toString('base64');
const mockContent2 = Buffer.from('pdf-content-2').toString('base64');
mockedGetFileStream
.mockResolvedValueOnce({
file: file1,
content: mockContent1,
metadata: file1,
})
.mockResolvedValueOnce({
file: file2,
content: mockContent2,
metadata: file2,
});
mockedValidatePdf.mockResolvedValue({ isValid: true });
await encodeAndFormatDocuments(
req,
[file1, file2],
{ provider: Providers.OPENAI },
mockStrategyFunctions,
);
expect(mockedValidatePdf).toHaveBeenCalledTimes(2);
expect(mockedValidatePdf).toHaveBeenNthCalledWith(
1,
expect.any(Buffer),
expect.any(Number),
Providers.OPENAI,
mbToBytes(10),
);
expect(mockedValidatePdf).toHaveBeenNthCalledWith(
2,
expect.any(Buffer),
expect.any(Number),
Providers.OPENAI,
mbToBytes(10),
);
});
});
describe('Document formatting after validation', () => {
it('should format Anthropic document with valid PDF', async () => {
const req = createMockRequest(30) as ServerRequest;
const file = createMockFile(20);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({ isValid: true });
const result = await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.ANTHROPIC },
mockStrategyFunctions,
);
expect(result.documents).toHaveLength(1);
expect(result.documents[0]).toMatchObject({
type: 'document',
source: {
type: 'base64',
media_type: 'application/pdf',
data: mockContent,
},
citations: { enabled: true },
});
});
it('should format Bedrock document with valid PDF', async () => {
const req = createMockRequest() as ServerRequest;
const file = createMockFile(3);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidateBedrockDocument.mockResolvedValue({ isValid: true });
const result = await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.BEDROCK },
mockStrategyFunctions,
);
expect(result.documents).toHaveLength(1);
expect(result.documents[0]).toMatchObject({
type: 'document',
document: {
name: 'test_pdf',
format: 'pdf',
source: {
bytes: expect.any(Buffer),
},
},
});
});
it('should format Bedrock CSV document', async () => {
const req = createMockRequest() as ServerRequest;
const file = createMockDocFile(1, 'text/csv', 'data.csv');
const mockContent = Buffer.from('col1,col2\nval1,val2').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidateBedrockDocument.mockResolvedValue({ isValid: true });
const result = await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.BEDROCK },
mockStrategyFunctions,
);
expect(result.documents).toHaveLength(1);
expect(result.documents[0]).toMatchObject({
type: 'document',
document: {
name: 'data_csv',
format: 'csv',
source: {
bytes: expect.any(Buffer),
},
},
});
});
it('should format Bedrock DOCX document', async () => {
const req = createMockRequest() as ServerRequest;
const mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
const file = createMockDocFile(2, mimeType, 'report.docx');
const mockContent = Buffer.from('docx-binary-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidateBedrockDocument.mockResolvedValue({ isValid: true });
const result = await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.BEDROCK },
mockStrategyFunctions,
);
expect(result.documents).toHaveLength(1);
expect(result.documents[0]).toMatchObject({
type: 'document',
document: {
name: 'report_docx',
format: 'docx',
source: {
bytes: expect.any(Buffer),
},
},
});
});
it('should format Bedrock plain text document', async () => {
const req = createMockRequest() as ServerRequest;
const file = createMockDocFile(0.5, 'text/plain', 'notes.txt');
const mockContent = Buffer.from('plain text content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidateBedrockDocument.mockResolvedValue({ isValid: true });
const result = await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.BEDROCK },
mockStrategyFunctions,
);
expect(result.documents).toHaveLength(1);
expect(result.documents[0]).toMatchObject({
type: 'document',
document: {
name: 'notes_txt',
format: 'txt',
source: {
bytes: expect.any(Buffer),
},
},
});
});
it('should reject Bedrock document when validation fails', async () => {
const req = createMockRequest() as ServerRequest;
const file = createMockDocFile(5, 'text/csv', 'big.csv');
const mockContent = Buffer.from('large-csv-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidateBedrockDocument.mockResolvedValue({
isValid: false,
error: 'File size (5.0MB) exceeds the 4.5MB limit for Bedrock',
});
await expect(
encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.BEDROCK },
mockStrategyFunctions,
),
).rejects.toThrow('Document validation failed');
});
it('should format OpenAI document with responses API', async () => {
const req = createMockRequest(15) as ServerRequest;
const file = createMockFile(10);
const mockContent = Buffer.from('test-pdf-content').toString('base64');
mockedGetFileStream.mockResolvedValue({
file,
content: mockContent,
metadata: file,
});
mockedValidatePdf.mockResolvedValue({ isValid: true });
const result = await encodeAndFormatDocuments(
req,
[file],
{ provider: Providers.OPENAI, useResponsesApi: true },
mockStrategyFunctions,
);
expect(result.documents).toHaveLength(1);
expect(result.documents[0]).toMatchObject({
type: 'input_file',
filename: 'test.pdf',
file_data: `data:application/pdf;base64,${mockContent}`,
});
});
});
});