| | 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'; |
| |
|
| | |
| | jest.mock('~/files/validation', () => ({ |
| | validatePdf: jest.fn(), |
| | })); |
| |
|
| | |
| | jest.mock('./utils', () => ({ |
| | getFileStream: jest.fn(), |
| | getConfiguredFileSizeLimit: jest.fn(), |
| | })); |
| |
|
| | import { validatePdf } from '~/files/validation'; |
| | import { getFileStream, getConfiguredFileSizeLimit } from './utils'; |
| | import { Types } from 'mongoose'; |
| |
|
| | const mockedValidatePdf = validatePdf as jest.MockedFunction<typeof validatePdf>; |
| | 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(); |
| | |
| | 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; |
| | }); |
| | }); |
| |
|
| | |
| | const createMockRequest = (fileSizeLimit?: number): Partial<AppConfig> => ({ |
| | config: |
| | fileSizeLimit !== undefined |
| | ? { |
| | fileConfig: { |
| | endpoints: { |
| | [Providers.OPENAI]: { |
| | fileSizeLimit, |
| | }, |
| | }, |
| | }, |
| | } |
| | : undefined, |
| | }); |
| |
|
| | |
| | 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; |
| |
|
| | 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, |
| | ); |
| |
|
| | |
| | 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: { |
| | |
| | [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, |
| | ); |
| |
|
| | |
| | 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 () => { |
| | |
| | |
| | |
| | |
| | |
| | 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 () => { |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 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}`, |
| | }); |
| | }); |
| | }); |
| | }); |
| |
|