Compare commits
46 Commits
main
...
refactor/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62b4d29967 | ||
|
|
f7ea232760 | ||
|
|
28cd234a6c | ||
|
|
550bf56077 | ||
|
|
ab4596206f | ||
|
|
84c78f5812 | ||
|
|
1bdfb143eb | ||
|
|
20a486a190 | ||
|
|
6873b396ea | ||
|
|
1c7b3b53da | ||
|
|
c160e7c7d5 | ||
|
|
c0d6404385 | ||
|
|
71a14517cd | ||
|
|
647b1bbac6 | ||
|
|
5eef6ea9e8 | ||
|
|
240e3bd59e | ||
|
|
89fb9c7e1c | ||
|
|
8525c8df36 | ||
|
|
46f9c90223 | ||
|
|
1d2be247cf | ||
|
|
d853c10920 | ||
|
|
5e70d518aa | ||
|
|
8df0ecd438 | ||
|
|
f25e4253d0 | ||
|
|
493f60fa54 | ||
|
|
1168a7d82e | ||
|
|
8f89fdc802 | ||
|
|
57513f7ac9 | ||
|
|
c82c47ab6a | ||
|
|
b0256510b5 | ||
|
|
e0980e796a | ||
|
|
370e2d7ade | ||
|
|
0e0758edaf | ||
|
|
50bd6d3a02 | ||
|
|
677481dde6 | ||
|
|
2229672929 | ||
|
|
68c6afd009 | ||
|
|
0b5816d1be | ||
|
|
e7af3bdaed | ||
|
|
b2b2aee945 | ||
|
|
2501d11fa0 | ||
|
|
eeab69ff7f | ||
|
|
5bb731764c | ||
|
|
5b43bf6c95 | ||
|
|
3ecb96149a | ||
|
|
b992fed16c |
@@ -11,6 +11,7 @@ const {
|
|||||||
Constants,
|
Constants,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const { getMessages, saveMessage, updateMessage, saveConvo, getConvo } = require('~/models');
|
const { getMessages, saveMessage, updateMessage, saveConvo, getConvo } = require('~/models');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { checkBalance } = require('~/models/balanceMethods');
|
const { checkBalance } = require('~/models/balanceMethods');
|
||||||
const { truncateToolCallOutputs } = require('./prompts');
|
const { truncateToolCallOutputs } = require('./prompts');
|
||||||
const { getFiles } = require('~/models/File');
|
const { getFiles } = require('~/models/File');
|
||||||
@@ -112,13 +113,15 @@ class BaseClient {
|
|||||||
* If a correction to the token usage is needed, the method should return an object with the corrected token counts.
|
* If a correction to the token usage is needed, the method should return an object with the corrected token counts.
|
||||||
* Should only be used if `recordCollectedUsage` was not used instead.
|
* Should only be used if `recordCollectedUsage` was not used instead.
|
||||||
* @param {string} [model]
|
* @param {string} [model]
|
||||||
|
* @param {AppConfig['balance']} [balance]
|
||||||
* @param {number} promptTokens
|
* @param {number} promptTokens
|
||||||
* @param {number} completionTokens
|
* @param {number} completionTokens
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async recordTokenUsage({ model, promptTokens, completionTokens }) {
|
async recordTokenUsage({ model, balance, promptTokens, completionTokens }) {
|
||||||
logger.debug('[BaseClient] `recordTokenUsage` not implemented.', {
|
logger.debug('[BaseClient] `recordTokenUsage` not implemented.', {
|
||||||
model,
|
model,
|
||||||
|
balance,
|
||||||
promptTokens,
|
promptTokens,
|
||||||
completionTokens,
|
completionTokens,
|
||||||
});
|
});
|
||||||
@@ -567,6 +570,7 @@ class BaseClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendMessage(message, opts = {}) {
|
async sendMessage(message, opts = {}) {
|
||||||
|
const appConfig = await getAppConfig({ role: this.options.req?.user?.role });
|
||||||
/** @type {Promise<TMessage>} */
|
/** @type {Promise<TMessage>} */
|
||||||
let userMessagePromise;
|
let userMessagePromise;
|
||||||
const { user, head, isEdited, conversationId, responseMessageId, saveOptions, userMessage } =
|
const { user, head, isEdited, conversationId, responseMessageId, saveOptions, userMessage } =
|
||||||
@@ -653,7 +657,7 @@ class BaseClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const balance = this.options.req?.app?.locals?.balance;
|
const balance = appConfig?.balance;
|
||||||
if (
|
if (
|
||||||
balance?.enabled &&
|
balance?.enabled &&
|
||||||
supportsBalanceCheck[this.options.endpointType ?? this.options.endpoint]
|
supportsBalanceCheck[this.options.endpointType ?? this.options.endpoint]
|
||||||
@@ -752,6 +756,7 @@ class BaseClient {
|
|||||||
completionTokens = responseMessage.tokenCount;
|
completionTokens = responseMessage.tokenCount;
|
||||||
await this.recordTokenUsage({
|
await this.recordTokenUsage({
|
||||||
usage,
|
usage,
|
||||||
|
balance,
|
||||||
promptTokens,
|
promptTokens,
|
||||||
completionTokens,
|
completionTokens,
|
||||||
model: responseMessage.model,
|
model: responseMessage.model,
|
||||||
|
|||||||
@@ -34,13 +34,14 @@ const {
|
|||||||
const { extractBaseURL, getModelMaxTokens, getModelMaxOutputTokens } = require('~/utils');
|
const { extractBaseURL, getModelMaxTokens, getModelMaxOutputTokens } = require('~/utils');
|
||||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||||
const { addSpaceIfNeeded, sleep } = require('~/server/utils');
|
const { addSpaceIfNeeded, sleep } = require('~/server/utils');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { spendTokens } = require('~/models/spendTokens');
|
const { spendTokens } = require('~/models/spendTokens');
|
||||||
const { handleOpenAIErrors } = require('./tools/util');
|
const { handleOpenAIErrors } = require('./tools/util');
|
||||||
const { createLLM, RunManager } = require('./llm');
|
|
||||||
const { summaryBuffer } = require('./memory');
|
const { summaryBuffer } = require('./memory');
|
||||||
const { runTitleChain } = require('./chains');
|
const { runTitleChain } = require('./chains');
|
||||||
const { tokenSplit } = require('./document');
|
const { tokenSplit } = require('./document');
|
||||||
const BaseClient = require('./BaseClient');
|
const BaseClient = require('./BaseClient');
|
||||||
|
const { createLLM } = require('./llm');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
class OpenAIClient extends BaseClient {
|
class OpenAIClient extends BaseClient {
|
||||||
@@ -618,10 +619,6 @@ class OpenAIClient extends BaseClient {
|
|||||||
temperature = 0.2,
|
temperature = 0.2,
|
||||||
max_tokens,
|
max_tokens,
|
||||||
streaming,
|
streaming,
|
||||||
context,
|
|
||||||
tokenBuffer,
|
|
||||||
initialMessageCount,
|
|
||||||
conversationId,
|
|
||||||
}) {
|
}) {
|
||||||
const modelOptions = {
|
const modelOptions = {
|
||||||
modelName: modelName ?? model,
|
modelName: modelName ?? model,
|
||||||
@@ -666,22 +663,12 @@ class OpenAIClient extends BaseClient {
|
|||||||
configOptions.httpsAgent = new HttpsProxyAgent(this.options.proxy);
|
configOptions.httpsAgent = new HttpsProxyAgent(this.options.proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { req, res, debug } = this.options;
|
|
||||||
const runManager = new RunManager({ req, res, debug, abortController: this.abortController });
|
|
||||||
this.runManager = runManager;
|
|
||||||
|
|
||||||
const llm = createLLM({
|
const llm = createLLM({
|
||||||
modelOptions,
|
modelOptions,
|
||||||
configOptions,
|
configOptions,
|
||||||
openAIApiKey: this.apiKey,
|
openAIApiKey: this.apiKey,
|
||||||
azure: this.azure,
|
azure: this.azure,
|
||||||
streaming,
|
streaming,
|
||||||
callbacks: runManager.createCallbacks({
|
|
||||||
context,
|
|
||||||
tokenBuffer,
|
|
||||||
conversationId: this.conversationId ?? conversationId,
|
|
||||||
initialMessageCount,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return llm;
|
return llm;
|
||||||
@@ -702,6 +689,7 @@ class OpenAIClient extends BaseClient {
|
|||||||
* In case of failure, it will return the default title, "New Chat".
|
* In case of failure, it will return the default title, "New Chat".
|
||||||
*/
|
*/
|
||||||
async titleConvo({ text, conversationId, responseText = '' }) {
|
async titleConvo({ text, conversationId, responseText = '' }) {
|
||||||
|
const appConfig = await getAppConfig({ role: this.options.req?.user?.role });
|
||||||
this.conversationId = conversationId;
|
this.conversationId = conversationId;
|
||||||
|
|
||||||
if (this.options.attachments) {
|
if (this.options.attachments) {
|
||||||
@@ -730,8 +718,7 @@ class OpenAIClient extends BaseClient {
|
|||||||
max_tokens: 16,
|
max_tokens: 16,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {TAzureConfig | undefined} */
|
const azureConfig = appConfig?.endpoints?.[EModelEndpoint.azureOpenAI];
|
||||||
const azureConfig = this.options?.req?.app?.locals?.[EModelEndpoint.azureOpenAI];
|
|
||||||
|
|
||||||
const resetTitleOptions = !!(
|
const resetTitleOptions = !!(
|
||||||
(this.azure && azureConfig) ||
|
(this.azure && azureConfig) ||
|
||||||
@@ -1120,6 +1107,7 @@ ${convo}
|
|||||||
}
|
}
|
||||||
|
|
||||||
async chatCompletion({ payload, onProgress, abortController = null }) {
|
async chatCompletion({ payload, onProgress, abortController = null }) {
|
||||||
|
const appConfig = await getAppConfig({ role: this.options.req?.user?.role });
|
||||||
let error = null;
|
let error = null;
|
||||||
let intermediateReply = [];
|
let intermediateReply = [];
|
||||||
const errorCallback = (err) => (error = err);
|
const errorCallback = (err) => (error = err);
|
||||||
@@ -1165,8 +1153,7 @@ ${convo}
|
|||||||
opts.fetchOptions.agent = new HttpsProxyAgent(this.options.proxy);
|
opts.fetchOptions.agent = new HttpsProxyAgent(this.options.proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {TAzureConfig | undefined} */
|
const azureConfig = appConfig?.endpoints?.[EModelEndpoint.azureOpenAI];
|
||||||
const azureConfig = this.options?.req?.app?.locals?.[EModelEndpoint.azureOpenAI];
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(this.azure && this.isVisionModel && azureConfig) ||
|
(this.azure && this.isVisionModel && azureConfig) ||
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
const { promptTokensEstimate } = require('openai-chat-tokens');
|
|
||||||
const { EModelEndpoint, supportsBalanceCheck } = require('librechat-data-provider');
|
|
||||||
const { formatFromLangChain } = require('~/app/clients/prompts');
|
|
||||||
const { getBalanceConfig } = require('~/server/services/Config');
|
|
||||||
const { checkBalance } = require('~/models/balanceMethods');
|
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
const createStartHandler = ({
|
|
||||||
context,
|
|
||||||
conversationId,
|
|
||||||
tokenBuffer = 0,
|
|
||||||
initialMessageCount,
|
|
||||||
manager,
|
|
||||||
}) => {
|
|
||||||
return async (_llm, _messages, runId, parentRunId, extraParams) => {
|
|
||||||
const { invocation_params } = extraParams;
|
|
||||||
const { model, functions, function_call } = invocation_params;
|
|
||||||
const messages = _messages[0].map(formatFromLangChain);
|
|
||||||
|
|
||||||
logger.debug(`[createStartHandler] handleChatModelStart: ${context}`, {
|
|
||||||
model,
|
|
||||||
function_call,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (context !== 'title') {
|
|
||||||
logger.debug(`[createStartHandler] handleChatModelStart: ${context}`, {
|
|
||||||
functions,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = { messages };
|
|
||||||
let prelimPromptTokens = 1;
|
|
||||||
|
|
||||||
if (functions) {
|
|
||||||
payload.functions = functions;
|
|
||||||
prelimPromptTokens += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (function_call) {
|
|
||||||
payload.function_call = function_call;
|
|
||||||
prelimPromptTokens -= 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
prelimPromptTokens += promptTokensEstimate(payload);
|
|
||||||
logger.debug('[createStartHandler]', {
|
|
||||||
prelimPromptTokens,
|
|
||||||
tokenBuffer,
|
|
||||||
});
|
|
||||||
prelimPromptTokens += tokenBuffer;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const balance = await getBalanceConfig();
|
|
||||||
if (balance?.enabled && supportsBalanceCheck[EModelEndpoint.openAI]) {
|
|
||||||
const generations =
|
|
||||||
initialMessageCount && messages.length > initialMessageCount
|
|
||||||
? messages.slice(initialMessageCount)
|
|
||||||
: null;
|
|
||||||
await checkBalance({
|
|
||||||
req: manager.req,
|
|
||||||
res: manager.res,
|
|
||||||
txData: {
|
|
||||||
user: manager.user,
|
|
||||||
tokenType: 'prompt',
|
|
||||||
amount: prelimPromptTokens,
|
|
||||||
debug: manager.debug,
|
|
||||||
generations,
|
|
||||||
model,
|
|
||||||
endpoint: EModelEndpoint.openAI,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`[createStartHandler][${context}] checkBalance error`, err);
|
|
||||||
manager.abortController.abort();
|
|
||||||
if (context === 'summary' || context === 'plugins') {
|
|
||||||
manager.addRun(runId, { conversationId, error: err.message });
|
|
||||||
throw new Error(err);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.addRun(runId, {
|
|
||||||
model,
|
|
||||||
messages,
|
|
||||||
functions,
|
|
||||||
function_call,
|
|
||||||
runId,
|
|
||||||
parentRunId,
|
|
||||||
conversationId,
|
|
||||||
prelimPromptTokens,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = createStartHandler;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
const createStartHandler = require('./createStartHandler');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createStartHandler,
|
|
||||||
};
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
const { createStartHandler } = require('~/app/clients/callbacks');
|
|
||||||
const { spendTokens } = require('~/models/spendTokens');
|
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
class RunManager {
|
|
||||||
constructor(fields) {
|
|
||||||
const { req, res, abortController, debug } = fields;
|
|
||||||
this.abortController = abortController;
|
|
||||||
this.user = req.user.id;
|
|
||||||
this.req = req;
|
|
||||||
this.res = res;
|
|
||||||
this.debug = debug;
|
|
||||||
this.runs = new Map();
|
|
||||||
this.convos = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
addRun(runId, runData) {
|
|
||||||
if (!this.runs.has(runId)) {
|
|
||||||
this.runs.set(runId, runData);
|
|
||||||
if (runData.conversationId) {
|
|
||||||
this.convos.set(runData.conversationId, runId);
|
|
||||||
}
|
|
||||||
return runData;
|
|
||||||
} else {
|
|
||||||
const existingData = this.runs.get(runId);
|
|
||||||
const update = { ...existingData, ...runData };
|
|
||||||
this.runs.set(runId, update);
|
|
||||||
if (update.conversationId) {
|
|
||||||
this.convos.set(update.conversationId, runId);
|
|
||||||
}
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeRun(runId) {
|
|
||||||
if (this.runs.has(runId)) {
|
|
||||||
this.runs.delete(runId);
|
|
||||||
} else {
|
|
||||||
logger.error(`[api/app/clients/llm/RunManager] Run with ID ${runId} does not exist.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllRuns() {
|
|
||||||
return Array.from(this.runs.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
getRunById(runId) {
|
|
||||||
return this.runs.get(runId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRunByConversationId(conversationId) {
|
|
||||||
const runId = this.convos.get(conversationId);
|
|
||||||
return { run: this.runs.get(runId), runId };
|
|
||||||
}
|
|
||||||
|
|
||||||
createCallbacks(metadata) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
handleChatModelStart: createStartHandler({ ...metadata, manager: this }),
|
|
||||||
handleLLMEnd: async (output, runId, _parentRunId) => {
|
|
||||||
const { llmOutput, ..._output } = output;
|
|
||||||
logger.debug(`[RunManager] handleLLMEnd: ${JSON.stringify(metadata)}`, {
|
|
||||||
runId,
|
|
||||||
_parentRunId,
|
|
||||||
llmOutput,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (metadata.context !== 'title') {
|
|
||||||
logger.debug('[RunManager] handleLLMEnd:', {
|
|
||||||
output: _output,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tokenUsage } = output.llmOutput;
|
|
||||||
const run = this.getRunById(runId);
|
|
||||||
this.removeRun(runId);
|
|
||||||
|
|
||||||
const txData = {
|
|
||||||
user: this.user,
|
|
||||||
model: run?.model ?? 'gpt-3.5-turbo',
|
|
||||||
...metadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
await spendTokens(txData, tokenUsage);
|
|
||||||
},
|
|
||||||
handleLLMError: async (err) => {
|
|
||||||
logger.error(`[RunManager] handleLLMError: ${JSON.stringify(metadata)}`, err);
|
|
||||||
if (metadata.context === 'title') {
|
|
||||||
return;
|
|
||||||
} else if (metadata.context === 'plugins') {
|
|
||||||
throw new Error(err);
|
|
||||||
}
|
|
||||||
const { conversationId } = metadata;
|
|
||||||
const { run } = this.getRunByConversationId(conversationId);
|
|
||||||
if (run && run.error) {
|
|
||||||
const { error } = run;
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = RunManager;
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
const createLLM = require('./createLLM');
|
const createLLM = require('./createLLM');
|
||||||
const RunManager = require('./RunManager');
|
|
||||||
const createCoherePayload = require('./createCoherePayload');
|
const createCoherePayload = require('./createCoherePayload');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createLLM,
|
createLLM,
|
||||||
RunManager,
|
|
||||||
createCoherePayload,
|
createCoherePayload,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ const { Constants } = require('librechat-data-provider');
|
|||||||
const { initializeFakeClient } = require('./FakeClient');
|
const { initializeFakeClient } = require('./FakeClient');
|
||||||
|
|
||||||
jest.mock('~/db/connect');
|
jest.mock('~/db/connect');
|
||||||
|
jest.mock('~/server/services/Config', () => ({
|
||||||
|
getAppConfig: jest.fn().mockResolvedValue({
|
||||||
|
// Default app config for tests
|
||||||
|
paths: { uploads: '/tmp' },
|
||||||
|
fileStrategy: 'local',
|
||||||
|
memory: { disabled: false },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
jest.mock('~/models', () => ({
|
jest.mock('~/models', () => ({
|
||||||
User: jest.fn(),
|
User: jest.fn(),
|
||||||
Key: jest.fn(),
|
Key: jest.fn(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const availableTools = require('./manifest.json');
|
const manifest = require('./manifest');
|
||||||
|
|
||||||
// Structured Tools
|
// Structured Tools
|
||||||
const DALLE3 = require('./structured/DALLE3');
|
const DALLE3 = require('./structured/DALLE3');
|
||||||
@@ -13,23 +13,8 @@ const TraversaalSearch = require('./structured/TraversaalSearch');
|
|||||||
const createOpenAIImageTools = require('./structured/OpenAIImageTools');
|
const createOpenAIImageTools = require('./structured/OpenAIImageTools');
|
||||||
const TavilySearchResults = require('./structured/TavilySearchResults');
|
const TavilySearchResults = require('./structured/TavilySearchResults');
|
||||||
|
|
||||||
/** @type {Record<string, TPlugin | undefined>} */
|
|
||||||
const manifestToolMap = {};
|
|
||||||
|
|
||||||
/** @type {Array<TPlugin>} */
|
|
||||||
const toolkits = [];
|
|
||||||
|
|
||||||
availableTools.forEach((tool) => {
|
|
||||||
manifestToolMap[tool.pluginKey] = tool;
|
|
||||||
if (tool.toolkit === true) {
|
|
||||||
toolkits.push(tool);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
toolkits,
|
...manifest,
|
||||||
availableTools,
|
|
||||||
manifestToolMap,
|
|
||||||
// Structured Tools
|
// Structured Tools
|
||||||
DALLE3,
|
DALLE3,
|
||||||
FluxAPI,
|
FluxAPI,
|
||||||
|
|||||||
20
api/app/clients/tools/manifest.js
Normal file
20
api/app/clients/tools/manifest.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const availableTools = require('./manifest.json');
|
||||||
|
|
||||||
|
/** @type {Record<string, TPlugin | undefined>} */
|
||||||
|
const manifestToolMap = {};
|
||||||
|
|
||||||
|
/** @type {Array<TPlugin>} */
|
||||||
|
const toolkits = [];
|
||||||
|
|
||||||
|
availableTools.forEach((tool) => {
|
||||||
|
manifestToolMap[tool.pluginKey] = tool;
|
||||||
|
if (tool.toolkit === true) {
|
||||||
|
toolkits.push(tool);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
toolkits,
|
||||||
|
availableTools,
|
||||||
|
manifestToolMap,
|
||||||
|
};
|
||||||
@@ -5,10 +5,10 @@ const fetch = require('node-fetch');
|
|||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { ProxyAgent } = require('undici');
|
const { ProxyAgent } = require('undici');
|
||||||
const { Tool } = require('@langchain/core/tools');
|
const { Tool } = require('@langchain/core/tools');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { getImageBasename } = require('@librechat/api');
|
||||||
const { FileContext, ContentTypes } = require('librechat-data-provider');
|
const { FileContext, ContentTypes } = require('librechat-data-provider');
|
||||||
const { getImageBasename } = require('~/server/services/Files/images');
|
|
||||||
const extractBaseURL = require('~/utils/extractBaseURL');
|
const extractBaseURL = require('~/utils/extractBaseURL');
|
||||||
const logger = require('~/config/winston');
|
|
||||||
|
|
||||||
const displayMessage =
|
const displayMessage =
|
||||||
"DALL-E displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user.";
|
"DALL-E displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user.";
|
||||||
|
|||||||
@@ -1,69 +1,16 @@
|
|||||||
const { z } = require('zod');
|
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { v4 } = require('uuid');
|
const { v4 } = require('uuid');
|
||||||
const OpenAI = require('openai');
|
const OpenAI = require('openai');
|
||||||
const FormData = require('form-data');
|
const FormData = require('form-data');
|
||||||
const { ProxyAgent } = require('undici');
|
const { ProxyAgent } = require('undici');
|
||||||
const { tool } = require('@langchain/core/tools');
|
const { tool } = require('@langchain/core/tools');
|
||||||
const { logAxiosError } = require('@librechat/api');
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { logAxiosError, oaiToolkit } = require('@librechat/api');
|
||||||
const { ContentTypes, EImageOutputType } = require('librechat-data-provider');
|
const { ContentTypes, EImageOutputType } = require('librechat-data-provider');
|
||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
const { extractBaseURL } = require('~/utils');
|
const extractBaseURL = require('~/utils/extractBaseURL');
|
||||||
const { getFiles } = require('~/models/File');
|
const { getFiles } = require('~/models/File');
|
||||||
|
|
||||||
/** Default descriptions for image generation tool */
|
|
||||||
const DEFAULT_IMAGE_GEN_DESCRIPTION = `
|
|
||||||
Generates high-quality, original images based solely on text, not using any uploaded reference images.
|
|
||||||
|
|
||||||
When to use \`image_gen_oai\`:
|
|
||||||
- To create entirely new images from detailed text descriptions that do NOT reference any image files.
|
|
||||||
|
|
||||||
When NOT to use \`image_gen_oai\`:
|
|
||||||
- If the user has uploaded any images and requests modifications, enhancements, or remixing based on those uploads → use \`image_edit_oai\` instead.
|
|
||||||
|
|
||||||
Generated image IDs will be returned in the response, so you can refer to them in future requests made to \`image_edit_oai\`.
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
/** Default description for image editing tool */
|
|
||||||
const DEFAULT_IMAGE_EDIT_DESCRIPTION =
|
|
||||||
`Generates high-quality, original images based on text and one or more uploaded/referenced images.
|
|
||||||
|
|
||||||
When to use \`image_edit_oai\`:
|
|
||||||
- The user wants to modify, extend, or remix one **or more** uploaded images, either:
|
|
||||||
- Previously generated, or in the current request (both to be included in the \`image_ids\` array).
|
|
||||||
- Always when the user refers to uploaded images for editing, enhancement, remixing, style transfer, or combining elements.
|
|
||||||
- Any current or existing images are to be used as visual guides.
|
|
||||||
- If there are any files in the current request, they are more likely than not expected as references for image edit requests.
|
|
||||||
|
|
||||||
When NOT to use \`image_edit_oai\`:
|
|
||||||
- Brand-new generations that do not rely on an existing image → use \`image_gen_oai\` instead.
|
|
||||||
|
|
||||||
Both generated and referenced image IDs will be returned in the response, so you can refer to them in future requests made to \`image_edit_oai\`.
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
/** Default prompt descriptions */
|
|
||||||
const DEFAULT_IMAGE_GEN_PROMPT_DESCRIPTION = `Describe the image you want in detail.
|
|
||||||
Be highly specific—break your idea into layers:
|
|
||||||
(1) main concept and subject,
|
|
||||||
(2) composition and position,
|
|
||||||
(3) lighting and mood,
|
|
||||||
(4) style, medium, or camera details,
|
|
||||||
(5) important features (age, expression, clothing, etc.),
|
|
||||||
(6) background.
|
|
||||||
Use positive, descriptive language and specify what should be included, not what to avoid.
|
|
||||||
List number and characteristics of people/objects, and mention style/technical requirements (e.g., "DSLR photo, 85mm lens, golden hour").
|
|
||||||
Do not reference any uploaded images—use for new image creation from text only.`;
|
|
||||||
|
|
||||||
const DEFAULT_IMAGE_EDIT_PROMPT_DESCRIPTION = `Describe the changes, enhancements, or new ideas to apply to the uploaded image(s).
|
|
||||||
Be highly specific—break your request into layers:
|
|
||||||
(1) main concept or transformation,
|
|
||||||
(2) specific edits/replacements or composition guidance,
|
|
||||||
(3) desired style, mood, or technique,
|
|
||||||
(4) features/items to keep, change, or add (such as objects, people, clothing, lighting, etc.).
|
|
||||||
Use positive, descriptive language and clarify what should be included or changed, not what to avoid.
|
|
||||||
Always base this prompt on the most recently uploaded reference images.`;
|
|
||||||
|
|
||||||
const displayMessage =
|
const displayMessage =
|
||||||
"The tool displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user.";
|
"The tool displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user.";
|
||||||
|
|
||||||
@@ -91,22 +38,6 @@ function returnValue(value) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getImageGenDescription = () => {
|
|
||||||
return process.env.IMAGE_GEN_OAI_DESCRIPTION || DEFAULT_IMAGE_GEN_DESCRIPTION;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getImageEditDescription = () => {
|
|
||||||
return process.env.IMAGE_EDIT_OAI_DESCRIPTION || DEFAULT_IMAGE_EDIT_DESCRIPTION;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getImageGenPromptDescription = () => {
|
|
||||||
return process.env.IMAGE_GEN_OAI_PROMPT_DESCRIPTION || DEFAULT_IMAGE_GEN_PROMPT_DESCRIPTION;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getImageEditPromptDescription = () => {
|
|
||||||
return process.env.IMAGE_EDIT_OAI_PROMPT_DESCRIPTION || DEFAULT_IMAGE_EDIT_PROMPT_DESCRIPTION;
|
|
||||||
};
|
|
||||||
|
|
||||||
function createAbortHandler() {
|
function createAbortHandler() {
|
||||||
return function () {
|
return function () {
|
||||||
logger.debug('[ImageGenOAI] Image generation aborted');
|
logger.debug('[ImageGenOAI] Image generation aborted');
|
||||||
@@ -121,7 +52,9 @@ function createAbortHandler() {
|
|||||||
* @param {string} fields.IMAGE_GEN_OAI_API_KEY - The OpenAI API key
|
* @param {string} fields.IMAGE_GEN_OAI_API_KEY - The OpenAI API key
|
||||||
* @param {boolean} [fields.override] - Whether to override the API key check, necessary for app initialization
|
* @param {boolean} [fields.override] - Whether to override the API key check, necessary for app initialization
|
||||||
* @param {MongoFile[]} [fields.imageFiles] - The images to be used for editing
|
* @param {MongoFile[]} [fields.imageFiles] - The images to be used for editing
|
||||||
* @returns {Array} - Array of image tools
|
* @param {string} [fields.imageOutputType] - The image output type configuration
|
||||||
|
* @param {string} [fields.fileStrategy] - The file storage strategy
|
||||||
|
* @returns {Array<ReturnType<tool>>} - Array of image tools
|
||||||
*/
|
*/
|
||||||
function createOpenAIImageTools(fields = {}) {
|
function createOpenAIImageTools(fields = {}) {
|
||||||
/** @type {boolean} Used to initialize the Tool without necessary variables. */
|
/** @type {boolean} Used to initialize the Tool without necessary variables. */
|
||||||
@@ -131,8 +64,8 @@ function createOpenAIImageTools(fields = {}) {
|
|||||||
throw new Error('This tool is only available for agents.');
|
throw new Error('This tool is only available for agents.');
|
||||||
}
|
}
|
||||||
const { req } = fields;
|
const { req } = fields;
|
||||||
const imageOutputType = req?.app.locals.imageOutputType || EImageOutputType.PNG;
|
const imageOutputType = fields.imageOutputType || EImageOutputType.PNG;
|
||||||
const appFileStrategy = req?.app.locals.fileStrategy;
|
const appFileStrategy = fields.fileStrategy;
|
||||||
|
|
||||||
const getApiKey = () => {
|
const getApiKey = () => {
|
||||||
const apiKey = process.env.IMAGE_GEN_OAI_API_KEY ?? '';
|
const apiKey = process.env.IMAGE_GEN_OAI_API_KEY ?? '';
|
||||||
@@ -285,46 +218,7 @@ Error Message: ${error.message}`);
|
|||||||
];
|
];
|
||||||
return [response, { content, file_ids }];
|
return [response, { content, file_ids }];
|
||||||
},
|
},
|
||||||
{
|
oaiToolkit.image_gen_oai,
|
||||||
name: 'image_gen_oai',
|
|
||||||
description: getImageGenDescription(),
|
|
||||||
schema: z.object({
|
|
||||||
prompt: z.string().max(32000).describe(getImageGenPromptDescription()),
|
|
||||||
background: z
|
|
||||||
.enum(['transparent', 'opaque', 'auto'])
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'Sets transparency for the background. Must be one of transparent, opaque or auto (default). When transparent, the output format should be png or webp.',
|
|
||||||
),
|
|
||||||
/*
|
|
||||||
n: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1)
|
|
||||||
.max(10)
|
|
||||||
.optional()
|
|
||||||
.describe('The number of images to generate. Must be between 1 and 10.'),
|
|
||||||
output_compression: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(0)
|
|
||||||
.max(100)
|
|
||||||
.optional()
|
|
||||||
.describe('The compression level (0-100%) for webp or jpeg formats. Defaults to 100.'),
|
|
||||||
*/
|
|
||||||
quality: z
|
|
||||||
.enum(['auto', 'high', 'medium', 'low'])
|
|
||||||
.optional()
|
|
||||||
.describe('The quality of the image. One of auto (default), high, medium, or low.'),
|
|
||||||
size: z
|
|
||||||
.enum(['auto', '1024x1024', '1536x1024', '1024x1536'])
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'The size of the generated image. One of 1024x1024, 1536x1024 (landscape), 1024x1536 (portrait), or auto (default).',
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
responseFormat: 'content_and_artifact',
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -517,48 +411,7 @@ Error Message: ${error.message || 'Unknown error'}`);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
oaiToolkit.image_edit_oai,
|
||||||
name: 'image_edit_oai',
|
|
||||||
description: getImageEditDescription(),
|
|
||||||
schema: z.object({
|
|
||||||
image_ids: z
|
|
||||||
.array(z.string())
|
|
||||||
.min(1)
|
|
||||||
.describe(
|
|
||||||
`
|
|
||||||
IDs (image ID strings) of previously generated or uploaded images that should guide the edit.
|
|
||||||
|
|
||||||
Guidelines:
|
|
||||||
- If the user's request depends on any prior image(s), copy their image IDs into the \`image_ids\` array (in the same order the user refers to them).
|
|
||||||
- Never invent or hallucinate IDs; only use IDs that are still visible in the conversation context.
|
|
||||||
- If no earlier image is relevant, omit the field entirely.
|
|
||||||
`.trim(),
|
|
||||||
),
|
|
||||||
prompt: z.string().max(32000).describe(getImageEditPromptDescription()),
|
|
||||||
/*
|
|
||||||
n: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1)
|
|
||||||
.max(10)
|
|
||||||
.optional()
|
|
||||||
.describe('The number of images to generate. Must be between 1 and 10. Defaults to 1.'),
|
|
||||||
*/
|
|
||||||
quality: z
|
|
||||||
.enum(['auto', 'high', 'medium', 'low'])
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'The quality of the image. One of auto (default), high, medium, or low. High/medium/low only supported for gpt-image-1.',
|
|
||||||
),
|
|
||||||
size: z
|
|
||||||
.enum(['auto', '1024x1024', '1536x1024', '1024x1536', '256x256', '512x512'])
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'The size of the generated images. For gpt-image-1: auto (default), 1024x1024, 1536x1024, 1024x1536. For dall-e-2: 256x256, 512x512, 1024x1024.',
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
responseFormat: 'content_and_artifact',
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return [imageGenTool, imageEditTool];
|
return [imageGenTool, imageEditTool];
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const { z } = require('zod');
|
const { ytToolkit } = require('@librechat/api');
|
||||||
const { tool } = require('@langchain/core/tools');
|
const { tool } = require('@langchain/core/tools');
|
||||||
const { youtube } = require('@googleapis/youtube');
|
const { youtube } = require('@googleapis/youtube');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { YoutubeTranscript } = require('youtube-transcript');
|
const { YoutubeTranscript } = require('youtube-transcript');
|
||||||
const { getApiKey } = require('./credentials');
|
const { getApiKey } = require('./credentials');
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
function extractVideoId(url) {
|
function extractVideoId(url) {
|
||||||
const rawIdRegex = /^[a-zA-Z0-9_-]{11}$/;
|
const rawIdRegex = /^[a-zA-Z0-9_-]{11}$/;
|
||||||
@@ -29,7 +29,7 @@ function parseTranscript(transcriptResponse) {
|
|||||||
.map((entry) => entry.text.trim())
|
.map((entry) => entry.text.trim())
|
||||||
.filter((text) => text)
|
.filter((text) => text)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
.replaceAll('&#39;', '\'');
|
.replaceAll('&#39;', "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createYouTubeTools(fields = {}) {
|
function createYouTubeTools(fields = {}) {
|
||||||
@@ -42,160 +42,94 @@ function createYouTubeTools(fields = {}) {
|
|||||||
auth: apiKey,
|
auth: apiKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchTool = tool(
|
const searchTool = tool(async ({ query, maxResults = 5 }) => {
|
||||||
async ({ query, maxResults = 5 }) => {
|
const response = await youtubeClient.search.list({
|
||||||
const response = await youtubeClient.search.list({
|
part: 'snippet',
|
||||||
part: 'snippet',
|
q: query,
|
||||||
q: query,
|
type: 'video',
|
||||||
type: 'video',
|
maxResults: maxResults || 5,
|
||||||
maxResults: maxResults || 5,
|
});
|
||||||
});
|
const result = response.data.items.map((item) => ({
|
||||||
const result = response.data.items.map((item) => ({
|
title: item.snippet.title,
|
||||||
title: item.snippet.title,
|
description: item.snippet.description,
|
||||||
description: item.snippet.description,
|
url: `https://www.youtube.com/watch?v=${item.id.videoId}`,
|
||||||
url: `https://www.youtube.com/watch?v=${item.id.videoId}`,
|
}));
|
||||||
}));
|
return JSON.stringify(result, null, 2);
|
||||||
return JSON.stringify(result, null, 2);
|
}, ytToolkit.youtube_search);
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'youtube_search',
|
|
||||||
description: `Search for YouTube videos by keyword or phrase.
|
|
||||||
- Required: query (search terms to find videos)
|
|
||||||
- Optional: maxResults (number of videos to return, 1-50, default: 5)
|
|
||||||
- Returns: List of videos with titles, descriptions, and URLs
|
|
||||||
- Use for: Finding specific videos, exploring content, research
|
|
||||||
Example: query="cooking pasta tutorials" maxResults=3`,
|
|
||||||
schema: z.object({
|
|
||||||
query: z.string().describe('Search query terms'),
|
|
||||||
maxResults: z.number().int().min(1).max(50).optional().describe('Number of results (1-50)'),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const infoTool = tool(
|
const infoTool = tool(async ({ url }) => {
|
||||||
async ({ url }) => {
|
const videoId = extractVideoId(url);
|
||||||
const videoId = extractVideoId(url);
|
if (!videoId) {
|
||||||
if (!videoId) {
|
throw new Error('Invalid YouTube URL or video ID');
|
||||||
throw new Error('Invalid YouTube URL or video ID');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const response = await youtubeClient.videos.list({
|
const response = await youtubeClient.videos.list({
|
||||||
part: 'snippet,statistics',
|
part: 'snippet,statistics',
|
||||||
id: videoId,
|
id: videoId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.data.items?.length) {
|
if (!response.data.items?.length) {
|
||||||
throw new Error('Video not found');
|
throw new Error('Video not found');
|
||||||
}
|
}
|
||||||
const video = response.data.items[0];
|
const video = response.data.items[0];
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
title: video.snippet.title,
|
title: video.snippet.title,
|
||||||
description: video.snippet.description,
|
description: video.snippet.description,
|
||||||
views: video.statistics.viewCount,
|
views: video.statistics.viewCount,
|
||||||
likes: video.statistics.likeCount,
|
likes: video.statistics.likeCount,
|
||||||
comments: video.statistics.commentCount,
|
comments: video.statistics.commentCount,
|
||||||
};
|
};
|
||||||
return JSON.stringify(result, null, 2);
|
return JSON.stringify(result, null, 2);
|
||||||
},
|
}, ytToolkit.youtube_info);
|
||||||
{
|
|
||||||
name: 'youtube_info',
|
|
||||||
description: `Get detailed metadata and statistics for a specific YouTube video.
|
|
||||||
- Required: url (full YouTube URL or video ID)
|
|
||||||
- Returns: Video title, description, view count, like count, comment count
|
|
||||||
- Use for: Getting video metrics and basic metadata
|
|
||||||
- DO NOT USE FOR VIDEO SUMMARIES, USE TRANSCRIPTS FOR COMPREHENSIVE ANALYSIS
|
|
||||||
- Accepts both full URLs and video IDs
|
|
||||||
Example: url="https://youtube.com/watch?v=abc123" or url="abc123"`,
|
|
||||||
schema: z.object({
|
|
||||||
url: z.string().describe('YouTube video URL or ID'),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const commentsTool = tool(
|
const commentsTool = tool(async ({ url, maxResults = 10 }) => {
|
||||||
async ({ url, maxResults = 10 }) => {
|
const videoId = extractVideoId(url);
|
||||||
const videoId = extractVideoId(url);
|
if (!videoId) {
|
||||||
if (!videoId) {
|
throw new Error('Invalid YouTube URL or video ID');
|
||||||
throw new Error('Invalid YouTube URL or video ID');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const response = await youtubeClient.commentThreads.list({
|
const response = await youtubeClient.commentThreads.list({
|
||||||
part: 'snippet',
|
part: 'snippet',
|
||||||
videoId,
|
videoId,
|
||||||
maxResults: maxResults || 10,
|
maxResults: maxResults || 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = response.data.items.map((item) => ({
|
const result = response.data.items.map((item) => ({
|
||||||
author: item.snippet.topLevelComment.snippet.authorDisplayName,
|
author: item.snippet.topLevelComment.snippet.authorDisplayName,
|
||||||
text: item.snippet.topLevelComment.snippet.textDisplay,
|
text: item.snippet.topLevelComment.snippet.textDisplay,
|
||||||
likes: item.snippet.topLevelComment.snippet.likeCount,
|
likes: item.snippet.topLevelComment.snippet.likeCount,
|
||||||
}));
|
}));
|
||||||
return JSON.stringify(result, null, 2);
|
return JSON.stringify(result, null, 2);
|
||||||
},
|
}, ytToolkit.youtube_comments);
|
||||||
{
|
|
||||||
name: 'youtube_comments',
|
|
||||||
description: `Retrieve top-level comments from a YouTube video.
|
|
||||||
- Required: url (full YouTube URL or video ID)
|
|
||||||
- Optional: maxResults (number of comments, 1-50, default: 10)
|
|
||||||
- Returns: Comment text, author names, like counts
|
|
||||||
- Use for: Sentiment analysis, audience feedback, engagement review
|
|
||||||
Example: url="abc123" maxResults=20`,
|
|
||||||
schema: z.object({
|
|
||||||
url: z.string().describe('YouTube video URL or ID'),
|
|
||||||
maxResults: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1)
|
|
||||||
.max(50)
|
|
||||||
.optional()
|
|
||||||
.describe('Number of comments to retrieve'),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const transcriptTool = tool(
|
const transcriptTool = tool(async ({ url }) => {
|
||||||
async ({ url }) => {
|
const videoId = extractVideoId(url);
|
||||||
const videoId = extractVideoId(url);
|
if (!videoId) {
|
||||||
if (!videoId) {
|
throw new Error('Invalid YouTube URL or video ID');
|
||||||
throw new Error('Invalid YouTube URL or video ID');
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
const transcript = await YoutubeTranscript.fetchTranscript(videoId, { lang: 'en' });
|
||||||
|
return parseTranscript(transcript);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
const transcript = await YoutubeTranscript.fetchTranscript(videoId, { lang: 'de' });
|
||||||
const transcript = await YoutubeTranscript.fetchTranscript(videoId, { lang: 'en' });
|
|
||||||
return parseTranscript(transcript);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const transcript = await YoutubeTranscript.fetchTranscript(videoId, { lang: 'de' });
|
|
||||||
return parseTranscript(transcript);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
const transcript = await YoutubeTranscript.fetchTranscript(videoId);
|
|
||||||
return parseTranscript(transcript);
|
return parseTranscript(transcript);
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
throw new Error(`Failed to fetch transcript: ${error.message}`);
|
logger.error(e);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
const transcript = await YoutubeTranscript.fetchTranscript(videoId);
|
||||||
name: 'youtube_transcript',
|
return parseTranscript(transcript);
|
||||||
description: `Fetch and parse the transcript/captions of a YouTube video.
|
} catch (error) {
|
||||||
- Required: url (full YouTube URL or video ID)
|
throw new Error(`Failed to fetch transcript: ${error.message}`);
|
||||||
- Returns: Full video transcript as plain text
|
}
|
||||||
- Use for: Content analysis, summarization, translation reference
|
}, ytToolkit.youtube_transcript);
|
||||||
- This is the "Go-to" tool for analyzing actual video content
|
|
||||||
- Attempts to fetch English first, then German, then any available language
|
|
||||||
Example: url="https://youtube.com/watch?v=abc123"`,
|
|
||||||
schema: z.object({
|
|
||||||
url: z.string().describe('YouTube video URL or ID'),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return [searchTool, infoTool, commentsTool, transcriptTool];
|
return [searchTool, infoTool, commentsTool, transcriptTool];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,9 @@
|
|||||||
const DALLE3 = require('../DALLE3');
|
const DALLE3 = require('../DALLE3');
|
||||||
const { ProxyAgent } = require('undici');
|
const { ProxyAgent } = require('undici');
|
||||||
|
|
||||||
|
jest.mock('tiktoken');
|
||||||
const processFileURL = jest.fn();
|
const processFileURL = jest.fn();
|
||||||
|
|
||||||
jest.mock('~/server/services/Files/images', () => ({
|
|
||||||
getImageBasename: jest.fn().mockImplementation((url) => {
|
|
||||||
const parts = url.split('/');
|
|
||||||
const lastPart = parts.pop();
|
|
||||||
const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i;
|
|
||||||
if (imageExtensionRegex.test(lastPart)) {
|
|
||||||
return lastPart;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('fs', () => {
|
|
||||||
return {
|
|
||||||
existsSync: jest.fn(),
|
|
||||||
mkdirSync: jest.fn(),
|
|
||||||
promises: {
|
|
||||||
writeFile: jest.fn(),
|
|
||||||
readFile: jest.fn(),
|
|
||||||
unlink: jest.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('path', () => {
|
|
||||||
return {
|
|
||||||
resolve: jest.fn(),
|
|
||||||
join: jest.fn(),
|
|
||||||
relative: jest.fn(),
|
|
||||||
extname: jest.fn().mockImplementation((filename) => {
|
|
||||||
return filename.slice(filename.lastIndexOf('.'));
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('DALLE3 Proxy Configuration', () => {
|
describe('DALLE3 Proxy Configuration', () => {
|
||||||
let originalEnv;
|
let originalEnv;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
const OpenAI = require('openai');
|
const OpenAI = require('openai');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const DALLE3 = require('../DALLE3');
|
const DALLE3 = require('../DALLE3');
|
||||||
const logger = require('~/config/winston');
|
|
||||||
|
|
||||||
jest.mock('openai');
|
jest.mock('openai');
|
||||||
|
|
||||||
jest.mock('@librechat/data-schemas', () => {
|
jest.mock('@librechat/data-schemas', () => {
|
||||||
return {
|
return {
|
||||||
logger: {
|
logger: {
|
||||||
@@ -26,25 +25,6 @@ jest.mock('tiktoken', () => {
|
|||||||
|
|
||||||
const processFileURL = jest.fn();
|
const processFileURL = jest.fn();
|
||||||
|
|
||||||
jest.mock('~/server/services/Files/images', () => ({
|
|
||||||
getImageBasename: jest.fn().mockImplementation((url) => {
|
|
||||||
// Split the URL by '/'
|
|
||||||
const parts = url.split('/');
|
|
||||||
|
|
||||||
// Get the last part of the URL
|
|
||||||
const lastPart = parts.pop();
|
|
||||||
|
|
||||||
// Check if the last part of the URL matches the image extension regex
|
|
||||||
const imageExtensionRegex = /\.(jpg|jpeg|png|gif|bmp|tiff|svg)$/i;
|
|
||||||
if (imageExtensionRegex.test(lastPart)) {
|
|
||||||
return lastPart;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the regex test fails, return an empty string
|
|
||||||
return '';
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const generate = jest.fn();
|
const generate = jest.fn();
|
||||||
OpenAI.mockImplementation(() => ({
|
OpenAI.mockImplementation(() => ({
|
||||||
images: {
|
images: {
|
||||||
|
|||||||
@@ -121,16 +121,19 @@ const getAuthFields = (toolKey) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {object} object
|
* @param {object} params
|
||||||
* @param {string} object.user
|
* @param {string} params.user
|
||||||
* @param {Pick<Agent, 'id' | 'provider' | 'model'>} [object.agent]
|
* @param {Pick<Agent, 'id' | 'provider' | 'model'>} [params.agent]
|
||||||
* @param {string} [object.model]
|
* @param {string} [params.model]
|
||||||
* @param {EModelEndpoint} [object.endpoint]
|
* @param {EModelEndpoint} [params.endpoint]
|
||||||
* @param {LoadToolOptions} [object.options]
|
* @param {LoadToolOptions} [params.options]
|
||||||
* @param {boolean} [object.useSpecs]
|
* @param {boolean} [params.useSpecs]
|
||||||
* @param {Array<string>} object.tools
|
* @param {Array<string>} params.tools
|
||||||
* @param {boolean} [object.functions]
|
* @param {boolean} [params.functions]
|
||||||
* @param {boolean} [object.returnMap]
|
* @param {boolean} [params.returnMap]
|
||||||
|
* @param {AppConfig['webSearch']} [params.webSearch]
|
||||||
|
* @param {AppConfig['fileStrategy']} [params.fileStrategy]
|
||||||
|
* @param {AppConfig['imageOutputType']} [params.imageOutputType]
|
||||||
* @returns {Promise<{ loadedTools: Tool[], toolContextMap: Object<string, any> } | Record<string,Tool>>}
|
* @returns {Promise<{ loadedTools: Tool[], toolContextMap: Object<string, any> } | Record<string,Tool>>}
|
||||||
*/
|
*/
|
||||||
const loadTools = async ({
|
const loadTools = async ({
|
||||||
@@ -142,6 +145,9 @@ const loadTools = async ({
|
|||||||
options = {},
|
options = {},
|
||||||
functions = true,
|
functions = true,
|
||||||
returnMap = false,
|
returnMap = false,
|
||||||
|
webSearch,
|
||||||
|
fileStrategy,
|
||||||
|
imageOutputType,
|
||||||
}) => {
|
}) => {
|
||||||
const toolConstructors = {
|
const toolConstructors = {
|
||||||
flux: FluxAPI,
|
flux: FluxAPI,
|
||||||
@@ -200,6 +206,8 @@ const loadTools = async ({
|
|||||||
...authValues,
|
...authValues,
|
||||||
isAgent: !!agent,
|
isAgent: !!agent,
|
||||||
req: options.req,
|
req: options.req,
|
||||||
|
imageOutputType,
|
||||||
|
fileStrategy,
|
||||||
imageFiles,
|
imageFiles,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -215,7 +223,7 @@ const loadTools = async ({
|
|||||||
const imageGenOptions = {
|
const imageGenOptions = {
|
||||||
isAgent: !!agent,
|
isAgent: !!agent,
|
||||||
req: options.req,
|
req: options.req,
|
||||||
fileStrategy: options.fileStrategy,
|
fileStrategy,
|
||||||
processFileURL: options.processFileURL,
|
processFileURL: options.processFileURL,
|
||||||
returnMetadata: options.returnMetadata,
|
returnMetadata: options.returnMetadata,
|
||||||
uploadImageBuffer: options.uploadImageBuffer,
|
uploadImageBuffer: options.uploadImageBuffer,
|
||||||
@@ -272,11 +280,10 @@ const loadTools = async ({
|
|||||||
};
|
};
|
||||||
continue;
|
continue;
|
||||||
} else if (tool === Tools.web_search) {
|
} else if (tool === Tools.web_search) {
|
||||||
const webSearchConfig = options?.req?.app?.locals?.webSearch;
|
|
||||||
const result = await loadWebSearchAuth({
|
const result = await loadWebSearchAuth({
|
||||||
userId: user,
|
userId: user,
|
||||||
loadAuthValues,
|
loadAuthValues,
|
||||||
webSearchConfig,
|
webSearchConfig: webSearch,
|
||||||
});
|
});
|
||||||
const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {};
|
const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {};
|
||||||
requestedTools[tool] = async () => {
|
requestedTools[tool] = async () => {
|
||||||
|
|||||||
@@ -9,6 +9,27 @@ const mockPluginService = {
|
|||||||
|
|
||||||
jest.mock('~/server/services/PluginService', () => mockPluginService);
|
jest.mock('~/server/services/PluginService', () => mockPluginService);
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Config', () => ({
|
||||||
|
getAppConfig: jest.fn().mockResolvedValue({
|
||||||
|
// Default app config for tool tests
|
||||||
|
paths: { uploads: '/tmp' },
|
||||||
|
fileStrategy: 'local',
|
||||||
|
filteredTools: [],
|
||||||
|
includedTools: [],
|
||||||
|
}),
|
||||||
|
getCachedTools: jest.fn().mockResolvedValue({
|
||||||
|
// Default cached tools for tests
|
||||||
|
dalle: {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'dalle',
|
||||||
|
description: 'DALL-E image generation',
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const { BaseLLM } = require('@langchain/openai');
|
const { BaseLLM } = require('@langchain/openai');
|
||||||
const { Calculator } = require('@langchain/community/tools/calculator');
|
const { Calculator } = require('@langchain/community/tools/calculator');
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { createTempChatExpirationDate } = require('@librechat/api');
|
const { createTempChatExpirationDate } = require('@librechat/api');
|
||||||
const { getCustomConfig } = require('~/server/services/Config/getCustomConfig');
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
const { getMessages, deleteMessages } = require('./Message');
|
const { getMessages, deleteMessages } = require('./Message');
|
||||||
const { Conversation } = require('~/db/models');
|
const { Conversation } = require('~/db/models');
|
||||||
|
|
||||||
@@ -102,8 +102,10 @@ module.exports = {
|
|||||||
|
|
||||||
if (req?.body?.isTemporary) {
|
if (req?.body?.isTemporary) {
|
||||||
try {
|
try {
|
||||||
const customConfig = await getCustomConfig();
|
const appConfig = await getAppConfig({
|
||||||
update.expiredAt = createTempChatExpirationDate(customConfig);
|
role: req.user.role,
|
||||||
|
});
|
||||||
|
update.expiredAt = createTempChatExpirationDate(appConfig?.interfaceConfig);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error creating temporary chat expiration date:', err);
|
logger.error('Error creating temporary chat expiration date:', err);
|
||||||
logger.info(`---\`saveConvo\` context: ${metadata?.context}`);
|
logger.info(`---\`saveConvo\` context: ${metadata?.context}`);
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ const {
|
|||||||
saveConvo,
|
saveConvo,
|
||||||
getConvo,
|
getConvo,
|
||||||
} = require('./Conversation');
|
} = require('./Conversation');
|
||||||
jest.mock('~/server/services/Config/getCustomConfig');
|
jest.mock('~/server/services/Config/app');
|
||||||
jest.mock('./Message');
|
jest.mock('./Message');
|
||||||
const { getCustomConfig } = require('~/server/services/Config/getCustomConfig');
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
const { getMessages, deleteMessages } = require('./Message');
|
const { getMessages, deleteMessages } = require('./Message');
|
||||||
|
|
||||||
const { Conversation } = require('~/db/models');
|
const { Conversation } = require('~/db/models');
|
||||||
@@ -118,9 +118,9 @@ describe('Conversation Operations', () => {
|
|||||||
|
|
||||||
describe('isTemporary conversation handling', () => {
|
describe('isTemporary conversation handling', () => {
|
||||||
it('should save a conversation with expiredAt when isTemporary is true', async () => {
|
it('should save a conversation with expiredAt when isTemporary is true', async () => {
|
||||||
// Mock custom config with 24 hour retention
|
// Mock app config with 24 hour retention
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 24,
|
temporaryChatRetention: 24,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -167,9 +167,9 @@ describe('Conversation Operations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use custom retention period from config', async () => {
|
it('should use custom retention period from config', async () => {
|
||||||
// Mock custom config with 48 hour retention
|
// Mock app config with 48 hour retention
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 48,
|
temporaryChatRetention: 48,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -194,9 +194,9 @@ describe('Conversation Operations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle minimum retention period (1 hour)', async () => {
|
it('should handle minimum retention period (1 hour)', async () => {
|
||||||
// Mock custom config with less than minimum retention
|
// Mock app config with less than minimum retention
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 0.5, // Half hour - should be clamped to 1 hour
|
temporaryChatRetention: 0.5, // Half hour - should be clamped to 1 hour
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -221,9 +221,9 @@ describe('Conversation Operations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle maximum retention period (8760 hours)', async () => {
|
it('should handle maximum retention period (8760 hours)', async () => {
|
||||||
// Mock custom config with more than maximum retention
|
// Mock app config with more than maximum retention
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 10000, // Should be clamped to 8760 hours
|
temporaryChatRetention: 10000, // Should be clamped to 8760 hours
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -247,9 +247,9 @@ describe('Conversation Operations', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle getCustomConfig errors gracefully', async () => {
|
it('should handle getAppConfig errors gracefully', async () => {
|
||||||
// Mock getCustomConfig to throw an error
|
// Mock getAppConfig to throw an error
|
||||||
getCustomConfig.mockRejectedValue(new Error('Config service unavailable'));
|
getAppConfig.mockRejectedValue(new Error('Config service unavailable'));
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
@@ -261,8 +261,8 @@ describe('Conversation Operations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use default retention when config is not provided', async () => {
|
it('should use default retention when config is not provided', async () => {
|
||||||
// Mock getCustomConfig to return empty config
|
// Mock getAppConfig to return empty config
|
||||||
getCustomConfig.mockResolvedValue({});
|
getAppConfig.mockResolvedValue({});
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
@@ -285,8 +285,8 @@ describe('Conversation Operations', () => {
|
|||||||
|
|
||||||
it('should update expiredAt when saving existing temporary conversation', async () => {
|
it('should update expiredAt when saving existing temporary conversation', async () => {
|
||||||
// First save a temporary conversation
|
// First save a temporary conversation
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 24,
|
temporaryChatRetention: 24,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const { z } = require('zod');
|
const { z } = require('zod');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { createTempChatExpirationDate } = require('@librechat/api');
|
const { createTempChatExpirationDate } = require('@librechat/api');
|
||||||
const { getCustomConfig } = require('~/server/services/Config/getCustomConfig');
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
const { Message } = require('~/db/models');
|
const { Message } = require('~/db/models');
|
||||||
|
|
||||||
const idSchema = z.string().uuid();
|
const idSchema = z.string().uuid();
|
||||||
@@ -57,8 +57,10 @@ async function saveMessage(req, params, metadata) {
|
|||||||
|
|
||||||
if (req?.body?.isTemporary) {
|
if (req?.body?.isTemporary) {
|
||||||
try {
|
try {
|
||||||
const customConfig = await getCustomConfig();
|
const appConfig = await getAppConfig({
|
||||||
update.expiredAt = createTempChatExpirationDate(customConfig);
|
role: req.user.role,
|
||||||
|
});
|
||||||
|
update.expiredAt = createTempChatExpirationDate(appConfig?.interfaceConfig);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error creating temporary chat expiration date:', err);
|
logger.error('Error creating temporary chat expiration date:', err);
|
||||||
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
|
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ const {
|
|||||||
deleteMessagesSince,
|
deleteMessagesSince,
|
||||||
} = require('./Message');
|
} = require('./Message');
|
||||||
|
|
||||||
jest.mock('~/server/services/Config/getCustomConfig');
|
jest.mock('~/server/services/Config/app');
|
||||||
const { getCustomConfig } = require('~/server/services/Config/getCustomConfig');
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('mongoose').Model<import('@librechat/data-schemas').IMessage>}
|
* @type {import('mongoose').Model<import('@librechat/data-schemas').IMessage>}
|
||||||
@@ -326,9 +326,9 @@ describe('Message Operations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should save a message with expiredAt when isTemporary is true', async () => {
|
it('should save a message with expiredAt when isTemporary is true', async () => {
|
||||||
// Mock custom config with 24 hour retention
|
// Mock app config with 24 hour retention
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 24,
|
temporaryChatRetention: 24,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -375,9 +375,9 @@ describe('Message Operations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use custom retention period from config', async () => {
|
it('should use custom retention period from config', async () => {
|
||||||
// Mock custom config with 48 hour retention
|
// Mock app config with 48 hour retention
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 48,
|
temporaryChatRetention: 48,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -402,9 +402,9 @@ describe('Message Operations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle minimum retention period (1 hour)', async () => {
|
it('should handle minimum retention period (1 hour)', async () => {
|
||||||
// Mock custom config with less than minimum retention
|
// Mock app config with less than minimum retention
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 0.5, // Half hour - should be clamped to 1 hour
|
temporaryChatRetention: 0.5, // Half hour - should be clamped to 1 hour
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -429,9 +429,9 @@ describe('Message Operations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle maximum retention period (8760 hours)', async () => {
|
it('should handle maximum retention period (8760 hours)', async () => {
|
||||||
// Mock custom config with more than maximum retention
|
// Mock app config with more than maximum retention
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 10000, // Should be clamped to 8760 hours
|
temporaryChatRetention: 10000, // Should be clamped to 8760 hours
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -455,9 +455,9 @@ describe('Message Operations', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle getCustomConfig errors gracefully', async () => {
|
it('should handle getAppConfig errors gracefully', async () => {
|
||||||
// Mock getCustomConfig to throw an error
|
// Mock getAppConfig to throw an error
|
||||||
getCustomConfig.mockRejectedValue(new Error('Config service unavailable'));
|
getAppConfig.mockRejectedValue(new Error('Config service unavailable'));
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
@@ -469,8 +469,8 @@ describe('Message Operations', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use default retention when config is not provided', async () => {
|
it('should use default retention when config is not provided', async () => {
|
||||||
// Mock getCustomConfig to return empty config
|
// Mock getAppConfig to return empty config
|
||||||
getCustomConfig.mockResolvedValue({});
|
getAppConfig.mockResolvedValue({});
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
@@ -493,8 +493,8 @@ describe('Message Operations', () => {
|
|||||||
|
|
||||||
it('should not update expiredAt on message update', async () => {
|
it('should not update expiredAt on message update', async () => {
|
||||||
// First save a temporary message
|
// First save a temporary message
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 24,
|
temporaryChatRetention: 24,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -520,8 +520,8 @@ describe('Message Operations', () => {
|
|||||||
|
|
||||||
it('should preserve expiredAt when saving existing temporary message', async () => {
|
it('should preserve expiredAt when saving existing temporary message', async () => {
|
||||||
// First save a temporary message
|
// First save a temporary message
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
interface: {
|
interfaceConfig: {
|
||||||
temporaryChatRetention: 24,
|
temporaryChatRetention: 24,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { getBalanceConfig } = require('~/server/services/Config');
|
|
||||||
const { getMultiplier, getCacheMultiplier } = require('./tx');
|
const { getMultiplier, getCacheMultiplier } = require('./tx');
|
||||||
const { Transaction, Balance } = require('~/db/models');
|
const { Transaction, Balance } = require('~/db/models');
|
||||||
|
|
||||||
@@ -187,9 +186,10 @@ async function createAutoRefillTransaction(txData) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Static method to create a transaction and update the balance
|
* Static method to create a transaction and update the balance
|
||||||
* @param {txData} txData - Transaction data.
|
* @param {txData} _txData - Transaction data.
|
||||||
*/
|
*/
|
||||||
async function createTransaction(txData) {
|
async function createTransaction(_txData) {
|
||||||
|
const { balance, ...txData } = _txData;
|
||||||
if (txData.rawAmount != null && isNaN(txData.rawAmount)) {
|
if (txData.rawAmount != null && isNaN(txData.rawAmount)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -199,8 +199,6 @@ async function createTransaction(txData) {
|
|||||||
calculateTokenValue(transaction);
|
calculateTokenValue(transaction);
|
||||||
|
|
||||||
await transaction.save();
|
await transaction.save();
|
||||||
|
|
||||||
const balance = await getBalanceConfig();
|
|
||||||
if (!balance?.enabled) {
|
if (!balance?.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -221,9 +219,10 @@ async function createTransaction(txData) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Static method to create a structured transaction and update the balance
|
* Static method to create a structured transaction and update the balance
|
||||||
* @param {txData} txData - Transaction data.
|
* @param {txData} _txData - Transaction data.
|
||||||
*/
|
*/
|
||||||
async function createStructuredTransaction(txData) {
|
async function createStructuredTransaction(_txData) {
|
||||||
|
const { balance, ...txData } = _txData;
|
||||||
const transaction = new Transaction({
|
const transaction = new Transaction({
|
||||||
...txData,
|
...txData,
|
||||||
endpointTokenConfig: txData.endpointTokenConfig,
|
endpointTokenConfig: txData.endpointTokenConfig,
|
||||||
@@ -233,7 +232,6 @@ async function createStructuredTransaction(txData) {
|
|||||||
|
|
||||||
await transaction.save();
|
await transaction.save();
|
||||||
|
|
||||||
const balance = await getBalanceConfig();
|
|
||||||
if (!balance?.enabled) {
|
if (!balance?.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||||
const { spendTokens, spendStructuredTokens } = require('./spendTokens');
|
const { spendTokens, spendStructuredTokens } = require('./spendTokens');
|
||||||
const { getBalanceConfig } = require('~/server/services/Config');
|
|
||||||
const { getMultiplier, getCacheMultiplier } = require('./tx');
|
const { getMultiplier, getCacheMultiplier } = require('./tx');
|
||||||
const { createTransaction } = require('./Transaction');
|
const { createTransaction } = require('./Transaction');
|
||||||
const { Balance } = require('~/db/models');
|
const { Balance } = require('~/db/models');
|
||||||
|
|
||||||
// Mock the custom config module so we can control the balance flag.
|
|
||||||
jest.mock('~/server/services/Config');
|
|
||||||
|
|
||||||
let mongoServer;
|
let mongoServer;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
mongoServer = await MongoMemoryServer.create();
|
mongoServer = await MongoMemoryServer.create();
|
||||||
@@ -23,8 +20,6 @@ afterAll(async () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await mongoose.connection.dropDatabase();
|
await mongoose.connection.dropDatabase();
|
||||||
// Default: enable balance updates in tests.
|
|
||||||
getBalanceConfig.mockResolvedValue({ enabled: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Regular Token Spending Tests', () => {
|
describe('Regular Token Spending Tests', () => {
|
||||||
@@ -41,6 +36,7 @@ describe('Regular Token Spending Tests', () => {
|
|||||||
model,
|
model,
|
||||||
context: 'test',
|
context: 'test',
|
||||||
endpointTokenConfig: null,
|
endpointTokenConfig: null,
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
@@ -74,6 +70,7 @@ describe('Regular Token Spending Tests', () => {
|
|||||||
model,
|
model,
|
||||||
context: 'test',
|
context: 'test',
|
||||||
endpointTokenConfig: null,
|
endpointTokenConfig: null,
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
@@ -104,6 +101,7 @@ describe('Regular Token Spending Tests', () => {
|
|||||||
model,
|
model,
|
||||||
context: 'test',
|
context: 'test',
|
||||||
endpointTokenConfig: null,
|
endpointTokenConfig: null,
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = {};
|
const tokenUsage = {};
|
||||||
@@ -128,6 +126,7 @@ describe('Regular Token Spending Tests', () => {
|
|||||||
model,
|
model,
|
||||||
context: 'test',
|
context: 'test',
|
||||||
endpointTokenConfig: null,
|
endpointTokenConfig: null,
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = { promptTokens: 100 };
|
const tokenUsage = { promptTokens: 100 };
|
||||||
@@ -143,8 +142,7 @@ describe('Regular Token Spending Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('spendTokens should not update balance when balance feature is disabled', async () => {
|
test('spendTokens should not update balance when balance feature is disabled', async () => {
|
||||||
// Arrange: Override the config to disable balance updates.
|
// Arrange: Balance config is now passed directly in txData
|
||||||
getBalanceConfig.mockResolvedValue({ balance: { enabled: false } });
|
|
||||||
const userId = new mongoose.Types.ObjectId();
|
const userId = new mongoose.Types.ObjectId();
|
||||||
const initialBalance = 10000000;
|
const initialBalance = 10000000;
|
||||||
await Balance.create({ user: userId, tokenCredits: initialBalance });
|
await Balance.create({ user: userId, tokenCredits: initialBalance });
|
||||||
@@ -156,6 +154,7 @@ describe('Regular Token Spending Tests', () => {
|
|||||||
model,
|
model,
|
||||||
context: 'test',
|
context: 'test',
|
||||||
endpointTokenConfig: null,
|
endpointTokenConfig: null,
|
||||||
|
balance: { enabled: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
@@ -186,6 +185,7 @@ describe('Structured Token Spending Tests', () => {
|
|||||||
model,
|
model,
|
||||||
context: 'message',
|
context: 'message',
|
||||||
endpointTokenConfig: null,
|
endpointTokenConfig: null,
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
@@ -239,6 +239,7 @@ describe('Structured Token Spending Tests', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model,
|
model,
|
||||||
context: 'message',
|
context: 'message',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
@@ -271,6 +272,7 @@ describe('Structured Token Spending Tests', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model,
|
model,
|
||||||
context: 'message',
|
context: 'message',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
@@ -302,6 +304,7 @@ describe('Structured Token Spending Tests', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model,
|
model,
|
||||||
context: 'message',
|
context: 'message',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = {};
|
const tokenUsage = {};
|
||||||
@@ -328,6 +331,7 @@ describe('Structured Token Spending Tests', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model,
|
model,
|
||||||
context: 'incomplete',
|
context: 'incomplete',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
@@ -364,6 +368,7 @@ describe('NaN Handling Tests', () => {
|
|||||||
endpointTokenConfig: null,
|
endpointTokenConfig: null,
|
||||||
rawAmount: NaN,
|
rawAmount: NaN,
|
||||||
tokenType: 'prompt',
|
tokenType: 'prompt',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
@@ -24,8 +24,15 @@ const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversa
|
|||||||
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
|
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
|
||||||
const { File } = require('~/db/models');
|
const { File } = require('~/db/models');
|
||||||
|
|
||||||
|
const seedDatabase = async () => {
|
||||||
|
await methods.initializeRoles();
|
||||||
|
await methods.seedDefaultRoles();
|
||||||
|
await methods.ensureDefaultCategories();
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...methods,
|
...methods,
|
||||||
|
seedDatabase,
|
||||||
comparePassword,
|
comparePassword,
|
||||||
findFileById,
|
findFileById,
|
||||||
createFile,
|
createFile,
|
||||||
|
|||||||
24
api/models/interface.js
Normal file
24
api/models/interface.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { updateInterfacePermissions: updateInterfacePerms } = require('@librechat/api');
|
||||||
|
const { getRoleByName, updateAccessPermissions } = require('./Role');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update interface permissions based on app configuration.
|
||||||
|
* Must be done independently from loading the app config.
|
||||||
|
* @param {AppConfig} appConfig
|
||||||
|
*/
|
||||||
|
async function updateInterfacePermissions(appConfig) {
|
||||||
|
try {
|
||||||
|
await updateInterfacePerms({
|
||||||
|
appConfig,
|
||||||
|
getRoleByName,
|
||||||
|
updateAccessPermissions,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error updating interface permissions:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
updateInterfacePermissions,
|
||||||
|
};
|
||||||
@@ -5,13 +5,7 @@ const { createTransaction, createStructuredTransaction } = require('./Transactio
|
|||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
* @async
|
* @async
|
||||||
* @param {Object} txData - Transaction data.
|
* @param {txData} txData - Transaction data.
|
||||||
* @param {mongoose.Schema.Types.ObjectId} txData.user - The user ID.
|
|
||||||
* @param {String} txData.conversationId - The ID of the conversation.
|
|
||||||
* @param {String} txData.model - The model name.
|
|
||||||
* @param {String} txData.context - The context in which the transaction is made.
|
|
||||||
* @param {EndpointTokenConfig} [txData.endpointTokenConfig] - The current endpoint token config.
|
|
||||||
* @param {String} [txData.valueKey] - The value key (optional).
|
|
||||||
* @param {Object} tokenUsage - The number of tokens used.
|
* @param {Object} tokenUsage - The number of tokens used.
|
||||||
* @param {Number} tokenUsage.promptTokens - The number of prompt tokens used.
|
* @param {Number} tokenUsage.promptTokens - The number of prompt tokens used.
|
||||||
* @param {Number} tokenUsage.completionTokens - The number of completion tokens used.
|
* @param {Number} tokenUsage.completionTokens - The number of completion tokens used.
|
||||||
@@ -69,13 +63,7 @@ const spendTokens = async (txData, tokenUsage) => {
|
|||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
* @async
|
* @async
|
||||||
* @param {Object} txData - Transaction data.
|
* @param {txData} txData - Transaction data.
|
||||||
* @param {mongoose.Schema.Types.ObjectId} txData.user - The user ID.
|
|
||||||
* @param {String} txData.conversationId - The ID of the conversation.
|
|
||||||
* @param {String} txData.model - The model name.
|
|
||||||
* @param {String} txData.context - The context in which the transaction is made.
|
|
||||||
* @param {EndpointTokenConfig} [txData.endpointTokenConfig] - The current endpoint token config.
|
|
||||||
* @param {String} [txData.valueKey] - The value key (optional).
|
|
||||||
* @param {Object} tokenUsage - The number of tokens used.
|
* @param {Object} tokenUsage - The number of tokens used.
|
||||||
* @param {Object} tokenUsage.promptTokens - The number of prompt tokens used.
|
* @param {Object} tokenUsage.promptTokens - The number of prompt tokens used.
|
||||||
* @param {Number} tokenUsage.promptTokens.input - The number of input tokens.
|
* @param {Number} tokenUsage.promptTokens.input - The number of input tokens.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ const { createTransaction, createAutoRefillTransaction } = require('./Transactio
|
|||||||
|
|
||||||
require('~/db/models');
|
require('~/db/models');
|
||||||
|
|
||||||
// Mock the logger to prevent console output during tests
|
|
||||||
jest.mock('~/config', () => ({
|
jest.mock('~/config', () => ({
|
||||||
logger: {
|
logger: {
|
||||||
debug: jest.fn(),
|
debug: jest.fn(),
|
||||||
@@ -13,10 +12,6 @@ jest.mock('~/config', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock the Config service
|
|
||||||
const { getBalanceConfig } = require('~/server/services/Config');
|
|
||||||
jest.mock('~/server/services/Config');
|
|
||||||
|
|
||||||
describe('spendTokens', () => {
|
describe('spendTokens', () => {
|
||||||
let mongoServer;
|
let mongoServer;
|
||||||
let userId;
|
let userId;
|
||||||
@@ -44,8 +39,7 @@ describe('spendTokens', () => {
|
|||||||
// Create a new user ID for each test
|
// Create a new user ID for each test
|
||||||
userId = new mongoose.Types.ObjectId();
|
userId = new mongoose.Types.ObjectId();
|
||||||
|
|
||||||
// Mock the balance config to be enabled by default
|
// Balance config is now passed directly in txData
|
||||||
getBalanceConfig.mockResolvedValue({ enabled: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create transactions for both prompt and completion tokens', async () => {
|
it('should create transactions for both prompt and completion tokens', async () => {
|
||||||
@@ -60,6 +54,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model: 'gpt-3.5-turbo',
|
model: 'gpt-3.5-turbo',
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
promptTokens: 100,
|
promptTokens: 100,
|
||||||
@@ -98,6 +93,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model: 'gpt-3.5-turbo',
|
model: 'gpt-3.5-turbo',
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
promptTokens: 100,
|
promptTokens: 100,
|
||||||
@@ -127,6 +123,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model: 'gpt-3.5-turbo',
|
model: 'gpt-3.5-turbo',
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
const tokenUsage = {};
|
const tokenUsage = {};
|
||||||
|
|
||||||
@@ -138,8 +135,7 @@ describe('spendTokens', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not update balance when the balance feature is disabled', async () => {
|
it('should not update balance when the balance feature is disabled', async () => {
|
||||||
// Override configuration: disable balance updates
|
// Balance is now passed directly in txData
|
||||||
getBalanceConfig.mockResolvedValue({ enabled: false });
|
|
||||||
// Create a balance for the user
|
// Create a balance for the user
|
||||||
await Balance.create({
|
await Balance.create({
|
||||||
user: userId,
|
user: userId,
|
||||||
@@ -151,6 +147,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model: 'gpt-3.5-turbo',
|
model: 'gpt-3.5-turbo',
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: false },
|
||||||
};
|
};
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
promptTokens: 100,
|
promptTokens: 100,
|
||||||
@@ -180,6 +177,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model: 'gpt-4', // Using a more expensive model
|
model: 'gpt-4', // Using a more expensive model
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Spending more tokens than the user has balance for
|
// Spending more tokens than the user has balance for
|
||||||
@@ -233,6 +231,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo-1',
|
conversationId: 'test-convo-1',
|
||||||
model: 'gpt-4',
|
model: 'gpt-4',
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage1 = {
|
const tokenUsage1 = {
|
||||||
@@ -252,6 +251,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo-2',
|
conversationId: 'test-convo-2',
|
||||||
model: 'gpt-4',
|
model: 'gpt-4',
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage2 = {
|
const tokenUsage2 = {
|
||||||
@@ -292,6 +292,7 @@ describe('spendTokens', () => {
|
|||||||
tokenType: 'completion',
|
tokenType: 'completion',
|
||||||
rawAmount: -100,
|
rawAmount: -100,
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Direct Transaction.create result:', directResult);
|
console.log('Direct Transaction.create result:', directResult);
|
||||||
@@ -316,6 +317,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: `test-convo-${model}`,
|
conversationId: `test-convo-${model}`,
|
||||||
model,
|
model,
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
@@ -352,6 +354,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo-1',
|
conversationId: 'test-convo-1',
|
||||||
model: 'claude-3-5-sonnet',
|
model: 'claude-3-5-sonnet',
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage1 = {
|
const tokenUsage1 = {
|
||||||
@@ -375,6 +378,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo-2',
|
conversationId: 'test-convo-2',
|
||||||
model: 'claude-3-5-sonnet',
|
model: 'claude-3-5-sonnet',
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenUsage2 = {
|
const tokenUsage2 = {
|
||||||
@@ -426,6 +430,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model: 'claude-3-5-sonnet', // Using a model that supports structured tokens
|
model: 'claude-3-5-sonnet', // Using a model that supports structured tokens
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Spending more tokens than the user has balance for
|
// Spending more tokens than the user has balance for
|
||||||
@@ -505,6 +510,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId,
|
conversationId,
|
||||||
user: userId,
|
user: userId,
|
||||||
model: usage.model,
|
model: usage.model,
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate expected spend for this transaction
|
// Calculate expected spend for this transaction
|
||||||
@@ -617,6 +623,7 @@ describe('spendTokens', () => {
|
|||||||
tokenType: 'credits',
|
tokenType: 'credits',
|
||||||
context: 'concurrent-refill-test',
|
context: 'concurrent-refill-test',
|
||||||
rawAmount: refillAmount,
|
rawAmount: refillAmount,
|
||||||
|
balance: { enabled: true },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -683,6 +690,7 @@ describe('spendTokens', () => {
|
|||||||
conversationId: 'test-convo',
|
conversationId: 'test-convo',
|
||||||
model: 'claude-3-5-sonnet',
|
model: 'claude-3-5-sonnet',
|
||||||
context: 'test',
|
context: 'test',
|
||||||
|
balance: { enabled: true },
|
||||||
};
|
};
|
||||||
const tokenUsage = {
|
const tokenUsage = {
|
||||||
promptTokens: {
|
promptTokens: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const {
|
|||||||
filterUniquePlugins,
|
filterUniquePlugins,
|
||||||
convertMCPToolsToPlugins,
|
convertMCPToolsToPlugins,
|
||||||
} = require('@librechat/api');
|
} = require('@librechat/api');
|
||||||
const { getCustomConfig, getCachedTools } = require('~/server/services/Config');
|
const { getCachedTools, getAppConfig } = require('~/server/services/Config');
|
||||||
const { availableTools, toolkits } = require('~/app/clients/tools');
|
const { availableTools, toolkits } = require('~/app/clients/tools');
|
||||||
const { getMCPManager, getFlowStateManager } = require('~/config');
|
const { getMCPManager, getFlowStateManager } = require('~/config');
|
||||||
const { getLogStores } = require('~/cache');
|
const { getLogStores } = require('~/cache');
|
||||||
@@ -20,8 +20,9 @@ const getAvailablePluginsController = async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
/** @type {{ filteredTools: string[], includedTools: string[] }} */
|
/** @type {{ filteredTools: string[], includedTools: string[] }} */
|
||||||
const { filteredTools = [], includedTools = [] } = req.app.locals;
|
const { filteredTools = [], includedTools = [] } = appConfig;
|
||||||
const pluginManifest = availableTools;
|
const pluginManifest = availableTools;
|
||||||
|
|
||||||
const uniquePlugins = filterUniquePlugins(pluginManifest);
|
const uniquePlugins = filterUniquePlugins(pluginManifest);
|
||||||
@@ -101,11 +102,13 @@ function createGetServerTools() {
|
|||||||
const getAvailableTools = async (req, res) => {
|
const getAvailableTools = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.id;
|
||||||
const customConfig = await getCustomConfig();
|
const appConfig = await getAppConfig();
|
||||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||||
const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
|
const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
|
||||||
const cachedUserTools = await getCachedTools({ userId });
|
const cachedUserTools = await getCachedTools({ userId });
|
||||||
const userPlugins = convertMCPToolsToPlugins({ functionTools: cachedUserTools, customConfig });
|
|
||||||
|
const mcpManager = getMCPManager();
|
||||||
|
const userPlugins = convertMCPToolsToPlugins({ functionTools: cachedUserTools, mcpManager });
|
||||||
|
|
||||||
if (cachedToolsArray != null && userPlugins != null) {
|
if (cachedToolsArray != null && userPlugins != null) {
|
||||||
const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]);
|
const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]);
|
||||||
@@ -114,9 +117,8 @@ const getAvailableTools = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let pluginManifest = availableTools;
|
let pluginManifest = availableTools;
|
||||||
if (customConfig?.mcpServers != null) {
|
if (appConfig?.mcpConfig != null) {
|
||||||
try {
|
try {
|
||||||
const mcpManager = getMCPManager();
|
|
||||||
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
||||||
const flowManager = flowsCache ? getFlowStateManager(flowsCache) : null;
|
const flowManager = flowsCache ? getFlowStateManager(flowsCache) : null;
|
||||||
const serverToolsCallback = createServerToolsCallback();
|
const serverToolsCallback = createServerToolsCallback();
|
||||||
@@ -170,7 +172,7 @@ const getAvailableTools = async (req, res) => {
|
|||||||
|
|
||||||
const parts = plugin.pluginKey.split(Constants.mcp_delimiter);
|
const parts = plugin.pluginKey.split(Constants.mcp_delimiter);
|
||||||
const serverName = parts[parts.length - 1];
|
const serverName = parts[parts.length - 1];
|
||||||
const serverConfig = customConfig?.mcpServers?.[serverName];
|
const serverConfig = appConfig?.mcpConfig?.[serverName];
|
||||||
|
|
||||||
if (!serverConfig?.customUserVars) {
|
if (!serverConfig?.customUserVars) {
|
||||||
toolsOutput.push(toolToAdd);
|
toolsOutput.push(toolToAdd);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
const { Constants } = require('librechat-data-provider');
|
const { Constants } = require('librechat-data-provider');
|
||||||
const { getCustomConfig, getCachedTools } = require('~/server/services/Config');
|
const { getCachedTools, getAppConfig } = require('~/server/services/Config');
|
||||||
const { getLogStores } = require('~/cache');
|
const { getLogStores } = require('~/cache');
|
||||||
|
|
||||||
// Mock the dependencies
|
|
||||||
jest.mock('@librechat/data-schemas', () => ({
|
jest.mock('@librechat/data-schemas', () => ({
|
||||||
logger: {
|
logger: {
|
||||||
debug: jest.fn(),
|
debug: jest.fn(),
|
||||||
@@ -11,8 +10,8 @@ jest.mock('@librechat/data-schemas', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('~/server/services/Config', () => ({
|
jest.mock('~/server/services/Config', () => ({
|
||||||
getCustomConfig: jest.fn(),
|
|
||||||
getCachedTools: jest.fn(),
|
getCachedTools: jest.fn(),
|
||||||
|
getAppConfig: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('~/server/services/ToolService', () => ({
|
jest.mock('~/server/services/ToolService', () => ({
|
||||||
@@ -22,6 +21,7 @@ jest.mock('~/server/services/ToolService', () => ({
|
|||||||
jest.mock('~/config', () => ({
|
jest.mock('~/config', () => ({
|
||||||
getMCPManager: jest.fn(() => ({
|
getMCPManager: jest.fn(() => ({
|
||||||
loadManifestTools: jest.fn().mockResolvedValue([]),
|
loadManifestTools: jest.fn().mockResolvedValue([]),
|
||||||
|
getRawConfig: jest.fn(),
|
||||||
})),
|
})),
|
||||||
getFlowStateManager: jest.fn(),
|
getFlowStateManager: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -64,7 +64,10 @@ describe('PluginController', () => {
|
|||||||
|
|
||||||
describe('getAvailablePluginsController', () => {
|
describe('getAvailablePluginsController', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReq.app = { locals: { filteredTools: [], includedTools: [] } };
|
getAppConfig.mockResolvedValue({
|
||||||
|
filteredTools: [],
|
||||||
|
includedTools: [],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use filterUniquePlugins to remove duplicate plugins', async () => {
|
it('should use filterUniquePlugins to remove duplicate plugins', async () => {
|
||||||
@@ -122,7 +125,10 @@ describe('PluginController', () => {
|
|||||||
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
|
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
|
||||||
];
|
];
|
||||||
|
|
||||||
mockReq.app.locals.includedTools = ['key1'];
|
getAppConfig.mockResolvedValue({
|
||||||
|
filteredTools: [],
|
||||||
|
includedTools: ['key1'],
|
||||||
|
});
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
filterUniquePlugins.mockReturnValue(mockPlugins);
|
filterUniquePlugins.mockReturnValue(mockPlugins);
|
||||||
checkPluginAuth.mockReturnValue(false);
|
checkPluginAuth.mockReturnValue(false);
|
||||||
@@ -154,13 +160,12 @@ describe('PluginController', () => {
|
|||||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||||
convertMCPToolsToPlugins.mockReturnValue(mockConvertedPlugins);
|
convertMCPToolsToPlugins.mockReturnValue(mockConvertedPlugins);
|
||||||
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||||
functionTools: mockUserTools,
|
functionTools: mockUserTools,
|
||||||
customConfig: null,
|
mcpManager: expect.any(Object),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,7 +181,6 @@ describe('PluginController', () => {
|
|||||||
getCachedTools.mockResolvedValueOnce({});
|
getCachedTools.mockResolvedValueOnce({});
|
||||||
convertMCPToolsToPlugins.mockReturnValue(mockUserPlugins);
|
convertMCPToolsToPlugins.mockReturnValue(mockUserPlugins);
|
||||||
filterUniquePlugins.mockReturnValue([...mockUserPlugins, ...mockManifestPlugins]);
|
filterUniquePlugins.mockReturnValue([...mockUserPlugins, ...mockManifestPlugins]);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
@@ -195,7 +199,6 @@ describe('PluginController', () => {
|
|||||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||||
filterUniquePlugins.mockReturnValue([mockPlugin]);
|
filterUniquePlugins.mockReturnValue([mockPlugin]);
|
||||||
checkPluginAuth.mockReturnValue(true);
|
checkPluginAuth.mockReturnValue(true);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
|
||||||
|
|
||||||
// Mock getCachedTools second call to return tool definitions
|
// Mock getCachedTools second call to return tool definitions
|
||||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({ tool1: true });
|
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({ tool1: true });
|
||||||
@@ -219,7 +222,6 @@ describe('PluginController', () => {
|
|||||||
filterUniquePlugins.mockReturnValue([mockToolkit]);
|
filterUniquePlugins.mockReturnValue([mockToolkit]);
|
||||||
checkPluginAuth.mockReturnValue(false);
|
checkPluginAuth.mockReturnValue(false);
|
||||||
getToolkitKey.mockReturnValue('toolkit1');
|
getToolkitKey.mockReturnValue('toolkit1');
|
||||||
getCustomConfig.mockResolvedValue(null);
|
|
||||||
|
|
||||||
// Mock getCachedTools second call to return tool definitions
|
// Mock getCachedTools second call to return tool definitions
|
||||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({
|
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({
|
||||||
@@ -233,9 +235,8 @@ describe('PluginController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('plugin.icon behavior', () => {
|
describe('plugin.icon behavior', () => {
|
||||||
const callGetAvailableToolsWithMCPServer = async (mcpServers) => {
|
const callGetAvailableToolsWithMCPServer = async (serverConfig) => {
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCustomConfig.mockResolvedValue({ mcpServers });
|
|
||||||
|
|
||||||
const functionTools = {
|
const functionTools = {
|
||||||
[`test-tool${Constants.mcp_delimiter}test-server`]: {
|
[`test-tool${Constants.mcp_delimiter}test-server`]: {
|
||||||
@@ -247,11 +248,18 @@ describe('PluginController', () => {
|
|||||||
name: 'test-tool',
|
name: 'test-tool',
|
||||||
pluginKey: `test-tool${Constants.mcp_delimiter}test-server`,
|
pluginKey: `test-tool${Constants.mcp_delimiter}test-server`,
|
||||||
description: 'A test tool',
|
description: 'A test tool',
|
||||||
icon: mcpServers['test-server']?.iconPath,
|
icon: serverConfig?.iconPath,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
authConfig: [],
|
authConfig: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock the MCP manager to return the server config
|
||||||
|
const mockMCPManager = {
|
||||||
|
loadManifestTools: jest.fn().mockResolvedValue([]),
|
||||||
|
getRawConfig: jest.fn().mockReturnValue(serverConfig),
|
||||||
|
};
|
||||||
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
getCachedTools.mockResolvedValueOnce(functionTools);
|
getCachedTools.mockResolvedValueOnce(functionTools);
|
||||||
convertMCPToolsToPlugins.mockReturnValue([mockConvertedPlugin]);
|
convertMCPToolsToPlugins.mockReturnValue([mockConvertedPlugin]);
|
||||||
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
||||||
@@ -268,28 +276,24 @@ describe('PluginController', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('should set plugin.icon when iconPath is defined', async () => {
|
it('should set plugin.icon when iconPath is defined', async () => {
|
||||||
const mcpServers = {
|
const serverConfig = {
|
||||||
'test-server': {
|
iconPath: '/path/to/icon.png',
|
||||||
iconPath: '/path/to/icon.png',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const testTool = await callGetAvailableToolsWithMCPServer(mcpServers);
|
const testTool = await callGetAvailableToolsWithMCPServer(serverConfig);
|
||||||
expect(testTool.icon).toBe('/path/to/icon.png');
|
expect(testTool.icon).toBe('/path/to/icon.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set plugin.icon to undefined when iconPath is not defined', async () => {
|
it('should set plugin.icon to undefined when iconPath is not defined', async () => {
|
||||||
const mcpServers = {
|
const serverConfig = {};
|
||||||
'test-server': {},
|
const testTool = await callGetAvailableToolsWithMCPServer(serverConfig);
|
||||||
};
|
|
||||||
const testTool = await callGetAvailableToolsWithMCPServer(mcpServers);
|
|
||||||
expect(testTool.icon).toBeUndefined();
|
expect(testTool.icon).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('helper function integration', () => {
|
describe('helper function integration', () => {
|
||||||
it('should properly handle MCP tools with custom user variables', async () => {
|
it('should properly handle MCP tools with custom user variables', async () => {
|
||||||
const customConfig = {
|
const appConfig = {
|
||||||
mcpServers: {
|
mcpConfig: {
|
||||||
'test-server': {
|
'test-server': {
|
||||||
customUserVars: {
|
customUserVars: {
|
||||||
API_KEY: { title: 'API Key', description: 'Your API key' },
|
API_KEY: { title: 'API Key', description: 'Your API key' },
|
||||||
@@ -311,11 +315,12 @@ describe('PluginController', () => {
|
|||||||
// Mock the MCP manager to return tools
|
// Mock the MCP manager to return tools
|
||||||
const mockMCPManager = {
|
const mockMCPManager = {
|
||||||
loadManifestTools: jest.fn().mockResolvedValue(mcpManagerTools),
|
loadManifestTools: jest.fn().mockResolvedValue(mcpManagerTools),
|
||||||
|
getRawConfig: jest.fn(),
|
||||||
};
|
};
|
||||||
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCustomConfig.mockResolvedValue(customConfig);
|
getAppConfig.mockResolvedValue(appConfig);
|
||||||
|
|
||||||
// First call returns user tools (empty in this case)
|
// First call returns user tools (empty in this case)
|
||||||
getCachedTools.mockResolvedValueOnce({});
|
getCachedTools.mockResolvedValueOnce({});
|
||||||
@@ -375,13 +380,12 @@ describe('PluginController', () => {
|
|||||||
getCachedTools.mockResolvedValue(null);
|
getCachedTools.mockResolvedValue(null);
|
||||||
convertMCPToolsToPlugins.mockReturnValue(undefined);
|
convertMCPToolsToPlugins.mockReturnValue(undefined);
|
||||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||||
functionTools: null,
|
functionTools: null,
|
||||||
customConfig: null,
|
mcpManager: expect.any(Object),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -390,7 +394,6 @@ describe('PluginController', () => {
|
|||||||
getCachedTools.mockResolvedValue(undefined);
|
getCachedTools.mockResolvedValue(undefined);
|
||||||
convertMCPToolsToPlugins.mockReturnValue(undefined);
|
convertMCPToolsToPlugins.mockReturnValue(undefined);
|
||||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
|
||||||
checkPluginAuth.mockReturnValue(false);
|
checkPluginAuth.mockReturnValue(false);
|
||||||
|
|
||||||
// Mock getCachedTools to return undefined for both calls
|
// Mock getCachedTools to return undefined for both calls
|
||||||
@@ -401,7 +404,7 @@ describe('PluginController', () => {
|
|||||||
|
|
||||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||||
functionTools: undefined,
|
functionTools: undefined,
|
||||||
customConfig: null,
|
mcpManager: expect.any(Object),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -428,7 +431,6 @@ describe('PluginController', () => {
|
|||||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({});
|
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({});
|
||||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
|
||||||
checkPluginAuth.mockReturnValue(true);
|
checkPluginAuth.mockReturnValue(true);
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
@@ -452,8 +454,19 @@ describe('PluginController', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock the MCP manager to return server config without customUserVars
|
||||||
|
const mockMCPManager = {
|
||||||
|
loadManifestTools: jest.fn().mockResolvedValue([]),
|
||||||
|
getRawConfig: jest.fn().mockReturnValue(customConfig.mcpServers['test-server']),
|
||||||
|
};
|
||||||
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCustomConfig.mockResolvedValue(customConfig);
|
getAppConfig.mockResolvedValue({
|
||||||
|
mcpConfig: customConfig.mcpServers,
|
||||||
|
filteredTools: [],
|
||||||
|
includedTools: [],
|
||||||
|
});
|
||||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||||
|
|
||||||
const mockPlugin = {
|
const mockPlugin = {
|
||||||
@@ -480,8 +493,8 @@ describe('PluginController', () => {
|
|||||||
expect(responseData[0].authConfig).toEqual([]);
|
expect(responseData[0].authConfig).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle req.app.locals with undefined filteredTools and includedTools', async () => {
|
it('should handle undefined filteredTools and includedTools', async () => {
|
||||||
mockReq.app = { locals: {} };
|
getAppConfig.mockResolvedValue({});
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
filterUniquePlugins.mockReturnValue([]);
|
filterUniquePlugins.mockReturnValue([]);
|
||||||
checkPluginAuth.mockReturnValue(false);
|
checkPluginAuth.mockReturnValue(false);
|
||||||
@@ -506,7 +519,6 @@ describe('PluginController', () => {
|
|||||||
filterUniquePlugins.mockReturnValue([mockToolkit]);
|
filterUniquePlugins.mockReturnValue([mockToolkit]);
|
||||||
checkPluginAuth.mockReturnValue(false);
|
checkPluginAuth.mockReturnValue(false);
|
||||||
getToolkitKey.mockReturnValue(undefined);
|
getToolkitKey.mockReturnValue(undefined);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
|
||||||
|
|
||||||
// Mock getCachedTools second call to return null
|
// Mock getCachedTools second call to return null
|
||||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce(null);
|
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce(null);
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud')
|
|||||||
const { Tools, Constants, FileSources } = require('librechat-data-provider');
|
const { Tools, Constants, FileSources } = require('librechat-data-provider');
|
||||||
const { processDeleteRequest } = require('~/server/services/Files/process');
|
const { processDeleteRequest } = require('~/server/services/Files/process');
|
||||||
const { Transaction, Balance, User } = require('~/db/models');
|
const { Transaction, Balance, User } = require('~/db/models');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { deleteToolCalls } = require('~/models/ToolCall');
|
const { deleteToolCalls } = require('~/models/ToolCall');
|
||||||
const { deleteAllSharedLinks } = require('~/models');
|
const { deleteAllSharedLinks } = require('~/models');
|
||||||
const { getMCPManager } = require('~/config');
|
const { getMCPManager } = require('~/config');
|
||||||
|
|
||||||
const getUserController = async (req, res) => {
|
const getUserController = async (req, res) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
/** @type {MongoUser} */
|
/** @type {MongoUser} */
|
||||||
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
|
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
|
||||||
/**
|
/**
|
||||||
@@ -31,7 +33,7 @@ const getUserController = async (req, res) => {
|
|||||||
delete userData.password;
|
delete userData.password;
|
||||||
delete userData.totpSecret;
|
delete userData.totpSecret;
|
||||||
delete userData.backupCodes;
|
delete userData.backupCodes;
|
||||||
if (req.app.locals.fileStrategy === FileSources.s3 && userData.avatar) {
|
if (appConfig.fileStrategy === FileSources.s3 && userData.avatar) {
|
||||||
const avatarNeedsRefresh = needsRefresh(userData.avatar, 3600);
|
const avatarNeedsRefresh = needsRefresh(userData.avatar, 3600);
|
||||||
if (!avatarNeedsRefresh) {
|
if (!avatarNeedsRefresh) {
|
||||||
return res.status(200).send(userData);
|
return res.status(200).send(userData);
|
||||||
@@ -87,6 +89,7 @@ const deleteUserFiles = async (req) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateUserPluginsController = async (req, res) => {
|
const updateUserPluginsController = async (req, res) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
const { pluginKey, action, auth, isEntityTool } = req.body;
|
const { pluginKey, action, auth, isEntityTool } = req.body;
|
||||||
try {
|
try {
|
||||||
@@ -131,7 +134,7 @@ const updateUserPluginsController = async (req, res) => {
|
|||||||
|
|
||||||
if (pluginKey === Tools.web_search) {
|
if (pluginKey === Tools.web_search) {
|
||||||
/** @type {TCustomConfig['webSearch']} */
|
/** @type {TCustomConfig['webSearch']} */
|
||||||
const webSearchConfig = req.app.locals?.webSearch;
|
const webSearchConfig = appConfig?.webSearch;
|
||||||
keys = extractWebSearchEnvVars({
|
keys = extractWebSearchEnvVars({
|
||||||
keys: action === 'install' ? keys : webSearchKeys,
|
keys: action === 'install' ? keys : webSearchKeys,
|
||||||
config: webSearchConfig,
|
config: webSearchConfig,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const {
|
|||||||
createRun,
|
createRun,
|
||||||
Tokenizer,
|
Tokenizer,
|
||||||
checkAccess,
|
checkAccess,
|
||||||
|
hasCustomUserVars,
|
||||||
|
getUserMCPAuthMap,
|
||||||
memoryInstructions,
|
memoryInstructions,
|
||||||
formatContentStrings,
|
formatContentStrings,
|
||||||
createMemoryProcessor,
|
createMemoryProcessor,
|
||||||
@@ -39,10 +41,10 @@ const {
|
|||||||
deleteMemory,
|
deleteMemory,
|
||||||
setMemory,
|
setMemory,
|
||||||
} = require('~/models');
|
} = require('~/models');
|
||||||
const { getMCPAuthMap, checkCapability, hasCustomUserVars } = require('~/server/services/Config');
|
|
||||||
const { addCacheControl, createContextHandlers } = require('~/app/clients/prompts');
|
const { addCacheControl, createContextHandlers } = require('~/app/clients/prompts');
|
||||||
const { initializeAgent } = require('~/server/services/Endpoints/agents/agent');
|
const { initializeAgent } = require('~/server/services/Endpoints/agents/agent');
|
||||||
const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens');
|
const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens');
|
||||||
|
const { checkCapability, getAppConfig } = require('~/server/services/Config');
|
||||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||||
const { getProviderConfig } = require('~/server/services/Endpoints');
|
const { getProviderConfig } = require('~/server/services/Endpoints');
|
||||||
const BaseClient = require('~/app/clients/BaseClient');
|
const BaseClient = require('~/app/clients/BaseClient');
|
||||||
@@ -451,8 +453,8 @@ class AgentClient extends BaseClient {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/** @type {TCustomConfig['memory']} */
|
const appConfig = await getAppConfig({ role: user.role });
|
||||||
const memoryConfig = this.options.req?.app?.locals?.memory;
|
const memoryConfig = appConfig.memory;
|
||||||
if (!memoryConfig || memoryConfig.disabled === true) {
|
if (!memoryConfig || memoryConfig.disabled === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -460,7 +462,7 @@ class AgentClient extends BaseClient {
|
|||||||
/** @type {Agent} */
|
/** @type {Agent} */
|
||||||
let prelimAgent;
|
let prelimAgent;
|
||||||
const allowedProviders = new Set(
|
const allowedProviders = new Set(
|
||||||
this.options.req?.app?.locals?.[EModelEndpoint.agents]?.allowedProviders,
|
appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
if (memoryConfig.agent?.id != null && memoryConfig.agent.id !== this.options.agent.id) {
|
if (memoryConfig.agent?.id != null && memoryConfig.agent.id !== this.options.agent.id) {
|
||||||
@@ -582,8 +584,8 @@ class AgentClient extends BaseClient {
|
|||||||
if (this.processMemory == null) {
|
if (this.processMemory == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/** @type {TCustomConfig['memory']} */
|
const appConfig = await getAppConfig({ role: this.options.req.user?.role });
|
||||||
const memoryConfig = this.options.req?.app?.locals?.memory;
|
const memoryConfig = appConfig.memory;
|
||||||
const messageWindowSize = memoryConfig?.messageWindowSize ?? 5;
|
const messageWindowSize = memoryConfig?.messageWindowSize ?? 5;
|
||||||
|
|
||||||
let messagesToProcess = [...messages];
|
let messagesToProcess = [...messages];
|
||||||
@@ -624,9 +626,15 @@ class AgentClient extends BaseClient {
|
|||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
* @param {string} [params.model]
|
* @param {string} [params.model]
|
||||||
* @param {string} [params.context='message']
|
* @param {string} [params.context='message']
|
||||||
|
* @param {AppConfig['balance']} [params.balance]
|
||||||
* @param {UsageMetadata[]} [params.collectedUsage=this.collectedUsage]
|
* @param {UsageMetadata[]} [params.collectedUsage=this.collectedUsage]
|
||||||
*/
|
*/
|
||||||
async recordCollectedUsage({ model, context = 'message', collectedUsage = this.collectedUsage }) {
|
async recordCollectedUsage({
|
||||||
|
model,
|
||||||
|
balance,
|
||||||
|
context = 'message',
|
||||||
|
collectedUsage = this.collectedUsage,
|
||||||
|
}) {
|
||||||
if (!collectedUsage || !collectedUsage.length) {
|
if (!collectedUsage || !collectedUsage.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -648,6 +656,7 @@ class AgentClient extends BaseClient {
|
|||||||
|
|
||||||
const txMetadata = {
|
const txMetadata = {
|
||||||
context,
|
context,
|
||||||
|
balance,
|
||||||
conversationId: this.conversationId,
|
conversationId: this.conversationId,
|
||||||
user: this.user ?? this.options.req.user?.id,
|
user: this.user ?? this.options.req.user?.id,
|
||||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||||
@@ -759,8 +768,9 @@ class AgentClient extends BaseClient {
|
|||||||
abortController = new AbortController();
|
abortController = new AbortController();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {TCustomConfig['endpoints']['agents']} */
|
const appConfig = await getAppConfig({ role: this.options.req.user?.role });
|
||||||
const agentsEConfig = this.options.req.app.locals[EModelEndpoint.agents];
|
/** @type {AppConfig['endpoints']['agents']} */
|
||||||
|
const agentsEConfig = appConfig.endpoints?.[EModelEndpoint.agents];
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
configurable: {
|
configurable: {
|
||||||
@@ -904,10 +914,10 @@ class AgentClient extends BaseClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await hasCustomUserVars()) {
|
if (agent.tools?.length && hasCustomUserVars(appConfig)) {
|
||||||
config.configurable.userMCPAuthMap = await getMCPAuthMap({
|
config.configurable.userMCPAuthMap = await getUserMCPAuthMap({
|
||||||
tools: agent.tools,
|
|
||||||
userId: this.options.req.user.id,
|
userId: this.options.req.user.id,
|
||||||
|
tools: agent.tools,
|
||||||
findPluginAuthsByKeys,
|
findPluginAuthsByKeys,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1040,7 +1050,7 @@ class AgentClient extends BaseClient {
|
|||||||
this.artifactPromises.push(...attachments);
|
this.artifactPromises.push(...attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.recordCollectedUsage({ context: 'message' });
|
await this.recordCollectedUsage({ context: 'message', balance: appConfig?.balance });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'[api/server/controllers/agents/client.js #chatCompletion] Error recording collected usage',
|
'[api/server/controllers/agents/client.js #chatCompletion] Error recording collected usage',
|
||||||
@@ -1081,6 +1091,7 @@ class AgentClient extends BaseClient {
|
|||||||
}
|
}
|
||||||
const { handleLLMEnd, collected: collectedMetadata } = createMetadataAggregator();
|
const { handleLLMEnd, collected: collectedMetadata } = createMetadataAggregator();
|
||||||
const { req, res, agent } = this.options;
|
const { req, res, agent } = this.options;
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
let endpoint = agent.endpoint;
|
let endpoint = agent.endpoint;
|
||||||
|
|
||||||
/** @type {import('@librechat/agents').ClientOptions} */
|
/** @type {import('@librechat/agents').ClientOptions} */
|
||||||
@@ -1088,11 +1099,13 @@ class AgentClient extends BaseClient {
|
|||||||
model: agent.model || agent.model_parameters.model,
|
model: agent.model || agent.model_parameters.model,
|
||||||
};
|
};
|
||||||
|
|
||||||
let titleProviderConfig = await getProviderConfig(endpoint);
|
let titleProviderConfig = getProviderConfig({ provider: endpoint, appConfig });
|
||||||
|
|
||||||
/** @type {TEndpoint | undefined} */
|
/** @type {TEndpoint | undefined} */
|
||||||
const endpointConfig =
|
const endpointConfig =
|
||||||
req.app.locals.all ?? req.app.locals[endpoint] ?? titleProviderConfig.customEndpointConfig;
|
appConfig.endpoints?.all ??
|
||||||
|
appConfig.endpoints?.[endpoint] ??
|
||||||
|
titleProviderConfig.customEndpointConfig;
|
||||||
if (!endpointConfig) {
|
if (!endpointConfig) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'[api/server/controllers/agents/client.js #titleConvo] Error getting endpoint config',
|
'[api/server/controllers/agents/client.js #titleConvo] Error getting endpoint config',
|
||||||
@@ -1101,7 +1114,10 @@ class AgentClient extends BaseClient {
|
|||||||
|
|
||||||
if (endpointConfig?.titleEndpoint && endpointConfig.titleEndpoint !== endpoint) {
|
if (endpointConfig?.titleEndpoint && endpointConfig.titleEndpoint !== endpoint) {
|
||||||
try {
|
try {
|
||||||
titleProviderConfig = await getProviderConfig(endpointConfig.titleEndpoint);
|
titleProviderConfig = getProviderConfig({
|
||||||
|
provider: endpointConfig.titleEndpoint,
|
||||||
|
appConfig,
|
||||||
|
});
|
||||||
endpoint = endpointConfig.titleEndpoint;
|
endpoint = endpointConfig.titleEndpoint;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -1110,7 +1126,7 @@ class AgentClient extends BaseClient {
|
|||||||
);
|
);
|
||||||
// Fall back to original provider config
|
// Fall back to original provider config
|
||||||
endpoint = agent.endpoint;
|
endpoint = agent.endpoint;
|
||||||
titleProviderConfig = await getProviderConfig(endpoint);
|
titleProviderConfig = getProviderConfig({ provider: endpoint, appConfig });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1214,9 +1230,10 @@ class AgentClient extends BaseClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.recordCollectedUsage({
|
await this.recordCollectedUsage({
|
||||||
model: clientOptions.model,
|
|
||||||
context: 'title',
|
|
||||||
collectedUsage,
|
collectedUsage,
|
||||||
|
context: 'title',
|
||||||
|
model: clientOptions.model,
|
||||||
|
balance: appConfig?.balance,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
'[api/server/controllers/agents/client.js #titleConvo] Error recording collected usage',
|
'[api/server/controllers/agents/client.js #titleConvo] Error recording collected usage',
|
||||||
@@ -1235,17 +1252,26 @@ class AgentClient extends BaseClient {
|
|||||||
* @param {object} params
|
* @param {object} params
|
||||||
* @param {number} params.promptTokens
|
* @param {number} params.promptTokens
|
||||||
* @param {number} params.completionTokens
|
* @param {number} params.completionTokens
|
||||||
* @param {OpenAIUsageMetadata} [params.usage]
|
|
||||||
* @param {string} [params.model]
|
* @param {string} [params.model]
|
||||||
|
* @param {OpenAIUsageMetadata} [params.usage]
|
||||||
|
* @param {AppConfig['balance']} [params.balance]
|
||||||
* @param {string} [params.context='message']
|
* @param {string} [params.context='message']
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async recordTokenUsage({ model, promptTokens, completionTokens, usage, context = 'message' }) {
|
async recordTokenUsage({
|
||||||
|
model,
|
||||||
|
usage,
|
||||||
|
balance,
|
||||||
|
promptTokens,
|
||||||
|
completionTokens,
|
||||||
|
context = 'message',
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
await spendTokens(
|
await spendTokens(
|
||||||
{
|
{
|
||||||
model,
|
model,
|
||||||
context,
|
context,
|
||||||
|
balance,
|
||||||
conversationId: this.conversationId,
|
conversationId: this.conversationId,
|
||||||
user: this.user ?? this.options.req.user?.id,
|
user: this.user ?? this.options.req.user?.id,
|
||||||
endpointTokenConfig: this.options.endpointTokenConfig,
|
endpointTokenConfig: this.options.endpointTokenConfig,
|
||||||
@@ -1262,6 +1288,7 @@ class AgentClient extends BaseClient {
|
|||||||
await spendTokens(
|
await spendTokens(
|
||||||
{
|
{
|
||||||
model,
|
model,
|
||||||
|
balance,
|
||||||
context: 'reasoning',
|
context: 'reasoning',
|
||||||
conversationId: this.conversationId,
|
conversationId: this.conversationId,
|
||||||
user: this.user ?? this.options.req.user?.id,
|
user: this.user ?? this.options.req.user?.id,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { Providers } = require('@librechat/agents');
|
const { Providers } = require('@librechat/agents');
|
||||||
const { Constants, EModelEndpoint } = require('librechat-data-provider');
|
const { Constants, EModelEndpoint } = require('librechat-data-provider');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const AgentClient = require('./client');
|
const AgentClient = require('./client');
|
||||||
|
|
||||||
jest.mock('@librechat/agents', () => ({
|
jest.mock('@librechat/agents', () => ({
|
||||||
@@ -10,6 +11,10 @@ jest.mock('@librechat/agents', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Config', () => ({
|
||||||
|
getAppConfig: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('AgentClient - titleConvo', () => {
|
describe('AgentClient - titleConvo', () => {
|
||||||
let client;
|
let client;
|
||||||
let mockRun;
|
let mockRun;
|
||||||
@@ -39,19 +44,21 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock request and response
|
// Mock getAppConfig to return endpoint configurations
|
||||||
mockReq = {
|
getAppConfig.mockResolvedValue({
|
||||||
app: {
|
endpoints: {
|
||||||
locals: {
|
[EModelEndpoint.openAI]: {
|
||||||
[EModelEndpoint.openAI]: {
|
// Match the agent endpoint
|
||||||
// Match the agent endpoint
|
titleModel: 'gpt-3.5-turbo',
|
||||||
titleModel: 'gpt-3.5-turbo',
|
titlePrompt: 'Custom title prompt',
|
||||||
titlePrompt: 'Custom title prompt',
|
titleMethod: 'structured',
|
||||||
titleMethod: 'structured',
|
titlePromptTemplate: 'Template: {{content}}',
|
||||||
titlePromptTemplate: 'Template: {{content}}',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock request and response
|
||||||
|
mockReq = {
|
||||||
user: {
|
user: {
|
||||||
id: 'user-123',
|
id: 'user-123',
|
||||||
},
|
},
|
||||||
@@ -143,7 +150,7 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
|
|
||||||
it('should handle missing endpoint config gracefully', async () => {
|
it('should handle missing endpoint config gracefully', async () => {
|
||||||
// Remove endpoint config
|
// Remove endpoint config
|
||||||
mockReq.app.locals[EModelEndpoint.openAI] = undefined;
|
getAppConfig.mockResolvedValue({ endpoints: {} });
|
||||||
|
|
||||||
const text = 'Test conversation text';
|
const text = 'Test conversation text';
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -161,7 +168,16 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
|
|
||||||
it('should use agent model when titleModel is not provided', async () => {
|
it('should use agent model when titleModel is not provided', async () => {
|
||||||
// Remove titleModel from config
|
// Remove titleModel from config
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI].titleModel;
|
getAppConfig.mockResolvedValue({
|
||||||
|
endpoints: {
|
||||||
|
[EModelEndpoint.openAI]: {
|
||||||
|
titlePrompt: 'Custom title prompt',
|
||||||
|
titleMethod: 'structured',
|
||||||
|
titlePromptTemplate: 'Template: {{content}}',
|
||||||
|
// titleModel is omitted
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const text = 'Test conversation text';
|
const text = 'Test conversation text';
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -173,7 +189,16 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not use titleModel when it equals CURRENT_MODEL constant', async () => {
|
it('should not use titleModel when it equals CURRENT_MODEL constant', async () => {
|
||||||
mockReq.app.locals[EModelEndpoint.openAI].titleModel = Constants.CURRENT_MODEL;
|
getAppConfig.mockResolvedValue({
|
||||||
|
endpoints: {
|
||||||
|
[EModelEndpoint.openAI]: {
|
||||||
|
titleModel: Constants.CURRENT_MODEL,
|
||||||
|
titlePrompt: 'Custom title prompt',
|
||||||
|
titleMethod: 'structured',
|
||||||
|
titlePromptTemplate: 'Template: {{content}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const text = 'Test conversation text';
|
const text = 'Test conversation text';
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -245,10 +270,17 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
process.env.ANTHROPIC_API_KEY = 'test-api-key';
|
process.env.ANTHROPIC_API_KEY = 'test-api-key';
|
||||||
|
|
||||||
// Add titleEndpoint to the config
|
// Add titleEndpoint to the config
|
||||||
mockReq.app.locals[EModelEndpoint.openAI].titleEndpoint = EModelEndpoint.anthropic;
|
getAppConfig.mockResolvedValue({
|
||||||
mockReq.app.locals[EModelEndpoint.openAI].titleMethod = 'structured';
|
endpoints: {
|
||||||
mockReq.app.locals[EModelEndpoint.openAI].titlePrompt = 'Custom title prompt';
|
[EModelEndpoint.openAI]: {
|
||||||
mockReq.app.locals[EModelEndpoint.openAI].titlePromptTemplate = 'Custom template';
|
titleModel: 'gpt-3.5-turbo',
|
||||||
|
titleEndpoint: EModelEndpoint.anthropic,
|
||||||
|
titleMethod: 'structured',
|
||||||
|
titlePrompt: 'Custom title prompt',
|
||||||
|
titlePromptTemplate: 'Custom template',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const text = 'Test conversation text';
|
const text = 'Test conversation text';
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -274,19 +306,17 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use all config when endpoint config is missing', async () => {
|
it('should use all config when endpoint config is missing', async () => {
|
||||||
// Remove endpoint-specific config
|
// Set 'all' config without endpoint-specific config
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI].titleModel;
|
getAppConfig.mockResolvedValue({
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI].titlePrompt;
|
endpoints: {
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI].titleMethod;
|
all: {
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI].titlePromptTemplate;
|
titleModel: 'gpt-4o-mini',
|
||||||
|
titlePrompt: 'All config title prompt',
|
||||||
// Set 'all' config
|
titleMethod: 'completion',
|
||||||
mockReq.app.locals.all = {
|
titlePromptTemplate: 'All config template: {{content}}',
|
||||||
titleModel: 'gpt-4o-mini',
|
},
|
||||||
titlePrompt: 'All config title prompt',
|
},
|
||||||
titleMethod: 'completion',
|
});
|
||||||
titlePromptTemplate: 'All config template: {{content}}',
|
|
||||||
};
|
|
||||||
|
|
||||||
const text = 'Test conversation text';
|
const text = 'Test conversation text';
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -309,18 +339,22 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
|
|
||||||
it('should prioritize all config over endpoint config for title settings', async () => {
|
it('should prioritize all config over endpoint config for title settings', async () => {
|
||||||
// Set both endpoint and 'all' config
|
// Set both endpoint and 'all' config
|
||||||
mockReq.app.locals[EModelEndpoint.openAI].titleModel = 'gpt-3.5-turbo';
|
getAppConfig.mockResolvedValue({
|
||||||
mockReq.app.locals[EModelEndpoint.openAI].titlePrompt = 'Endpoint title prompt';
|
endpoints: {
|
||||||
mockReq.app.locals[EModelEndpoint.openAI].titleMethod = 'structured';
|
[EModelEndpoint.openAI]: {
|
||||||
// Remove titlePromptTemplate from endpoint config to test fallback
|
titleModel: 'gpt-3.5-turbo',
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI].titlePromptTemplate;
|
titlePrompt: 'Endpoint title prompt',
|
||||||
|
titleMethod: 'structured',
|
||||||
mockReq.app.locals.all = {
|
// titlePromptTemplate is omitted to test fallback
|
||||||
titleModel: 'gpt-4o-mini',
|
},
|
||||||
titlePrompt: 'All config title prompt',
|
all: {
|
||||||
titleMethod: 'completion',
|
titleModel: 'gpt-4o-mini',
|
||||||
titlePromptTemplate: 'All config template',
|
titlePrompt: 'All config title prompt',
|
||||||
};
|
titleMethod: 'completion',
|
||||||
|
titlePromptTemplate: 'All config template',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const text = 'Test conversation text';
|
const text = 'Test conversation text';
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -346,18 +380,19 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
const originalApiKey = process.env.ANTHROPIC_API_KEY;
|
const originalApiKey = process.env.ANTHROPIC_API_KEY;
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
|
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
|
||||||
|
|
||||||
// Remove endpoint-specific config to test 'all' config
|
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI];
|
|
||||||
|
|
||||||
// Set comprehensive 'all' config with all new title options
|
// Set comprehensive 'all' config with all new title options
|
||||||
mockReq.app.locals.all = {
|
getAppConfig.mockResolvedValue({
|
||||||
titleConvo: true,
|
endpoints: {
|
||||||
titleModel: 'claude-3-haiku-20240307',
|
all: {
|
||||||
titleMethod: 'completion', // Testing the new default method
|
titleConvo: true,
|
||||||
titlePrompt: 'Generate a concise, descriptive title for this conversation',
|
titleModel: 'claude-3-haiku-20240307',
|
||||||
titlePromptTemplate: 'Conversation summary: {{content}}',
|
titleMethod: 'completion', // Testing the new default method
|
||||||
titleEndpoint: EModelEndpoint.anthropic, // Should switch provider to Anthropic
|
titlePrompt: 'Generate a concise, descriptive title for this conversation',
|
||||||
};
|
titlePromptTemplate: 'Conversation summary: {{content}}',
|
||||||
|
titleEndpoint: EModelEndpoint.anthropic, // Should switch provider to Anthropic
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const text = 'Test conversation about AI and machine learning';
|
const text = 'Test conversation about AI and machine learning';
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -402,16 +437,17 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
// Clear previous calls
|
// Clear previous calls
|
||||||
mockRun.generateTitle.mockClear();
|
mockRun.generateTitle.mockClear();
|
||||||
|
|
||||||
// Remove endpoint config
|
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI];
|
|
||||||
|
|
||||||
// Set 'all' config with specific titleMethod
|
// Set 'all' config with specific titleMethod
|
||||||
mockReq.app.locals.all = {
|
getAppConfig.mockResolvedValue({
|
||||||
titleModel: 'gpt-4o-mini',
|
endpoints: {
|
||||||
titleMethod: method,
|
all: {
|
||||||
titlePrompt: `Testing ${method} method`,
|
titleModel: 'gpt-4o-mini',
|
||||||
titlePromptTemplate: `Template for ${method}: {{content}}`,
|
titleMethod: method,
|
||||||
};
|
titlePrompt: `Testing ${method} method`,
|
||||||
|
titlePromptTemplate: `Template for ${method}: {{content}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const text = `Test conversation for ${method} method`;
|
const text = `Test conversation for ${method} method`;
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -455,32 +491,36 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
// Set up Azure endpoint with serverless config
|
// Set up Azure endpoint with serverless config
|
||||||
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
|
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockAgent.provider = EModelEndpoint.azureOpenAI;
|
mockAgent.provider = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.app.locals[EModelEndpoint.azureOpenAI] = {
|
getAppConfig.mockResolvedValue({
|
||||||
titleConvo: true,
|
endpoints: {
|
||||||
titleModel: 'grok-3',
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
titleMethod: 'completion',
|
titleConvo: true,
|
||||||
titlePrompt: 'Azure serverless title prompt',
|
titleModel: 'grok-3',
|
||||||
streamRate: 35,
|
titleMethod: 'completion',
|
||||||
modelGroupMap: {
|
titlePrompt: 'Azure serverless title prompt',
|
||||||
'grok-3': {
|
streamRate: 35,
|
||||||
group: 'Azure AI Foundry',
|
modelGroupMap: {
|
||||||
deploymentName: 'grok-3',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
groupMap: {
|
|
||||||
'Azure AI Foundry': {
|
|
||||||
apiKey: '${AZURE_API_KEY}',
|
|
||||||
baseURL: 'https://test.services.ai.azure.com/models',
|
|
||||||
version: '2024-05-01-preview',
|
|
||||||
serverless: true,
|
|
||||||
models: {
|
|
||||||
'grok-3': {
|
'grok-3': {
|
||||||
|
group: 'Azure AI Foundry',
|
||||||
deploymentName: 'grok-3',
|
deploymentName: 'grok-3',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
groupMap: {
|
||||||
|
'Azure AI Foundry': {
|
||||||
|
apiKey: '${AZURE_API_KEY}',
|
||||||
|
baseURL: 'https://test.services.ai.azure.com/models',
|
||||||
|
version: '2024-05-01-preview',
|
||||||
|
serverless: true,
|
||||||
|
models: {
|
||||||
|
'grok-3': {
|
||||||
|
deploymentName: 'grok-3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.body.model = 'grok-3';
|
mockReq.body.model = 'grok-3';
|
||||||
|
|
||||||
@@ -503,31 +543,35 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
// Set up Azure endpoint
|
// Set up Azure endpoint
|
||||||
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
|
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockAgent.provider = EModelEndpoint.azureOpenAI;
|
mockAgent.provider = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.app.locals[EModelEndpoint.azureOpenAI] = {
|
getAppConfig.mockResolvedValue({
|
||||||
titleConvo: true,
|
endpoints: {
|
||||||
titleModel: 'gpt-4o',
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
titleMethod: 'structured',
|
titleConvo: true,
|
||||||
titlePrompt: 'Azure instance title prompt',
|
titleModel: 'gpt-4o',
|
||||||
streamRate: 35,
|
titleMethod: 'structured',
|
||||||
modelGroupMap: {
|
titlePrompt: 'Azure instance title prompt',
|
||||||
'gpt-4o': {
|
streamRate: 35,
|
||||||
group: 'eastus',
|
modelGroupMap: {
|
||||||
deploymentName: 'gpt-4o',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
groupMap: {
|
|
||||||
eastus: {
|
|
||||||
apiKey: '${EASTUS_API_KEY}',
|
|
||||||
instanceName: 'region-instance',
|
|
||||||
version: '2024-02-15-preview',
|
|
||||||
models: {
|
|
||||||
'gpt-4o': {
|
'gpt-4o': {
|
||||||
|
group: 'eastus',
|
||||||
deploymentName: 'gpt-4o',
|
deploymentName: 'gpt-4o',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
groupMap: {
|
||||||
|
eastus: {
|
||||||
|
apiKey: '${EASTUS_API_KEY}',
|
||||||
|
instanceName: 'region-instance',
|
||||||
|
version: '2024-02-15-preview',
|
||||||
|
models: {
|
||||||
|
'gpt-4o': {
|
||||||
|
deploymentName: 'gpt-4o',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.body.model = 'gpt-4o';
|
mockReq.body.model = 'gpt-4o';
|
||||||
|
|
||||||
@@ -551,32 +595,36 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
|
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockAgent.provider = EModelEndpoint.azureOpenAI;
|
mockAgent.provider = EModelEndpoint.azureOpenAI;
|
||||||
mockAgent.model_parameters.model = 'gpt-4o-latest';
|
mockAgent.model_parameters.model = 'gpt-4o-latest';
|
||||||
mockReq.app.locals[EModelEndpoint.azureOpenAI] = {
|
getAppConfig.mockResolvedValue({
|
||||||
titleConvo: true,
|
endpoints: {
|
||||||
titleModel: Constants.CURRENT_MODEL,
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
titleMethod: 'functions',
|
titleConvo: true,
|
||||||
streamRate: 35,
|
titleModel: Constants.CURRENT_MODEL,
|
||||||
modelGroupMap: {
|
titleMethod: 'functions',
|
||||||
'gpt-4o-latest': {
|
streamRate: 35,
|
||||||
group: 'region-eastus',
|
modelGroupMap: {
|
||||||
deploymentName: 'gpt-4o-mini',
|
|
||||||
version: '2024-02-15-preview',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
groupMap: {
|
|
||||||
'region-eastus': {
|
|
||||||
apiKey: '${EASTUS2_API_KEY}',
|
|
||||||
instanceName: 'test-instance',
|
|
||||||
version: '2024-12-01-preview',
|
|
||||||
models: {
|
|
||||||
'gpt-4o-latest': {
|
'gpt-4o-latest': {
|
||||||
|
group: 'region-eastus',
|
||||||
deploymentName: 'gpt-4o-mini',
|
deploymentName: 'gpt-4o-mini',
|
||||||
version: '2024-02-15-preview',
|
version: '2024-02-15-preview',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
groupMap: {
|
||||||
|
'region-eastus': {
|
||||||
|
apiKey: '${EASTUS2_API_KEY}',
|
||||||
|
instanceName: 'test-instance',
|
||||||
|
version: '2024-12-01-preview',
|
||||||
|
models: {
|
||||||
|
'gpt-4o-latest': {
|
||||||
|
deploymentName: 'gpt-4o-mini',
|
||||||
|
version: '2024-02-15-preview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.body.model = 'gpt-4o-latest';
|
mockReq.body.model = 'gpt-4o-latest';
|
||||||
|
|
||||||
@@ -598,59 +646,63 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
// Set up Azure endpoint
|
// Set up Azure endpoint
|
||||||
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
|
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockAgent.provider = EModelEndpoint.azureOpenAI;
|
mockAgent.provider = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.app.locals[EModelEndpoint.azureOpenAI] = {
|
getAppConfig.mockResolvedValue({
|
||||||
titleConvo: true,
|
endpoints: {
|
||||||
titleModel: 'o1-mini',
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
titleMethod: 'completion',
|
titleConvo: true,
|
||||||
streamRate: 35,
|
titleModel: 'o1-mini',
|
||||||
modelGroupMap: {
|
titleMethod: 'completion',
|
||||||
'gpt-4o': {
|
streamRate: 35,
|
||||||
group: 'eastus',
|
modelGroupMap: {
|
||||||
deploymentName: 'gpt-4o',
|
|
||||||
},
|
|
||||||
'o1-mini': {
|
|
||||||
group: 'region-eastus',
|
|
||||||
deploymentName: 'o1-mini',
|
|
||||||
},
|
|
||||||
'codex-mini': {
|
|
||||||
group: 'codex-mini',
|
|
||||||
deploymentName: 'codex-mini',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
groupMap: {
|
|
||||||
eastus: {
|
|
||||||
apiKey: '${EASTUS_API_KEY}',
|
|
||||||
instanceName: 'region-eastus',
|
|
||||||
version: '2024-02-15-preview',
|
|
||||||
models: {
|
|
||||||
'gpt-4o': {
|
'gpt-4o': {
|
||||||
|
group: 'eastus',
|
||||||
deploymentName: 'gpt-4o',
|
deploymentName: 'gpt-4o',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
'region-eastus': {
|
|
||||||
apiKey: '${EASTUS2_API_KEY}',
|
|
||||||
instanceName: 'region-eastus2',
|
|
||||||
version: '2024-12-01-preview',
|
|
||||||
models: {
|
|
||||||
'o1-mini': {
|
'o1-mini': {
|
||||||
|
group: 'region-eastus',
|
||||||
deploymentName: 'o1-mini',
|
deploymentName: 'o1-mini',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
'codex-mini': {
|
|
||||||
apiKey: '${AZURE_API_KEY}',
|
|
||||||
baseURL: 'https://example.cognitiveservices.azure.com/openai/',
|
|
||||||
version: '2025-04-01-preview',
|
|
||||||
serverless: true,
|
|
||||||
models: {
|
|
||||||
'codex-mini': {
|
'codex-mini': {
|
||||||
|
group: 'codex-mini',
|
||||||
deploymentName: 'codex-mini',
|
deploymentName: 'codex-mini',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
groupMap: {
|
||||||
|
eastus: {
|
||||||
|
apiKey: '${EASTUS_API_KEY}',
|
||||||
|
instanceName: 'region-eastus',
|
||||||
|
version: '2024-02-15-preview',
|
||||||
|
models: {
|
||||||
|
'gpt-4o': {
|
||||||
|
deploymentName: 'gpt-4o',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'region-eastus': {
|
||||||
|
apiKey: '${EASTUS2_API_KEY}',
|
||||||
|
instanceName: 'region-eastus2',
|
||||||
|
version: '2024-12-01-preview',
|
||||||
|
models: {
|
||||||
|
'o1-mini': {
|
||||||
|
deploymentName: 'o1-mini',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'codex-mini': {
|
||||||
|
apiKey: '${AZURE_API_KEY}',
|
||||||
|
baseURL: 'https://example.cognitiveservices.azure.com/openai/',
|
||||||
|
version: '2025-04-01-preview',
|
||||||
|
serverless: true,
|
||||||
|
models: {
|
||||||
|
'codex-mini': {
|
||||||
|
deploymentName: 'codex-mini',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.body.model = 'o1-mini';
|
mockReq.body.model = 'o1-mini';
|
||||||
|
|
||||||
@@ -679,36 +731,37 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.body.model = 'gpt-4';
|
mockReq.body.model = 'gpt-4';
|
||||||
|
|
||||||
// Remove Azure-specific config
|
|
||||||
delete mockReq.app.locals[EModelEndpoint.azureOpenAI];
|
|
||||||
|
|
||||||
// Set 'all' config as fallback with a serverless Azure config
|
// Set 'all' config as fallback with a serverless Azure config
|
||||||
mockReq.app.locals.all = {
|
getAppConfig.mockResolvedValue({
|
||||||
titleConvo: true,
|
endpoints: {
|
||||||
titleModel: 'gpt-4',
|
all: {
|
||||||
titleMethod: 'structured',
|
titleConvo: true,
|
||||||
titlePrompt: 'Fallback title prompt from all config',
|
titleModel: 'gpt-4',
|
||||||
titlePromptTemplate: 'Template: {{content}}',
|
titleMethod: 'structured',
|
||||||
modelGroupMap: {
|
titlePrompt: 'Fallback title prompt from all config',
|
||||||
'gpt-4': {
|
titlePromptTemplate: 'Template: {{content}}',
|
||||||
group: 'default-group',
|
modelGroupMap: {
|
||||||
deploymentName: 'gpt-4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
groupMap: {
|
|
||||||
'default-group': {
|
|
||||||
apiKey: '${AZURE_API_KEY}',
|
|
||||||
baseURL: 'https://default.openai.azure.com/',
|
|
||||||
version: '2024-02-15-preview',
|
|
||||||
serverless: true,
|
|
||||||
models: {
|
|
||||||
'gpt-4': {
|
'gpt-4': {
|
||||||
|
group: 'default-group',
|
||||||
deploymentName: 'gpt-4',
|
deploymentName: 'gpt-4',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
groupMap: {
|
||||||
|
'default-group': {
|
||||||
|
apiKey: '${AZURE_API_KEY}',
|
||||||
|
baseURL: 'https://default.openai.azure.com/',
|
||||||
|
version: '2024-02-15-preview',
|
||||||
|
serverless: true,
|
||||||
|
models: {
|
||||||
|
'gpt-4': {
|
||||||
|
deploymentName: 'gpt-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const text = 'Test Azure with all config fallback';
|
const text = 'Test Azure with all config fallback';
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -982,13 +1035,6 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
mockReq = {
|
mockReq = {
|
||||||
app: {
|
|
||||||
locals: {
|
|
||||||
memory: {
|
|
||||||
messageWindowSize: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user: {
|
user: {
|
||||||
id: 'user-123',
|
id: 'user-123',
|
||||||
personalization: {
|
personalization: {
|
||||||
@@ -997,6 +1043,13 @@ describe('AgentClient - titleConvo', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock getAppConfig for memory tests
|
||||||
|
getAppConfig.mockResolvedValue({
|
||||||
|
memory: {
|
||||||
|
messageWindowSize: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
mockRes = {};
|
mockRes = {};
|
||||||
|
|
||||||
mockOptions = {
|
mockOptions = {
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ const {
|
|||||||
grantPermission,
|
grantPermission,
|
||||||
} = require('~/server/services/PermissionService');
|
} = require('~/server/services/PermissionService');
|
||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
|
const { getCachedTools, getAppConfig } = require('~/server/services/Config');
|
||||||
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
|
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
|
||||||
const { getFileStrategy } = require('~/server/utils/getFileStrategy');
|
const { getFileStrategy } = require('~/server/utils/getFileStrategy');
|
||||||
const { refreshS3Url } = require('~/server/services/Files/S3/crud');
|
const { refreshS3Url } = require('~/server/services/Files/S3/crud');
|
||||||
const { filterFile } = require('~/server/services/Files/process');
|
const { filterFile } = require('~/server/services/Files/process');
|
||||||
const { updateAction, getActions } = require('~/models/Action');
|
const { updateAction, getActions } = require('~/models/Action');
|
||||||
const { getCachedTools } = require('~/server/services/Config');
|
|
||||||
const { deleteFileByFilter } = require('~/models/File');
|
const { deleteFileByFilter } = require('~/models/File');
|
||||||
const { getCategoriesWithCounts } = require('~/models');
|
const { getCategoriesWithCounts } = require('~/models');
|
||||||
|
|
||||||
@@ -487,6 +487,7 @@ const getListAgentsHandler = async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
const uploadAgentAvatarHandler = async (req, res) => {
|
const uploadAgentAvatarHandler = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
filterFile({ req, file: req.file, image: true, isAvatar: true });
|
filterFile({ req, file: req.file, image: true, isAvatar: true });
|
||||||
const { agent_id } = req.params;
|
const { agent_id } = req.params;
|
||||||
if (!agent_id) {
|
if (!agent_id) {
|
||||||
@@ -510,9 +511,7 @@ const uploadAgentAvatarHandler = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buffer = await fs.readFile(req.file.path);
|
const buffer = await fs.readFile(req.file.path);
|
||||||
|
const fileStrategy = getFileStrategy(appConfig, { isAvatar: true });
|
||||||
const fileStrategy = getFileStrategy(req.app.locals, { isAvatar: true });
|
|
||||||
|
|
||||||
const resizedBuffer = await resizeAvatar({
|
const resizedBuffer = await resizeAvatar({
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
input: buffer,
|
input: buffer,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const { createRun, StreamRunManager } = require('~/server/services/Runs');
|
|||||||
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
||||||
const { createRunBody } = require('~/server/services/createRunBody');
|
const { createRunBody } = require('~/server/services/createRunBody');
|
||||||
const { sendResponse } = require('~/server/middleware/error');
|
const { sendResponse } = require('~/server/middleware/error');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { getTransactions } = require('~/models/Transaction');
|
const { getTransactions } = require('~/models/Transaction');
|
||||||
const { checkBalance } = require('~/models/balanceMethods');
|
const { checkBalance } = require('~/models/balanceMethods');
|
||||||
const { getConvo } = require('~/models/Conversation');
|
const { getConvo } = require('~/models/Conversation');
|
||||||
@@ -47,6 +48,7 @@ const { getOpenAIClient } = require('./helpers');
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const chatV1 = async (req, res) => {
|
const chatV1 = async (req, res) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
logger.debug('[/assistants/chat/] req.body', req.body);
|
logger.debug('[/assistants/chat/] req.body', req.body);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -251,7 +253,7 @@ const chatV1 = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkBalanceBeforeRun = async () => {
|
const checkBalanceBeforeRun = async () => {
|
||||||
const balance = req.app?.locals?.balance;
|
const balance = appConfig?.balance;
|
||||||
if (!balance?.enabled) {
|
if (!balance?.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
|
|||||||
const { createRun, StreamRunManager } = require('~/server/services/Runs');
|
const { createRun, StreamRunManager } = require('~/server/services/Runs');
|
||||||
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
const { addTitle } = require('~/server/services/Endpoints/assistants');
|
||||||
const { createRunBody } = require('~/server/services/createRunBody');
|
const { createRunBody } = require('~/server/services/createRunBody');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { getTransactions } = require('~/models/Transaction');
|
const { getTransactions } = require('~/models/Transaction');
|
||||||
const { checkBalance } = require('~/models/balanceMethods');
|
const { checkBalance } = require('~/models/balanceMethods');
|
||||||
const { getConvo } = require('~/models/Conversation');
|
const { getConvo } = require('~/models/Conversation');
|
||||||
@@ -44,6 +45,7 @@ const { getOpenAIClient } = require('./helpers');
|
|||||||
*/
|
*/
|
||||||
const chatV2 = async (req, res) => {
|
const chatV2 = async (req, res) => {
|
||||||
logger.debug('[/assistants/chat/] req.body', req.body);
|
logger.debug('[/assistants/chat/] req.body', req.body);
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
|
||||||
/** @type {{files: MongoFile[]}} */
|
/** @type {{files: MongoFile[]}} */
|
||||||
const {
|
const {
|
||||||
@@ -126,7 +128,7 @@ const chatV2 = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkBalanceBeforeRun = async () => {
|
const checkBalanceBeforeRun = async () => {
|
||||||
const balance = req.app?.locals?.balance;
|
const balance = appConfig?.balance;
|
||||||
if (!balance?.enabled) {
|
if (!balance?.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -374,9 +376,9 @@ const chatV2 = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** @type {undefined | TAssistantEndpoint} */
|
/** @type {undefined | TAssistantEndpoint} */
|
||||||
const config = req.app.locals[endpoint] ?? {};
|
const config = appConfig.endpoints?.[endpoint] ?? {};
|
||||||
/** @type {undefined | TBaseEndpoint} */
|
/** @type {undefined | TBaseEndpoint} */
|
||||||
const allConfig = req.app.locals.all;
|
const allConfig = appConfig.endpoints?.all;
|
||||||
|
|
||||||
const streamRunManager = new StreamRunManager({
|
const streamRunManager = new StreamRunManager({
|
||||||
req,
|
req,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ const {
|
|||||||
const {
|
const {
|
||||||
initializeClient: initAzureClient,
|
initializeClient: initAzureClient,
|
||||||
} = require('~/server/services/Endpoints/azureAssistants');
|
} = require('~/server/services/Endpoints/azureAssistants');
|
||||||
|
const { getEndpointsConfig, getAppConfig } = require('~/server/services/Config');
|
||||||
const { initializeClient } = require('~/server/services/Endpoints/assistants');
|
const { initializeClient } = require('~/server/services/Endpoints/assistants');
|
||||||
const { getEndpointsConfig } = require('~/server/services/Config');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Express.Request} req
|
* @param {Express.Request} req
|
||||||
@@ -210,6 +210,7 @@ async function getOpenAIClient({ req, res, endpointOption, initAppClient, overri
|
|||||||
* @returns {Promise<AssistantListResponse>} 200 - success response - application/json
|
* @returns {Promise<AssistantListResponse>} 200 - success response - application/json
|
||||||
*/
|
*/
|
||||||
const fetchAssistants = async ({ req, res, overrideEndpoint }) => {
|
const fetchAssistants = async ({ req, res, overrideEndpoint }) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const {
|
const {
|
||||||
limit = 100,
|
limit = 100,
|
||||||
order = 'desc',
|
order = 'desc',
|
||||||
@@ -230,20 +231,20 @@ const fetchAssistants = async ({ req, res, overrideEndpoint }) => {
|
|||||||
if (endpoint === EModelEndpoint.assistants) {
|
if (endpoint === EModelEndpoint.assistants) {
|
||||||
({ body } = await listAllAssistants({ req, res, version, query }));
|
({ body } = await listAllAssistants({ req, res, version, query }));
|
||||||
} else if (endpoint === EModelEndpoint.azureAssistants) {
|
} else if (endpoint === EModelEndpoint.azureAssistants) {
|
||||||
const azureConfig = req.app.locals[EModelEndpoint.azureOpenAI];
|
const azureConfig = appConfig.endpoints?.[EModelEndpoint.azureOpenAI];
|
||||||
body = await listAssistantsForAzure({ req, res, version, azureConfig, query });
|
body = await listAssistantsForAzure({ req, res, version, azureConfig, query });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.user.role === SystemRoles.ADMIN) {
|
if (req.user.role === SystemRoles.ADMIN) {
|
||||||
return body;
|
return body;
|
||||||
} else if (!req.app.locals[endpoint]) {
|
} else if (!appConfig.endpoints?.[endpoint]) {
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.data = filterAssistants({
|
body.data = filterAssistants({
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
assistants: body.data,
|
assistants: body.data,
|
||||||
assistantsConfig: req.app.locals[endpoint],
|
assistantsConfig: appConfig.endpoints?.[endpoint],
|
||||||
});
|
});
|
||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ const { uploadImageBuffer, filterFile } = require('~/server/services/Files/proce
|
|||||||
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
|
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
|
||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
const { deleteAssistantActions } = require('~/server/services/ActionService');
|
const { deleteAssistantActions } = require('~/server/services/ActionService');
|
||||||
|
const { getCachedTools, getAppConfig } = require('~/server/services/Config');
|
||||||
const { updateAssistantDoc, getAssistants } = require('~/models/Assistant');
|
const { updateAssistantDoc, getAssistants } = require('~/models/Assistant');
|
||||||
const { getOpenAIClient, fetchAssistants } = require('./helpers');
|
const { getOpenAIClient, fetchAssistants } = require('./helpers');
|
||||||
const { getCachedTools } = require('~/server/services/Config');
|
|
||||||
const { manifestToolMap } = require('~/app/clients/tools');
|
const { manifestToolMap } = require('~/app/clients/tools');
|
||||||
const { deleteFileByFilter } = require('~/models/File');
|
const { deleteFileByFilter } = require('~/models/File');
|
||||||
|
|
||||||
@@ -258,8 +258,9 @@ function filterAssistantDocs({ documents, userId, assistantsConfig = {} }) {
|
|||||||
*/
|
*/
|
||||||
const getAssistantDocuments = async (req, res) => {
|
const getAssistantDocuments = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const endpoint = req.query;
|
const endpoint = req.query;
|
||||||
const assistantsConfig = req.app.locals[endpoint];
|
const assistantsConfig = appConfig.endpoints?.[endpoint];
|
||||||
const documents = await getAssistants(
|
const documents = await getAssistants(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
@@ -296,6 +297,7 @@ const getAssistantDocuments = async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
const uploadAssistantAvatar = async (req, res) => {
|
const uploadAssistantAvatar = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
filterFile({ req, file: req.file, image: true, isAvatar: true });
|
filterFile({ req, file: req.file, image: true, isAvatar: true });
|
||||||
const { assistant_id } = req.params;
|
const { assistant_id } = req.params;
|
||||||
if (!assistant_id) {
|
if (!assistant_id) {
|
||||||
@@ -337,7 +339,7 @@ const uploadAssistantAvatar = async (req, res) => {
|
|||||||
const metadata = {
|
const metadata = {
|
||||||
..._metadata,
|
..._metadata,
|
||||||
avatar: image.filepath,
|
avatar: image.filepath,
|
||||||
avatar_source: req.app.locals.fileStrategy,
|
avatar_source: appConfig.fileStrategy,
|
||||||
};
|
};
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
@@ -347,7 +349,7 @@ const uploadAssistantAvatar = async (req, res) => {
|
|||||||
{
|
{
|
||||||
avatar: {
|
avatar: {
|
||||||
filepath: image.filepath,
|
filepath: image.filepath,
|
||||||
source: req.app.locals.fileStrategy,
|
source: appConfig.fileStrategy,
|
||||||
},
|
},
|
||||||
user: req.user.id,
|
user: req.user.id,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/p
|
|||||||
const { processCodeOutput } = require('~/server/services/Files/Code/process');
|
const { processCodeOutput } = require('~/server/services/Files/Code/process');
|
||||||
const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall');
|
const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall');
|
||||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { loadTools } = require('~/app/clients/tools/util');
|
const { loadTools } = require('~/app/clients/tools/util');
|
||||||
const { getRoleByName } = require('~/models/Role');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
const { getMessage } = require('~/models/Message');
|
const { getMessage } = require('~/models/Message');
|
||||||
@@ -35,9 +36,10 @@ const toolAccessPermType = {
|
|||||||
*/
|
*/
|
||||||
const verifyWebSearchAuth = async (req, res) => {
|
const verifyWebSearchAuth = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
/** @type {TCustomConfig['webSearch']} */
|
/** @type {TCustomConfig['webSearch']} */
|
||||||
const webSearchConfig = req.app.locals?.webSearch || {};
|
const webSearchConfig = appConfig?.webSearch || {};
|
||||||
const result = await loadWebSearchAuth({
|
const result = await loadWebSearchAuth({
|
||||||
userId,
|
userId,
|
||||||
loadAuthValues,
|
loadAuthValues,
|
||||||
@@ -110,6 +112,7 @@ const verifyToolAuth = async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
const callTool = async (req, res) => {
|
const callTool = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const { toolId = '' } = req.params;
|
const { toolId = '' } = req.params;
|
||||||
if (!fieldsMap[toolId]) {
|
if (!fieldsMap[toolId]) {
|
||||||
logger.warn(`[${toolId}/call] User ${req.user.id} attempted call to invalid tool`);
|
logger.warn(`[${toolId}/call] User ${req.user.id} attempted call to invalid tool`);
|
||||||
@@ -155,8 +158,10 @@ const callTool = async (req, res) => {
|
|||||||
returnMetadata: true,
|
returnMetadata: true,
|
||||||
processFileURL,
|
processFileURL,
|
||||||
uploadImageBuffer,
|
uploadImageBuffer,
|
||||||
fileStrategy: req.app.locals.fileStrategy,
|
|
||||||
},
|
},
|
||||||
|
webSearch: appConfig.webSearch,
|
||||||
|
fileStrategy: appConfig.fileStrategy,
|
||||||
|
imageOutputType: appConfig.imageOutputType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tool = loadedTools[0];
|
const tool = loadedTools[0];
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ const { isEnabled, ErrorController } = require('@librechat/api');
|
|||||||
const { connectDb, indexSync } = require('~/db');
|
const { connectDb, indexSync } = require('~/db');
|
||||||
const validateImageRequest = require('./middleware/validateImageRequest');
|
const validateImageRequest = require('./middleware/validateImageRequest');
|
||||||
const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies');
|
const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies');
|
||||||
|
const { updateInterfacePermissions } = require('~/models/interface');
|
||||||
const { checkMigrations } = require('./services/start/migration');
|
const { checkMigrations } = require('./services/start/migration');
|
||||||
const initializeMCPs = require('./services/initializeMCPs');
|
const initializeMCPs = require('./services/initializeMCPs');
|
||||||
const configureSocialLogins = require('./socialLogins');
|
const configureSocialLogins = require('./socialLogins');
|
||||||
const AppService = require('./services/AppService');
|
const { getAppConfig } = require('./services/Config');
|
||||||
const staticCache = require('./utils/staticCache');
|
const staticCache = require('./utils/staticCache');
|
||||||
const noIndex = require('./middleware/noIndex');
|
const noIndex = require('./middleware/noIndex');
|
||||||
|
const { seedDatabase } = require('~/models');
|
||||||
const routes = require('./routes');
|
const routes = require('./routes');
|
||||||
|
|
||||||
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};
|
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};
|
||||||
@@ -45,9 +47,11 @@ const startServer = async () => {
|
|||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
app.set('trust proxy', trusted_proxy);
|
app.set('trust proxy', trusted_proxy);
|
||||||
|
|
||||||
await AppService(app);
|
await seedDatabase();
|
||||||
|
|
||||||
const indexPath = path.join(app.locals.paths.dist, 'index.html');
|
const appConfig = await getAppConfig();
|
||||||
|
await updateInterfacePermissions(appConfig);
|
||||||
|
const indexPath = path.join(appConfig.paths.dist, 'index.html');
|
||||||
const indexHTML = fs.readFileSync(indexPath, 'utf8');
|
const indexHTML = fs.readFileSync(indexPath, 'utf8');
|
||||||
|
|
||||||
app.get('/health', (_req, res) => res.status(200).send('OK'));
|
app.get('/health', (_req, res) => res.status(200).send('OK'));
|
||||||
@@ -66,10 +70,9 @@ const startServer = async () => {
|
|||||||
console.warn('Response compression has been disabled via DISABLE_COMPRESSION.');
|
console.warn('Response compression has been disabled via DISABLE_COMPRESSION.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve static assets with aggressive caching
|
app.use(staticCache(appConfig.paths.dist));
|
||||||
app.use(staticCache(app.locals.paths.dist));
|
app.use(staticCache(appConfig.paths.fonts));
|
||||||
app.use(staticCache(app.locals.paths.fonts));
|
app.use(staticCache(appConfig.paths.assets));
|
||||||
app.use(staticCache(app.locals.paths.assets));
|
|
||||||
|
|
||||||
if (!ALLOW_SOCIAL_LOGIN) {
|
if (!ALLOW_SOCIAL_LOGIN) {
|
||||||
console.warn('Social logins are disabled. Set ALLOW_SOCIAL_LOGIN=true to enable them.');
|
console.warn('Social logins are disabled. Set ALLOW_SOCIAL_LOGIN=true to enable them.');
|
||||||
@@ -146,7 +149,7 @@ const startServer = async () => {
|
|||||||
logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
|
logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeMCPs(app).then(() => checkMigrations());
|
initializeMCPs().then(() => checkMigrations());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,28 @@ const request = require('supertest');
|
|||||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
jest.mock('~/server/services/Config/loadCustomConfig', () => {
|
jest.mock('~/server/services/Config', () => ({
|
||||||
return jest.fn(() => Promise.resolve({}));
|
loadCustomConfig: jest.fn(() => Promise.resolve({})),
|
||||||
});
|
setAppConfig: jest.fn(),
|
||||||
|
getAppConfig: jest.fn().mockResolvedValue({
|
||||||
|
paths: {
|
||||||
|
uploads: '/tmp',
|
||||||
|
dist: '/tmp/dist',
|
||||||
|
fonts: '/tmp/fonts',
|
||||||
|
assets: '/tmp/assets',
|
||||||
|
},
|
||||||
|
fileStrategy: 'local',
|
||||||
|
imageOutputType: 'PNG',
|
||||||
|
}),
|
||||||
|
setCachedTools: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/app/clients/tools', () => ({
|
||||||
|
createOpenAIImageTools: jest.fn(() => []),
|
||||||
|
createYouTubeTools: jest.fn(() => []),
|
||||||
|
manifestToolMap: {},
|
||||||
|
toolkits: [],
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Server Configuration', () => {
|
describe('Server Configuration', () => {
|
||||||
// Increase the default timeout to allow for Mongo cleanup
|
// Increase the default timeout to allow for Mongo cleanup
|
||||||
@@ -31,6 +50,22 @@ describe('Server Configuration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
// Create the required directories and files for the test
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const dirs = ['/tmp/dist', '/tmp/fonts', '/tmp/assets'];
|
||||||
|
dirs.forEach((dir) => {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join('/tmp/dist', 'index.html'),
|
||||||
|
'<!DOCTYPE html><html><head><title>LibreChat</title></head><body><div id="root"></div></body></html>',
|
||||||
|
);
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create();
|
mongoServer = await MongoMemoryServer.create();
|
||||||
process.env.MONGO_URI = mongoServer.getUri();
|
process.env.MONGO_URI = mongoServer.getUri();
|
||||||
process.env.PORT = '0'; // Use a random available port
|
process.env.PORT = '0'; // Use a random available port
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { v4 } = require('uuid');
|
const { v4 } = require('uuid');
|
||||||
const { handleAbortError } = require('~/server/middleware/abortMiddleware');
|
const { handleAbortError } = require('~/server/middleware/abortMiddleware');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the assistant is supported or excluded
|
* Checks if the assistant is supported or excluded
|
||||||
@@ -12,8 +13,9 @@ const { handleAbortError } = require('~/server/middleware/abortMiddleware');
|
|||||||
const validateAssistant = async (req, res, next) => {
|
const validateAssistant = async (req, res, next) => {
|
||||||
const { endpoint, conversationId, assistant_id, messageId } = req.body;
|
const { endpoint, conversationId, assistant_id, messageId } = req.body;
|
||||||
|
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
/** @type {Partial<TAssistantEndpoint>} */
|
/** @type {Partial<TAssistantEndpoint>} */
|
||||||
const assistantsConfig = req.app.locals?.[endpoint];
|
const assistantsConfig = appConfig.endpoints?.[endpoint];
|
||||||
if (!assistantsConfig) {
|
if (!assistantsConfig) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const { SystemRoles } = require('librechat-data-provider');
|
const { SystemRoles } = require('librechat-data-provider');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
const { getAssistant } = require('~/models/Assistant');
|
const { getAssistant } = require('~/models/Assistant');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,8 +21,9 @@ const validateAuthor = async ({ req, openai, overrideEndpoint, overrideAssistant
|
|||||||
const assistant_id =
|
const assistant_id =
|
||||||
overrideAssistantId ?? req.params.id ?? req.body.assistant_id ?? req.query.assistant_id;
|
overrideAssistantId ?? req.params.id ?? req.body.assistant_id ?? req.query.assistant_id;
|
||||||
|
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
/** @type {Partial<TAssistantEndpoint>} */
|
/** @type {Partial<TAssistantEndpoint>} */
|
||||||
const assistantsConfig = req.app.locals?.[endpoint];
|
const assistantsConfig = appConfig.endpoints?.[endpoint];
|
||||||
if (!assistantsConfig) {
|
if (!assistantsConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const azureAssistants = require('~/server/services/Endpoints/azureAssistants');
|
|||||||
const assistants = require('~/server/services/Endpoints/assistants');
|
const assistants = require('~/server/services/Endpoints/assistants');
|
||||||
const { processFiles } = require('~/server/services/Files/process');
|
const { processFiles } = require('~/server/services/Files/process');
|
||||||
const anthropic = require('~/server/services/Endpoints/anthropic');
|
const anthropic = require('~/server/services/Endpoints/anthropic');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
const bedrock = require('~/server/services/Endpoints/bedrock');
|
const bedrock = require('~/server/services/Endpoints/bedrock');
|
||||||
const openAI = require('~/server/services/Endpoints/openAI');
|
const openAI = require('~/server/services/Endpoints/openAI');
|
||||||
const agents = require('~/server/services/Endpoints/agents');
|
const agents = require('~/server/services/Endpoints/agents');
|
||||||
@@ -40,9 +41,10 @@ async function buildEndpointOption(req, res, next) {
|
|||||||
return handleError(res, { text: 'Error parsing conversation' });
|
return handleError(res, { text: 'Error parsing conversation' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.app.locals.modelSpecs?.list && req.app.locals.modelSpecs?.enforce) {
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
if (appConfig.modelSpecs?.list && appConfig.modelSpecs?.enforce) {
|
||||||
/** @type {{ list: TModelSpec[] }}*/
|
/** @type {{ list: TModelSpec[] }}*/
|
||||||
const { list } = req.app.locals.modelSpecs;
|
const { list } = appConfig.modelSpecs;
|
||||||
const { spec } = parsedBody;
|
const { spec } = parsedBody;
|
||||||
|
|
||||||
if (!spec) {
|
if (!spec) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
||||||
const { logger } = require('~/config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the domain's social login is allowed
|
* Checks the domain's social login is allowed
|
||||||
@@ -14,7 +15,10 @@ const { logger } = require('~/config');
|
|||||||
*/
|
*/
|
||||||
const checkDomainAllowed = async (req, res, next = () => {}) => {
|
const checkDomainAllowed = async (req, res, next = () => {}) => {
|
||||||
const email = req?.user?.email;
|
const email = req?.user?.email;
|
||||||
if (email && !(await isEmailDomainAllowed(email))) {
|
const appConfig = getAppConfig({
|
||||||
|
role: req?.user?.role,
|
||||||
|
});
|
||||||
|
if (email && !(await isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains))) {
|
||||||
logger.error(`[Social Login] [Social Login not allowed] [Email: ${email}]`);
|
logger.error(`[Social Login] [Social Login not allowed] [Email: ${email}]`);
|
||||||
return res.redirect('/login');
|
return res.redirect('/login');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const validateImageRequest = require('~/server/middleware/validateImageRequest');
|
const validateImageRequest = require('~/server/middleware/validateImageRequest');
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Config/app', () => ({
|
||||||
|
getAppConfig: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('validateImageRequest middleware', () => {
|
describe('validateImageRequest middleware', () => {
|
||||||
let req, res, next;
|
let req, res, next;
|
||||||
const validObjectId = '65cfb246f7ecadb8b1e8036b';
|
const validObjectId = '65cfb246f7ecadb8b1e8036b';
|
||||||
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
req = {
|
req = {
|
||||||
app: { locals: { secureImageLinks: true } },
|
|
||||||
headers: {},
|
headers: {},
|
||||||
originalUrl: '',
|
originalUrl: '',
|
||||||
};
|
};
|
||||||
@@ -17,79 +22,86 @@ describe('validateImageRequest middleware', () => {
|
|||||||
};
|
};
|
||||||
next = jest.fn();
|
next = jest.fn();
|
||||||
process.env.JWT_REFRESH_SECRET = 'test-secret';
|
process.env.JWT_REFRESH_SECRET = 'test-secret';
|
||||||
|
|
||||||
|
// Mock getAppConfig to return secureImageLinks: true by default
|
||||||
|
getAppConfig.mockResolvedValue({
|
||||||
|
secureImageLinks: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call next() if secureImageLinks is false', () => {
|
test('should call next() if secureImageLinks is false', async () => {
|
||||||
req.app.locals.secureImageLinks = false;
|
getAppConfig.mockResolvedValue({
|
||||||
validateImageRequest(req, res, next);
|
secureImageLinks: false,
|
||||||
|
});
|
||||||
|
await validateImageRequest(req, res, next);
|
||||||
expect(next).toHaveBeenCalled();
|
expect(next).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 401 if refresh token is not provided', () => {
|
test('should return 401 if refresh token is not provided', async () => {
|
||||||
validateImageRequest(req, res, next);
|
await validateImageRequest(req, res, next);
|
||||||
expect(res.status).toHaveBeenCalledWith(401);
|
expect(res.status).toHaveBeenCalledWith(401);
|
||||||
expect(res.send).toHaveBeenCalledWith('Unauthorized');
|
expect(res.send).toHaveBeenCalledWith('Unauthorized');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 403 if refresh token is invalid', () => {
|
test('should return 403 if refresh token is invalid', async () => {
|
||||||
req.headers.cookie = 'refreshToken=invalid-token';
|
req.headers.cookie = 'refreshToken=invalid-token';
|
||||||
validateImageRequest(req, res, next);
|
await validateImageRequest(req, res, next);
|
||||||
expect(res.status).toHaveBeenCalledWith(403);
|
expect(res.status).toHaveBeenCalledWith(403);
|
||||||
expect(res.send).toHaveBeenCalledWith('Access Denied');
|
expect(res.send).toHaveBeenCalledWith('Access Denied');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 403 if refresh token is expired', () => {
|
test('should return 403 if refresh token is expired', async () => {
|
||||||
const expiredToken = jwt.sign(
|
const expiredToken = jwt.sign(
|
||||||
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) - 3600 },
|
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) - 3600 },
|
||||||
process.env.JWT_REFRESH_SECRET,
|
process.env.JWT_REFRESH_SECRET,
|
||||||
);
|
);
|
||||||
req.headers.cookie = `refreshToken=${expiredToken}`;
|
req.headers.cookie = `refreshToken=${expiredToken}`;
|
||||||
validateImageRequest(req, res, next);
|
await validateImageRequest(req, res, next);
|
||||||
expect(res.status).toHaveBeenCalledWith(403);
|
expect(res.status).toHaveBeenCalledWith(403);
|
||||||
expect(res.send).toHaveBeenCalledWith('Access Denied');
|
expect(res.send).toHaveBeenCalledWith('Access Denied');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call next() for valid image path', () => {
|
test('should call next() for valid image path', async () => {
|
||||||
const validToken = jwt.sign(
|
const validToken = jwt.sign(
|
||||||
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 },
|
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 },
|
||||||
process.env.JWT_REFRESH_SECRET,
|
process.env.JWT_REFRESH_SECRET,
|
||||||
);
|
);
|
||||||
req.headers.cookie = `refreshToken=${validToken}`;
|
req.headers.cookie = `refreshToken=${validToken}`;
|
||||||
req.originalUrl = `/images/${validObjectId}/example.jpg`;
|
req.originalUrl = `/images/${validObjectId}/example.jpg`;
|
||||||
validateImageRequest(req, res, next);
|
await validateImageRequest(req, res, next);
|
||||||
expect(next).toHaveBeenCalled();
|
expect(next).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 403 for invalid image path', () => {
|
test('should return 403 for invalid image path', async () => {
|
||||||
const validToken = jwt.sign(
|
const validToken = jwt.sign(
|
||||||
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 },
|
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 },
|
||||||
process.env.JWT_REFRESH_SECRET,
|
process.env.JWT_REFRESH_SECRET,
|
||||||
);
|
);
|
||||||
req.headers.cookie = `refreshToken=${validToken}`;
|
req.headers.cookie = `refreshToken=${validToken}`;
|
||||||
req.originalUrl = '/images/65cfb246f7ecadb8b1e8036c/example.jpg'; // Different ObjectId
|
req.originalUrl = '/images/65cfb246f7ecadb8b1e8036c/example.jpg'; // Different ObjectId
|
||||||
validateImageRequest(req, res, next);
|
await validateImageRequest(req, res, next);
|
||||||
expect(res.status).toHaveBeenCalledWith(403);
|
expect(res.status).toHaveBeenCalledWith(403);
|
||||||
expect(res.send).toHaveBeenCalledWith('Access Denied');
|
expect(res.send).toHaveBeenCalledWith('Access Denied');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return 403 for invalid ObjectId format', () => {
|
test('should return 403 for invalid ObjectId format', async () => {
|
||||||
const validToken = jwt.sign(
|
const validToken = jwt.sign(
|
||||||
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 },
|
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 },
|
||||||
process.env.JWT_REFRESH_SECRET,
|
process.env.JWT_REFRESH_SECRET,
|
||||||
);
|
);
|
||||||
req.headers.cookie = `refreshToken=${validToken}`;
|
req.headers.cookie = `refreshToken=${validToken}`;
|
||||||
req.originalUrl = '/images/123/example.jpg'; // Invalid ObjectId
|
req.originalUrl = '/images/123/example.jpg'; // Invalid ObjectId
|
||||||
validateImageRequest(req, res, next);
|
await validateImageRequest(req, res, next);
|
||||||
expect(res.status).toHaveBeenCalledWith(403);
|
expect(res.status).toHaveBeenCalledWith(403);
|
||||||
expect(res.send).toHaveBeenCalledWith('Access Denied');
|
expect(res.send).toHaveBeenCalledWith('Access Denied');
|
||||||
});
|
});
|
||||||
|
|
||||||
// File traversal tests
|
// File traversal tests
|
||||||
test('should prevent file traversal attempts', () => {
|
test('should prevent file traversal attempts', async () => {
|
||||||
const validToken = jwt.sign(
|
const validToken = jwt.sign(
|
||||||
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 },
|
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 },
|
||||||
process.env.JWT_REFRESH_SECRET,
|
process.env.JWT_REFRESH_SECRET,
|
||||||
@@ -103,23 +115,23 @@ describe('validateImageRequest middleware', () => {
|
|||||||
`/images/${validObjectId}/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd`,
|
`/images/${validObjectId}/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd`,
|
||||||
];
|
];
|
||||||
|
|
||||||
traversalAttempts.forEach((attempt) => {
|
for (const attempt of traversalAttempts) {
|
||||||
req.originalUrl = attempt;
|
req.originalUrl = attempt;
|
||||||
validateImageRequest(req, res, next);
|
await validateImageRequest(req, res, next);
|
||||||
expect(res.status).toHaveBeenCalledWith(403);
|
expect(res.status).toHaveBeenCalledWith(403);
|
||||||
expect(res.send).toHaveBeenCalledWith('Access Denied');
|
expect(res.send).toHaveBeenCalledWith('Access Denied');
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle URL encoded characters in valid paths', () => {
|
test('should handle URL encoded characters in valid paths', async () => {
|
||||||
const validToken = jwt.sign(
|
const validToken = jwt.sign(
|
||||||
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 },
|
{ id: validObjectId, exp: Math.floor(Date.now() / 1000) + 3600 },
|
||||||
process.env.JWT_REFRESH_SECRET,
|
process.env.JWT_REFRESH_SECRET,
|
||||||
);
|
);
|
||||||
req.headers.cookie = `refreshToken=${validToken}`;
|
req.headers.cookie = `refreshToken=${validToken}`;
|
||||||
req.originalUrl = `/images/${validObjectId}/image%20with%20spaces.jpg`;
|
req.originalUrl = `/images/${validObjectId}/image%20with%20spaces.jpg`;
|
||||||
validateImageRequest(req, res, next);
|
await validateImageRequest(req, res, next);
|
||||||
expect(next).toHaveBeenCalled();
|
expect(next).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const cookies = require('cookie');
|
const cookies = require('cookie');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
|
|
||||||
const OBJECT_ID_LENGTH = 24;
|
const OBJECT_ID_LENGTH = 24;
|
||||||
const OBJECT_ID_PATTERN = /^[0-9a-f]{24}$/i;
|
const OBJECT_ID_PATTERN = /^[0-9a-f]{24}$/i;
|
||||||
@@ -24,8 +25,9 @@ function isValidObjectId(id) {
|
|||||||
* Middleware to validate image request.
|
* Middleware to validate image request.
|
||||||
* Must be set by `secureImageLinks` via custom config file.
|
* Must be set by `secureImageLinks` via custom config file.
|
||||||
*/
|
*/
|
||||||
function validateImageRequest(req, res, next) {
|
async function validateImageRequest(req, res, next) {
|
||||||
if (!req.app.locals.secureImageLinks) {
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
if (!appConfig.secureImageLinks) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const { getAgent, updateAgent, getListAgentsByAccess } = require('~/models/Agent
|
|||||||
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
||||||
const { isActionDomainAllowed } = require('~/server/services/domains');
|
const { isActionDomainAllowed } = require('~/server/services/domains');
|
||||||
const { canAccessAgentResource } = require('~/server/middleware');
|
const { canAccessAgentResource } = require('~/server/middleware');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
const { getRoleByName } = require('~/models/Role');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -83,7 +84,11 @@ router.post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
|
let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
|
||||||
const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
|
const appConfig = await getAppConfig({ role: req.user.role });
|
||||||
|
const isDomainAllowed = await isActionDomainAllowed(
|
||||||
|
metadata.domain,
|
||||||
|
appConfig?.actions?.allowedDomains,
|
||||||
|
);
|
||||||
if (!isDomainAllowed) {
|
if (!isDomainAllowed) {
|
||||||
return res.status(400).json({ message: 'Domain not allowed' });
|
return res.status(400).json({ message: 'Domain not allowed' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
|||||||
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
||||||
const { updateAssistantDoc, getAssistant } = require('~/models/Assistant');
|
const { updateAssistantDoc, getAssistant } = require('~/models/Assistant');
|
||||||
const { isActionDomainAllowed } = require('~/server/services/domains');
|
const { isActionDomainAllowed } = require('~/server/services/domains');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -21,6 +22,7 @@ const router = express.Router();
|
|||||||
*/
|
*/
|
||||||
router.post('/:assistant_id', async (req, res) => {
|
router.post('/:assistant_id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const { assistant_id } = req.params;
|
const { assistant_id } = req.params;
|
||||||
|
|
||||||
/** @type {{ functions: FunctionTool[], action_id: string, metadata: ActionMetadata }} */
|
/** @type {{ functions: FunctionTool[], action_id: string, metadata: ActionMetadata }} */
|
||||||
@@ -30,7 +32,10 @@ router.post('/:assistant_id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
|
let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
|
||||||
const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
|
const isDomainAllowed = await isActionDomainAllowed(
|
||||||
|
metadata.domain,
|
||||||
|
appConfig?.actions?.allowedDomains,
|
||||||
|
);
|
||||||
if (!isDomainAllowed) {
|
if (!isDomainAllowed) {
|
||||||
return res.status(400).json({ message: 'Domain not allowed' });
|
return res.status(400).json({ message: 'Domain not allowed' });
|
||||||
}
|
}
|
||||||
@@ -125,7 +130,7 @@ router.post('/:assistant_id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Map Azure OpenAI model to the assistant as defined by config */
|
/* Map Azure OpenAI model to the assistant as defined by config */
|
||||||
if (req.app.locals[EModelEndpoint.azureOpenAI]?.assistants) {
|
if (appConfig.endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) {
|
||||||
updatedAssistant = {
|
updatedAssistant = {
|
||||||
...updatedAssistant,
|
...updatedAssistant,
|
||||||
model: req.body.model,
|
model: req.body.model,
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { isEnabled } = require('@librechat/api');
|
const { isEnabled } = require('@librechat/api');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { CacheKeys, defaultSocialLogins, Constants } = require('librechat-data-provider');
|
const {
|
||||||
const { getCustomConfig } = require('~/server/services/Config/getCustomConfig');
|
Constants,
|
||||||
|
CacheKeys,
|
||||||
|
removeNullishValues,
|
||||||
|
defaultSocialLogins,
|
||||||
|
} = require('librechat-data-provider');
|
||||||
const { getLdapConfig } = require('~/server/services/Config/ldap');
|
const { getLdapConfig } = require('~/server/services/Config/ldap');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
const { getProjectByName } = require('~/models/Project');
|
const { getProjectByName } = require('~/models/Project');
|
||||||
const { getMCPManager } = require('~/config');
|
const { getMCPManager } = require('~/config');
|
||||||
const { getLogStores } = require('~/cache');
|
const { getLogStores } = require('~/cache');
|
||||||
@@ -43,6 +48,8 @@ router.get('/', async function (req, res) {
|
|||||||
const ldap = getLdapConfig();
|
const ldap = getLdapConfig();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
|
||||||
const isOpenIdEnabled =
|
const isOpenIdEnabled =
|
||||||
!!process.env.OPENID_CLIENT_ID &&
|
!!process.env.OPENID_CLIENT_ID &&
|
||||||
!!process.env.OPENID_CLIENT_SECRET &&
|
!!process.env.OPENID_CLIENT_SECRET &&
|
||||||
@@ -58,7 +65,7 @@ router.get('/', async function (req, res) {
|
|||||||
/** @type {TStartupConfig} */
|
/** @type {TStartupConfig} */
|
||||||
const payload = {
|
const payload = {
|
||||||
appTitle: process.env.APP_TITLE || 'LibreChat',
|
appTitle: process.env.APP_TITLE || 'LibreChat',
|
||||||
socialLogins: req.app.locals.socialLogins ?? defaultSocialLogins,
|
socialLogins: appConfig?.registration?.socialLogins ?? defaultSocialLogins,
|
||||||
discordLoginEnabled: !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET,
|
discordLoginEnabled: !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET,
|
||||||
facebookLoginEnabled:
|
facebookLoginEnabled:
|
||||||
!!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET,
|
!!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET,
|
||||||
@@ -91,10 +98,10 @@ router.get('/', async function (req, res) {
|
|||||||
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
|
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
|
||||||
process.env.SHOW_BIRTHDAY_ICON === '',
|
process.env.SHOW_BIRTHDAY_ICON === '',
|
||||||
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
|
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
|
||||||
interface: req.app.locals.interfaceConfig,
|
interface: appConfig?.interfaceConfig,
|
||||||
turnstile: req.app.locals.turnstileConfig,
|
turnstile: appConfig?.turnstileConfig,
|
||||||
modelSpecs: req.app.locals.modelSpecs,
|
modelSpecs: appConfig?.modelSpecs,
|
||||||
balance: req.app.locals.balance,
|
balance: appConfig?.balance,
|
||||||
sharedLinksEnabled,
|
sharedLinksEnabled,
|
||||||
publicSharedLinksEnabled,
|
publicSharedLinksEnabled,
|
||||||
analyticsGtmId: process.env.ANALYTICS_GTM_ID,
|
analyticsGtmId: process.env.ANALYTICS_GTM_ID,
|
||||||
@@ -109,27 +116,31 @@ router.get('/', async function (req, res) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
payload.mcpServers = {};
|
payload.mcpServers = {};
|
||||||
const config = await getCustomConfig();
|
const getMCPServers = () => {
|
||||||
if (config?.mcpServers != null) {
|
|
||||||
try {
|
try {
|
||||||
const mcpManager = getMCPManager();
|
const mcpManager = getMCPManager();
|
||||||
|
if (!mcpManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mcpServers = mcpManager.getAllServers();
|
||||||
|
if (!mcpServers) return;
|
||||||
const oauthServers = mcpManager.getOAuthServers();
|
const oauthServers = mcpManager.getOAuthServers();
|
||||||
for (const serverName in config.mcpServers) {
|
for (const serverName in mcpServers) {
|
||||||
const serverConfig = config.mcpServers[serverName];
|
const serverConfig = mcpServers[serverName];
|
||||||
payload.mcpServers[serverName] = {
|
payload.mcpServers[serverName] = removeNullishValues({
|
||||||
startup: serverConfig?.startup,
|
startup: serverConfig?.startup,
|
||||||
chatMenu: serverConfig?.chatMenu,
|
chatMenu: serverConfig?.chatMenu,
|
||||||
isOAuth: oauthServers?.has(serverName),
|
isOAuth: oauthServers?.has(serverName),
|
||||||
customUserVars: serverConfig?.customUserVars || {},
|
customUserVars: serverConfig?.customUserVars,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logger.error('Error loading MCP servers', err);
|
logger.error('Error loading MCP servers', error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/** @type {TCustomConfig['webSearch']} */
|
getMCPServers();
|
||||||
const webSearchConfig = req.app.locals.webSearch;
|
const webSearchConfig = appConfig?.webSearch;
|
||||||
if (
|
if (
|
||||||
webSearchConfig != null &&
|
webSearchConfig != null &&
|
||||||
(webSearchConfig.searchProvider ||
|
(webSearchConfig.searchProvider ||
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ const fs = require('fs').promises;
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
|
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
|
||||||
const { filterFile } = require('~/server/services/Files/process');
|
|
||||||
const { getFileStrategy } = require('~/server/utils/getFileStrategy');
|
const { getFileStrategy } = require('~/server/utils/getFileStrategy');
|
||||||
|
const { filterFile } = require('~/server/services/Files/process');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
filterFile({ req, file: req.file, image: true, isAvatar: true });
|
filterFile({ req, file: req.file, image: true, isAvatar: true });
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
const { manual } = req.body;
|
const { manual } = req.body;
|
||||||
@@ -19,8 +21,8 @@ router.post('/', async (req, res) => {
|
|||||||
throw new Error('User ID is undefined');
|
throw new Error('User ID is undefined');
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileStrategy = getFileStrategy(req.app.locals, { isAvatar: true });
|
const fileStrategy = getFileStrategy(appConfig, { isAvatar: true });
|
||||||
const desiredFormat = req.app.locals.imageOutputType;
|
const desiredFormat = appConfig.imageOutputType;
|
||||||
const resizedBuffer = await resizeAvatar({
|
const resizedBuffer = await resizeAvatar({
|
||||||
userId,
|
userId,
|
||||||
input,
|
input,
|
||||||
@@ -39,7 +41,7 @@ router.post('/', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await fs.unlink(req.file.path);
|
await fs.unlink(req.file.path);
|
||||||
logger.debug('[/files/images/avatar] Temp. image upload file deleted');
|
logger.debug('[/files/images/avatar] Temp. image upload file deleted');
|
||||||
} catch (error) {
|
} catch {
|
||||||
logger.debug('[/files/images/avatar] Temp. image upload file already deleted');
|
logger.debug('[/files/images/avatar] Temp. image upload file already deleted');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
|||||||
const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud');
|
const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud');
|
||||||
const { hasAccessToFilesViaAgent } = require('~/server/services/Files');
|
const { hasAccessToFilesViaAgent } = require('~/server/services/Files');
|
||||||
const { getFiles, batchUpdateFiles } = require('~/models/File');
|
const { getFiles, batchUpdateFiles } = require('~/models/File');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { cleanFileName } = require('~/server/utils/files');
|
const { cleanFileName } = require('~/server/utils/files');
|
||||||
const { getAssistant } = require('~/models/Assistant');
|
const { getAssistant } = require('~/models/Assistant');
|
||||||
const { getAgent } = require('~/models/Agent');
|
const { getAgent } = require('~/models/Agent');
|
||||||
@@ -36,8 +37,9 @@ const router = express.Router();
|
|||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const files = await getFiles({ user: req.user.id });
|
const files = await getFiles({ user: req.user.id });
|
||||||
if (req.app.locals.fileStrategy === FileSources.s3) {
|
if (appConfig.fileStrategy === FileSources.s3) {
|
||||||
try {
|
try {
|
||||||
const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL);
|
const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL);
|
||||||
const alreadyChecked = await cache.get(req.user.id);
|
const alreadyChecked = await cache.get(req.user.id);
|
||||||
@@ -114,7 +116,8 @@ router.get('/agent/:agent_id', async (req, res) => {
|
|||||||
|
|
||||||
router.get('/config', async (req, res) => {
|
router.get('/config', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.status(200).json(req.app.locals.fileConfig);
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
res.status(200).json(appConfig.fileConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[/files] Error getting fileConfig', error);
|
logger.error('[/files] Error getting fileConfig', error);
|
||||||
res.status(400).json({ message: 'Error in request', error: error.message });
|
res.status(400).json({ message: 'Error in request', error: error.message });
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ const {
|
|||||||
processImageFile,
|
processImageFile,
|
||||||
processAgentFileUpload,
|
processAgentFileUpload,
|
||||||
} = require('~/server/services/Files/process');
|
} = require('~/server/services/Files/process');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const metadata = req.body;
|
const metadata = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -30,7 +32,7 @@ router.post('/', async (req, res) => {
|
|||||||
logger.error('[/files/images] Error processing file:', error);
|
logger.error('[/files/images] Error processing file:', error);
|
||||||
try {
|
try {
|
||||||
const filepath = path.join(
|
const filepath = path.join(
|
||||||
req.app.locals.paths.imageOutput,
|
appConfig.paths.imageOutput,
|
||||||
req.user.id,
|
req.user.id,
|
||||||
path.basename(req.file.filename),
|
path.basename(req.file.filename),
|
||||||
);
|
);
|
||||||
@@ -43,7 +45,7 @@ router.post('/', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await fs.unlink(req.file.path);
|
await fs.unlink(req.file.path);
|
||||||
logger.debug('[/files/images] Temp. image upload file deleted');
|
logger.debug('[/files/images] Temp. image upload file deleted');
|
||||||
} catch (error) {
|
} catch {
|
||||||
logger.debug('[/files/images] Temp. image upload file already deleted');
|
logger.debug('[/files/images] Temp. image upload file already deleted');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ const crypto = require('crypto');
|
|||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const { sanitizeFilename } = require('@librechat/api');
|
const { sanitizeFilename } = require('@librechat/api');
|
||||||
const { fileConfig: defaultFileConfig, mergeFileConfig } = require('librechat-data-provider');
|
const { fileConfig: defaultFileConfig, mergeFileConfig } = require('librechat-data-provider');
|
||||||
const { getCustomConfig } = require('~/server/services/Config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: function (req, file, cb) {
|
destination: function (req, file, cb) {
|
||||||
const outputPath = path.join(req.app.locals.paths.uploads, 'temp', req.user.id);
|
getAppConfig({ role: req.user?.role })
|
||||||
if (!fs.existsSync(outputPath)) {
|
.then((appConfig) => {
|
||||||
fs.mkdirSync(outputPath, { recursive: true });
|
const outputPath = path.join(appConfig.paths.uploads, 'temp', req.user.id);
|
||||||
}
|
if (!fs.existsSync(outputPath)) {
|
||||||
cb(null, outputPath);
|
fs.mkdirSync(outputPath, { recursive: true });
|
||||||
|
}
|
||||||
|
cb(null, outputPath);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
cb(error);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
filename: function (req, file, cb) {
|
filename: function (req, file, cb) {
|
||||||
req.file_id = crypto.randomUUID();
|
req.file_id = crypto.randomUUID();
|
||||||
@@ -68,8 +74,8 @@ const createFileFilter = (customFileConfig) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createMulterInstance = async () => {
|
const createMulterInstance = async () => {
|
||||||
const customConfig = await getCustomConfig();
|
const appConfig = await getAppConfig();
|
||||||
const fileConfig = mergeFileConfig(customConfig?.fileConfig);
|
const fileConfig = mergeFileConfig(appConfig?.fileConfig);
|
||||||
const fileFilter = createFileFilter(fileConfig);
|
const fileFilter = createFileFilter(fileConfig);
|
||||||
return multer({
|
return multer({
|
||||||
storage,
|
storage,
|
||||||
|
|||||||
@@ -8,21 +8,7 @@ const { createMulterInstance, storage, importFileFilter } = require('./multer');
|
|||||||
|
|
||||||
// Mock only the config service that requires external dependencies
|
// Mock only the config service that requires external dependencies
|
||||||
jest.mock('~/server/services/Config', () => ({
|
jest.mock('~/server/services/Config', () => ({
|
||||||
getCustomConfig: jest.fn(() =>
|
getAppConfig: jest.fn(),
|
||||||
Promise.resolve({
|
|
||||||
fileConfig: {
|
|
||||||
endpoints: {
|
|
||||||
openAI: {
|
|
||||||
supportedMimeTypes: ['image/jpeg', 'image/png', 'application/pdf'],
|
|
||||||
},
|
|
||||||
default: {
|
|
||||||
supportedMimeTypes: ['image/jpeg', 'image/png', 'text/plain'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
serverFileSizeLimit: 10000000, // 10MB
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Multer Configuration', () => {
|
describe('Multer Configuration', () => {
|
||||||
@@ -36,13 +22,6 @@ describe('Multer Configuration', () => {
|
|||||||
|
|
||||||
mockReq = {
|
mockReq = {
|
||||||
user: { id: 'test-user-123' },
|
user: { id: 'test-user-123' },
|
||||||
app: {
|
|
||||||
locals: {
|
|
||||||
paths: {
|
|
||||||
uploads: tempDir,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body: {},
|
body: {},
|
||||||
originalUrl: '/api/files/upload',
|
originalUrl: '/api/files/upload',
|
||||||
};
|
};
|
||||||
@@ -53,6 +32,14 @@ describe('Multer Configuration', () => {
|
|||||||
size: 1024,
|
size: 1024,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock getAppConfig to return paths
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
getAppConfig.mockResolvedValue({
|
||||||
|
paths: {
|
||||||
|
uploads: tempDir,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Clear mocks
|
// Clear mocks
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
@@ -79,7 +66,12 @@ describe('Multer Configuration', () => {
|
|||||||
|
|
||||||
it("should create directory recursively if it doesn't exist", (done) => {
|
it("should create directory recursively if it doesn't exist", (done) => {
|
||||||
const deepPath = path.join(tempDir, 'deep', 'nested', 'path');
|
const deepPath = path.join(tempDir, 'deep', 'nested', 'path');
|
||||||
mockReq.app.locals.paths.uploads = deepPath;
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
getAppConfig.mockResolvedValue({
|
||||||
|
paths: {
|
||||||
|
uploads: deepPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const cb = jest.fn((err, destination) => {
|
const cb = jest.fn((err, destination) => {
|
||||||
expect(err).toBeNull();
|
expect(err).toBeNull();
|
||||||
@@ -331,11 +323,11 @@ describe('Multer Configuration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use real config merging', async () => {
|
it('should use real config merging', async () => {
|
||||||
const { getCustomConfig } = require('~/server/services/Config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
const multerInstance = await createMulterInstance();
|
const multerInstance = await createMulterInstance();
|
||||||
|
|
||||||
expect(getCustomConfig).toHaveBeenCalled();
|
expect(getAppConfig).toHaveBeenCalled();
|
||||||
expect(multerInstance).toBeDefined();
|
expect(multerInstance).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -465,23 +457,24 @@ describe('Multer Configuration', () => {
|
|||||||
it('should handle file system errors when directory creation fails', (done) => {
|
it('should handle file system errors when directory creation fails', (done) => {
|
||||||
// Test with a non-existent parent directory to simulate fs issues
|
// Test with a non-existent parent directory to simulate fs issues
|
||||||
const invalidPath = '/nonexistent/path/that/should/not/exist';
|
const invalidPath = '/nonexistent/path/that/should/not/exist';
|
||||||
mockReq.app.locals.paths.uploads = invalidPath;
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
getAppConfig.mockResolvedValue({
|
||||||
|
paths: {
|
||||||
|
uploads: invalidPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
// Call getDestination which should fail due to permission/path issues
|
||||||
// Call getDestination which should fail due to permission/path issues
|
storage.getDestination(mockReq, mockFile, (err, destination) => {
|
||||||
storage.getDestination(mockReq, mockFile, (err, destination) => {
|
// Now we expect the error to be passed to the callback
|
||||||
// If callback is reached, we didn't get the expected error
|
expect(err).toBeDefined();
|
||||||
done(new Error('Expected mkdirSync to throw an error but callback was called'));
|
|
||||||
});
|
|
||||||
// If we get here without throwing, something unexpected happened
|
|
||||||
done(new Error('Expected mkdirSync to throw an error but no error was thrown'));
|
|
||||||
} catch (error) {
|
|
||||||
// This is the expected behavior - mkdirSync throws synchronously for invalid paths
|
// This is the expected behavior - mkdirSync throws synchronously for invalid paths
|
||||||
// On Linux, this typically returns EACCES (permission denied)
|
// On Linux, this typically returns EACCES (permission denied)
|
||||||
// On macOS/Darwin, this returns ENOENT (no such file or directory)
|
// On macOS/Darwin, this returns ENOENT (no such file or directory)
|
||||||
expect(['EACCES', 'ENOENT']).toContain(error.code);
|
expect(['EACCES', 'ENOENT']).toContain(err.code);
|
||||||
|
expect(destination).toBeUndefined();
|
||||||
done();
|
done();
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle malformed filenames with real sanitization', (done) => {
|
it('should handle malformed filenames with real sanitization', (done) => {
|
||||||
@@ -538,10 +531,10 @@ describe('Multer Configuration', () => {
|
|||||||
|
|
||||||
describe('Real Configuration Testing', () => {
|
describe('Real Configuration Testing', () => {
|
||||||
it('should handle missing custom config gracefully with real mergeFileConfig', async () => {
|
it('should handle missing custom config gracefully with real mergeFileConfig', async () => {
|
||||||
const { getCustomConfig } = require('~/server/services/Config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
// Mock getCustomConfig to return undefined
|
// Mock getAppConfig to return undefined
|
||||||
getCustomConfig.mockResolvedValueOnce(undefined);
|
getAppConfig.mockResolvedValueOnce(undefined);
|
||||||
|
|
||||||
const multerInstance = await createMulterInstance();
|
const multerInstance = await createMulterInstance();
|
||||||
expect(multerInstance).toBeDefined();
|
expect(multerInstance).toBeDefined();
|
||||||
@@ -549,25 +542,28 @@ describe('Multer Configuration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should properly integrate real fileConfig with custom endpoints', async () => {
|
it('should properly integrate real fileConfig with custom endpoints', async () => {
|
||||||
const { getCustomConfig } = require('~/server/services/Config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
// Mock a custom config with additional endpoints
|
// Mock appConfig with fileConfig
|
||||||
getCustomConfig.mockResolvedValueOnce({
|
getAppConfig.mockResolvedValueOnce({
|
||||||
|
paths: {
|
||||||
|
uploads: tempDir,
|
||||||
|
},
|
||||||
fileConfig: {
|
fileConfig: {
|
||||||
endpoints: {
|
endpoints: {
|
||||||
anthropic: {
|
anthropic: {
|
||||||
supportedMimeTypes: ['text/plain', 'image/png'],
|
supportedMimeTypes: ['text/plain', 'image/png'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
serverFileSizeLimit: 20, // 20 MB
|
serverFileSizeLimit: 20971520, // 20 MB in bytes (mergeFileConfig converts)
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const multerInstance = await createMulterInstance();
|
const multerInstance = await createMulterInstance();
|
||||||
expect(multerInstance).toBeDefined();
|
expect(multerInstance).toBeDefined();
|
||||||
|
|
||||||
// Verify that getCustomConfig was called (we can't spy on the actual merge function easily)
|
// Verify that getAppConfig was called
|
||||||
expect(getCustomConfig).toHaveBeenCalled();
|
expect(getAppConfig).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const {
|
|||||||
deleteMemory,
|
deleteMemory,
|
||||||
setMemory,
|
setMemory,
|
||||||
} = require('~/models');
|
} = require('~/models');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { requireJwtAuth } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
const { getRoleByName } = require('~/models/Role');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
@@ -60,7 +61,8 @@ router.get('/', checkMemoryRead, async (req, res) => {
|
|||||||
return sum + (memory.tokenCount || 0);
|
return sum + (memory.tokenCount || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const memoryConfig = req.app.locals?.memory;
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
const memoryConfig = appConfig?.memory;
|
||||||
const tokenLimit = memoryConfig?.tokenLimit;
|
const tokenLimit = memoryConfig?.tokenLimit;
|
||||||
const charLimit = memoryConfig?.charLimit || 10000;
|
const charLimit = memoryConfig?.charLimit || 10000;
|
||||||
|
|
||||||
@@ -98,7 +100,8 @@ router.post('/', memoryPayloadLimit, checkMemoryCreate, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Value is required and must be a non-empty string.' });
|
return res.status(400).json({ error: 'Value is required and must be a non-empty string.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const memoryConfig = req.app.locals?.memory;
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
const memoryConfig = appConfig?.memory;
|
||||||
const charLimit = memoryConfig?.charLimit || 10000;
|
const charLimit = memoryConfig?.charLimit || 10000;
|
||||||
|
|
||||||
if (key.length > 1000) {
|
if (key.length > 1000) {
|
||||||
@@ -117,6 +120,9 @@ router.post('/', memoryPayloadLimit, checkMemoryCreate, async (req, res) => {
|
|||||||
const tokenCount = Tokenizer.getTokenCount(value, 'o200k_base');
|
const tokenCount = Tokenizer.getTokenCount(value, 'o200k_base');
|
||||||
|
|
||||||
const memories = await getAllUserMemories(req.user.id);
|
const memories = await getAllUserMemories(req.user.id);
|
||||||
|
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
const memoryConfig = appConfig?.memory;
|
||||||
const tokenLimit = memoryConfig?.tokenLimit;
|
const tokenLimit = memoryConfig?.tokenLimit;
|
||||||
|
|
||||||
if (tokenLimit) {
|
if (tokenLimit) {
|
||||||
@@ -200,8 +206,8 @@ router.patch('/:key', memoryPayloadLimit, checkMemoryUpdate, async (req, res) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newKey = bodyKey || urlKey;
|
const newKey = bodyKey || urlKey;
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const memoryConfig = req.app.locals?.memory;
|
const memoryConfig = appConfig?.memory;
|
||||||
const charLimit = memoryConfig?.charLimit || 10000;
|
const charLimit = memoryConfig?.charLimit || 10000;
|
||||||
|
|
||||||
if (newKey.length > 1000) {
|
if (newKey.length > 1000) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const {
|
|||||||
// Mock modules before importing
|
// Mock modules before importing
|
||||||
jest.mock('~/server/services/Config', () => ({
|
jest.mock('~/server/services/Config', () => ({
|
||||||
getCachedTools: jest.fn().mockResolvedValue({}),
|
getCachedTools: jest.fn().mockResolvedValue({}),
|
||||||
getCustomConfig: jest.fn(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('~/models/Role', () => ({
|
jest.mock('~/models/Role', () => ({
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
const { Constants, EModelEndpoint, actionDomainSeparator } = require('librechat-data-provider');
|
const { Constants, actionDomainSeparator } = require('librechat-data-provider');
|
||||||
const { domainParser } = require('./ActionService');
|
const { domainParser } = require('./ActionService');
|
||||||
|
|
||||||
jest.mock('keyv');
|
jest.mock('keyv');
|
||||||
jest.mock('~/server/services/Config', () => ({
|
|
||||||
getCustomConfig: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const globalCache = {};
|
const globalCache = {};
|
||||||
jest.mock('~/cache/getLogStores', () => {
|
jest.mock('~/cache/getLogStores', () => {
|
||||||
@@ -53,26 +50,6 @@ jest.mock('~/cache/getLogStores', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('domainParser', () => {
|
describe('domainParser', () => {
|
||||||
const req = {
|
|
||||||
app: {
|
|
||||||
locals: {
|
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
|
||||||
assistants: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const reqNoAzure = {
|
|
||||||
app: {
|
|
||||||
locals: {
|
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
|
||||||
assistants: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const TLD = '.com';
|
const TLD = '.com';
|
||||||
|
|
||||||
// Non-azure request
|
// Non-azure request
|
||||||
|
|||||||
@@ -1,15 +1,4 @@
|
|||||||
jest.mock('~/models', () => ({
|
jest.mock('@librechat/data-schemas', () => ({
|
||||||
initializeRoles: jest.fn(),
|
|
||||||
seedDefaultRoles: jest.fn(),
|
|
||||||
ensureDefaultCategories: jest.fn(),
|
|
||||||
}));
|
|
||||||
jest.mock('~/models/Role', () => ({
|
|
||||||
updateAccessPermissions: jest.fn(),
|
|
||||||
getRoleByName: jest.fn().mockResolvedValue(null),
|
|
||||||
updateRoleByName: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('~/config', () => ({
|
|
||||||
logger: {
|
logger: {
|
||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
warn: jest.fn(),
|
warn: jest.fn(),
|
||||||
@@ -17,11 +6,11 @@ jest.mock('~/config', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./Config/loadCustomConfig', () => jest.fn());
|
jest.mock('@librechat/api', () => ({
|
||||||
jest.mock('./start/interface', () => ({
|
...jest.requireActual('@librechat/api'),
|
||||||
loadDefaultInterface: jest.fn(),
|
loadDefaultInterface: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('./ToolService', () => ({
|
jest.mock('./start/tools', () => ({
|
||||||
loadAndFormatTools: jest.fn().mockReturnValue({}),
|
loadAndFormatTools: jest.fn().mockReturnValue({}),
|
||||||
}));
|
}));
|
||||||
jest.mock('./start/checks', () => ({
|
jest.mock('./start/checks', () => ({
|
||||||
@@ -32,15 +21,15 @@ jest.mock('./start/checks', () => ({
|
|||||||
checkWebSearchConfig: jest.fn(),
|
checkWebSearchConfig: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('./Config/loadCustomConfig', () => jest.fn());
|
||||||
|
|
||||||
const AppService = require('./AppService');
|
const AppService = require('./AppService');
|
||||||
const { loadDefaultInterface } = require('./start/interface');
|
const { loadDefaultInterface } = require('@librechat/api');
|
||||||
|
|
||||||
describe('AppService interface configuration', () => {
|
describe('AppService interface configuration', () => {
|
||||||
let app;
|
|
||||||
let mockLoadCustomConfig;
|
let mockLoadCustomConfig;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
app = { locals: {} };
|
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockLoadCustomConfig = require('./Config/loadCustomConfig');
|
mockLoadCustomConfig = require('./Config/loadCustomConfig');
|
||||||
@@ -50,10 +39,16 @@ describe('AppService interface configuration', () => {
|
|||||||
mockLoadCustomConfig.mockResolvedValue({});
|
mockLoadCustomConfig.mockResolvedValue({});
|
||||||
loadDefaultInterface.mockResolvedValue({ prompts: true, bookmarks: true });
|
loadDefaultInterface.mockResolvedValue({ prompts: true, bookmarks: true });
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals.interfaceConfig.prompts).toBe(true);
|
expect(result).toEqual(
|
||||||
expect(app.locals.interfaceConfig.bookmarks).toBe(true);
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
prompts: true,
|
||||||
|
bookmarks: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
expect(loadDefaultInterface).toHaveBeenCalled();
|
expect(loadDefaultInterface).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,10 +56,16 @@ describe('AppService interface configuration', () => {
|
|||||||
mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: false, bookmarks: false } });
|
mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: false, bookmarks: false } });
|
||||||
loadDefaultInterface.mockResolvedValue({ prompts: false, bookmarks: false });
|
loadDefaultInterface.mockResolvedValue({ prompts: false, bookmarks: false });
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals.interfaceConfig.prompts).toBe(false);
|
expect(result).toEqual(
|
||||||
expect(app.locals.interfaceConfig.bookmarks).toBe(false);
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
prompts: false,
|
||||||
|
bookmarks: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
expect(loadDefaultInterface).toHaveBeenCalled();
|
expect(loadDefaultInterface).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,10 +73,17 @@ describe('AppService interface configuration', () => {
|
|||||||
mockLoadCustomConfig.mockResolvedValue({});
|
mockLoadCustomConfig.mockResolvedValue({});
|
||||||
loadDefaultInterface.mockResolvedValue({});
|
loadDefaultInterface.mockResolvedValue({});
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals.interfaceConfig.prompts).toBeUndefined();
|
expect(result).toEqual(
|
||||||
expect(app.locals.interfaceConfig.bookmarks).toBeUndefined();
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.anything(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that prompts and bookmarks are undefined when not provided
|
||||||
|
expect(result.interfaceConfig.prompts).toBeUndefined();
|
||||||
|
expect(result.interfaceConfig.bookmarks).toBeUndefined();
|
||||||
expect(loadDefaultInterface).toHaveBeenCalled();
|
expect(loadDefaultInterface).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,10 +91,16 @@ describe('AppService interface configuration', () => {
|
|||||||
mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: true, bookmarks: false } });
|
mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: true, bookmarks: false } });
|
||||||
loadDefaultInterface.mockResolvedValue({ prompts: true, bookmarks: false });
|
loadDefaultInterface.mockResolvedValue({ prompts: true, bookmarks: false });
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals.interfaceConfig.prompts).toBe(true);
|
expect(result).toEqual(
|
||||||
expect(app.locals.interfaceConfig.bookmarks).toBe(false);
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
prompts: true,
|
||||||
|
bookmarks: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
expect(loadDefaultInterface).toHaveBeenCalled();
|
expect(loadDefaultInterface).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,14 +122,19 @@ describe('AppService interface configuration', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
|
expect(result).toEqual(
|
||||||
expect(app.locals.interfaceConfig.peoplePicker).toMatchObject({
|
expect.objectContaining({
|
||||||
users: true,
|
interfaceConfig: expect.objectContaining({
|
||||||
groups: true,
|
peoplePicker: expect.objectContaining({
|
||||||
roles: true,
|
users: true,
|
||||||
});
|
groups: true,
|
||||||
|
roles: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
expect(loadDefaultInterface).toHaveBeenCalled();
|
expect(loadDefaultInterface).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,11 +156,19 @@ describe('AppService interface configuration', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals.interfaceConfig.peoplePicker.users).toBe(true);
|
expect(result).toEqual(
|
||||||
expect(app.locals.interfaceConfig.peoplePicker.groups).toBe(false);
|
expect.objectContaining({
|
||||||
expect(app.locals.interfaceConfig.peoplePicker.roles).toBe(true);
|
interfaceConfig: expect.objectContaining({
|
||||||
|
peoplePicker: expect.objectContaining({
|
||||||
|
users: true,
|
||||||
|
groups: false,
|
||||||
|
roles: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set default peoplePicker permissions when not provided', async () => {
|
it('should set default peoplePicker permissions when not provided', async () => {
|
||||||
@@ -154,11 +181,18 @@ describe('AppService interface configuration', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
|
expect(result).toEqual(
|
||||||
expect(app.locals.interfaceConfig.peoplePicker.users).toBe(true);
|
expect.objectContaining({
|
||||||
expect(app.locals.interfaceConfig.peoplePicker.groups).toBe(true);
|
interfaceConfig: expect.objectContaining({
|
||||||
expect(app.locals.interfaceConfig.peoplePicker.roles).toBe(true);
|
peoplePicker: expect.objectContaining({
|
||||||
|
users: true,
|
||||||
|
groups: true,
|
||||||
|
roles: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const {
|
|||||||
loadMemoryConfig,
|
loadMemoryConfig,
|
||||||
agentsConfigSetup,
|
agentsConfigSetup,
|
||||||
loadWebSearchConfig,
|
loadWebSearchConfig,
|
||||||
|
loadDefaultInterface,
|
||||||
} = require('@librechat/api');
|
} = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
FileSources,
|
FileSources,
|
||||||
@@ -12,35 +13,26 @@ const {
|
|||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
checkWebSearchConfig,
|
checkWebSearchConfig,
|
||||||
checkAzureVariables,
|
|
||||||
checkVariables,
|
checkVariables,
|
||||||
checkHealth,
|
checkHealth,
|
||||||
checkConfig,
|
checkConfig,
|
||||||
} = require('./start/checks');
|
} = require('./start/checks');
|
||||||
const { ensureDefaultCategories, seedDefaultRoles, initializeRoles } = require('~/models');
|
|
||||||
const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants');
|
|
||||||
const { initializeAzureBlobService } = require('./Files/Azure/initialize');
|
const { initializeAzureBlobService } = require('./Files/Azure/initialize');
|
||||||
const { initializeFirebase } = require('./Files/Firebase/initialize');
|
const { initializeFirebase } = require('./Files/Firebase/initialize');
|
||||||
const loadCustomConfig = require('./Config/loadCustomConfig');
|
|
||||||
const handleRateLimits = require('./Config/handleRateLimits');
|
const handleRateLimits = require('./Config/handleRateLimits');
|
||||||
const { loadDefaultInterface } = require('./start/interface');
|
const loadCustomConfig = require('./Config/loadCustomConfig');
|
||||||
const { loadTurnstileConfig } = require('./start/turnstile');
|
const { loadTurnstileConfig } = require('./start/turnstile');
|
||||||
const { azureConfigSetup } = require('./start/azureOpenAI');
|
|
||||||
const { processModelSpecs } = require('./start/modelSpecs');
|
const { processModelSpecs } = require('./start/modelSpecs');
|
||||||
const { initializeS3 } = require('./Files/S3/initialize');
|
const { initializeS3 } = require('./Files/S3/initialize');
|
||||||
const { loadAndFormatTools } = require('./ToolService');
|
const { loadAndFormatTools } = require('./start/tools');
|
||||||
const { setCachedTools } = require('./Config');
|
const { loadEndpoints } = require('./start/endpoints');
|
||||||
const paths = require('~/config/paths');
|
const paths = require('~/config/paths');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads custom config and initializes app-wide variables.
|
* Loads custom config and initializes app-wide variables.
|
||||||
* @function AppService
|
* @function AppService
|
||||||
* @param {Express.Application} app - The Express application object.
|
|
||||||
*/
|
*/
|
||||||
const AppService = async (app) => {
|
const AppService = async () => {
|
||||||
await initializeRoles();
|
|
||||||
await seedDefaultRoles();
|
|
||||||
await ensureDefaultCategories();
|
|
||||||
/** @type {TCustomConfig} */
|
/** @type {TCustomConfig} */
|
||||||
const config = (await loadCustomConfig()) ?? {};
|
const config = (await loadCustomConfig()) ?? {};
|
||||||
const configDefaults = getConfigDefaults();
|
const configDefaults = getConfigDefaults();
|
||||||
@@ -79,101 +71,57 @@ const AppService = async (app) => {
|
|||||||
directory: paths.structuredTools,
|
directory: paths.structuredTools,
|
||||||
});
|
});
|
||||||
|
|
||||||
await setCachedTools(availableTools, { isGlobal: true });
|
|
||||||
|
|
||||||
// Store MCP config for later initialization
|
|
||||||
const mcpConfig = config.mcpServers || null;
|
const mcpConfig = config.mcpServers || null;
|
||||||
|
const registration = config.registration ?? configDefaults.registration;
|
||||||
const socialLogins =
|
const interfaceConfig = await loadDefaultInterface({ config, configDefaults });
|
||||||
config?.registration?.socialLogins ?? configDefaults?.registration?.socialLogins;
|
|
||||||
const interfaceConfig = await loadDefaultInterface(config, configDefaults);
|
|
||||||
const turnstileConfig = loadTurnstileConfig(config, configDefaults);
|
const turnstileConfig = loadTurnstileConfig(config, configDefaults);
|
||||||
|
const speech = config.speech;
|
||||||
|
|
||||||
const defaultLocals = {
|
const defaultConfig = {
|
||||||
config,
|
|
||||||
ocr,
|
ocr,
|
||||||
paths,
|
paths,
|
||||||
|
config,
|
||||||
memory,
|
memory,
|
||||||
|
speech,
|
||||||
|
balance,
|
||||||
|
mcpConfig,
|
||||||
webSearch,
|
webSearch,
|
||||||
fileStrategy,
|
fileStrategy,
|
||||||
socialLogins,
|
registration,
|
||||||
filteredTools,
|
filteredTools,
|
||||||
includedTools,
|
includedTools,
|
||||||
|
availableTools,
|
||||||
imageOutputType,
|
imageOutputType,
|
||||||
interfaceConfig,
|
interfaceConfig,
|
||||||
turnstileConfig,
|
turnstileConfig,
|
||||||
balance,
|
fileStrategies: config.fileStrategies,
|
||||||
mcpConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const agentsDefaults = agentsConfigSetup(config);
|
const agentsDefaults = agentsConfigSetup(config);
|
||||||
|
|
||||||
if (!Object.keys(config).length) {
|
if (!Object.keys(config).length) {
|
||||||
app.locals = {
|
const appConfig = {
|
||||||
...defaultLocals,
|
...defaultConfig,
|
||||||
[EModelEndpoint.agents]: agentsDefaults,
|
endpoints: {
|
||||||
|
[EModelEndpoint.agents]: agentsDefaults,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return;
|
return appConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkConfig(config);
|
checkConfig(config);
|
||||||
handleRateLimits(config?.rateLimits);
|
handleRateLimits(config?.rateLimits);
|
||||||
|
const loadedEndpoints = loadEndpoints(config, agentsDefaults);
|
||||||
|
|
||||||
const endpointLocals = {};
|
const appConfig = {
|
||||||
const endpoints = config?.endpoints;
|
...defaultConfig,
|
||||||
|
|
||||||
if (endpoints?.[EModelEndpoint.azureOpenAI]) {
|
|
||||||
endpointLocals[EModelEndpoint.azureOpenAI] = azureConfigSetup(config);
|
|
||||||
checkAzureVariables();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) {
|
|
||||||
endpointLocals[EModelEndpoint.azureAssistants] = azureAssistantsDefaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoints?.[EModelEndpoint.azureAssistants]) {
|
|
||||||
endpointLocals[EModelEndpoint.azureAssistants] = assistantsConfigSetup(
|
|
||||||
config,
|
|
||||||
EModelEndpoint.azureAssistants,
|
|
||||||
endpointLocals[EModelEndpoint.azureAssistants],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoints?.[EModelEndpoint.assistants]) {
|
|
||||||
endpointLocals[EModelEndpoint.assistants] = assistantsConfigSetup(
|
|
||||||
config,
|
|
||||||
EModelEndpoint.assistants,
|
|
||||||
endpointLocals[EModelEndpoint.assistants],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointLocals[EModelEndpoint.agents] = agentsConfigSetup(config, agentsDefaults);
|
|
||||||
|
|
||||||
const endpointKeys = [
|
|
||||||
EModelEndpoint.openAI,
|
|
||||||
EModelEndpoint.google,
|
|
||||||
EModelEndpoint.bedrock,
|
|
||||||
EModelEndpoint.anthropic,
|
|
||||||
EModelEndpoint.gptPlugins,
|
|
||||||
];
|
|
||||||
|
|
||||||
endpointKeys.forEach((key) => {
|
|
||||||
if (endpoints?.[key]) {
|
|
||||||
endpointLocals[key] = endpoints[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (endpoints?.all) {
|
|
||||||
endpointLocals.all = endpoints.all;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.locals = {
|
|
||||||
...defaultLocals,
|
|
||||||
fileConfig: config?.fileConfig,
|
fileConfig: config?.fileConfig,
|
||||||
secureImageLinks: config?.secureImageLinks,
|
secureImageLinks: config?.secureImageLinks,
|
||||||
modelSpecs: processModelSpecs(endpoints, config.modelSpecs, interfaceConfig),
|
modelSpecs: processModelSpecs(config?.endpoints, config.modelSpecs, interfaceConfig),
|
||||||
...endpointLocals,
|
endpoints: loadedEndpoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return appConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = AppService;
|
module.exports = AppService;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ const { retrieveAndProcessFile } = require('~/server/services/Files/process');
|
|||||||
const { processRequiredActions } = require('~/server/services/ToolService');
|
const { processRequiredActions } = require('~/server/services/ToolService');
|
||||||
const { RunManager, waitForRun } = require('~/server/services/Runs');
|
const { RunManager, waitForRun } = require('~/server/services/Runs');
|
||||||
const { processMessages } = require('~/server/services/Threads');
|
const { processMessages } = require('~/server/services/Threads');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { createOnProgress } = require('~/server/utils');
|
const { createOnProgress } = require('~/server/utils');
|
||||||
const { TextStream } = require('~/app/clients');
|
const { TextStream } = require('~/app/clients');
|
||||||
|
|
||||||
@@ -350,6 +351,7 @@ async function runAssistant({
|
|||||||
accumulatedMessages = [],
|
accumulatedMessages = [],
|
||||||
in_progress: inProgress,
|
in_progress: inProgress,
|
||||||
}) {
|
}) {
|
||||||
|
const appConfig = await getAppConfig({ role: openai.req.user?.role });
|
||||||
let steps = accumulatedSteps;
|
let steps = accumulatedSteps;
|
||||||
let messages = accumulatedMessages;
|
let messages = accumulatedMessages;
|
||||||
const in_progress = inProgress ?? createInProgressHandler(openai, thread_id, messages);
|
const in_progress = inProgress ?? createInProgressHandler(openai, thread_id, messages);
|
||||||
@@ -396,8 +398,8 @@ async function runAssistant({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { endpoint = EModelEndpoint.azureAssistants } = openai.req.body;
|
const { endpoint = EModelEndpoint.azureAssistants } = openai.req.body;
|
||||||
/** @type {TCustomConfig.endpoints.assistants} */
|
/** @type {AppConfig['endpoints']['assistants']} */
|
||||||
const assistantsEndpointConfig = openai.req.app.locals?.[endpoint] ?? {};
|
const assistantsEndpointConfig = appConfig.endpoints?.[endpoint] ?? {};
|
||||||
const { pollIntervalMs, timeoutMs } = assistantsEndpointConfig;
|
const { pollIntervalMs, timeoutMs } = assistantsEndpointConfig;
|
||||||
|
|
||||||
const run = await waitForRun({
|
const run = await waitForRun({
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { webcrypto } = require('node:crypto');
|
const { webcrypto } = require('node:crypto');
|
||||||
const { isEnabled } = require('@librechat/api');
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { isEnabled, checkEmailConfig } = require('@librechat/api');
|
||||||
const { SystemRoles, errorsToString } = require('librechat-data-provider');
|
const { SystemRoles, errorsToString } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
findUser,
|
findUser,
|
||||||
@@ -21,9 +21,9 @@ const {
|
|||||||
generateRefreshToken,
|
generateRefreshToken,
|
||||||
} = require('~/models');
|
} = require('~/models');
|
||||||
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
||||||
const { checkEmailConfig, sendEmail } = require('~/server/utils');
|
|
||||||
const { getBalanceConfig } = require('~/server/services/Config');
|
|
||||||
const { registerSchema } = require('~/strategies/validators');
|
const { registerSchema } = require('~/strategies/validators');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
const { sendEmail } = require('~/server/utils');
|
||||||
|
|
||||||
const domains = {
|
const domains = {
|
||||||
client: process.env.DOMAIN_CLIENT,
|
client: process.env.DOMAIN_CLIENT,
|
||||||
@@ -195,7 +195,8 @@ const registerUser = async (user, additionalData = {}) => {
|
|||||||
return { status: 200, message: genericVerificationMessage };
|
return { status: 200, message: genericVerificationMessage };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await isEmailDomainAllowed(email))) {
|
const appConfig = await getAppConfig({ role: user.role });
|
||||||
|
if (!(await isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains))) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
'The email address provided cannot be used. Please use a different email address.';
|
'The email address provided cannot be used. Please use a different email address.';
|
||||||
logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`);
|
logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`);
|
||||||
@@ -219,9 +220,8 @@ const registerUser = async (user, additionalData = {}) => {
|
|||||||
|
|
||||||
const emailEnabled = checkEmailConfig();
|
const emailEnabled = checkEmailConfig();
|
||||||
const disableTTL = isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN);
|
const disableTTL = isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN);
|
||||||
const balanceConfig = await getBalanceConfig();
|
|
||||||
|
|
||||||
const newUser = await createUser(newUserData, balanceConfig, disableTTL, true);
|
const newUser = await createUser(newUserData, appConfig.balance, disableTTL, true);
|
||||||
newUserId = newUser._id;
|
newUserId = newUser._id;
|
||||||
if (emailEnabled && !newUser.emailVerified) {
|
if (emailEnabled && !newUser.emailVerified) {
|
||||||
await sendVerificationEmail({
|
await sendVerificationEmail({
|
||||||
|
|||||||
68
api/server/services/Config/app.js
Normal file
68
api/server/services/Config/app.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { CacheKeys } = require('librechat-data-provider');
|
||||||
|
const AppService = require('~/server/services/AppService');
|
||||||
|
const { setCachedTools } = require('./getCachedTools');
|
||||||
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the app configuration based on user context
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @param {string} [options.role] - User role for role-based config
|
||||||
|
* @param {boolean} [options.refresh] - Force refresh the cache
|
||||||
|
* @returns {Promise<AppConfig>}
|
||||||
|
*/
|
||||||
|
async function getAppConfig(options = {}) {
|
||||||
|
const { role, refresh } = options;
|
||||||
|
|
||||||
|
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||||
|
const cacheKey = role ? `${CacheKeys.APP_CONFIG}:${role}` : CacheKeys.APP_CONFIG;
|
||||||
|
|
||||||
|
if (!refresh) {
|
||||||
|
const cached = await cache.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseConfig = await cache.get(CacheKeys.APP_CONFIG);
|
||||||
|
if (!baseConfig) {
|
||||||
|
logger.info('[getAppConfig] App configuration not initialized. Initializing AppService...');
|
||||||
|
baseConfig = await AppService();
|
||||||
|
|
||||||
|
if (!baseConfig) {
|
||||||
|
throw new Error('Failed to initialize app configuration through AppService.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseConfig.availableTools) {
|
||||||
|
await setCachedTools(baseConfig.availableTools, { isGlobal: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await cache.set(CacheKeys.APP_CONFIG, baseConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, return the base config
|
||||||
|
// In the future, this is where we'll apply role-based modifications
|
||||||
|
if (role) {
|
||||||
|
// TODO: Apply role-based config modifications
|
||||||
|
// const roleConfig = await applyRoleBasedConfig(baseConfig, role);
|
||||||
|
// await cache.set(cacheKey, roleConfig);
|
||||||
|
// return roleConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the app configuration cache
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async function clearAppConfigCache() {
|
||||||
|
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||||
|
const cacheKey = CacheKeys.APP_CONFIG;
|
||||||
|
return await cache.delete(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAppConfig,
|
||||||
|
clearAppConfigCache,
|
||||||
|
};
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
const { isEnabled, getUserMCPAuthMap } = require('@librechat/api');
|
|
||||||
const { CacheKeys, EModelEndpoint } = require('librechat-data-provider');
|
|
||||||
const { normalizeEndpointName } = require('~/server/utils');
|
|
||||||
const loadCustomConfig = require('./loadCustomConfig');
|
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the configuration object
|
|
||||||
* @function getCustomConfig
|
|
||||||
* @returns {Promise<TCustomConfig | null>}
|
|
||||||
* */
|
|
||||||
async function getCustomConfig() {
|
|
||||||
const cache = getLogStores(CacheKeys.STATIC_CONFIG);
|
|
||||||
return (await cache.get(CacheKeys.LIBRECHAT_YAML_CONFIG)) || (await loadCustomConfig());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the configuration object
|
|
||||||
* @function getBalanceConfig
|
|
||||||
* @returns {Promise<TCustomConfig['balance'] | null>}
|
|
||||||
* */
|
|
||||||
async function getBalanceConfig() {
|
|
||||||
const isLegacyEnabled = isEnabled(process.env.CHECK_BALANCE);
|
|
||||||
const startBalance = process.env.START_BALANCE;
|
|
||||||
/** @type {TCustomConfig['balance']} */
|
|
||||||
const config = {
|
|
||||||
enabled: isLegacyEnabled,
|
|
||||||
startBalance: startBalance != null && startBalance ? parseInt(startBalance, 10) : undefined,
|
|
||||||
};
|
|
||||||
const customConfig = await getCustomConfig();
|
|
||||||
if (!customConfig) {
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
return { ...config, ...(customConfig?.['balance'] ?? {}) };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string | EModelEndpoint} endpoint
|
|
||||||
* @returns {Promise<TEndpoint | undefined>}
|
|
||||||
*/
|
|
||||||
const getCustomEndpointConfig = async (endpoint) => {
|
|
||||||
const customConfig = await getCustomConfig();
|
|
||||||
if (!customConfig) {
|
|
||||||
throw new Error(`Config not found for the ${endpoint} custom endpoint.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { endpoints = {} } = customConfig;
|
|
||||||
const customEndpoints = endpoints[EModelEndpoint.custom] ?? [];
|
|
||||||
return customEndpoints.find(
|
|
||||||
(endpointConfig) => normalizeEndpointName(endpointConfig.name) === endpoint,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} params
|
|
||||||
* @param {string} params.userId
|
|
||||||
* @param {GenericTool[]} [params.tools]
|
|
||||||
* @param {import('@librechat/data-schemas').PluginAuthMethods['findPluginAuthsByKeys']} params.findPluginAuthsByKeys
|
|
||||||
* @returns {Promise<Record<string, Record<string, string>> | undefined>}
|
|
||||||
*/
|
|
||||||
async function getMCPAuthMap({ userId, tools, findPluginAuthsByKeys }) {
|
|
||||||
try {
|
|
||||||
if (!tools || tools.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return await getUserMCPAuthMap({
|
|
||||||
tools,
|
|
||||||
userId,
|
|
||||||
findPluginAuthsByKeys,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(
|
|
||||||
`[api/server/controllers/agents/client.js #chatCompletion] Error getting custom user vars for agent`,
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
async function hasCustomUserVars() {
|
|
||||||
const customConfig = await getCustomConfig();
|
|
||||||
const mcpServers = customConfig?.mcpServers;
|
|
||||||
return Object.values(mcpServers ?? {}).some((server) => server.customUserVars);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getMCPAuthMap,
|
|
||||||
getCustomConfig,
|
|
||||||
getBalanceConfig,
|
|
||||||
hasCustomUserVars,
|
|
||||||
getCustomEndpointConfig,
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { loadCustomEndpointsConfig } = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
CacheKeys,
|
CacheKeys,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
@@ -6,8 +7,8 @@ const {
|
|||||||
defaultAgentCapabilities,
|
defaultAgentCapabilities,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const loadDefaultEndpointsConfig = require('./loadDefaultEConfig');
|
const loadDefaultEndpointsConfig = require('./loadDefaultEConfig');
|
||||||
const loadConfigEndpoints = require('./loadConfigEndpoints');
|
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
|
const { getAppConfig } = require('./app');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -21,14 +22,36 @@ async function getEndpointsConfig(req) {
|
|||||||
return cachedEndpointsConfig;
|
return cachedEndpointsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultEndpointsConfig = await loadDefaultEndpointsConfig(req);
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const customConfigEndpoints = await loadConfigEndpoints(req);
|
const defaultEndpointsConfig = await loadDefaultEndpointsConfig(appConfig);
|
||||||
|
const customEndpointsConfig = loadCustomEndpointsConfig(appConfig?.endpoints?.custom);
|
||||||
|
|
||||||
/** @type {TEndpointsConfig} */
|
/** @type {TEndpointsConfig} */
|
||||||
const mergedConfig = { ...defaultEndpointsConfig, ...customConfigEndpoints };
|
const mergedConfig = {
|
||||||
if (mergedConfig[EModelEndpoint.assistants] && req.app.locals?.[EModelEndpoint.assistants]) {
|
...defaultEndpointsConfig,
|
||||||
|
...customEndpointsConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (appConfig.endpoints?.[EModelEndpoint.azureOpenAI]) {
|
||||||
|
/** @type {Omit<TConfig, 'order'>} */
|
||||||
|
mergedConfig[EModelEndpoint.azureOpenAI] = {
|
||||||
|
userProvide: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConfig.endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) {
|
||||||
|
/** @type {Omit<TConfig, 'order'>} */
|
||||||
|
mergedConfig[EModelEndpoint.azureAssistants] = {
|
||||||
|
userProvide: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
mergedConfig[EModelEndpoint.assistants] &&
|
||||||
|
appConfig?.endpoints?.[EModelEndpoint.assistants]
|
||||||
|
) {
|
||||||
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
|
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
|
||||||
req.app.locals[EModelEndpoint.assistants];
|
appConfig.endpoints[EModelEndpoint.assistants];
|
||||||
|
|
||||||
mergedConfig[EModelEndpoint.assistants] = {
|
mergedConfig[EModelEndpoint.assistants] = {
|
||||||
...mergedConfig[EModelEndpoint.assistants],
|
...mergedConfig[EModelEndpoint.assistants],
|
||||||
@@ -38,9 +61,9 @@ async function getEndpointsConfig(req) {
|
|||||||
capabilities,
|
capabilities,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (mergedConfig[EModelEndpoint.agents] && req.app.locals?.[EModelEndpoint.agents]) {
|
if (mergedConfig[EModelEndpoint.agents] && appConfig?.endpoints?.[EModelEndpoint.agents]) {
|
||||||
const { disableBuilder, capabilities, allowedProviders, ..._rest } =
|
const { disableBuilder, capabilities, allowedProviders, ..._rest } =
|
||||||
req.app.locals[EModelEndpoint.agents];
|
appConfig.endpoints[EModelEndpoint.agents];
|
||||||
|
|
||||||
mergedConfig[EModelEndpoint.agents] = {
|
mergedConfig[EModelEndpoint.agents] = {
|
||||||
...mergedConfig[EModelEndpoint.agents],
|
...mergedConfig[EModelEndpoint.agents],
|
||||||
@@ -52,10 +75,10 @@ async function getEndpointsConfig(req) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
mergedConfig[EModelEndpoint.azureAssistants] &&
|
mergedConfig[EModelEndpoint.azureAssistants] &&
|
||||||
req.app.locals?.[EModelEndpoint.azureAssistants]
|
appConfig?.endpoints?.[EModelEndpoint.azureAssistants]
|
||||||
) {
|
) {
|
||||||
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
|
const { disableBuilder, retrievalModels, capabilities, version, ..._rest } =
|
||||||
req.app.locals[EModelEndpoint.azureAssistants];
|
appConfig.endpoints[EModelEndpoint.azureAssistants];
|
||||||
|
|
||||||
mergedConfig[EModelEndpoint.azureAssistants] = {
|
mergedConfig[EModelEndpoint.azureAssistants] = {
|
||||||
...mergedConfig[EModelEndpoint.azureAssistants],
|
...mergedConfig[EModelEndpoint.azureAssistants],
|
||||||
@@ -66,8 +89,8 @@ async function getEndpointsConfig(req) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mergedConfig[EModelEndpoint.bedrock] && req.app.locals?.[EModelEndpoint.bedrock]) {
|
if (mergedConfig[EModelEndpoint.bedrock] && appConfig?.endpoints?.[EModelEndpoint.bedrock]) {
|
||||||
const { availableRegions } = req.app.locals[EModelEndpoint.bedrock];
|
const { availableRegions } = appConfig.endpoints[EModelEndpoint.bedrock];
|
||||||
mergedConfig[EModelEndpoint.bedrock] = {
|
mergedConfig[EModelEndpoint.bedrock] = {
|
||||||
...mergedConfig[EModelEndpoint.bedrock],
|
...mergedConfig[EModelEndpoint.bedrock],
|
||||||
availableRegions,
|
availableRegions,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
const appConfig = require('./app');
|
||||||
const { config } = require('./EndpointService');
|
const { config } = require('./EndpointService');
|
||||||
const getCachedTools = require('./getCachedTools');
|
const getCachedTools = require('./getCachedTools');
|
||||||
const getCustomConfig = require('./getCustomConfig');
|
|
||||||
const loadCustomConfig = require('./loadCustomConfig');
|
const loadCustomConfig = require('./loadCustomConfig');
|
||||||
const loadConfigModels = require('./loadConfigModels');
|
const loadConfigModels = require('./loadConfigModels');
|
||||||
const loadDefaultModels = require('./loadDefaultModels');
|
const loadDefaultModels = require('./loadDefaultModels');
|
||||||
@@ -15,7 +15,7 @@ module.exports = {
|
|||||||
loadDefaultModels,
|
loadDefaultModels,
|
||||||
loadOverrideConfig,
|
loadOverrideConfig,
|
||||||
loadAsyncEndpoints,
|
loadAsyncEndpoints,
|
||||||
|
...appConfig,
|
||||||
...getCachedTools,
|
...getCachedTools,
|
||||||
...getCustomConfig,
|
|
||||||
...getEndpointsConfig,
|
...getEndpointsConfig,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { loadServiceKey, isUserProvided } = require('@librechat/api');
|
|
||||||
const { EModelEndpoint } = require('librechat-data-provider');
|
const { EModelEndpoint } = require('librechat-data-provider');
|
||||||
|
const { loadServiceKey, isUserProvided } = require('@librechat/api');
|
||||||
const { config } = require('./EndpointService');
|
const { config } = require('./EndpointService');
|
||||||
|
|
||||||
const { openAIApiKey, azureOpenAIApiKey, useAzurePlugins, userProvidedOpenAI, googleKey } = config;
|
const { openAIApiKey, azureOpenAIApiKey, useAzurePlugins, userProvidedOpenAI, googleKey } = config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load async endpoints and return a configuration object
|
* Load async endpoints and return a configuration object
|
||||||
* @param {Express.Request} req - The request object
|
* @param {AppConfig} [appConfig] - The app configuration object
|
||||||
*/
|
*/
|
||||||
async function loadAsyncEndpoints(req) {
|
async function loadAsyncEndpoints(appConfig) {
|
||||||
let serviceKey, googleUserProvides;
|
let serviceKey, googleUserProvides;
|
||||||
|
|
||||||
/** Check if GOOGLE_KEY is provided at all(including 'user_provided') */
|
/** Check if GOOGLE_KEY is provided at all(including 'user_provided') */
|
||||||
@@ -34,7 +34,7 @@ async function loadAsyncEndpoints(req) {
|
|||||||
|
|
||||||
const google = serviceKey || isGoogleKeyProvided ? { userProvide: googleUserProvides } : false;
|
const google = serviceKey || isGoogleKeyProvided ? { userProvide: googleUserProvides } : false;
|
||||||
|
|
||||||
const useAzure = req.app.locals[EModelEndpoint.azureOpenAI]?.plugins;
|
const useAzure = !!appConfig?.endpoints?.[EModelEndpoint.azureOpenAI]?.plugins;
|
||||||
const gptPlugins =
|
const gptPlugins =
|
||||||
useAzure || openAIApiKey || azureOpenAIApiKey
|
useAzure || openAIApiKey || azureOpenAIApiKey
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
const { EModelEndpoint, extractEnvVariable } = require('librechat-data-provider');
|
|
||||||
const { isUserProvided, normalizeEndpointName } = require('~/server/utils');
|
|
||||||
const { getCustomConfig } = require('./getCustomConfig');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load config endpoints from the cached configuration object
|
|
||||||
* @param {Express.Request} req - The request object
|
|
||||||
* @returns {Promise<TEndpointsConfig>} A promise that resolves to an object containing the endpoints configuration
|
|
||||||
*/
|
|
||||||
async function loadConfigEndpoints(req) {
|
|
||||||
const customConfig = await getCustomConfig();
|
|
||||||
|
|
||||||
if (!customConfig) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { endpoints = {} } = customConfig ?? {};
|
|
||||||
const endpointsConfig = {};
|
|
||||||
|
|
||||||
if (Array.isArray(endpoints[EModelEndpoint.custom])) {
|
|
||||||
const customEndpoints = endpoints[EModelEndpoint.custom].filter(
|
|
||||||
(endpoint) =>
|
|
||||||
endpoint.baseURL &&
|
|
||||||
endpoint.apiKey &&
|
|
||||||
endpoint.name &&
|
|
||||||
endpoint.models &&
|
|
||||||
(endpoint.models.fetch || endpoint.models.default),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 0; i < customEndpoints.length; i++) {
|
|
||||||
const endpoint = customEndpoints[i];
|
|
||||||
const {
|
|
||||||
baseURL,
|
|
||||||
apiKey,
|
|
||||||
name: configName,
|
|
||||||
iconURL,
|
|
||||||
modelDisplayLabel,
|
|
||||||
customParams,
|
|
||||||
} = endpoint;
|
|
||||||
const name = normalizeEndpointName(configName);
|
|
||||||
|
|
||||||
const resolvedApiKey = extractEnvVariable(apiKey);
|
|
||||||
const resolvedBaseURL = extractEnvVariable(baseURL);
|
|
||||||
|
|
||||||
endpointsConfig[name] = {
|
|
||||||
type: EModelEndpoint.custom,
|
|
||||||
userProvide: isUserProvided(resolvedApiKey),
|
|
||||||
userProvideURL: isUserProvided(resolvedBaseURL),
|
|
||||||
modelDisplayLabel,
|
|
||||||
iconURL,
|
|
||||||
customParams,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.app.locals[EModelEndpoint.azureOpenAI]) {
|
|
||||||
/** @type {Omit<TConfig, 'order'>} */
|
|
||||||
endpointsConfig[EModelEndpoint.azureOpenAI] = {
|
|
||||||
userProvide: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.app.locals[EModelEndpoint.azureOpenAI]?.assistants) {
|
|
||||||
/** @type {Omit<TConfig, 'order'>} */
|
|
||||||
endpointsConfig[EModelEndpoint.azureAssistants] = {
|
|
||||||
userProvide: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return endpointsConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = loadConfigEndpoints;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
const { isUserProvided, normalizeEndpointName } = require('@librechat/api');
|
||||||
const { EModelEndpoint, extractEnvVariable } = require('librechat-data-provider');
|
const { EModelEndpoint, extractEnvVariable } = require('librechat-data-provider');
|
||||||
const { isUserProvided, normalizeEndpointName } = require('~/server/utils');
|
|
||||||
const { fetchModels } = require('~/server/services/ModelService');
|
const { fetchModels } = require('~/server/services/ModelService');
|
||||||
const { getCustomConfig } = require('./getCustomConfig');
|
const { getAppConfig } = require('./app');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load config endpoints from the cached configuration object
|
* Load config endpoints from the cached configuration object
|
||||||
@@ -9,35 +9,31 @@ const { getCustomConfig } = require('./getCustomConfig');
|
|||||||
* @param {Express.Request} req - The Express request object.
|
* @param {Express.Request} req - The Express request object.
|
||||||
*/
|
*/
|
||||||
async function loadConfigModels(req) {
|
async function loadConfigModels(req) {
|
||||||
const customConfig = await getCustomConfig();
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
if (!appConfig) {
|
||||||
if (!customConfig) {
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { endpoints = {} } = customConfig ?? {};
|
|
||||||
const modelsConfig = {};
|
const modelsConfig = {};
|
||||||
const azureEndpoint = endpoints[EModelEndpoint.azureOpenAI];
|
const azureConfig = appConfig.endpoints?.[EModelEndpoint.azureOpenAI];
|
||||||
const azureConfig = req.app.locals[EModelEndpoint.azureOpenAI];
|
|
||||||
const { modelNames } = azureConfig ?? {};
|
const { modelNames } = azureConfig ?? {};
|
||||||
|
|
||||||
if (modelNames && azureEndpoint) {
|
if (modelNames && azureConfig) {
|
||||||
modelsConfig[EModelEndpoint.azureOpenAI] = modelNames;
|
modelsConfig[EModelEndpoint.azureOpenAI] = modelNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelNames && azureEndpoint && azureEndpoint.plugins) {
|
if (modelNames && azureConfig && azureConfig.plugins) {
|
||||||
modelsConfig[EModelEndpoint.gptPlugins] = modelNames;
|
modelsConfig[EModelEndpoint.gptPlugins] = modelNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (azureEndpoint?.assistants && azureConfig.assistantModels) {
|
if (azureConfig?.assistants && azureConfig.assistantModels) {
|
||||||
modelsConfig[EModelEndpoint.azureAssistants] = azureConfig.assistantModels;
|
modelsConfig[EModelEndpoint.azureAssistants] = azureConfig.assistantModels;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(endpoints[EModelEndpoint.custom])) {
|
if (!Array.isArray(appConfig.endpoints?.[EModelEndpoint.custom])) {
|
||||||
return modelsConfig;
|
return modelsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customEndpoints = endpoints[EModelEndpoint.custom].filter(
|
const customEndpoints = appConfig.endpoints[EModelEndpoint.custom].filter(
|
||||||
(endpoint) =>
|
(endpoint) =>
|
||||||
endpoint.baseURL &&
|
endpoint.baseURL &&
|
||||||
endpoint.apiKey &&
|
endpoint.apiKey &&
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const { fetchModels } = require('~/server/services/ModelService');
|
const { fetchModels } = require('~/server/services/ModelService');
|
||||||
const { getCustomConfig } = require('./getCustomConfig');
|
|
||||||
const loadConfigModels = require('./loadConfigModels');
|
const loadConfigModels = require('./loadConfigModels');
|
||||||
|
const { getAppConfig } = require('./app');
|
||||||
|
|
||||||
jest.mock('~/server/services/ModelService');
|
jest.mock('~/server/services/ModelService');
|
||||||
jest.mock('./getCustomConfig');
|
jest.mock('./app');
|
||||||
|
|
||||||
const exampleConfig = {
|
const exampleConfig = {
|
||||||
endpoints: {
|
endpoints: {
|
||||||
@@ -60,7 +60,7 @@ const exampleConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('loadConfigModels', () => {
|
describe('loadConfigModels', () => {
|
||||||
const mockRequest = { app: { locals: {} }, user: { id: 'testUserId' } };
|
const mockRequest = { user: { id: 'testUserId' } };
|
||||||
|
|
||||||
const originalEnv = process.env;
|
const originalEnv = process.env;
|
||||||
|
|
||||||
@@ -68,6 +68,9 @@ describe('loadConfigModels', () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
process.env = { ...originalEnv };
|
process.env = { ...originalEnv };
|
||||||
|
|
||||||
|
// Default mock for getAppConfig
|
||||||
|
getAppConfig.mockResolvedValue({});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -75,18 +78,15 @@ describe('loadConfigModels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return an empty object if customConfig is null', async () => {
|
it('should return an empty object if customConfig is null', async () => {
|
||||||
getCustomConfig.mockResolvedValue(null);
|
getAppConfig.mockResolvedValue(null);
|
||||||
const result = await loadConfigModels(mockRequest);
|
const result = await loadConfigModels(mockRequest);
|
||||||
expect(result).toEqual({});
|
expect(result).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles azure models and endpoint correctly', async () => {
|
it('handles azure models and endpoint correctly', async () => {
|
||||||
mockRequest.app.locals.azureOpenAI = { modelNames: ['model1', 'model2'] };
|
getAppConfig.mockResolvedValue({
|
||||||
getCustomConfig.mockResolvedValue({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
azureOpenAI: {
|
azureOpenAI: { modelNames: ['model1', 'model2'] },
|
||||||
models: ['model1', 'model2'],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,18 +97,16 @@ describe('loadConfigModels', () => {
|
|||||||
it('fetches custom models based on the unique key', async () => {
|
it('fetches custom models based on the unique key', async () => {
|
||||||
process.env.BASE_URL = 'http://example.com';
|
process.env.BASE_URL = 'http://example.com';
|
||||||
process.env.API_KEY = 'some-api-key';
|
process.env.API_KEY = 'some-api-key';
|
||||||
const customEndpoints = {
|
const customEndpoints = [
|
||||||
custom: [
|
{
|
||||||
{
|
baseURL: '${BASE_URL}',
|
||||||
baseURL: '${BASE_URL}',
|
apiKey: '${API_KEY}',
|
||||||
apiKey: '${API_KEY}',
|
name: 'CustomModel',
|
||||||
name: 'CustomModel',
|
models: { fetch: true },
|
||||||
models: { fetch: true },
|
},
|
||||||
},
|
];
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
getCustomConfig.mockResolvedValue({ endpoints: customEndpoints });
|
getAppConfig.mockResolvedValue({ endpoints: { custom: customEndpoints } });
|
||||||
fetchModels.mockResolvedValue(['customModel1', 'customModel2']);
|
fetchModels.mockResolvedValue(['customModel1', 'customModel2']);
|
||||||
|
|
||||||
const result = await loadConfigModels(mockRequest);
|
const result = await loadConfigModels(mockRequest);
|
||||||
@@ -117,7 +115,7 @@ describe('loadConfigModels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('correctly associates models to names using unique keys', async () => {
|
it('correctly associates models to names using unique keys', async () => {
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
custom: [
|
custom: [
|
||||||
{
|
{
|
||||||
@@ -146,7 +144,7 @@ describe('loadConfigModels', () => {
|
|||||||
|
|
||||||
it('correctly handles multiple endpoints with the same baseURL but different apiKeys', async () => {
|
it('correctly handles multiple endpoints with the same baseURL but different apiKeys', async () => {
|
||||||
// Mock the custom configuration to simulate the user's scenario
|
// Mock the custom configuration to simulate the user's scenario
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
custom: [
|
custom: [
|
||||||
{
|
{
|
||||||
@@ -210,7 +208,7 @@ describe('loadConfigModels', () => {
|
|||||||
process.env.MY_OPENROUTER_API_KEY = 'actual_openrouter_api_key';
|
process.env.MY_OPENROUTER_API_KEY = 'actual_openrouter_api_key';
|
||||||
// Setup custom configuration with specific API keys for Mistral and OpenRouter
|
// Setup custom configuration with specific API keys for Mistral and OpenRouter
|
||||||
// and "user_provided" for groq and Ollama, indicating no fetch for the latter two
|
// and "user_provided" for groq and Ollama, indicating no fetch for the latter two
|
||||||
getCustomConfig.mockResolvedValue(exampleConfig);
|
getAppConfig.mockResolvedValue(exampleConfig);
|
||||||
|
|
||||||
// Assuming fetchModels would be called only for Mistral and OpenRouter
|
// Assuming fetchModels would be called only for Mistral and OpenRouter
|
||||||
fetchModels.mockImplementation(({ name }) => {
|
fetchModels.mockImplementation(({ name }) => {
|
||||||
@@ -273,7 +271,7 @@ describe('loadConfigModels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to default models if fetching returns an empty array', async () => {
|
it('falls back to default models if fetching returns an empty array', async () => {
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
custom: [
|
custom: [
|
||||||
{
|
{
|
||||||
@@ -306,7 +304,7 @@ describe('loadConfigModels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to default models if fetching returns a falsy value', async () => {
|
it('falls back to default models if fetching returns a falsy value', async () => {
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
custom: [
|
custom: [
|
||||||
{
|
{
|
||||||
@@ -367,7 +365,7 @@ describe('loadConfigModels', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
getCustomConfig.mockResolvedValue({
|
getAppConfig.mockResolvedValue({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
custom: testCases,
|
custom: testCases,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ const { config } = require('./EndpointService');
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load async endpoints and return a configuration object
|
* Load async endpoints and return a configuration object
|
||||||
* @param {Express.Request} req - The request object
|
* @param {AppConfig} appConfig - The app configuration object
|
||||||
* @returns {Promise<Object.<string, EndpointWithOrder>>} An object whose keys are endpoint names and values are objects that contain the endpoint configuration and an order.
|
* @returns {Promise<Object.<string, EndpointWithOrder>>} An object whose keys are endpoint names and values are objects that contain the endpoint configuration and an order.
|
||||||
*/
|
*/
|
||||||
async function loadDefaultEndpointsConfig(req) {
|
async function loadDefaultEndpointsConfig(appConfig) {
|
||||||
const { google, gptPlugins } = await loadAsyncEndpoints(req);
|
const { google, gptPlugins } = await loadAsyncEndpoints(appConfig);
|
||||||
const { assistants, azureAssistants, azureOpenAI, chatGPTBrowser } = config;
|
const { assistants, azureAssistants, azureOpenAI, chatGPTBrowser } = config;
|
||||||
|
|
||||||
const enabledEndpoints = getEnabledEndpoints();
|
const enabledEndpoints = getEnabledEndpoints();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
|
|||||||
const { getProviderConfig } = require('~/server/services/Endpoints');
|
const { getProviderConfig } = require('~/server/services/Endpoints');
|
||||||
const { processFiles } = require('~/server/services/Files/process');
|
const { processFiles } = require('~/server/services/Files/process');
|
||||||
const { getFiles, getToolFilesByIds } = require('~/models/File');
|
const { getFiles, getToolFilesByIds } = require('~/models/File');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { getConvoFiles } = require('~/models/Conversation');
|
const { getConvoFiles } = require('~/models/Conversation');
|
||||||
const { getModelMaxTokens } = require('~/utils');
|
const { getModelMaxTokens } = require('~/utils');
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ const initializeAgent = async ({
|
|||||||
allowedProviders,
|
allowedProviders,
|
||||||
isInitialAgent = false,
|
isInitialAgent = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
if (
|
if (
|
||||||
isAgentsEndpoint(endpointOption?.endpoint) &&
|
isAgentsEndpoint(endpointOption?.endpoint) &&
|
||||||
allowedProviders.size > 0 &&
|
allowedProviders.size > 0 &&
|
||||||
@@ -84,10 +86,11 @@ const initializeAgent = async ({
|
|||||||
const { attachments, tool_resources } = await primeResources({
|
const { attachments, tool_resources } = await primeResources({
|
||||||
req,
|
req,
|
||||||
getFiles,
|
getFiles,
|
||||||
|
appConfig,
|
||||||
|
agentId: agent.id,
|
||||||
attachments: currentFiles,
|
attachments: currentFiles,
|
||||||
tool_resources: agent.tool_resources,
|
tool_resources: agent.tool_resources,
|
||||||
requestFileSet: new Set(requestFiles?.map((file) => file.file_id)),
|
requestFileSet: new Set(requestFiles?.map((file) => file.file_id)),
|
||||||
agentId: agent.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const provider = agent.provider;
|
const provider = agent.provider;
|
||||||
@@ -103,7 +106,7 @@ const initializeAgent = async ({
|
|||||||
})) ?? {};
|
})) ?? {};
|
||||||
|
|
||||||
agent.endpoint = provider;
|
agent.endpoint = provider;
|
||||||
const { getOptions, overrideProvider } = await getProviderConfig(provider);
|
const { getOptions, overrideProvider } = getProviderConfig({ provider, appConfig });
|
||||||
if (overrideProvider !== agent.provider) {
|
if (overrideProvider !== agent.provider) {
|
||||||
agent.provider = overrideProvider;
|
agent.provider = overrideProvider;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { validateAgentModel } = require('@librechat/api');
|
|
||||||
const { createContentAggregator } = require('@librechat/agents');
|
const { createContentAggregator } = require('@librechat/agents');
|
||||||
|
const { validateAgentModel, getCustomEndpointConfig } = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
Constants,
|
Constants,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
@@ -13,9 +13,9 @@ const {
|
|||||||
} = require('~/server/controllers/agents/callbacks');
|
} = require('~/server/controllers/agents/callbacks');
|
||||||
const { initializeAgent } = require('~/server/services/Endpoints/agents/agent');
|
const { initializeAgent } = require('~/server/services/Endpoints/agents/agent');
|
||||||
const { getModelsConfig } = require('~/server/controllers/ModelController');
|
const { getModelsConfig } = require('~/server/controllers/ModelController');
|
||||||
const { getCustomEndpointConfig } = require('~/server/services/Config');
|
|
||||||
const { loadAgentTools } = require('~/server/services/ToolService');
|
const { loadAgentTools } = require('~/server/services/ToolService');
|
||||||
const AgentClient = require('~/server/controllers/agents/client');
|
const AgentClient = require('~/server/controllers/agents/client');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { getAgent } = require('~/models/Agent');
|
const { getAgent } = require('~/models/Agent');
|
||||||
const { logViolation } = require('~/cache');
|
const { logViolation } = require('~/cache');
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||||||
if (!endpointOption) {
|
if (!endpointOption) {
|
||||||
throw new Error('Endpoint option not provided');
|
throw new Error('Endpoint option not provided');
|
||||||
}
|
}
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
|
||||||
// TODO: use endpointOption to determine options/modelOptions
|
// TODO: use endpointOption to determine options/modelOptions
|
||||||
/** @type {Array<UsageMetadata>} */
|
/** @type {Array<UsageMetadata>} */
|
||||||
@@ -89,8 +90,7 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const agentConfigs = new Map();
|
const agentConfigs = new Map();
|
||||||
/** @type {Set<string>} */
|
const allowedProviders = new Set(appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders);
|
||||||
const allowedProviders = new Set(req?.app?.locals?.[EModelEndpoint.agents]?.allowedProviders);
|
|
||||||
|
|
||||||
const loadTools = createToolLoader();
|
const loadTools = createToolLoader();
|
||||||
/** @type {Array<MongoFile>} */
|
/** @type {Array<MongoFile>} */
|
||||||
@@ -144,10 +144,13 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let endpointConfig = req.app.locals[primaryConfig.endpoint];
|
let endpointConfig = appConfig.endpoints?.[primaryConfig.endpoint];
|
||||||
if (!isAgentsEndpoint(primaryConfig.endpoint) && !endpointConfig) {
|
if (!isAgentsEndpoint(primaryConfig.endpoint) && !endpointConfig) {
|
||||||
try {
|
try {
|
||||||
endpointConfig = await getCustomEndpointConfig(primaryConfig.endpoint);
|
endpointConfig = getCustomEndpointConfig({
|
||||||
|
endpoint: primaryConfig.endpoint,
|
||||||
|
appConfig,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'[api/server/controllers/agents/client.js #titleConvo] Error getting custom endpoint config',
|
'[api/server/controllers/agents/client.js #titleConvo] Error getting custom endpoint config',
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ const { EModelEndpoint } = require('librechat-data-provider');
|
|||||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||||
const { getLLMConfig } = require('~/server/services/Endpoints/anthropic/llm');
|
const { getLLMConfig } = require('~/server/services/Endpoints/anthropic/llm');
|
||||||
const AnthropicClient = require('~/app/clients/AnthropicClient');
|
const AnthropicClient = require('~/app/clients/AnthropicClient');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
|
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const { ANTHROPIC_API_KEY, ANTHROPIC_REVERSE_PROXY, PROXY } = process.env;
|
const { ANTHROPIC_API_KEY, ANTHROPIC_REVERSE_PROXY, PROXY } = process.env;
|
||||||
const expiresAt = req.body.key;
|
const expiresAt = req.body.key;
|
||||||
const isUserProvided = ANTHROPIC_API_KEY === 'user_provided';
|
const isUserProvided = ANTHROPIC_API_KEY === 'user_provided';
|
||||||
@@ -23,15 +25,14 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
|
|||||||
let clientOptions = {};
|
let clientOptions = {};
|
||||||
|
|
||||||
/** @type {undefined | TBaseEndpoint} */
|
/** @type {undefined | TBaseEndpoint} */
|
||||||
const anthropicConfig = req.app.locals[EModelEndpoint.anthropic];
|
const anthropicConfig = appConfig.endpoints?.[EModelEndpoint.anthropic];
|
||||||
|
|
||||||
if (anthropicConfig) {
|
if (anthropicConfig) {
|
||||||
clientOptions.streamRate = anthropicConfig.streamRate;
|
clientOptions.streamRate = anthropicConfig.streamRate;
|
||||||
clientOptions.titleModel = anthropicConfig.titleModel;
|
clientOptions.titleModel = anthropicConfig.titleModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {undefined | TBaseEndpoint} */
|
const allConfig = appConfig.endpoints?.all;
|
||||||
const allConfig = req.app.locals.all;
|
|
||||||
if (allConfig) {
|
if (allConfig) {
|
||||||
clientOptions.streamRate = allConfig.streamRate;
|
clientOptions.streamRate = allConfig.streamRate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const {
|
|||||||
getUserKeyValues,
|
getUserKeyValues,
|
||||||
getUserKeyExpiry,
|
getUserKeyExpiry,
|
||||||
} = require('~/server/services/UserService');
|
} = require('~/server/services/UserService');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const OAIClient = require('~/app/clients/OpenAIClient');
|
const OAIClient = require('~/app/clients/OpenAIClient');
|
||||||
|
|
||||||
class Files {
|
class Files {
|
||||||
@@ -48,6 +49,7 @@ class Files {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initializeClient = async ({ req, res, version, endpointOption, initAppClient = false }) => {
|
const initializeClient = async ({ req, res, version, endpointOption, initAppClient = false }) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const { PROXY, OPENAI_ORGANIZATION, AZURE_ASSISTANTS_API_KEY, AZURE_ASSISTANTS_BASE_URL } =
|
const { PROXY, OPENAI_ORGANIZATION, AZURE_ASSISTANTS_API_KEY, AZURE_ASSISTANTS_BASE_URL } =
|
||||||
process.env;
|
process.env;
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** @type {TAzureConfig | undefined} */
|
/** @type {TAzureConfig | undefined} */
|
||||||
const azureConfig = req.app.locals[EModelEndpoint.azureOpenAI];
|
const azureConfig = appConfig.endpoints?.[EModelEndpoint.azureOpenAI];
|
||||||
|
|
||||||
/** @type {AzureOptions | undefined} */
|
/** @type {AzureOptions | undefined} */
|
||||||
let azureOptions;
|
let azureOptions;
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ jest.mock('~/server/services/UserService', () => ({
|
|||||||
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
|
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Config', () => ({
|
||||||
|
getAppConfig: jest.fn().mockResolvedValue({
|
||||||
|
azureAssistants: {
|
||||||
|
apiKey: 'test-key',
|
||||||
|
baseURL: 'https://test.url',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const tenDaysFromToday = new Date(today.setDate(today.getDate() + 10));
|
const tenDaysFromToday = new Date(today.setDate(today.getDate() + 10));
|
||||||
const isoString = tenDaysFromToday.toISOString();
|
const isoString = tenDaysFromToday.toISOString();
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ const {
|
|||||||
removeNullishValues,
|
removeNullishValues,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
const getOptions = async ({ req, overrideModel, endpointOption }) => {
|
const getOptions = async ({ req, overrideModel, endpointOption }) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const {
|
const {
|
||||||
BEDROCK_AWS_SECRET_ACCESS_KEY,
|
BEDROCK_AWS_SECRET_ACCESS_KEY,
|
||||||
BEDROCK_AWS_ACCESS_KEY_ID,
|
BEDROCK_AWS_ACCESS_KEY_ID,
|
||||||
@@ -50,14 +52,13 @@ const getOptions = async ({ req, overrideModel, endpointOption }) => {
|
|||||||
let streamRate = Constants.DEFAULT_STREAM_RATE;
|
let streamRate = Constants.DEFAULT_STREAM_RATE;
|
||||||
|
|
||||||
/** @type {undefined | TBaseEndpoint} */
|
/** @type {undefined | TBaseEndpoint} */
|
||||||
const bedrockConfig = req.app.locals[EModelEndpoint.bedrock];
|
const bedrockConfig = appConfig.endpoints?.[EModelEndpoint.bedrock];
|
||||||
|
|
||||||
if (bedrockConfig && bedrockConfig.streamRate) {
|
if (bedrockConfig && bedrockConfig.streamRate) {
|
||||||
streamRate = bedrockConfig.streamRate;
|
streamRate = bedrockConfig.streamRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {undefined | TBaseEndpoint} */
|
const allConfig = appConfig.endpoints?.all;
|
||||||
const allConfig = req.app.locals.all;
|
|
||||||
if (allConfig && allConfig.streamRate) {
|
if (allConfig && allConfig.streamRate) {
|
||||||
streamRate = allConfig.streamRate;
|
streamRate = allConfig.streamRate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
const { Providers } = require('@librechat/agents');
|
||||||
|
const { isUserProvided, getCustomEndpointConfig } = require('@librechat/api');
|
||||||
|
const { getOpenAIConfig, createHandleLLMNewToken, resolveHeaders } = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
CacheKeys,
|
CacheKeys,
|
||||||
ErrorTypes,
|
ErrorTypes,
|
||||||
@@ -5,22 +8,23 @@ const {
|
|||||||
FetchTokenConfig,
|
FetchTokenConfig,
|
||||||
extractEnvVariable,
|
extractEnvVariable,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const { Providers } = require('@librechat/agents');
|
|
||||||
const { getOpenAIConfig, createHandleLLMNewToken, resolveHeaders } = require('@librechat/api');
|
|
||||||
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||||
const { getCustomEndpointConfig } = require('~/server/services/Config');
|
|
||||||
const { fetchModels } = require('~/server/services/ModelService');
|
const { fetchModels } = require('~/server/services/ModelService');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const OpenAIClient = require('~/app/clients/OpenAIClient');
|
const OpenAIClient = require('~/app/clients/OpenAIClient');
|
||||||
const { isUserProvided } = require('~/server/utils');
|
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
|
|
||||||
const { PROXY } = process.env;
|
const { PROXY } = process.env;
|
||||||
|
|
||||||
const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrideEndpoint }) => {
|
const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrideEndpoint }) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const { key: expiresAt } = req.body;
|
const { key: expiresAt } = req.body;
|
||||||
const endpoint = overrideEndpoint ?? req.body.endpoint;
|
const endpoint = overrideEndpoint ?? req.body.endpoint;
|
||||||
|
|
||||||
const endpointConfig = await getCustomEndpointConfig(endpoint);
|
const endpointConfig = getCustomEndpointConfig({
|
||||||
|
endpoint,
|
||||||
|
appConfig,
|
||||||
|
});
|
||||||
if (!endpointConfig) {
|
if (!endpointConfig) {
|
||||||
throw new Error(`Config not found for the ${endpoint} custom endpoint.`);
|
throw new Error(`Config not found for the ${endpoint} custom endpoint.`);
|
||||||
}
|
}
|
||||||
@@ -117,8 +121,7 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
|
|||||||
endpointTokenConfig,
|
endpointTokenConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {undefined | TBaseEndpoint} */
|
const allConfig = appConfig.endpoints?.all;
|
||||||
const allConfig = req.app.locals.all;
|
|
||||||
if (allConfig) {
|
if (allConfig) {
|
||||||
customOptions.streamRate = allConfig.streamRate;
|
customOptions.streamRate = allConfig.streamRate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
const initializeClient = require('./initialize');
|
const initializeClient = require('./initialize');
|
||||||
|
|
||||||
jest.mock('@librechat/api', () => ({
|
jest.mock('@librechat/api', () => ({
|
||||||
|
...jest.requireActual('@librechat/api'),
|
||||||
resolveHeaders: jest.fn(),
|
resolveHeaders: jest.fn(),
|
||||||
getOpenAIConfig: jest.fn(),
|
getOpenAIConfig: jest.fn(),
|
||||||
createHandleLLMNewToken: jest.fn(),
|
createHandleLLMNewToken: jest.fn(),
|
||||||
}));
|
getCustomEndpointConfig: jest.fn().mockReturnValue({
|
||||||
|
apiKey: 'test-key',
|
||||||
jest.mock('librechat-data-provider', () => ({
|
baseURL: 'https://test.com',
|
||||||
CacheKeys: { TOKEN_CONFIG: 'token_config' },
|
headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
||||||
ErrorTypes: { NO_USER_KEY: 'NO_USER_KEY', NO_BASE_URL: 'NO_BASE_URL' },
|
models: { default: ['test-model'] },
|
||||||
envVarRegex: /\$\{([^}]+)\}/,
|
}),
|
||||||
FetchTokenConfig: {},
|
|
||||||
extractEnvVariable: jest.fn((value) => value),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@librechat/agents', () => ({
|
|
||||||
Providers: { OLLAMA: 'ollama' },
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('~/server/services/UserService', () => ({
|
jest.mock('~/server/services/UserService', () => ({
|
||||||
@@ -24,11 +19,11 @@ jest.mock('~/server/services/UserService', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('~/server/services/Config', () => ({
|
jest.mock('~/server/services/Config', () => ({
|
||||||
getCustomEndpointConfig: jest.fn().mockResolvedValue({
|
getAppConfig: jest.fn().mockResolvedValue({
|
||||||
apiKey: 'test-key',
|
'test-endpoint': {
|
||||||
baseURL: 'https://test.com',
|
apiKey: 'test-key',
|
||||||
headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
baseURL: 'https://test.com',
|
||||||
models: { default: ['test-model'] },
|
},
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -42,10 +37,6 @@ jest.mock('~/app/clients/OpenAIClient', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('~/server/utils', () => ({
|
|
||||||
isUserProvided: jest.fn().mockReturnValue(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('~/cache/getLogStores', () =>
|
jest.mock('~/cache/getLogStores', () =>
|
||||||
jest.fn().mockReturnValue({
|
jest.fn().mockReturnValue({
|
||||||
get: jest.fn(),
|
get: jest.fn(),
|
||||||
@@ -55,13 +46,25 @@ jest.mock('~/cache/getLogStores', () =>
|
|||||||
describe('custom/initializeClient', () => {
|
describe('custom/initializeClient', () => {
|
||||||
const mockRequest = {
|
const mockRequest = {
|
||||||
body: { endpoint: 'test-endpoint' },
|
body: { endpoint: 'test-endpoint' },
|
||||||
user: { id: 'user-123', email: 'test@example.com' },
|
user: { id: 'user-123', email: 'test@example.com', role: 'user' },
|
||||||
app: { locals: {} },
|
app: { locals: {} },
|
||||||
};
|
};
|
||||||
const mockResponse = {};
|
const mockResponse = {};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
const { getCustomEndpointConfig, resolveHeaders, getOpenAIConfig } = require('@librechat/api');
|
||||||
|
getCustomEndpointConfig.mockReturnValue({
|
||||||
|
apiKey: 'test-key',
|
||||||
|
baseURL: 'https://test.com',
|
||||||
|
headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
||||||
|
models: { default: ['test-model'] },
|
||||||
|
});
|
||||||
|
resolveHeaders.mockReturnValue({ 'x-user': 'user-123', 'x-email': 'test@example.com' });
|
||||||
|
getOpenAIConfig.mockReturnValue({
|
||||||
|
useLegacyContent: true,
|
||||||
|
endpointTokenConfig: null,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls resolveHeaders with headers, user, and body for body placeholder support', async () => {
|
it('calls resolveHeaders with headers, user, and body for body placeholder support', async () => {
|
||||||
@@ -69,14 +72,14 @@ describe('custom/initializeClient', () => {
|
|||||||
await initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true });
|
await initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true });
|
||||||
expect(resolveHeaders).toHaveBeenCalledWith({
|
expect(resolveHeaders).toHaveBeenCalledWith({
|
||||||
headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
||||||
user: { id: 'user-123', email: 'test@example.com' },
|
user: { id: 'user-123', email: 'test@example.com', role: 'user' },
|
||||||
body: { endpoint: 'test-endpoint' }, // body - supports {{LIBRECHAT_BODY_*}} placeholders
|
body: { endpoint: 'test-endpoint' }, // body - supports {{LIBRECHAT_BODY_*}} placeholders
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if endpoint config is missing', async () => {
|
it('throws if endpoint config is missing', async () => {
|
||||||
const { getCustomEndpointConfig } = require('~/server/services/Config');
|
const { getCustomEndpointConfig } = require('@librechat/api');
|
||||||
getCustomEndpointConfig.mockResolvedValueOnce(null);
|
getCustomEndpointConfig.mockReturnValueOnce(null);
|
||||||
await expect(
|
await expect(
|
||||||
initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true }),
|
initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true }),
|
||||||
).rejects.toThrow('Config not found for the test-endpoint custom endpoint.');
|
).rejects.toThrow('Config not found for the test-endpoint custom endpoint.');
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const path = require('path');
|
|||||||
const { EModelEndpoint, AuthKeys } = require('librechat-data-provider');
|
const { EModelEndpoint, AuthKeys } = require('librechat-data-provider');
|
||||||
const { getGoogleConfig, isEnabled, loadServiceKey } = require('@librechat/api');
|
const { getGoogleConfig, isEnabled, loadServiceKey } = require('@librechat/api');
|
||||||
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { GoogleClient } = require('~/app');
|
const { GoogleClient } = require('~/app');
|
||||||
|
|
||||||
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
|
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
|
||||||
@@ -46,10 +47,11 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
|
|||||||
|
|
||||||
let clientOptions = {};
|
let clientOptions = {};
|
||||||
|
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
/** @type {undefined | TBaseEndpoint} */
|
/** @type {undefined | TBaseEndpoint} */
|
||||||
const allConfig = req.app.locals.all;
|
const allConfig = appConfig.endpoints?.all;
|
||||||
/** @type {undefined | TBaseEndpoint} */
|
/** @type {undefined | TBaseEndpoint} */
|
||||||
const googleConfig = req.app.locals[EModelEndpoint.google];
|
const googleConfig = appConfig.endpoints?.[EModelEndpoint.google];
|
||||||
|
|
||||||
if (googleConfig) {
|
if (googleConfig) {
|
||||||
clientOptions.streamRate = googleConfig.streamRate;
|
clientOptions.streamRate = googleConfig.streamRate;
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ jest.mock('~/server/services/UserService', () => ({
|
|||||||
getUserKey: jest.fn().mockImplementation(() => ({})),
|
getUserKey: jest.fn().mockImplementation(() => ({})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Config', () => ({
|
||||||
|
getAppConfig: jest.fn().mockResolvedValue({
|
||||||
|
google: {
|
||||||
|
apiKey: 'test-key',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const app = { locals: {} };
|
const app = { locals: {} };
|
||||||
|
|
||||||
describe('google/initializeClient', () => {
|
describe('google/initializeClient', () => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
const { isEnabled } = require('@librechat/api');
|
||||||
const { EModelEndpoint, CacheKeys, Constants, googleSettings } = require('librechat-data-provider');
|
const { EModelEndpoint, CacheKeys, Constants, googleSettings } = require('librechat-data-provider');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
const initializeClient = require('./initialize');
|
const initializeClient = require('./initialize');
|
||||||
const { isEnabled } = require('~/server/utils');
|
|
||||||
const { saveConvo } = require('~/models');
|
const { saveConvo } = require('~/models');
|
||||||
|
|
||||||
const addTitle = async (req, { text, response, client }) => {
|
const addTitle = async (req, { text, response, client }) => {
|
||||||
@@ -14,7 +15,8 @@ const addTitle = async (req, { text, response, client }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { GOOGLE_TITLE_MODEL } = process.env ?? {};
|
const { GOOGLE_TITLE_MODEL } = process.env ?? {};
|
||||||
const providerConfig = req.app.locals[EModelEndpoint.google];
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
const providerConfig = appConfig.endpoints?.[EModelEndpoint.google];
|
||||||
let model =
|
let model =
|
||||||
providerConfig?.titleModel ??
|
providerConfig?.titleModel ??
|
||||||
GOOGLE_TITLE_MODEL ??
|
GOOGLE_TITLE_MODEL ??
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
const { Providers } = require('@librechat/agents');
|
const { Providers } = require('@librechat/agents');
|
||||||
const { EModelEndpoint } = require('librechat-data-provider');
|
const { EModelEndpoint } = require('librechat-data-provider');
|
||||||
|
const { getCustomEndpointConfig } = require('@librechat/api');
|
||||||
const initAnthropic = require('~/server/services/Endpoints/anthropic/initialize');
|
const initAnthropic = require('~/server/services/Endpoints/anthropic/initialize');
|
||||||
const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options');
|
const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options');
|
||||||
const initOpenAI = require('~/server/services/Endpoints/openAI/initialize');
|
const initOpenAI = require('~/server/services/Endpoints/openAI/initialize');
|
||||||
const initCustom = require('~/server/services/Endpoints/custom/initialize');
|
const initCustom = require('~/server/services/Endpoints/custom/initialize');
|
||||||
const initGoogle = require('~/server/services/Endpoints/google/initialize');
|
const initGoogle = require('~/server/services/Endpoints/google/initialize');
|
||||||
const { getCustomEndpointConfig } = require('~/server/services/Config');
|
|
||||||
|
|
||||||
/** Check if the provider is a known custom provider
|
/** Check if the provider is a known custom provider
|
||||||
* @param {string | undefined} [provider] - The provider string
|
* @param {string | undefined} [provider] - The provider string
|
||||||
@@ -31,14 +31,16 @@ const providerConfigMap = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the provider configuration and override endpoint based on the provider string
|
* Get the provider configuration and override endpoint based on the provider string
|
||||||
* @param {string} provider - The provider string
|
* @param {Object} params
|
||||||
* @returns {Promise<{
|
* @param {string} params.provider - The provider string
|
||||||
* getOptions: Function,
|
* @param {AppConfig} params.appConfig - The application configuration
|
||||||
|
* @returns {{
|
||||||
|
* getOptions: (typeof providerConfigMap)[keyof typeof providerConfigMap],
|
||||||
* overrideProvider: string,
|
* overrideProvider: string,
|
||||||
* customEndpointConfig?: TEndpoint
|
* customEndpointConfig?: TEndpoint
|
||||||
* }>}
|
* }}
|
||||||
*/
|
*/
|
||||||
async function getProviderConfig(provider) {
|
function getProviderConfig({ provider, appConfig }) {
|
||||||
let getOptions = providerConfigMap[provider];
|
let getOptions = providerConfigMap[provider];
|
||||||
let overrideProvider = provider;
|
let overrideProvider = provider;
|
||||||
/** @type {TEndpoint | undefined} */
|
/** @type {TEndpoint | undefined} */
|
||||||
@@ -48,7 +50,7 @@ async function getProviderConfig(provider) {
|
|||||||
overrideProvider = provider.toLowerCase();
|
overrideProvider = provider.toLowerCase();
|
||||||
getOptions = providerConfigMap[overrideProvider];
|
getOptions = providerConfigMap[overrideProvider];
|
||||||
} else if (!getOptions) {
|
} else if (!getOptions) {
|
||||||
customEndpointConfig = await getCustomEndpointConfig(provider);
|
customEndpointConfig = getCustomEndpointConfig({ endpoint: provider, appConfig });
|
||||||
if (!customEndpointConfig) {
|
if (!customEndpointConfig) {
|
||||||
throw new Error(`Provider ${provider} not supported`);
|
throw new Error(`Provider ${provider} not supported`);
|
||||||
}
|
}
|
||||||
@@ -57,7 +59,7 @@ async function getProviderConfig(provider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isKnownCustomProvider(overrideProvider) && !customEndpointConfig) {
|
if (isKnownCustomProvider(overrideProvider) && !customEndpointConfig) {
|
||||||
customEndpointConfig = await getCustomEndpointConfig(provider);
|
customEndpointConfig = getCustomEndpointConfig({ endpoint: provider, appConfig });
|
||||||
if (!customEndpointConfig) {
|
if (!customEndpointConfig) {
|
||||||
throw new Error(`Provider ${provider} not supported`);
|
throw new Error(`Provider ${provider} not supported`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const {
|
|||||||
createHandleLLMNewToken,
|
createHandleLLMNewToken,
|
||||||
} = require('@librechat/api');
|
} = require('@librechat/api');
|
||||||
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const OpenAIClient = require('~/app/clients/OpenAIClient');
|
const OpenAIClient = require('~/app/clients/OpenAIClient');
|
||||||
|
|
||||||
const initializeClient = async ({
|
const initializeClient = async ({
|
||||||
@@ -18,6 +19,7 @@ const initializeClient = async ({
|
|||||||
overrideEndpoint,
|
overrideEndpoint,
|
||||||
overrideModel,
|
overrideModel,
|
||||||
}) => {
|
}) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const {
|
const {
|
||||||
PROXY,
|
PROXY,
|
||||||
OPENAI_API_KEY,
|
OPENAI_API_KEY,
|
||||||
@@ -64,7 +66,7 @@ const initializeClient = async ({
|
|||||||
|
|
||||||
const isAzureOpenAI = endpoint === EModelEndpoint.azureOpenAI;
|
const isAzureOpenAI = endpoint === EModelEndpoint.azureOpenAI;
|
||||||
/** @type {false | TAzureConfig} */
|
/** @type {false | TAzureConfig} */
|
||||||
const azureConfig = isAzureOpenAI && req.app.locals[EModelEndpoint.azureOpenAI];
|
const azureConfig = isAzureOpenAI && appConfig.endpoints?.[EModelEndpoint.azureOpenAI];
|
||||||
let serverless = false;
|
let serverless = false;
|
||||||
if (isAzureOpenAI && azureConfig) {
|
if (isAzureOpenAI && azureConfig) {
|
||||||
const { modelGroupMap, groupMap } = azureConfig;
|
const { modelGroupMap, groupMap } = azureConfig;
|
||||||
@@ -113,15 +115,14 @@ const initializeClient = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @type {undefined | TBaseEndpoint} */
|
/** @type {undefined | TBaseEndpoint} */
|
||||||
const openAIConfig = req.app.locals[EModelEndpoint.openAI];
|
const openAIConfig = appConfig.endpoints?.[EModelEndpoint.openAI];
|
||||||
|
|
||||||
if (!isAzureOpenAI && openAIConfig) {
|
if (!isAzureOpenAI && openAIConfig) {
|
||||||
clientOptions.streamRate = openAIConfig.streamRate;
|
clientOptions.streamRate = openAIConfig.streamRate;
|
||||||
clientOptions.titleModel = openAIConfig.titleModel;
|
clientOptions.titleModel = openAIConfig.titleModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {undefined | TBaseEndpoint} */
|
const allConfig = appConfig.endpoints?.all;
|
||||||
const allConfig = req.app.locals.all;
|
|
||||||
if (allConfig) {
|
if (allConfig) {
|
||||||
clientOptions.streamRate = allConfig.streamRate;
|
clientOptions.streamRate = allConfig.streamRate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
jest.mock('~/cache/getLogStores');
|
jest.mock('~/cache/getLogStores', () => ({
|
||||||
|
getLogStores: jest.fn().mockReturnValue({
|
||||||
|
get: jest.fn().mockResolvedValue({
|
||||||
|
openAI: { apiKey: 'test-key' },
|
||||||
|
}),
|
||||||
|
set: jest.fn(),
|
||||||
|
delete: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const { EModelEndpoint, ErrorTypes, validateAzureGroups } = require('librechat-data-provider');
|
const { EModelEndpoint, ErrorTypes, validateAzureGroups } = require('librechat-data-provider');
|
||||||
const { getUserKey, getUserKeyValues } = require('~/server/services/UserService');
|
const { getUserKey, getUserKeyValues } = require('~/server/services/UserService');
|
||||||
const initializeClient = require('./initialize');
|
const initializeClient = require('./initialize');
|
||||||
@@ -11,6 +20,40 @@ jest.mock('~/server/services/UserService', () => ({
|
|||||||
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
|
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('~/server/services/Config', () => ({
|
||||||
|
getAppConfig: jest.fn().mockResolvedValue({
|
||||||
|
endpoints: {
|
||||||
|
openAI: {
|
||||||
|
apiKey: 'test-key',
|
||||||
|
},
|
||||||
|
azureOpenAI: {
|
||||||
|
apiKey: 'test-azure-key',
|
||||||
|
modelNames: ['gpt-4-vision-preview', 'gpt-3.5-turbo', 'gpt-4'],
|
||||||
|
modelGroupMap: {
|
||||||
|
'gpt-4-vision-preview': {
|
||||||
|
group: 'librechat-westus',
|
||||||
|
deploymentName: 'gpt-4-vision-preview',
|
||||||
|
version: '2024-02-15-preview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groupMap: {
|
||||||
|
'librechat-westus': {
|
||||||
|
apiKey: 'WESTUS_API_KEY',
|
||||||
|
instanceName: 'librechat-westus',
|
||||||
|
version: '2023-12-01-preview',
|
||||||
|
models: {
|
||||||
|
'gpt-4-vision-preview': {
|
||||||
|
deploymentName: 'gpt-4-vision-preview',
|
||||||
|
version: '2024-02-15-preview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('initializeClient', () => {
|
describe('initializeClient', () => {
|
||||||
// Set up environment variables
|
// Set up environment variables
|
||||||
const originalEnvironment = process.env;
|
const originalEnvironment = process.env;
|
||||||
@@ -79,7 +122,7 @@ describe('initializeClient', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const { modelNames, modelGroupMap, groupMap } = validateAzureGroups(validAzureConfigs);
|
const { modelNames } = validateAzureGroups(validAzureConfigs);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetModules(); // Clears the cache
|
jest.resetModules(); // Clears the cache
|
||||||
@@ -112,25 +155,29 @@ describe('initializeClient', () => {
|
|||||||
test('should initialize client with Azure credentials when endpoint is azureOpenAI', async () => {
|
test('should initialize client with Azure credentials when endpoint is azureOpenAI', async () => {
|
||||||
process.env.AZURE_API_KEY = 'test-azure-api-key';
|
process.env.AZURE_API_KEY = 'test-azure-api-key';
|
||||||
(process.env.AZURE_OPENAI_API_INSTANCE_NAME = 'some-value'),
|
(process.env.AZURE_OPENAI_API_INSTANCE_NAME = 'some-value'),
|
||||||
(process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME = 'some-value'),
|
(process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME = 'some-value'),
|
||||||
(process.env.AZURE_OPENAI_API_VERSION = 'some-value'),
|
(process.env.AZURE_OPENAI_API_VERSION = 'some-value'),
|
||||||
(process.env.AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME = 'some-value'),
|
(process.env.AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME = 'some-value'),
|
||||||
(process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME = 'some-value'),
|
(process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME = 'some-value'),
|
||||||
(process.env.OPENAI_API_KEY = 'test-openai-api-key');
|
(process.env.OPENAI_API_KEY = 'test-openai-api-key');
|
||||||
process.env.DEBUG_OPENAI = 'false';
|
process.env.DEBUG_OPENAI = 'false';
|
||||||
process.env.OPENAI_SUMMARIZE = 'false';
|
process.env.OPENAI_SUMMARIZE = 'false';
|
||||||
|
|
||||||
const req = {
|
const req = {
|
||||||
body: { key: null, endpoint: 'azureOpenAI' },
|
body: {
|
||||||
|
key: null,
|
||||||
|
endpoint: 'azureOpenAI',
|
||||||
|
model: 'gpt-4-vision-preview',
|
||||||
|
},
|
||||||
user: { id: '123' },
|
user: { id: '123' },
|
||||||
app,
|
app,
|
||||||
};
|
};
|
||||||
const res = {};
|
const res = {};
|
||||||
const endpointOption = { modelOptions: { model: 'test-model' } };
|
const endpointOption = {};
|
||||||
|
|
||||||
const client = await initializeClient({ req, res, endpointOption });
|
const client = await initializeClient({ req, res, endpointOption });
|
||||||
|
|
||||||
expect(client.openAIApiKey).toBe('test-azure-api-key');
|
expect(client.openAIApiKey).toBe('WESTUS_API_KEY');
|
||||||
expect(client.client).toBeInstanceOf(OpenAIClient);
|
expect(client.client).toBeInstanceOf(OpenAIClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,7 +338,7 @@ describe('initializeClient', () => {
|
|||||||
let userValues = getUserKey();
|
let userValues = getUserKey();
|
||||||
try {
|
try {
|
||||||
userValues = JSON.parse(userValues);
|
userValues = JSON.parse(userValues);
|
||||||
} catch (e) {
|
} catch {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: ErrorTypes.INVALID_USER_KEY,
|
type: ErrorTypes.INVALID_USER_KEY,
|
||||||
@@ -307,6 +354,9 @@ describe('initializeClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should initialize client correctly for Azure OpenAI with valid configuration', async () => {
|
test('should initialize client correctly for Azure OpenAI with valid configuration', async () => {
|
||||||
|
// Set up Azure environment variables
|
||||||
|
process.env.WESTUS_API_KEY = 'test-westus-key';
|
||||||
|
|
||||||
const req = {
|
const req = {
|
||||||
body: {
|
body: {
|
||||||
key: null,
|
key: null,
|
||||||
@@ -314,15 +364,6 @@ describe('initializeClient', () => {
|
|||||||
model: modelNames[0],
|
model: modelNames[0],
|
||||||
},
|
},
|
||||||
user: { id: '123' },
|
user: { id: '123' },
|
||||||
app: {
|
|
||||||
locals: {
|
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
|
||||||
modelNames,
|
|
||||||
modelGroupMap,
|
|
||||||
groupMap,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const res = {};
|
const res = {};
|
||||||
const endpointOption = {};
|
const endpointOption = {};
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ const axios = require('axios');
|
|||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const FormData = require('form-data');
|
const FormData = require('form-data');
|
||||||
const { Readable } = require('stream');
|
const { Readable } = require('stream');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { genAzureEndpoint } = require('@librechat/api');
|
const { genAzureEndpoint } = require('@librechat/api');
|
||||||
const { extractEnvVariable, STTProviders } = require('librechat-data-provider');
|
const { extractEnvVariable, STTProviders } = require('librechat-data-provider');
|
||||||
const { getCustomConfig } = require('~/server/services/Config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps MIME types to their corresponding file extensions for audio files.
|
* Maps MIME types to their corresponding file extensions for audio files.
|
||||||
@@ -84,12 +84,7 @@ function getFileExtensionFromMime(mimeType) {
|
|||||||
* @class
|
* @class
|
||||||
*/
|
*/
|
||||||
class STTService {
|
class STTService {
|
||||||
/**
|
constructor() {
|
||||||
* Creates an instance of STTService.
|
|
||||||
* @param {Object} customConfig - The custom configuration object.
|
|
||||||
*/
|
|
||||||
constructor(customConfig) {
|
|
||||||
this.customConfig = customConfig;
|
|
||||||
this.providerStrategies = {
|
this.providerStrategies = {
|
||||||
[STTProviders.OPENAI]: this.openAIProvider,
|
[STTProviders.OPENAI]: this.openAIProvider,
|
||||||
[STTProviders.AZURE_OPENAI]: this.azureOpenAIProvider,
|
[STTProviders.AZURE_OPENAI]: this.azureOpenAIProvider,
|
||||||
@@ -104,21 +99,20 @@ class STTService {
|
|||||||
* @throws {Error} If the custom config is not found.
|
* @throws {Error} If the custom config is not found.
|
||||||
*/
|
*/
|
||||||
static async getInstance() {
|
static async getInstance() {
|
||||||
const customConfig = await getCustomConfig();
|
return new STTService();
|
||||||
if (!customConfig) {
|
|
||||||
throw new Error('Custom config not found');
|
|
||||||
}
|
|
||||||
return new STTService(customConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the configured STT provider and its schema.
|
* Retrieves the configured STT provider and its schema.
|
||||||
|
* @param {ServerRequest} req - The request object.
|
||||||
* @returns {Promise<[string, Object]>} A promise that resolves to an array containing the provider name and its schema.
|
* @returns {Promise<[string, Object]>} A promise that resolves to an array containing the provider name and its schema.
|
||||||
* @throws {Error} If no STT schema is set, multiple providers are set, or no provider is set.
|
* @throws {Error} If no STT schema is set, multiple providers are set, or no provider is set.
|
||||||
*/
|
*/
|
||||||
async getProviderSchema() {
|
async getProviderSchema(req) {
|
||||||
const sttSchema = this.customConfig.speech.stt;
|
const appConfig = await getAppConfig({
|
||||||
|
role: req?.user?.role,
|
||||||
|
});
|
||||||
|
const sttSchema = appConfig?.speech?.stt;
|
||||||
if (!sttSchema) {
|
if (!sttSchema) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'No STT schema is set. Did you configure STT in the custom config (librechat.yaml)?',
|
'No STT schema is set. Did you configure STT in the custom config (librechat.yaml)?',
|
||||||
@@ -274,7 +268,7 @@ class STTService {
|
|||||||
* @param {Object} res - The response object.
|
* @param {Object} res - The response object.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async processTextToSpeech(req, res) {
|
async processSpeechToText(req, res) {
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
return res.status(400).json({ message: 'No audio file provided in the FormData' });
|
return res.status(400).json({ message: 'No audio file provided in the FormData' });
|
||||||
}
|
}
|
||||||
@@ -287,7 +281,7 @@ class STTService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [provider, sttSchema] = await this.getProviderSchema();
|
const [provider, sttSchema] = await this.getProviderSchema(req);
|
||||||
const text = await this.sttRequest(provider, sttSchema, { audioBuffer, audioFile });
|
const text = await this.sttRequest(provider, sttSchema, { audioBuffer, audioFile });
|
||||||
res.json({ text });
|
res.json({ text });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -297,7 +291,7 @@ class STTService {
|
|||||||
try {
|
try {
|
||||||
await fs.unlink(req.file.path);
|
await fs.unlink(req.file.path);
|
||||||
logger.debug('[/speech/stt] Temp. audio upload file deleted');
|
logger.debug('[/speech/stt] Temp. audio upload file deleted');
|
||||||
} catch (error) {
|
} catch {
|
||||||
logger.debug('[/speech/stt] Temp. audio upload file already deleted');
|
logger.debug('[/speech/stt] Temp. audio upload file already deleted');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +316,7 @@ async function createSTTService() {
|
|||||||
*/
|
*/
|
||||||
async function speechToText(req, res) {
|
async function speechToText(req, res) {
|
||||||
const sttService = await createSTTService();
|
const sttService = await createSTTService();
|
||||||
await sttService.processTextToSpeech(req, res);
|
await sttService.processSpeechToText(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { speechToText };
|
module.exports = { speechToText };
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { genAzureEndpoint } = require('@librechat/api');
|
const { genAzureEndpoint } = require('@librechat/api');
|
||||||
const { extractEnvVariable, TTSProviders } = require('librechat-data-provider');
|
const { extractEnvVariable, TTSProviders } = require('librechat-data-provider');
|
||||||
const { getRandomVoiceId, createChunkProcessor, splitTextIntoChunks } = require('./streamAudio');
|
const { getRandomVoiceId, createChunkProcessor, splitTextIntoChunks } = require('./streamAudio');
|
||||||
const { getCustomConfig } = require('~/server/services/Config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service class for handling Text-to-Speech (TTS) operations.
|
* Service class for handling Text-to-Speech (TTS) operations.
|
||||||
@@ -32,11 +32,7 @@ class TTSService {
|
|||||||
* @throws {Error} If the custom config is not found.
|
* @throws {Error} If the custom config is not found.
|
||||||
*/
|
*/
|
||||||
static async getInstance() {
|
static async getInstance() {
|
||||||
const customConfig = await getCustomConfig();
|
return new TTSService();
|
||||||
if (!customConfig) {
|
|
||||||
throw new Error('Custom config not found');
|
|
||||||
}
|
|
||||||
return new TTSService(customConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -293,10 +289,13 @@ class TTSService {
|
|||||||
return res.status(400).send('Missing text in request body');
|
return res.status(400).send('Missing text in request body');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appConfig = await getAppConfig({
|
||||||
|
role: req.user?.role,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
res.setHeader('Content-Type', 'audio/mpeg');
|
res.setHeader('Content-Type', 'audio/mpeg');
|
||||||
const provider = this.getProvider();
|
const provider = this.getProvider();
|
||||||
const ttsSchema = this.customConfig.speech.tts[provider];
|
const ttsSchema = appConfig?.speech?.tts?.[provider];
|
||||||
const voice = await this.getVoice(ttsSchema, requestVoice);
|
const voice = await this.getVoice(ttsSchema, requestVoice);
|
||||||
|
|
||||||
if (input.length < 4096) {
|
if (input.length < 4096) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { getCustomConfig } = require('~/server/services/Config');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { logger } = require('~/config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function retrieves the speechTab settings from the custom configuration
|
* This function retrieves the speechTab settings from the custom configuration
|
||||||
@@ -15,26 +15,28 @@ const { logger } = require('~/config');
|
|||||||
*/
|
*/
|
||||||
async function getCustomConfigSpeech(req, res) {
|
async function getCustomConfigSpeech(req, res) {
|
||||||
try {
|
try {
|
||||||
const customConfig = await getCustomConfig();
|
const appConfig = await getAppConfig({
|
||||||
|
role: req.user?.role,
|
||||||
|
});
|
||||||
|
|
||||||
if (!customConfig) {
|
if (!appConfig) {
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'not_found',
|
message: 'not_found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const sttExternal = !!customConfig.speech?.stt;
|
const sttExternal = !!appConfig.speech?.stt;
|
||||||
const ttsExternal = !!customConfig.speech?.tts;
|
const ttsExternal = !!appConfig.speech?.tts;
|
||||||
let settings = {
|
let settings = {
|
||||||
sttExternal,
|
sttExternal,
|
||||||
ttsExternal,
|
ttsExternal,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!customConfig.speech?.speechTab) {
|
if (!appConfig.speech?.speechTab) {
|
||||||
return res.status(200).send(settings);
|
return res.status(200).send(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
const speechTab = customConfig.speech.speechTab;
|
const speechTab = appConfig.speech.speechTab;
|
||||||
|
|
||||||
if (speechTab.advancedMode !== undefined) {
|
if (speechTab.advancedMode !== undefined) {
|
||||||
settings.advancedMode = speechTab.advancedMode;
|
settings.advancedMode = speechTab.advancedMode;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { TTSProviders } = require('librechat-data-provider');
|
const { TTSProviders } = require('librechat-data-provider');
|
||||||
const { getCustomConfig } = require('~/server/services/Config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { getProvider } = require('./TTSService');
|
const { getProvider } = require('./TTSService');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,13 +14,15 @@ const { getProvider } = require('./TTSService');
|
|||||||
*/
|
*/
|
||||||
async function getVoices(req, res) {
|
async function getVoices(req, res) {
|
||||||
try {
|
try {
|
||||||
const customConfig = await getCustomConfig();
|
const appConfig = await getAppConfig({
|
||||||
|
role: req.user?.role,
|
||||||
|
});
|
||||||
|
|
||||||
if (!customConfig || !customConfig?.speech?.tts) {
|
if (!appConfig || !appConfig?.speech?.tts) {
|
||||||
throw new Error('Configuration or TTS schema is missing');
|
throw new Error('Configuration or TTS schema is missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
const ttsSchema = customConfig?.speech?.tts;
|
const ttsSchema = appConfig?.speech?.tts;
|
||||||
const provider = await getProvider(ttsSchema);
|
const provider = await getProvider(ttsSchema);
|
||||||
let voices;
|
let voices;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const path = require('path');
|
|||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { resizeImageBuffer } = require('../images/resize');
|
const { resizeImageBuffer } = require('../images/resize');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { updateUser, updateFile } = require('~/models');
|
const { updateUser, updateFile } = require('~/models');
|
||||||
const { saveBufferToAzure } = require('./crud');
|
const { saveBufferToAzure } = require('./crud');
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ async function uploadImageToAzure({
|
|||||||
containerName,
|
containerName,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const inputFilePath = file.path;
|
const inputFilePath = file.path;
|
||||||
const inputBuffer = await fs.promises.readFile(inputFilePath);
|
const inputBuffer = await fs.promises.readFile(inputFilePath);
|
||||||
const {
|
const {
|
||||||
@@ -41,12 +43,12 @@ async function uploadImageToAzure({
|
|||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
let webPBuffer;
|
let webPBuffer;
|
||||||
let fileName = `${file_id}__${path.basename(inputFilePath)}`;
|
let fileName = `${file_id}__${path.basename(inputFilePath)}`;
|
||||||
const targetExtension = `.${req.app.locals.imageOutputType}`;
|
const targetExtension = `.${appConfig.imageOutputType}`;
|
||||||
|
|
||||||
if (extension.toLowerCase() === targetExtension) {
|
if (extension.toLowerCase() === targetExtension) {
|
||||||
webPBuffer = resizedBuffer;
|
webPBuffer = resizedBuffer;
|
||||||
} else {
|
} else {
|
||||||
webPBuffer = await sharp(resizedBuffer).toFormat(req.app.locals.imageOutputType).toBuffer();
|
webPBuffer = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer();
|
||||||
const extRegExp = new RegExp(path.extname(fileName) + '$');
|
const extRegExp = new RegExp(path.extname(fileName) + '$');
|
||||||
fileName = fileName.replace(extRegExp, targetExtension);
|
fileName = fileName.replace(extRegExp, targetExtension);
|
||||||
if (!path.extname(fileName)) {
|
if (!path.extname(fileName)) {
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const { checkAccess } = require('@librechat/api');
|
const { checkAccess } = require('@librechat/api');
|
||||||
const { Tools, PermissionTypes, Permissions } = require('librechat-data-provider');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { getCustomConfig } = require('~/server/services/Config/getCustomConfig');
|
const {
|
||||||
|
Tools,
|
||||||
|
Permissions,
|
||||||
|
FileSources,
|
||||||
|
EModelEndpoint,
|
||||||
|
PermissionTypes,
|
||||||
|
} = require('librechat-data-provider');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config/app');
|
||||||
const { getRoleByName } = require('~/models/Role');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
const { logger } = require('~/config');
|
|
||||||
const { Files } = require('~/models');
|
const { Files } = require('~/models');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,10 +50,12 @@ async function processFileCitations({ user, toolArtifact, toolCallId, metadata }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const customConfig = await getCustomConfig();
|
const appConfig = await getAppConfig({ role: user?.role });
|
||||||
const maxCitations = customConfig?.endpoints?.agents?.maxCitations ?? 30;
|
const maxCitations = appConfig.endpoints?.[EModelEndpoint.agents]?.maxCitations ?? 30;
|
||||||
const maxCitationsPerFile = customConfig?.endpoints?.agents?.maxCitationsPerFile ?? 5;
|
const maxCitationsPerFile =
|
||||||
const minRelevanceScore = customConfig?.endpoints?.agents?.minRelevanceScore ?? 0.45;
|
appConfig.endpoints?.[EModelEndpoint.agents]?.maxCitationsPerFile ?? 5;
|
||||||
|
const minRelevanceScore =
|
||||||
|
appConfig.endpoints?.[EModelEndpoint.agents]?.minRelevanceScore ?? 0.45;
|
||||||
|
|
||||||
const sources = toolArtifact[Tools.file_search].sources || [];
|
const sources = toolArtifact[Tools.file_search].sources || [];
|
||||||
const filteredSources = sources.filter((source) => source.relevance >= minRelevanceScore);
|
const filteredSources = sources.filter((source) => source.relevance >= minRelevanceScore);
|
||||||
@@ -59,7 +67,7 @@ async function processFileCitations({ user, toolArtifact, toolCallId, metadata }
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedSources = applyCitationLimits(filteredSources, maxCitations, maxCitationsPerFile);
|
const selectedSources = applyCitationLimits(filteredSources, maxCitations, maxCitationsPerFile);
|
||||||
const enhancedSources = await enhanceSourcesWithMetadata(selectedSources, customConfig);
|
const enhancedSources = await enhanceSourcesWithMetadata(selectedSources, appConfig);
|
||||||
|
|
||||||
if (enhancedSources.length > 0) {
|
if (enhancedSources.length > 0) {
|
||||||
const fileSearchAttachment = {
|
const fileSearchAttachment = {
|
||||||
@@ -110,10 +118,10 @@ function applyCitationLimits(sources, maxCitations, maxCitationsPerFile) {
|
|||||||
/**
|
/**
|
||||||
* Enhance sources with file metadata from database
|
* Enhance sources with file metadata from database
|
||||||
* @param {Array} sources - Selected sources
|
* @param {Array} sources - Selected sources
|
||||||
* @param {Object} customConfig - Custom configuration
|
* @param {AppConfig} appConfig - Custom configuration
|
||||||
* @returns {Promise<Array>} Enhanced sources
|
* @returns {Promise<Array>} Enhanced sources
|
||||||
*/
|
*/
|
||||||
async function enhanceSourcesWithMetadata(sources, customConfig) {
|
async function enhanceSourcesWithMetadata(sources, appConfig) {
|
||||||
const fileIds = [...new Set(sources.map((source) => source.fileId))];
|
const fileIds = [...new Set(sources.map((source) => source.fileId))];
|
||||||
|
|
||||||
let fileMetadataMap = {};
|
let fileMetadataMap = {};
|
||||||
@@ -129,7 +137,7 @@ async function enhanceSourcesWithMetadata(sources, customConfig) {
|
|||||||
|
|
||||||
return sources.map((source) => {
|
return sources.map((source) => {
|
||||||
const fileRecord = fileMetadataMap[source.fileId] || {};
|
const fileRecord = fileMetadataMap[source.fileId] || {};
|
||||||
const configuredStorageType = fileRecord.source || customConfig?.fileStrategy || 'local';
|
const configuredStorageType = fileRecord.source || appConfig?.fileStrategy || FileSources.local;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...source,
|
...source,
|
||||||
|
|||||||
@@ -43,8 +43,7 @@ async function getCodeOutputDownloadStream(fileIdentifier, apiKey) {
|
|||||||
/**
|
/**
|
||||||
* Uploads a file to the Code Environment server.
|
* Uploads a file to the Code Environment server.
|
||||||
* @param {Object} params - The params object.
|
* @param {Object} params - The params object.
|
||||||
* @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id`
|
* @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id` representing the user
|
||||||
* representing the user, and an `app.locals.paths` object with an `uploads` path.
|
|
||||||
* @param {import('fs').ReadStream | import('stream').Readable} params.stream - The read stream for the file.
|
* @param {import('fs').ReadStream | import('stream').Readable} params.stream - The read stream for the file.
|
||||||
* @param {string} params.filename - The name of the file.
|
* @param {string} params.filename - The name of the file.
|
||||||
* @param {string} params.apiKey - The API key for authentication.
|
* @param {string} params.apiKey - The API key for authentication.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const { filterFilesByAgentAccess } = require('~/server/services/Files/permission
|
|||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
const { convertImage } = require('~/server/services/Files/images/convert');
|
const { convertImage } = require('~/server/services/Files/images/convert');
|
||||||
const { createFile, getFiles, updateFile } = require('~/models/File');
|
const { createFile, getFiles, updateFile } = require('~/models/File');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process OpenAI image files, convert to target format, save and return file metadata.
|
* Process OpenAI image files, convert to target format, save and return file metadata.
|
||||||
@@ -38,6 +39,7 @@ const processCodeOutput = async ({
|
|||||||
messageId,
|
messageId,
|
||||||
session_id,
|
session_id,
|
||||||
}) => {
|
}) => {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
const baseURL = getCodeBaseURL();
|
const baseURL = getCodeBaseURL();
|
||||||
const fileExt = path.extname(name);
|
const fileExt = path.extname(name);
|
||||||
@@ -77,10 +79,10 @@ const processCodeOutput = async ({
|
|||||||
filename: name,
|
filename: name,
|
||||||
conversationId,
|
conversationId,
|
||||||
user: req.user.id,
|
user: req.user.id,
|
||||||
type: `image/${req.app.locals.imageOutputType}`,
|
type: `image/${appConfig.imageOutputType}`,
|
||||||
createdAt: formattedDate,
|
createdAt: formattedDate,
|
||||||
updatedAt: formattedDate,
|
updatedAt: formattedDate,
|
||||||
source: req.app.locals.fileStrategy,
|
source: appConfig.fileStrategy,
|
||||||
context: FileContext.execute_code,
|
context: FileContext.execute_code,
|
||||||
};
|
};
|
||||||
createFile(file, true);
|
createFile(file, true);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { resizeImageBuffer } = require('../images/resize');
|
const { resizeImageBuffer } = require('../images/resize');
|
||||||
const { updateUser, updateFile } = require('~/models');
|
const { updateUser, updateFile } = require('~/models');
|
||||||
const { saveBufferToFirebase } = require('./crud');
|
const { saveBufferToFirebase } = require('./crud');
|
||||||
@@ -11,8 +12,7 @@ const { saveBufferToFirebase } = require('./crud');
|
|||||||
* resolution.
|
* resolution.
|
||||||
*
|
*
|
||||||
* @param {Object} params - The params object.
|
* @param {Object} params - The params object.
|
||||||
* @param {Express.Request} params.req - The request object from Express. It should have a `user` property with an `id`
|
* @param {Express.Request} params.req - The request object from Express. It should have a `user` property with an `id` representing the user
|
||||||
* representing the user, and an `app.locals.paths` object with an `imageOutput` path.
|
|
||||||
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
||||||
* have a `path` property that points to the location of the uploaded file.
|
* have a `path` property that points to the location of the uploaded file.
|
||||||
* @param {EModelEndpoint} params.endpoint - The params object.
|
* @param {EModelEndpoint} params.endpoint - The params object.
|
||||||
@@ -26,6 +26,7 @@ const { saveBufferToFirebase } = require('./crud');
|
|||||||
* - height: The height of the converted image.
|
* - height: The height of the converted image.
|
||||||
*/
|
*/
|
||||||
async function uploadImageToFirebase({ req, file, file_id, endpoint, resolution = 'high' }) {
|
async function uploadImageToFirebase({ req, file, file_id, endpoint, resolution = 'high' }) {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const inputFilePath = file.path;
|
const inputFilePath = file.path;
|
||||||
const inputBuffer = await fs.promises.readFile(inputFilePath);
|
const inputBuffer = await fs.promises.readFile(inputFilePath);
|
||||||
const {
|
const {
|
||||||
@@ -38,11 +39,11 @@ async function uploadImageToFirebase({ req, file, file_id, endpoint, resolution
|
|||||||
|
|
||||||
let webPBuffer;
|
let webPBuffer;
|
||||||
let fileName = `${file_id}__${path.basename(inputFilePath)}`;
|
let fileName = `${file_id}__${path.basename(inputFilePath)}`;
|
||||||
const targetExtension = `.${req.app.locals.imageOutputType}`;
|
const targetExtension = `.${appConfig.imageOutputType}`;
|
||||||
if (extension.toLowerCase() === targetExtension) {
|
if (extension.toLowerCase() === targetExtension) {
|
||||||
webPBuffer = resizedBuffer;
|
webPBuffer = resizedBuffer;
|
||||||
} else {
|
} else {
|
||||||
webPBuffer = await sharp(resizedBuffer).toFormat(req.app.locals.imageOutputType).toBuffer();
|
webPBuffer = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer();
|
||||||
// Replace or append the correct extension
|
// Replace or append the correct extension
|
||||||
const extRegExp = new RegExp(path.extname(fileName) + '$');
|
const extRegExp = new RegExp(path.extname(fileName) + '$');
|
||||||
fileName = fileName.replace(extRegExp, targetExtension);
|
fileName = fileName.replace(extRegExp, targetExtension);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const axios = require('axios');
|
|||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { EModelEndpoint } = require('librechat-data-provider');
|
const { EModelEndpoint } = require('librechat-data-provider');
|
||||||
const { generateShortLivedToken } = require('~/server/services/AuthService');
|
const { generateShortLivedToken } = require('~/server/services/AuthService');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { getBufferMetadata } = require('~/server/utils');
|
const { getBufferMetadata } = require('~/server/utils');
|
||||||
const paths = require('~/config/paths');
|
const paths = require('~/config/paths');
|
||||||
|
|
||||||
@@ -45,7 +46,8 @@ async function saveLocalFile(file, outputPath, outputFilename) {
|
|||||||
* @throws Will throw an error if the image saving process fails.
|
* @throws Will throw an error if the image saving process fails.
|
||||||
*/
|
*/
|
||||||
const saveLocalImage = async (req, file, filename) => {
|
const saveLocalImage = async (req, file, filename) => {
|
||||||
const imagePath = req.app.locals.paths.imageOutput;
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
const imagePath = appConfig.paths.imageOutput;
|
||||||
const outputPath = path.join(imagePath, req.user.id ?? '');
|
const outputPath = path.join(imagePath, req.user.id ?? '');
|
||||||
await saveLocalFile(file, outputPath, filename);
|
await saveLocalFile(file, outputPath, filename);
|
||||||
};
|
};
|
||||||
@@ -191,8 +193,7 @@ const unlinkFile = async (filepath) => {
|
|||||||
* Deletes a file from the filesystem. This function takes a file object, constructs the full path, and
|
* Deletes a file from the filesystem. This function takes a file object, constructs the full path, and
|
||||||
* verifies the path's validity before deleting the file. If the path is invalid, an error is thrown.
|
* verifies the path's validity before deleting the file. If the path is invalid, an error is thrown.
|
||||||
*
|
*
|
||||||
* @param {Express.Request} req - The request object from Express. It should have an `app.locals.paths` object with
|
* @param {Express.Request} req - The request object from Express.
|
||||||
* a `publicPath` property.
|
|
||||||
* @param {MongoFile} file - The file object to be deleted. It should have a `filepath` property that is
|
* @param {MongoFile} file - The file object to be deleted. It should have a `filepath` property that is
|
||||||
* a string representing the path of the file relative to the publicPath.
|
* a string representing the path of the file relative to the publicPath.
|
||||||
*
|
*
|
||||||
@@ -201,7 +202,8 @@ const unlinkFile = async (filepath) => {
|
|||||||
* file path is invalid or if there is an error in deletion.
|
* file path is invalid or if there is an error in deletion.
|
||||||
*/
|
*/
|
||||||
const deleteLocalFile = async (req, file) => {
|
const deleteLocalFile = async (req, file) => {
|
||||||
const { publicPath, uploads } = req.app.locals.paths;
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
const { publicPath, uploads } = appConfig.paths;
|
||||||
|
|
||||||
/** Filepath stripped of query parameters (e.g., ?manual=true) */
|
/** Filepath stripped of query parameters (e.g., ?manual=true) */
|
||||||
const cleanFilepath = file.filepath.split('?')[0];
|
const cleanFilepath = file.filepath.split('?')[0];
|
||||||
@@ -256,8 +258,7 @@ const deleteLocalFile = async (req, file) => {
|
|||||||
* Uploads a file to the specified upload directory.
|
* Uploads a file to the specified upload directory.
|
||||||
*
|
*
|
||||||
* @param {Object} params - The params object.
|
* @param {Object} params - The params object.
|
||||||
* @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id`
|
* @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id` representing the user
|
||||||
* representing the user, and an `app.locals.paths` object with an `uploads` path.
|
|
||||||
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
||||||
* have a `path` property that points to the location of the uploaded file.
|
* have a `path` property that points to the location of the uploaded file.
|
||||||
* @param {string} params.file_id - The file ID.
|
* @param {string} params.file_id - The file ID.
|
||||||
@@ -268,11 +269,12 @@ const deleteLocalFile = async (req, file) => {
|
|||||||
* - bytes: The size of the file in bytes.
|
* - bytes: The size of the file in bytes.
|
||||||
*/
|
*/
|
||||||
async function uploadLocalFile({ req, file, file_id }) {
|
async function uploadLocalFile({ req, file, file_id }) {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const inputFilePath = file.path;
|
const inputFilePath = file.path;
|
||||||
const inputBuffer = await fs.promises.readFile(inputFilePath);
|
const inputBuffer = await fs.promises.readFile(inputFilePath);
|
||||||
const bytes = Buffer.byteLength(inputBuffer);
|
const bytes = Buffer.byteLength(inputBuffer);
|
||||||
|
|
||||||
const { uploads } = req.app.locals.paths;
|
const { uploads } = appConfig.paths;
|
||||||
const userPath = path.join(uploads, req.user.id);
|
const userPath = path.join(uploads, req.user.id);
|
||||||
|
|
||||||
if (!fs.existsSync(userPath)) {
|
if (!fs.existsSync(userPath)) {
|
||||||
@@ -295,8 +297,9 @@ async function uploadLocalFile({ req, file, file_id }) {
|
|||||||
* @param {string} filepath - The filepath.
|
* @param {string} filepath - The filepath.
|
||||||
* @returns {ReadableStream} A readable stream of the file.
|
* @returns {ReadableStream} A readable stream of the file.
|
||||||
*/
|
*/
|
||||||
function getLocalFileStream(req, filepath) {
|
async function getLocalFileStream(req, filepath) {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
if (filepath.includes('/uploads/')) {
|
if (filepath.includes('/uploads/')) {
|
||||||
const basePath = filepath.split('/uploads/')[1];
|
const basePath = filepath.split('/uploads/')[1];
|
||||||
|
|
||||||
@@ -305,8 +308,8 @@ function getLocalFileStream(req, filepath) {
|
|||||||
throw new Error(`Invalid file path: ${filepath}`);
|
throw new Error(`Invalid file path: ${filepath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = path.join(req.app.locals.paths.uploads, basePath);
|
const fullPath = path.join(appConfig.paths.uploads, basePath);
|
||||||
const uploadsDir = req.app.locals.paths.uploads;
|
const uploadsDir = appConfig.paths.uploads;
|
||||||
|
|
||||||
const rel = path.relative(uploadsDir, fullPath);
|
const rel = path.relative(uploadsDir, fullPath);
|
||||||
if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) {
|
if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) {
|
||||||
@@ -323,8 +326,8 @@ function getLocalFileStream(req, filepath) {
|
|||||||
throw new Error(`Invalid file path: ${filepath}`);
|
throw new Error(`Invalid file path: ${filepath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = path.join(req.app.locals.paths.imageOutput, basePath);
|
const fullPath = path.join(appConfig.paths.imageOutput, basePath);
|
||||||
const publicDir = req.app.locals.paths.imageOutput;
|
const publicDir = appConfig.paths.imageOutput;
|
||||||
|
|
||||||
const rel = path.relative(publicDir, fullPath);
|
const rel = path.relative(publicDir, fullPath);
|
||||||
if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) {
|
if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { resizeImageBuffer } = require('../images/resize');
|
const { resizeImageBuffer } = require('../images/resize');
|
||||||
const { updateUser, updateFile } = require('~/models');
|
const { updateUser, updateFile } = require('~/models');
|
||||||
|
|
||||||
@@ -13,8 +14,7 @@ const { updateUser, updateFile } = require('~/models');
|
|||||||
*
|
*
|
||||||
* The original image is deleted after conversion.
|
* The original image is deleted after conversion.
|
||||||
* @param {Object} params - The params object.
|
* @param {Object} params - The params object.
|
||||||
* @param {Object} params.req - The request object from Express. It should have a `user` property with an `id`
|
* @param {Object} params.req - The request object from Express. It should have a `user` property with an `id` representing the user
|
||||||
* representing the user, and an `app.locals.paths` object with an `imageOutput` path.
|
|
||||||
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
||||||
* have a `path` property that points to the location of the uploaded file.
|
* have a `path` property that points to the location of the uploaded file.
|
||||||
* @param {string} params.file_id - The file ID.
|
* @param {string} params.file_id - The file ID.
|
||||||
@@ -29,6 +29,7 @@ const { updateUser, updateFile } = require('~/models');
|
|||||||
* - height: The height of the converted image.
|
* - height: The height of the converted image.
|
||||||
*/
|
*/
|
||||||
async function uploadLocalImage({ req, file, file_id, endpoint, resolution = 'high' }) {
|
async function uploadLocalImage({ req, file, file_id, endpoint, resolution = 'high' }) {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const inputFilePath = file.path;
|
const inputFilePath = file.path;
|
||||||
const inputBuffer = await fs.promises.readFile(inputFilePath);
|
const inputBuffer = await fs.promises.readFile(inputFilePath);
|
||||||
const {
|
const {
|
||||||
@@ -38,7 +39,7 @@ async function uploadLocalImage({ req, file, file_id, endpoint, resolution = 'hi
|
|||||||
} = await resizeImageBuffer(inputBuffer, resolution, endpoint);
|
} = await resizeImageBuffer(inputBuffer, resolution, endpoint);
|
||||||
const extension = path.extname(inputFilePath);
|
const extension = path.extname(inputFilePath);
|
||||||
|
|
||||||
const { imageOutput } = req.app.locals.paths;
|
const { imageOutput } = appConfig.paths;
|
||||||
const userPath = path.join(imageOutput, req.user.id);
|
const userPath = path.join(imageOutput, req.user.id);
|
||||||
|
|
||||||
if (!fs.existsSync(userPath)) {
|
if (!fs.existsSync(userPath)) {
|
||||||
@@ -47,7 +48,7 @@ async function uploadLocalImage({ req, file, file_id, endpoint, resolution = 'hi
|
|||||||
|
|
||||||
const fileName = `${file_id}__${path.basename(inputFilePath)}`;
|
const fileName = `${file_id}__${path.basename(inputFilePath)}`;
|
||||||
const newPath = path.join(userPath, fileName);
|
const newPath = path.join(userPath, fileName);
|
||||||
const targetExtension = `.${req.app.locals.imageOutputType}`;
|
const targetExtension = `.${appConfig.imageOutputType}`;
|
||||||
|
|
||||||
if (extension.toLowerCase() === targetExtension) {
|
if (extension.toLowerCase() === targetExtension) {
|
||||||
const bytes = Buffer.byteLength(resizedBuffer);
|
const bytes = Buffer.byteLength(resizedBuffer);
|
||||||
@@ -57,7 +58,7 @@ async function uploadLocalImage({ req, file, file_id, endpoint, resolution = 'hi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const outputFilePath = newPath.replace(extension, targetExtension);
|
const outputFilePath = newPath.replace(extension, targetExtension);
|
||||||
const data = await sharp(resizedBuffer).toFormat(req.app.locals.imageOutputType).toBuffer();
|
const data = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer();
|
||||||
await fs.promises.writeFile(outputFilePath, data);
|
await fs.promises.writeFile(outputFilePath, data);
|
||||||
const bytes = Buffer.byteLength(data);
|
const bytes = Buffer.byteLength(data);
|
||||||
const filepath = path.posix.join('/', 'images', req.user.id, path.basename(outputFilePath));
|
const filepath = path.posix.join('/', 'images', req.user.id, path.basename(outputFilePath));
|
||||||
@@ -90,7 +91,8 @@ function encodeImage(imagePath) {
|
|||||||
* @returns {Promise<[MongoFile, string]>} - A promise that resolves to an array of results from updateFile and encodeImage.
|
* @returns {Promise<[MongoFile, string]>} - A promise that resolves to an array of results from updateFile and encodeImage.
|
||||||
*/
|
*/
|
||||||
async function prepareImagesLocal(req, file) {
|
async function prepareImagesLocal(req, file) {
|
||||||
const { publicPath, imageOutput } = req.app.locals.paths;
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
const { publicPath, imageOutput } = appConfig.paths;
|
||||||
const userPath = path.join(imageOutput, req.user.id);
|
const userPath = path.join(imageOutput, req.user.id);
|
||||||
|
|
||||||
if (!fs.existsSync(userPath)) {
|
if (!fs.existsSync(userPath)) {
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ const { logger } = require('~/config');
|
|||||||
* Uploads a file that can be used across various OpenAI services.
|
* Uploads a file that can be used across various OpenAI services.
|
||||||
*
|
*
|
||||||
* @param {Object} params - The params object.
|
* @param {Object} params - The params object.
|
||||||
* @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id`
|
* @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id` representing the user
|
||||||
* representing the user, and an `app.locals.paths` object with an `imageOutput` path.
|
|
||||||
* @param {Express.Multer.File} params.file - The file uploaded to the server via multer.
|
* @param {Express.Multer.File} params.file - The file uploaded to the server via multer.
|
||||||
* @param {OpenAIClient} params.openai - The initialized OpenAI client.
|
* @param {OpenAIClient} params.openai - The initialized OpenAI client.
|
||||||
* @returns {Promise<OpenAIFile>}
|
* @returns {Promise<OpenAIFile>}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { resizeImageBuffer } = require('../images/resize');
|
const { resizeImageBuffer } = require('../images/resize');
|
||||||
const { updateUser, updateFile } = require('~/models');
|
const { updateUser, updateFile } = require('~/models');
|
||||||
const { saveBufferToS3 } = require('./crud');
|
const { saveBufferToS3 } = require('./crud');
|
||||||
@@ -12,7 +13,7 @@ const defaultBasePath = 'images';
|
|||||||
* Resizes, converts, and uploads an image file to S3.
|
* Resizes, converts, and uploads an image file to S3.
|
||||||
*
|
*
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
* @param {import('express').Request} params.req - Express request (expects user and app.locals.imageOutputType).
|
* @param {import('express').Request} params.req - Express request (expects `user` and `appConfig.imageOutputType`).
|
||||||
* @param {Express.Multer.File} params.file - File object from Multer.
|
* @param {Express.Multer.File} params.file - File object from Multer.
|
||||||
* @param {string} params.file_id - Unique file identifier.
|
* @param {string} params.file_id - Unique file identifier.
|
||||||
* @param {any} params.endpoint - Endpoint identifier used in image processing.
|
* @param {any} params.endpoint - Endpoint identifier used in image processing.
|
||||||
@@ -29,6 +30,7 @@ async function uploadImageToS3({
|
|||||||
basePath = defaultBasePath,
|
basePath = defaultBasePath,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
const inputFilePath = file.path;
|
const inputFilePath = file.path;
|
||||||
const inputBuffer = await fs.promises.readFile(inputFilePath);
|
const inputBuffer = await fs.promises.readFile(inputFilePath);
|
||||||
const {
|
const {
|
||||||
@@ -41,14 +43,12 @@ async function uploadImageToS3({
|
|||||||
|
|
||||||
let processedBuffer;
|
let processedBuffer;
|
||||||
let fileName = `${file_id}__${path.basename(inputFilePath)}`;
|
let fileName = `${file_id}__${path.basename(inputFilePath)}`;
|
||||||
const targetExtension = `.${req.app.locals.imageOutputType}`;
|
const targetExtension = `.${appConfig.imageOutputType}`;
|
||||||
|
|
||||||
if (extension.toLowerCase() === targetExtension) {
|
if (extension.toLowerCase() === targetExtension) {
|
||||||
processedBuffer = resizedBuffer;
|
processedBuffer = resizedBuffer;
|
||||||
} else {
|
} else {
|
||||||
processedBuffer = await sharp(resizedBuffer)
|
processedBuffer = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer();
|
||||||
.toFormat(req.app.locals.imageOutputType)
|
|
||||||
.toBuffer();
|
|
||||||
fileName = fileName.replace(new RegExp(path.extname(fileName) + '$'), targetExtension);
|
fileName = fileName.replace(new RegExp(path.extname(fileName) + '$'), targetExtension);
|
||||||
if (!path.extname(fileName)) {
|
if (!path.extname(fileName)) {
|
||||||
fileName += targetExtension;
|
fileName += targetExtension;
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ const { generateShortLivedToken } = require('~/server/services/AuthService');
|
|||||||
* Deletes a file from the vector database. This function takes a file object, constructs the full path, and
|
* Deletes a file from the vector database. This function takes a file object, constructs the full path, and
|
||||||
* verifies the path's validity before deleting the file. If the path is invalid, an error is thrown.
|
* verifies the path's validity before deleting the file. If the path is invalid, an error is thrown.
|
||||||
*
|
*
|
||||||
* @param {ServerRequest} req - The request object from Express. It should have an `app.locals.paths` object with
|
* @param {ServerRequest} req - The request object from Express.
|
||||||
* a `publicPath` property.
|
|
||||||
* @param {MongoFile} file - The file object to be deleted. It should have a `filepath` property that is
|
* @param {MongoFile} file - The file object to be deleted. It should have a `filepath` property that is
|
||||||
* a string representing the path of the file relative to the publicPath.
|
* a string representing the path of the file relative to the publicPath.
|
||||||
*
|
*
|
||||||
@@ -54,8 +53,7 @@ const deleteVectors = async (req, file) => {
|
|||||||
* Uploads a file to the configured Vector database
|
* Uploads a file to the configured Vector database
|
||||||
*
|
*
|
||||||
* @param {Object} params - The params object.
|
* @param {Object} params - The params object.
|
||||||
* @param {Object} params.req - The request object from Express. It should have a `user` property with an `id`
|
* @param {Object} params.req - The request object from Express. It should have a `user` property with an `id` representing the user
|
||||||
* representing the user, and an `app.locals.paths` object with an `uploads` path.
|
|
||||||
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
* @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should
|
||||||
* have a `path` property that points to the location of the uploaded file.
|
* have a `path` property that points to the location of the uploaded file.
|
||||||
* @param {string} params.file_id - The file ID.
|
* @param {string} params.file_id - The file ID.
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
const { resizeImageBuffer } = require('./resize');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { getStrategyFunctions } = require('../strategies');
|
const { getStrategyFunctions } = require('../strategies');
|
||||||
|
const { resizeImageBuffer } = require('./resize');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,6 +18,7 @@ const { logger } = require('~/config');
|
|||||||
*/
|
*/
|
||||||
async function convertImage(req, file, resolution = 'high', basename = '') {
|
async function convertImage(req, file, resolution = 'high', basename = '') {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
let inputBuffer;
|
let inputBuffer;
|
||||||
let outputBuffer;
|
let outputBuffer;
|
||||||
let extension = path.extname(file.path ?? basename).toLowerCase();
|
let extension = path.extname(file.path ?? basename).toLowerCase();
|
||||||
@@ -39,11 +41,11 @@ async function convertImage(req, file, resolution = 'high', basename = '') {
|
|||||||
} = await resizeImageBuffer(inputBuffer, resolution);
|
} = await resizeImageBuffer(inputBuffer, resolution);
|
||||||
|
|
||||||
// Check if the file is already in target format; if it isn't, convert it:
|
// Check if the file is already in target format; if it isn't, convert it:
|
||||||
const targetExtension = `.${req.app.locals.imageOutputType}`;
|
const targetExtension = `.${appConfig.imageOutputType}`;
|
||||||
if (extension === targetExtension) {
|
if (extension === targetExtension) {
|
||||||
outputBuffer = resizedBuffer;
|
outputBuffer = resizedBuffer;
|
||||||
} else {
|
} else {
|
||||||
outputBuffer = await sharp(resizedBuffer).toFormat(req.app.locals.imageOutputType).toBuffer();
|
outputBuffer = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer();
|
||||||
extension = targetExtension;
|
extension = targetExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ async function convertImage(req, file, resolution = 'high', basename = '') {
|
|||||||
const newFileName =
|
const newFileName =
|
||||||
path.basename(file.path ?? basename, path.extname(file.path ?? basename)) + extension;
|
path.basename(file.path ?? basename, path.extname(file.path ?? basename)) + extension;
|
||||||
|
|
||||||
const { saveBuffer } = getStrategyFunctions(req.app.locals.fileStrategy);
|
const { saveBuffer } = getStrategyFunctions(appConfig.fileStrategy);
|
||||||
|
|
||||||
const savedFilePath = await saveBuffer({
|
const savedFilePath = await saveBuffer({
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
const avatar = require('./avatar');
|
const avatar = require('./avatar');
|
||||||
const convert = require('./convert');
|
const convert = require('./convert');
|
||||||
const encode = require('./encode');
|
const encode = require('./encode');
|
||||||
const parse = require('./parse');
|
|
||||||
const resize = require('./resize');
|
const resize = require('./resize');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...convert,
|
...convert,
|
||||||
...encode,
|
...encode,
|
||||||
...parse,
|
|
||||||
...resize,
|
...resize,
|
||||||
avatar,
|
avatar,
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user