| const path = require('path'); |
| const mime = require('mime'); |
| const { v4 } = require('uuid'); |
| const { |
| isUUID, |
| megabyte, |
| FileContext, |
| FileSources, |
| imageExtRegex, |
| EModelEndpoint, |
| mergeFileConfig, |
| hostImageIdSuffix, |
| checkOpenAIStorage, |
| hostImageNamePrefix, |
| isAssistantsEndpoint, |
| } = require('librechat-data-provider'); |
| const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2'); |
| const { convertImage, resizeAndConvert } = require('~/server/services/Files/images'); |
| const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); |
| const { createFile, updateFileUsage, deleteFiles } = require('~/models/File'); |
| const { LB_QueueAsyncCall } = require('~/server/utils/queue'); |
| const { getStrategyFunctions } = require('./strategies'); |
| const { determineFileType } = require('~/server/utils'); |
| const { logger } = require('~/config'); |
|
|
| const processFiles = async (files) => { |
| const promises = []; |
| for (let file of files) { |
| const { file_id } = file; |
| promises.push(updateFileUsage({ file_id })); |
| } |
|
|
| |
| return await Promise.all(promises); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileIds, openai }) { |
| if (checkOpenAIStorage(file.source)) { |
| |
| promises.push( |
| new Promise((resolve, reject) => { |
| LB_QueueAsyncCall( |
| () => deleteFile(req, file, openai), |
| [], |
| (err, result) => { |
| if (err) { |
| logger.error('Error deleting file from OpenAI source', err); |
| reject(err); |
| } else { |
| resolvedFileIds.push(file.file_id); |
| resolve(result); |
| } |
| }, |
| ); |
| }), |
| ); |
| } else { |
| |
| promises.push( |
| deleteFile(req, file) |
| .then(() => resolvedFileIds.push(file.file_id)) |
| .catch((err) => { |
| logger.error('Error deleting file', err); |
| return Promise.reject(err); |
| }), |
| ); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const processDeleteRequest = async ({ req, files }) => { |
| const resolvedFileIds = []; |
| const deletionMethods = {}; |
| const promises = []; |
|
|
| |
| const client = { [FileSources.openai]: undefined, [FileSources.azure]: undefined }; |
| const initializeClients = async () => { |
| const openAIClient = await getOpenAIClient({ |
| req, |
| overrideEndpoint: EModelEndpoint.assistants, |
| }); |
| client[FileSources.openai] = openAIClient.openai; |
|
|
| if (!req.app.locals[EModelEndpoint.azureOpenAI]?.assistants) { |
| return; |
| } |
|
|
| const azureClient = await getOpenAIClient({ |
| req, |
| overrideEndpoint: EModelEndpoint.azureAssistants, |
| }); |
| client[FileSources.azure] = azureClient.openai; |
| }; |
|
|
| if (req.body.assistant_id !== undefined) { |
| await initializeClients(); |
| } |
|
|
| for (const file of files) { |
| const source = file.source ?? FileSources.local; |
|
|
| if (checkOpenAIStorage(source) && !client[source]) { |
| await initializeClients(); |
| } |
|
|
| const openai = client[source]; |
|
|
| if (req.body.assistant_id && req.body.tool_resource) { |
| promises.push( |
| deleteResourceFileId({ |
| req, |
| openai, |
| file_id: file.file_id, |
| assistant_id: req.body.assistant_id, |
| tool_resource: req.body.tool_resource, |
| }), |
| ); |
| } else if (req.body.assistant_id) { |
| promises.push(openai.beta.assistants.files.del(req.body.assistant_id, file.file_id)); |
| } |
|
|
| if (deletionMethods[source]) { |
| enqueueDeleteOperation({ |
| req, |
| file, |
| deleteFile: deletionMethods[source], |
| promises, |
| resolvedFileIds, |
| openai, |
| }); |
| continue; |
| } |
|
|
| const { deleteFile } = getStrategyFunctions(source); |
| if (!deleteFile) { |
| throw new Error(`Delete function not implemented for ${source}`); |
| } |
|
|
| deletionMethods[source] = deleteFile; |
| enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileIds, openai }); |
| } |
|
|
| await Promise.allSettled(promises); |
| await deleteFiles(resolvedFileIds); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const processFileURL = async ({ fileStrategy, userId, URL, fileName, basePath, context }) => { |
| const { saveURL, getFileURL } = getStrategyFunctions(fileStrategy); |
| try { |
| const { |
| bytes = 0, |
| type = '', |
| dimensions = {}, |
| } = (await saveURL({ userId, URL, fileName, basePath })) || {}; |
| const filepath = await getFileURL({ fileName: `${userId}/${fileName}`, basePath }); |
| return await createFile( |
| { |
| user: userId, |
| file_id: v4(), |
| bytes, |
| filepath, |
| filename: fileName, |
| source: fileStrategy, |
| type, |
| context, |
| width: dimensions.width, |
| height: dimensions.height, |
| }, |
| true, |
| ); |
| } catch (error) { |
| logger.error(`Error while processing the image with ${fileStrategy}:`, error); |
| throw new Error(`Failed to process the image with ${fileStrategy}. ${error.message}`); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const processImageFile = async ({ req, res, file, metadata, returnFile = false }) => { |
| const source = req.app.locals.fileStrategy; |
| const { handleImageUpload } = getStrategyFunctions(source); |
| const { file_id, temp_file_id, endpoint } = metadata; |
|
|
| const { filepath, bytes, width, height } = await handleImageUpload({ |
| req, |
| file, |
| file_id, |
| endpoint, |
| }); |
|
|
| const result = await createFile( |
| { |
| user: req.user.id, |
| file_id, |
| temp_file_id, |
| bytes, |
| filepath, |
| filename: file.originalname, |
| context: FileContext.message_attachment, |
| source, |
| type: `image/${req.app.locals.imageOutputType}`, |
| width, |
| height, |
| }, |
| true, |
| ); |
|
|
| if (returnFile) { |
| return result; |
| } |
| res.status(200).json({ message: 'File uploaded and processed successfully', ...result }); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const uploadImageBuffer = async ({ req, context, metadata = {}, resize = true }) => { |
| const source = req.app.locals.fileStrategy; |
| const { saveBuffer } = getStrategyFunctions(source); |
| let { buffer, width, height, bytes, filename, file_id, type } = metadata; |
| if (resize) { |
| file_id = v4(); |
| type = `image/${req.app.locals.imageOutputType}`; |
| ({ buffer, width, height, bytes } = await resizeAndConvert({ |
| inputBuffer: buffer, |
| desiredFormat: req.app.locals.imageOutputType, |
| })); |
| filename = `${path.basename(req.file.originalname, path.extname(req.file.originalname))}.${ |
| req.app.locals.imageOutputType |
| }`; |
| } |
|
|
| const filepath = await saveBuffer({ userId: req.user.id, fileName: filename, buffer }); |
| return await createFile( |
| { |
| user: req.user.id, |
| file_id, |
| bytes, |
| filepath, |
| filename, |
| context, |
| source, |
| type, |
| width, |
| height, |
| }, |
| true, |
| ); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const processFileUpload = async ({ req, res, file, metadata }) => { |
| const isAssistantUpload = isAssistantsEndpoint(metadata.endpoint); |
| const assistantSource = |
| metadata.endpoint === EModelEndpoint.azureAssistants ? FileSources.azure : FileSources.openai; |
| const source = isAssistantUpload ? assistantSource : FileSources.vectordb; |
| const { handleFileUpload } = getStrategyFunctions(source); |
| const { file_id, temp_file_id } = metadata; |
|
|
| |
| let openai; |
| if (checkOpenAIStorage(source)) { |
| ({ openai } = await getOpenAIClient({ req })); |
| } |
|
|
| const { |
| id, |
| bytes, |
| filename, |
| filepath: _filepath, |
| embedded, |
| height, |
| width, |
| } = await handleFileUpload({ |
| req, |
| file, |
| file_id, |
| openai, |
| }); |
|
|
| if (isAssistantUpload && !metadata.message_file && !metadata.tool_resource) { |
| await openai.beta.assistants.files.create(metadata.assistant_id, { |
| file_id: id, |
| }); |
| } else if (isAssistantUpload && !metadata.message_file) { |
| await addResourceFileId({ |
| req, |
| openai, |
| file_id: id, |
| assistant_id: metadata.assistant_id, |
| tool_resource: metadata.tool_resource, |
| }); |
| } |
|
|
| let filepath = isAssistantUpload ? `${openai.baseURL}/files/${id}` : _filepath; |
| if (isAssistantUpload && file.mimetype.startsWith('image')) { |
| const result = await processImageFile({ |
| req, |
| file, |
| metadata: { file_id: v4() }, |
| returnFile: true, |
| }); |
| filepath = result.filepath; |
| } |
|
|
| const result = await createFile( |
| { |
| user: req.user.id, |
| file_id: id ?? file_id, |
| temp_file_id, |
| bytes, |
| filepath, |
| filename: filename ?? file.originalname, |
| context: isAssistantUpload ? FileContext.assistants : FileContext.message_attachment, |
| model: isAssistantUpload ? req.body.model : undefined, |
| type: file.mimetype, |
| embedded, |
| source, |
| height, |
| width, |
| }, |
| true, |
| ); |
| res.status(200).json({ message: 'File uploaded and processed successfully', ...result }); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const processOpenAIFile = async ({ |
| openai, |
| file_id, |
| userId, |
| filename, |
| saveFile = false, |
| updateUsage = false, |
| }) => { |
| const _file = await openai.files.retrieve(file_id); |
| const originalName = filename ?? (_file.filename ? path.basename(_file.filename) : undefined); |
| const filepath = `${openai.baseURL}/files/${userId}/${file_id}${ |
| originalName ? `/${originalName}` : '' |
| }`; |
| const type = mime.getType(originalName ?? file_id); |
| const source = |
| openai.req.body.endpoint === EModelEndpoint.azureAssistants |
| ? FileSources.azure |
| : FileSources.openai; |
| const file = { |
| ..._file, |
| type, |
| file_id, |
| filepath, |
| usage: 1, |
| user: userId, |
| context: _file.purpose, |
| source, |
| model: openai.req.body.model, |
| filename: originalName ?? file_id, |
| }; |
|
|
| if (saveFile) { |
| await createFile(file, true); |
| } else if (updateUsage) { |
| try { |
| await updateFileUsage({ file_id }); |
| } catch (error) { |
| logger.error('Error updating file usage', error); |
| } |
| } |
|
|
| return file; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const processOpenAIImageOutput = async ({ req, buffer, file_id, filename, fileExt }) => { |
| const currentDate = new Date(); |
| const formattedDate = currentDate.toISOString(); |
| const _file = await convertImage(req, buffer, 'high', `${file_id}${fileExt}`); |
| const file = { |
| ..._file, |
| usage: 1, |
| user: req.user.id, |
| type: `image/${req.app.locals.imageOutputType}`, |
| createdAt: formattedDate, |
| updatedAt: formattedDate, |
| source: req.app.locals.fileStrategy, |
| context: FileContext.assistants_output, |
| file_id: `${file_id}${hostImageIdSuffix}`, |
| filename: `${hostImageNamePrefix}${filename}`, |
| }; |
| createFile(file, true); |
| const source = |
| req.body.endpoint === EModelEndpoint.azureAssistants ? FileSources.azure : FileSources.openai; |
| createFile( |
| { |
| ...file, |
| file_id, |
| filename, |
| source, |
| type: mime.getType(fileExt), |
| }, |
| true, |
| ); |
| return file; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function retrieveAndProcessFile({ |
| openai, |
| client, |
| file_id, |
| basename: _basename, |
| unknownType, |
| }) { |
| if (!file_id) { |
| return null; |
| } |
|
|
| let basename = _basename; |
| const processArgs = { openai, file_id, filename: basename, userId: client.req.user.id }; |
|
|
| |
| if (!basename) { |
| return await processOpenAIFile({ ...processArgs, saveFile: true }); |
| } |
|
|
| const fileExt = path.extname(basename); |
| if (client.attachedFileIds?.has(file_id) || client.processedFileIds?.has(file_id)) { |
| return processOpenAIFile({ ...processArgs, updateUsage: true }); |
| } |
|
|
| |
| |
| |
| const getDataBuffer = async () => { |
| const response = await openai.files.content(file_id); |
| const arrayBuffer = await response.arrayBuffer(); |
| return Buffer.from(arrayBuffer); |
| }; |
|
|
| let dataBuffer; |
| if (unknownType || !fileExt || imageExtRegex.test(basename)) { |
| try { |
| dataBuffer = await getDataBuffer(); |
| } catch (error) { |
| logger.error('Error downloading file from OpenAI:', error); |
| dataBuffer = null; |
| } |
| } |
|
|
| if (!dataBuffer) { |
| return await processOpenAIFile({ ...processArgs, saveFile: true }); |
| } |
|
|
| |
| if (dataBuffer && (unknownType || !fileExt)) { |
| const detectedExt = await determineFileType(dataBuffer); |
| const isImageOutput = detectedExt && imageExtRegex.test('.' + detectedExt); |
|
|
| if (!isImageOutput) { |
| return await processOpenAIFile({ ...processArgs, saveFile: true }); |
| } |
|
|
| return await processOpenAIImageOutput({ |
| file_id, |
| req: client.req, |
| buffer: dataBuffer, |
| filename: basename, |
| fileExt: detectedExt, |
| }); |
| } else if (dataBuffer && imageExtRegex.test(basename)) { |
| return await processOpenAIImageOutput({ |
| file_id, |
| req: client.req, |
| buffer: dataBuffer, |
| filename: basename, |
| fileExt, |
| }); |
| } else { |
| logger.debug(`[retrieveAndProcessFile] Non-image file type detected: ${basename}`); |
| return await processOpenAIFile({ ...processArgs, saveFile: true }); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function filterFile({ req, file, image }) { |
| const { endpoint, file_id, width, height } = req.body; |
|
|
| if (!file_id) { |
| throw new Error('No file_id provided'); |
| } |
|
|
| if (file.size === 0) { |
| throw new Error('Empty file uploaded'); |
| } |
|
|
| |
| isUUID.parse(file_id); |
|
|
| if (!endpoint) { |
| throw new Error('No endpoint provided'); |
| } |
|
|
| const fileConfig = mergeFileConfig(req.app.locals.fileConfig); |
|
|
| const { fileSizeLimit, supportedMimeTypes } = |
| fileConfig.endpoints[endpoint] ?? fileConfig.endpoints.default; |
|
|
| if (file.size > fileSizeLimit) { |
| throw new Error( |
| `File size limit of ${fileSizeLimit / megabyte} MB exceeded for ${endpoint} endpoint`, |
| ); |
| } |
|
|
| const isSupportedMimeType = fileConfig.checkType(file.mimetype, supportedMimeTypes); |
|
|
| if (!isSupportedMimeType) { |
| throw new Error('Unsupported file type'); |
| } |
|
|
| if (!image) { |
| return; |
| } |
|
|
| if (!width) { |
| throw new Error('No width provided'); |
| } |
|
|
| if (!height) { |
| throw new Error('No height provided'); |
| } |
| } |
|
|
| module.exports = { |
| filterFile, |
| processFiles, |
| processFileURL, |
| processImageFile, |
| uploadImageBuffer, |
| processFileUpload, |
| processDeleteRequest, |
| retrieveAndProcessFile, |
| }; |
|
|