mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-08 15:41:38 +00:00
🐛 fix: Only stream provisioning sources with the standard contract
Provisioning called getDownloadStream(req, filepath) for every source, but that contract is not universal: openai takes (file_id, client) and execute_code takes (fileIdentifier, identity, req) and returns an Axios response, so files backed by those sources were mis-invoked rather than provisioned. Streaming now goes through an allowlist of storage-backed sources, so an unfamiliar source is skipped with a warning instead of called incorrectly.
This commit is contained in:
parent
0eb921eb1c
commit
bc2e9c868f
2 changed files with 53 additions and 8 deletions
|
|
@ -20,6 +20,34 @@ const { getStrategyFunctions } = require('./strategies');
|
|||
|
||||
const axios = createAxiosInstance();
|
||||
|
||||
/* Sources whose `getDownloadStream` takes `(req, filepath)` and returns a readable.
|
||||
* Others diverge: `openai` takes `(file_id, client)` and `execute_code` takes
|
||||
* `(fileIdentifier, identity, req)` returning an Axios response, so calling them
|
||||
* through this contract fails. An allowlist keeps an unfamiliar source skipped
|
||||
* rather than mis-invoked. */
|
||||
const STORAGE_STREAM_SOURCES = new Set([
|
||||
FileSources.local,
|
||||
FileSources.s3,
|
||||
FileSources.cloudfront,
|
||||
FileSources.azure_blob,
|
||||
FileSources.firebase,
|
||||
]);
|
||||
|
||||
/** Resolves a storage download stream, or null when the source uses a different contract. */
|
||||
async function getStorageStream(file, req) {
|
||||
if (!STORAGE_STREAM_SOURCES.has(file.source)) {
|
||||
logger.warn(
|
||||
`[provision] Cannot stream "${file.filename}" (${file.file_id}) from source "${file.source}": unsupported download contract`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const { getDownloadStream } = getStrategyFunctions(file.source);
|
||||
if (!getDownloadStream) {
|
||||
return null;
|
||||
}
|
||||
return await getDownloadStream(req, file.filepath);
|
||||
}
|
||||
|
||||
/** Composes code-API auth: legacy X-API-Key when configured, plus JWT bearer when enabled. */
|
||||
async function buildCodeApiHeaders({ apiKey, req }) {
|
||||
return {
|
||||
|
|
@ -82,16 +110,14 @@ async function loadCodeApiKey(userId) {
|
|||
* Merged pointers plus the deferred DB update
|
||||
*/
|
||||
async function provisionToCodeEnv({ req, file, entity_id }) {
|
||||
const { getDownloadStream } = getStrategyFunctions(file.source);
|
||||
if (!getDownloadStream) {
|
||||
const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions(FileSources.execute_code);
|
||||
const stream = await getStorageStream(file, req);
|
||||
if (!stream) {
|
||||
throw new Error(
|
||||
`Cannot provision file "${file.filename}" to code env: storage source "${file.source}" does not support download streams`,
|
||||
);
|
||||
}
|
||||
|
||||
const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions(FileSources.execute_code);
|
||||
const stream = await getDownloadStream(req, file.filepath);
|
||||
|
||||
const kind = entity_id ? 'agent' : 'user';
|
||||
const id = entity_id ?? req.user.id;
|
||||
|
||||
|
|
@ -147,13 +173,12 @@ async function provisionToVectorDB({ req, file, entity_id, existingStream }) {
|
|||
try {
|
||||
let stream = existingStream;
|
||||
if (!stream) {
|
||||
const { getDownloadStream } = getStrategyFunctions(file.source);
|
||||
if (!getDownloadStream) {
|
||||
stream = await getStorageStream(file, req);
|
||||
if (!stream) {
|
||||
throw new Error(
|
||||
`Cannot provision file "${file.filename}" to vector DB: storage source "${file.source}" does not support download streams`,
|
||||
);
|
||||
}
|
||||
stream = await getDownloadStream(req, file.filepath);
|
||||
}
|
||||
|
||||
// uploadVectors expects a file-like object with a `path` property for fs.createReadStream.
|
||||
|
|
|
|||
|
|
@ -157,6 +157,26 @@ describe('provisionToCodeEnv', () => {
|
|||
expect(result.referenceSet.codeEnvRefs.default.executionProfile).toBe('default');
|
||||
});
|
||||
|
||||
it('refuses sources whose download contract differs instead of mis-invoking them', async () => {
|
||||
const uploadCodeEnvFile = jest.fn();
|
||||
setupStrategies(uploadCodeEnvFile);
|
||||
|
||||
await expect(
|
||||
provisionToCodeEnv({
|
||||
req: { user: { id: 'u1' } },
|
||||
file: {
|
||||
file_id: 'f-openai',
|
||||
filename: 'doc.pdf',
|
||||
type: 'application/pdf',
|
||||
source: 'openai',
|
||||
filepath: '/x/doc.pdf',
|
||||
metadata: {},
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/does not support download streams/);
|
||||
expect(uploadCodeEnvFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps filenames untouched when the extension already matches or the file is not an image', async () => {
|
||||
const uploadCodeEnvFile = jest
|
||||
.fn()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue