mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix: bound peak memory of concurrent base64 attachment encoding (#14023)
* fix: bound peak memory of concurrent base64 attachment encoding * chore: sort encode imports --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
38ab4add3d
commit
b20abb2593
10 changed files with 395 additions and 13 deletions
|
|
@ -1,6 +1,6 @@
|
|||
const axios = require('axios');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { logAxiosError, validateImage } = require('@librechat/api');
|
||||
const { logAxiosError, validateImage, runGuardedEncode } = require('@librechat/api');
|
||||
const {
|
||||
FileSources,
|
||||
VisionModes,
|
||||
|
|
@ -136,9 +136,12 @@ async function encodeAndFormat(req, files, params, mode) {
|
|||
if (blobStorageSources.has(source)) {
|
||||
try {
|
||||
const downloadStream = encodingMethods[source].getDownloadStream;
|
||||
let stream = await downloadStream(req, file.filepath);
|
||||
let base64Data = await streamToBase64(stream);
|
||||
stream = null;
|
||||
let base64Data = await runGuardedEncode(file.bytes ?? 0, async () => {
|
||||
let stream = await downloadStream(req, file.filepath);
|
||||
const data = await streamToBase64(stream);
|
||||
stream = null;
|
||||
return data;
|
||||
});
|
||||
promises.push([file, base64Data]);
|
||||
base64Data = null;
|
||||
continue;
|
||||
|
|
@ -146,8 +149,11 @@ async function encodeAndFormat(req, files, params, mode) {
|
|||
logger.error('Error processing image from blob storage:', error);
|
||||
}
|
||||
} else if (source !== FileSources.local && base64Only.has(effectiveEndpoint)) {
|
||||
const [_file, imageURL] = await preparePayload(req, file);
|
||||
promises.push([_file, await fetchImageToBase64(imageURL)]);
|
||||
const entry = await runGuardedEncode(file.bytes ?? 0, async () => {
|
||||
const [_file, imageURL] = await preparePayload(req, file);
|
||||
return [_file, await fetchImageToBase64(imageURL)];
|
||||
});
|
||||
promises.push(entry);
|
||||
continue;
|
||||
}
|
||||
promises.push(preparePayload(req, file));
|
||||
|
|
|
|||
111
api/server/services/Files/images/encode.spec.js
Normal file
111
api/server/services/Files/images/encode.spec.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
const { Readable } = require('stream');
|
||||
|
||||
const mockRunGuardedEncode = jest.fn((_bytes, task) => task());
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('@librechat/api', () => ({
|
||||
logAxiosError: jest.fn(({ message }) => message),
|
||||
validateImage: jest.fn().mockResolvedValue({ isValid: true }),
|
||||
runGuardedEncode: (...args) => mockRunGuardedEncode(...args),
|
||||
}));
|
||||
jest.mock('@librechat/data-schemas', () => ({
|
||||
logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
|
||||
}));
|
||||
|
||||
const mockPrepareImagePayload = jest.fn();
|
||||
const mockGetDownloadStream = jest.fn();
|
||||
jest.mock('~/server/services/Files/strategies', () => ({
|
||||
getStrategyFunctions: jest.fn(() => ({
|
||||
prepareImagePayload: mockPrepareImagePayload,
|
||||
getDownloadStream: mockGetDownloadStream,
|
||||
})),
|
||||
}));
|
||||
|
||||
const axios = require('axios');
|
||||
const { FileSources } = require('librechat-data-provider');
|
||||
const { encodeAndFormat } = require('./encode');
|
||||
|
||||
const makeReq = () => ({ body: {}, config: {} });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRunGuardedEncode.mockImplementation((_bytes, task) => task());
|
||||
});
|
||||
|
||||
describe('encodeAndFormat - request memory guard', () => {
|
||||
it('gates blob-storage byte pulls and returns [file, base64]', async () => {
|
||||
mockGetDownloadStream.mockResolvedValue(Readable.from([Buffer.from('blob-image-bytes')]));
|
||||
const file = {
|
||||
source: FileSources.s3,
|
||||
height: 10,
|
||||
width: 10,
|
||||
type: 'image/png',
|
||||
file_id: 'f-blob',
|
||||
filepath: 'bucket/a.png',
|
||||
filename: 'a.png',
|
||||
bytes: 4321,
|
||||
};
|
||||
|
||||
const result = await encodeAndFormat(makeReq(), [file], { endpoint: 'openai' });
|
||||
|
||||
expect(mockRunGuardedEncode).toHaveBeenCalledTimes(1);
|
||||
expect(mockRunGuardedEncode.mock.calls[0][0]).toBe(4321);
|
||||
|
||||
const expectedBase64 = Buffer.from('blob-image-bytes').toString('base64');
|
||||
expect(result.image_urls).toHaveLength(1);
|
||||
expect(result.image_urls[0].image_url.url).toBe(`data:image/png;base64,${expectedBase64}`);
|
||||
});
|
||||
|
||||
it('gates base64Only URL fetches and returns [file, base64]', async () => {
|
||||
mockPrepareImagePayload.mockResolvedValue([
|
||||
{ source: FileSources.vectordb, type: 'image/png' },
|
||||
'https://images.example/x.png',
|
||||
]);
|
||||
axios.get.mockResolvedValue({ data: Buffer.from('url-image-bytes') });
|
||||
|
||||
const file = {
|
||||
source: FileSources.vectordb,
|
||||
height: 10,
|
||||
width: 10,
|
||||
type: 'image/png',
|
||||
file_id: 'f-url',
|
||||
filepath: 'remote/x.png',
|
||||
filename: 'x.png',
|
||||
bytes: 9876,
|
||||
};
|
||||
|
||||
const result = await encodeAndFormat(makeReq(), [file], { endpoint: 'anthropic' });
|
||||
|
||||
expect(mockRunGuardedEncode).toHaveBeenCalledTimes(1);
|
||||
expect(mockRunGuardedEncode.mock.calls[0][0]).toBe(9876);
|
||||
|
||||
const expectedBase64 = Buffer.from('url-image-bytes').toString('base64');
|
||||
expect(result.image_urls).toHaveLength(1);
|
||||
expect(result.image_urls[0].source.data).toBe(expectedBase64);
|
||||
});
|
||||
|
||||
it('does not gate the non-buffering local prepare path', async () => {
|
||||
const localBase64 = Buffer.from('local-image').toString('base64');
|
||||
mockPrepareImagePayload.mockResolvedValue([
|
||||
{ source: FileSources.local, type: 'image/png' },
|
||||
localBase64,
|
||||
]);
|
||||
|
||||
const file = {
|
||||
source: FileSources.local,
|
||||
height: 10,
|
||||
width: 10,
|
||||
type: 'image/png',
|
||||
file_id: 'f-local',
|
||||
filepath: 'local/p.png',
|
||||
filename: 'p.png',
|
||||
bytes: 555,
|
||||
};
|
||||
|
||||
const result = await encodeAndFormat(makeReq(), [file], { endpoint: 'openai' });
|
||||
|
||||
expect(mockRunGuardedEncode).not.toHaveBeenCalled();
|
||||
expect(result.image_urls).toHaveLength(1);
|
||||
expect(result.image_urls[0].image_url.url).toBe(`data:image/png;base64,${localBase64}`);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue