mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
🛜 refactor: Streamline App Config Usage (#9234)
* WIP: app.locals refactoring
WIP: appConfig
fix: update memory configuration retrieval to use getAppConfig based on user role
fix: update comment for AppConfig interface to clarify purpose
🏷️ refactor: Update tests to use getAppConfig for endpoint configurations
ci: Update AppService tests to initialize app config instead of app.locals
ci: Integrate getAppConfig into remaining tests
refactor: Update multer storage destination to use promise-based getAppConfig and improve error handling in tests
refactor: Rename initializeAppConfig to setAppConfig and update related tests
ci: Mock getAppConfig in various tests to provide default configurations
refactor: Update convertMCPToolsToPlugins to use mcpManager for server configuration and adjust related tests
chore: rename `Config/getAppConfig` -> `Config/app`
fix: streamline OpenAI image tools configuration by removing direct appConfig dependency and using function parameters
chore: correct parameter documentation for imageOutputType in ToolService.js
refactor: remove `getCustomConfig` dependency in config route
refactor: update domain validation to use appConfig for allowed domains
refactor: use appConfig registration property
chore: remove app parameter from AppService invocation
refactor: update AppConfig interface to correct registration and turnstile configurations
refactor: remove getCustomConfig dependency and use getAppConfig in PluginController, multer, and MCP services
refactor: replace getCustomConfig with getAppConfig in STTService, TTSService, and related files
refactor: replace getCustomConfig with getAppConfig in Conversation and Message models, update tempChatRetention functions to use AppConfig type
refactor: update getAppConfig calls in Conversation and Message models to include user role for temporary chat expiration
ci: update related tests
refactor: update getAppConfig call in getCustomConfigSpeech to include user role
fix: update appConfig usage to access allowedDomains from actions instead of registration
refactor: enhance AppConfig to include fileStrategies and update related file strategy logic
refactor: update imports to use normalizeEndpointName from @librechat/api and remove redundant definitions
chore: remove deprecated unused RunManager
refactor: get balance config primarily from appConfig
refactor: remove customConfig dependency for appConfig and streamline loadConfigModels logic
refactor: remove getCustomConfig usage and use app config in file citations
refactor: consolidate endpoint loading logic into loadEndpoints function
refactor: update appConfig access to use endpoints structure across various services
refactor: implement custom endpoints configuration and streamline endpoint loading logic
refactor: update getAppConfig call to include user role parameter
refactor: streamline endpoint configuration and enhance appConfig usage across services
refactor: replace getMCPAuthMap with getUserMCPAuthMap and remove unused getCustomConfig file
refactor: add type annotation for loadedEndpoints in loadEndpoints function
refactor: move /services/Files/images/parse to TS API
chore: add missing FILE_CITATIONS permission to IRole interface
refactor: restructure toolkits to TS API
refactor: separate manifest logic into its own module
refactor: consolidate tool loading logic into a new tools module for startup logic
refactor: move interface config logic to TS API
refactor: migrate checkEmailConfig to TypeScript and update imports
refactor: add FunctionTool interface and availableTools to AppConfig
refactor: decouple caching and DB operations from AppService, make part of consolidated `getAppConfig`
WIP: fix tests
* fix: rebase conflicts
* refactor: remove app.locals references
* refactor: replace getBalanceConfig with getAppConfig in various strategies and middleware
* refactor: replace appConfig?.balance with getBalanceConfig in various controllers and clients
* test: add balance configuration to titleConvo method in AgentClient tests
* chore: remove unused `openai-chat-tokens` package
* chore: remove unused imports in initializeMCPs.js
* refactor: update balance configuration to use getAppConfig instead of getBalanceConfig
* refactor: integrate configMiddleware for centralized configuration handling
* refactor: optimize email domain validation by removing unnecessary async calls
* refactor: simplify multer storage configuration by removing async calls
* refactor: reorder imports for better readability in user.js
* refactor: replace getAppConfig calls with req.config for improved performance
* chore: replace getAppConfig calls with req.config in tests for centralized configuration handling
* chore: remove unused override config
* refactor: add configMiddleware to endpoint route and replace getAppConfig with req.config
* chore: remove customConfig parameter from TTSService constructor
* refactor: pass appConfig from request to processFileCitations for improved configuration handling
* refactor: remove configMiddleware from endpoint route and retrieve appConfig directly in getEndpointsConfig if not in `req.config`
* test: add mockAppConfig to processFileCitations tests for improved configuration handling
* fix: pass req.config to hasCustomUserVars and call without await after synchronous refactor
* fix: type safety in useExportConversation
* refactor: retrieve appConfig using getAppConfig in PluginController and remove configMiddleware from plugins route, to avoid always retrieving when plugins are cached
* chore: change `MongoUser` typedef to `IUser`
* fix: Add `user` and `config` fields to ServerRequest and update JSDoc type annotations from Express.Request to ServerRequest
* fix: remove unused setAppConfig mock from Server configuration tests
This commit is contained in:
parent
e1ad235f17
commit
9a210971f5
210 changed files with 4102 additions and 3465 deletions
|
@ -1,5 +1,7 @@
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { getBalanceConfig } = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
supportsBalanceCheck,
|
supportsBalanceCheck,
|
||||||
isAgentsEndpoint,
|
isAgentsEndpoint,
|
||||||
|
@ -15,7 +17,6 @@ const { checkBalance } = require('~/models/balanceMethods');
|
||||||
const { truncateToolCallOutputs } = require('./prompts');
|
const { truncateToolCallOutputs } = require('./prompts');
|
||||||
const { getFiles } = require('~/models/File');
|
const { getFiles } = require('~/models/File');
|
||||||
const TextStream = require('./TextStream');
|
const TextStream = require('./TextStream');
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
class BaseClient {
|
class BaseClient {
|
||||||
constructor(apiKey, options = {}) {
|
constructor(apiKey, options = {}) {
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -571,6 +574,7 @@ class BaseClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessage(message, opts = {}) {
|
async sendMessage(message, opts = {}) {
|
||||||
|
const appConfig = this.options.req?.config;
|
||||||
/** @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 } =
|
||||||
|
@ -657,9 +661,9 @@ class BaseClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const balance = this.options.req?.app?.locals?.balance;
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
if (
|
if (
|
||||||
balance?.enabled &&
|
balanceConfig?.enabled &&
|
||||||
supportsBalanceCheck[this.options.endpointType ?? this.options.endpoint]
|
supportsBalanceCheck[this.options.endpointType ?? this.options.endpoint]
|
||||||
) {
|
) {
|
||||||
await checkBalance({
|
await checkBalance({
|
||||||
|
@ -758,6 +762,7 @@ class BaseClient {
|
||||||
usage,
|
usage,
|
||||||
promptTokens,
|
promptTokens,
|
||||||
completionTokens,
|
completionTokens,
|
||||||
|
balance: balanceConfig,
|
||||||
model: responseMessage.model,
|
model: responseMessage.model,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,11 +36,11 @@ const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||||
const { addSpaceIfNeeded, sleep } = require('~/server/utils');
|
const { addSpaceIfNeeded, sleep } = require('~/server/utils');
|
||||||
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 +618,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 +662,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 +688,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 = this.options.req?.config;
|
||||||
this.conversationId = conversationId;
|
this.conversationId = conversationId;
|
||||||
|
|
||||||
if (this.options.attachments) {
|
if (this.options.attachments) {
|
||||||
|
@ -730,8 +717,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 +1106,7 @@ ${convo}
|
||||||
}
|
}
|
||||||
|
|
||||||
async chatCompletion({ payload, onProgress, abortController = null }) {
|
async chatCompletion({ payload, onProgress, abortController = null }) {
|
||||||
|
const appConfig = this.options.req?.config;
|
||||||
let error = null;
|
let error = null;
|
||||||
let intermediateReply = [];
|
let intermediateReply = [];
|
||||||
const errorCallback = (err) => (error = err);
|
const errorCallback = (err) => (error = err);
|
||||||
|
@ -1165,8 +1152,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];
|
||||||
|
|
|
@ -11,14 +11,14 @@ const paths = require('~/config/paths');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const displayMessage =
|
const displayMessage =
|
||||||
'Stable Diffusion 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.';
|
"Stable Diffusion 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.";
|
||||||
|
|
||||||
class StableDiffusionAPI extends Tool {
|
class StableDiffusionAPI extends Tool {
|
||||||
constructor(fields) {
|
constructor(fields) {
|
||||||
super();
|
super();
|
||||||
/** @type {string} User ID */
|
/** @type {string} User ID */
|
||||||
this.userId = fields.userId;
|
this.userId = fields.userId;
|
||||||
/** @type {Express.Request | undefined} Express Request object, only provided by ToolService */
|
/** @type {ServerRequest | undefined} Express Request object, only provided by ToolService */
|
||||||
this.req = fields.req;
|
this.req = fields.req;
|
||||||
/** @type {boolean} Used to initialize the Tool without necessary variables. */
|
/** @type {boolean} Used to initialize the Tool without necessary variables. */
|
||||||
this.override = fields.override ?? false;
|
this.override = fields.override ?? false;
|
||||||
|
@ -44,7 +44,7 @@ class StableDiffusionAPI extends Tool {
|
||||||
// "negative_prompt":"semi-realistic, cgi, 3d, render, sketch, cartoon, drawing, anime, out of frame, low quality, ugly, mutation, deformed"
|
// "negative_prompt":"semi-realistic, cgi, 3d, render, sketch, cartoon, drawing, anime, out of frame, low quality, ugly, mutation, deformed"
|
||||||
// - Generate images only once per human query unless explicitly requested by the user`;
|
// - Generate images only once per human query unless explicitly requested by the user`;
|
||||||
this.description =
|
this.description =
|
||||||
'You can generate images using text with \'stable-diffusion\'. This tool is exclusively for visual content.';
|
"You can generate images using text with 'stable-diffusion'. This tool is exclusively for visual content.";
|
||||||
this.schema = z.object({
|
this.schema = z.object({
|
||||||
prompt: z
|
prompt: z
|
||||||
.string()
|
.string()
|
||||||
|
|
|
@ -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,8 +42,7 @@ 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,
|
||||||
|
@ -56,24 +55,9 @@ function createYouTubeTools(fields = {}) {
|
||||||
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');
|
||||||
|
@ -97,24 +81,9 @@ Example: query="cooking pasta tutorials" maxResults=3`,
|
||||||
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');
|
||||||
|
@ -132,30 +101,9 @@ Example: url="https://youtube.com/watch?v=abc123" or url="abc123"`,
|
||||||
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');
|
||||||
|
@ -181,21 +129,7 @@ Example: url="abc123" maxResults=20`,
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to fetch transcript: ${error.message}`);
|
throw new Error(`Failed to fetch transcript: ${error.message}`);
|
||||||
}
|
}
|
||||||
},
|
}, ytToolkit.youtube_transcript);
|
||||||
{
|
|
||||||
name: 'youtube_transcript',
|
|
||||||
description: `Fetch and parse the transcript/captions of a YouTube video.
|
|
||||||
- Required: url (full YouTube URL or video ID)
|
|
||||||
- Returns: Full video transcript as plain text
|
|
||||||
- Use for: Content analysis, summarization, translation reference
|
|
||||||
- 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,18 +121,21 @@ const getAuthFields = (toolKey) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {object} object
|
* @param {object} params
|
||||||
* @param {string} object.user
|
* @param {string} params.user
|
||||||
* @param {Record<string, Record<string, string>>} [object.userMCPAuthMap]
|
* @param {Record<string, Record<string, string>>} [object.userMCPAuthMap]
|
||||||
* @param {AbortSignal} [object.signal]
|
* @param {AbortSignal} [object.signal]
|
||||||
* @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 ({
|
||||||
|
@ -146,6 +149,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,
|
||||||
|
@ -204,6 +210,8 @@ const loadTools = async ({
|
||||||
...authValues,
|
...authValues,
|
||||||
isAgent: !!agent,
|
isAgent: !!agent,
|
||||||
req: options.req,
|
req: options.req,
|
||||||
|
imageOutputType,
|
||||||
|
fileStrategy,
|
||||||
imageFiles,
|
imageFiles,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -219,7 +227,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,
|
||||||
|
@ -277,11 +285,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');
|
||||||
|
|
||||||
|
|
|
@ -681,7 +681,7 @@ const getListAgents = async (searchParameter) => {
|
||||||
* This function also updates the corresponding projects to include or exclude the agent ID.
|
* This function also updates the corresponding projects to include or exclude the agent ID.
|
||||||
*
|
*
|
||||||
* @param {Object} params - Parameters for updating the agent's projects.
|
* @param {Object} params - Parameters for updating the agent's projects.
|
||||||
* @param {MongoUser} params.user - Parameters for updating the agent's projects.
|
* @param {IUser} params.user - Parameters for updating the agent's projects.
|
||||||
* @param {string} params.agentId - The ID of the agent to update.
|
* @param {string} params.agentId - The ID of the agent to update.
|
||||||
* @param {string[]} [params.projectIds] - Array of project IDs to add to the agent.
|
* @param {string[]} [params.projectIds] - Array of project IDs to add to the agent.
|
||||||
* @param {string[]} [params.removeProjectIds] - Array of project IDs to remove from the agent.
|
* @param {string[]} [params.removeProjectIds] - Array of project IDs to remove from the agent.
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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 { getMessages, deleteMessages } = require('./Message');
|
const { getMessages, deleteMessages } = require('./Message');
|
||||||
const { Conversation } = require('~/db/models');
|
const { Conversation } = require('~/db/models');
|
||||||
|
|
||||||
|
@ -102,8 +101,8 @@ module.exports = {
|
||||||
|
|
||||||
if (req?.body?.isTemporary) {
|
if (req?.body?.isTemporary) {
|
||||||
try {
|
try {
|
||||||
const customConfig = await getCustomConfig();
|
const appConfig = req.config;
|
||||||
update.expiredAt = createTempChatExpirationDate(customConfig);
|
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,8 @@ 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 { getMessages, deleteMessages } = require('./Message');
|
const { getMessages, deleteMessages } = require('./Message');
|
||||||
|
|
||||||
const { Conversation } = require('~/db/models');
|
const { Conversation } = require('~/db/models');
|
||||||
|
@ -50,6 +49,11 @@ describe('Conversation Operations', () => {
|
||||||
mockReq = {
|
mockReq = {
|
||||||
user: { id: 'user123' },
|
user: { id: 'user123' },
|
||||||
body: {},
|
body: {},
|
||||||
|
config: {
|
||||||
|
interfaceConfig: {
|
||||||
|
temporaryChatRetention: 24, // Default 24 hours
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
mockConversationData = {
|
mockConversationData = {
|
||||||
|
@ -118,12 +122,8 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 24;
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 24,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
@ -167,12 +167,8 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 48;
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 48,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
@ -194,12 +190,8 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 0.5; // Half hour - should be clamped to 1 hour
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 0.5, // Half hour - should be clamped to 1 hour
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
@ -221,12 +213,8 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 10000; // Should be clamped to 8760 hours
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 10000, // Should be clamped to 8760 hours
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
@ -247,22 +235,36 @@ describe('Conversation Operations', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle getCustomConfig errors gracefully', async () => {
|
it('should handle missing config gracefully', async () => {
|
||||||
// Mock getCustomConfig to throw an error
|
// Simulate missing config - should use default retention period
|
||||||
getCustomConfig.mockRejectedValue(new Error('Config service unavailable'));
|
delete mockReq.config;
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
const beforeSave = new Date();
|
||||||
const result = await saveConvo(mockReq, mockConversationData);
|
const result = await saveConvo(mockReq, mockConversationData);
|
||||||
|
const afterSave = new Date();
|
||||||
|
|
||||||
// Should still save the conversation but with expiredAt as null
|
// Should still save the conversation with default retention period (30 days)
|
||||||
expect(result.conversationId).toBe(mockConversationData.conversationId);
|
expect(result.conversationId).toBe(mockConversationData.conversationId);
|
||||||
expect(result.expiredAt).toBeNull();
|
expect(result.expiredAt).toBeDefined();
|
||||||
|
expect(result.expiredAt).toBeInstanceOf(Date);
|
||||||
|
|
||||||
|
// Verify expiredAt is approximately 30 days in the future (720 hours)
|
||||||
|
const expectedExpirationTime = new Date(beforeSave.getTime() + 720 * 60 * 60 * 1000);
|
||||||
|
const actualExpirationTime = new Date(result.expiredAt);
|
||||||
|
|
||||||
|
expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual(
|
||||||
|
expectedExpirationTime.getTime() - 1000,
|
||||||
|
);
|
||||||
|
expect(actualExpirationTime.getTime()).toBeLessThanOrEqual(
|
||||||
|
new Date(afterSave.getTime() + 720 * 60 * 60 * 1000 + 1000).getTime(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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({});
|
mockReq.config = {}; // Empty config
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
@ -285,11 +287,7 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 24;
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 24,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
const firstSave = await saveConvo(mockReq, mockConversationData);
|
const firstSave = await saveConvo(mockReq, mockConversationData);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
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 { Message } = require('~/db/models');
|
const { Message } = require('~/db/models');
|
||||||
|
|
||||||
const idSchema = z.string().uuid();
|
const idSchema = z.string().uuid();
|
||||||
|
@ -11,7 +10,7 @@ const idSchema = z.string().uuid();
|
||||||
*
|
*
|
||||||
* @async
|
* @async
|
||||||
* @function saveMessage
|
* @function saveMessage
|
||||||
* @param {Express.Request} req - The request object containing user information.
|
* @param {ServerRequest} req - The request object containing user information.
|
||||||
* @param {Object} params - The message data object.
|
* @param {Object} params - The message data object.
|
||||||
* @param {string} params.endpoint - The endpoint where the message originated.
|
* @param {string} params.endpoint - The endpoint where the message originated.
|
||||||
* @param {string} params.iconURL - The URL of the sender's icon.
|
* @param {string} params.iconURL - The URL of the sender's icon.
|
||||||
|
@ -57,8 +56,8 @@ async function saveMessage(req, params, metadata) {
|
||||||
|
|
||||||
if (req?.body?.isTemporary) {
|
if (req?.body?.isTemporary) {
|
||||||
try {
|
try {
|
||||||
const customConfig = await getCustomConfig();
|
const appConfig = req.config;
|
||||||
update.expiredAt = createTempChatExpirationDate(customConfig);
|
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,7 @@ 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');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('mongoose').Model<import('@librechat/data-schemas').IMessage>}
|
* @type {import('mongoose').Model<import('@librechat/data-schemas').IMessage>}
|
||||||
|
@ -44,6 +43,11 @@ describe('Message Operations', () => {
|
||||||
|
|
||||||
mockReq = {
|
mockReq = {
|
||||||
user: { id: 'user123' },
|
user: { id: 'user123' },
|
||||||
|
config: {
|
||||||
|
interfaceConfig: {
|
||||||
|
temporaryChatRetention: 24, // Default 24 hours
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
mockMessageData = {
|
mockMessageData = {
|
||||||
|
@ -326,12 +330,8 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 24;
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 24,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
@ -375,12 +375,8 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 48;
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 48,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
@ -402,12 +398,8 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 0.5; // Half hour - should be clamped to 1 hour
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 0.5, // Half hour - should be clamped to 1 hour
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
@ -429,12 +421,8 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 10000; // Should be clamped to 8760 hours
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 10000, // Should be clamped to 8760 hours
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
@ -455,22 +443,36 @@ describe('Message Operations', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle getCustomConfig errors gracefully', async () => {
|
it('should handle missing config gracefully', async () => {
|
||||||
// Mock getCustomConfig to throw an error
|
// Simulate missing config - should use default retention period
|
||||||
getCustomConfig.mockRejectedValue(new Error('Config service unavailable'));
|
delete mockReq.config;
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
const beforeSave = new Date();
|
||||||
const result = await saveMessage(mockReq, mockMessageData);
|
const result = await saveMessage(mockReq, mockMessageData);
|
||||||
|
const afterSave = new Date();
|
||||||
|
|
||||||
// Should still save the message but with expiredAt as null
|
// Should still save the message with default retention period (30 days)
|
||||||
expect(result.messageId).toBe('msg123');
|
expect(result.messageId).toBe('msg123');
|
||||||
expect(result.expiredAt).toBeNull();
|
expect(result.expiredAt).toBeDefined();
|
||||||
|
expect(result.expiredAt).toBeInstanceOf(Date);
|
||||||
|
|
||||||
|
// Verify expiredAt is approximately 30 days in the future (720 hours)
|
||||||
|
const expectedExpirationTime = new Date(beforeSave.getTime() + 720 * 60 * 60 * 1000);
|
||||||
|
const actualExpirationTime = new Date(result.expiredAt);
|
||||||
|
|
||||||
|
expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual(
|
||||||
|
expectedExpirationTime.getTime() - 1000,
|
||||||
|
);
|
||||||
|
expect(actualExpirationTime.getTime()).toBeLessThanOrEqual(
|
||||||
|
new Date(afterSave.getTime() + 720 * 60 * 60 * 1000 + 1000).getTime(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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({});
|
mockReq.config = {}; // Empty config
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
|
|
||||||
|
@ -493,11 +495,7 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 24;
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 24,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
const savedMessage = await saveMessage(mockReq, mockMessageData);
|
const savedMessage = await saveMessage(mockReq, mockMessageData);
|
||||||
|
@ -520,11 +518,7 @@ 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({
|
mockReq.config.interfaceConfig.temporaryChatRetention = 24;
|
||||||
interface: {
|
|
||||||
temporaryChatRetention: 24,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
mockReq.body = { isTemporary: true };
|
mockReq.body = { isTemporary: true };
|
||||||
const firstSave = await saveMessage(mockReq, mockMessageData);
|
const firstSave = await saveMessage(mockReq, mockMessageData);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -118,7 +118,7 @@ const addIntervalToDate = (date, value, unit) => {
|
||||||
* @async
|
* @async
|
||||||
* @function
|
* @function
|
||||||
* @param {Object} params - The function parameters.
|
* @param {Object} params - The function parameters.
|
||||||
* @param {Express.Request} params.req - The Express request object.
|
* @param {ServerRequest} params.req - The Express request object.
|
||||||
* @param {Express.Response} params.res - The Express response object.
|
* @param {Express.Response} params.res - The Express response object.
|
||||||
* @param {Object} params.txData - The transaction data.
|
* @param {Object} params.txData - The transaction data.
|
||||||
* @param {string} params.txData.user - The user ID or identifier.
|
* @param {string} params.txData.user - The user ID or identifier.
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -3,7 +3,7 @@ const bcrypt = require('bcryptjs');
|
||||||
/**
|
/**
|
||||||
* Compares the provided password with the user's password.
|
* Compares the provided password with the user's password.
|
||||||
*
|
*
|
||||||
* @param {MongoUser} user - The user to compare the password for.
|
* @param {IUser} user - The user to compare the password for.
|
||||||
* @param {string} candidatePassword - The password to test against the user's password.
|
* @param {string} candidatePassword - The password to test against the user's password.
|
||||||
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating if the password matches.
|
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating if the password matches.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -97,7 +97,6 @@
|
||||||
"nodemailer": "^6.9.15",
|
"nodemailer": "^6.9.15",
|
||||||
"ollama": "^0.5.0",
|
"ollama": "^0.5.0",
|
||||||
"openai": "^5.10.1",
|
"openai": "^5.10.1",
|
||||||
"openai-chat-tokens": "^0.2.8",
|
|
||||||
"openid-client": "^6.5.0",
|
"openid-client": "^6.5.0",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-apple": "^2.0.2",
|
"passport-apple": "^2.0.2",
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
const { CacheKeys } = require('librechat-data-provider');
|
|
||||||
const { loadOverrideConfig } = require('~/server/services/Config');
|
|
||||||
const { getLogStores } = require('~/cache');
|
|
||||||
|
|
||||||
async function overrideController(req, res) {
|
|
||||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
|
||||||
let overrideConfig = await cache.get(CacheKeys.OVERRIDE_CONFIG);
|
|
||||||
if (overrideConfig) {
|
|
||||||
res.send(overrideConfig);
|
|
||||||
return;
|
|
||||||
} else if (overrideConfig === false) {
|
|
||||||
res.send(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
overrideConfig = await loadOverrideConfig();
|
|
||||||
const { endpointsConfig, modelsConfig } = overrideConfig;
|
|
||||||
if (endpointsConfig) {
|
|
||||||
await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig);
|
|
||||||
}
|
|
||||||
if (modelsConfig) {
|
|
||||||
await cache.set(CacheKeys.MODELS_CONFIG, modelsConfig);
|
|
||||||
}
|
|
||||||
await cache.set(CacheKeys.OVERRIDE_CONFIG, overrideConfig);
|
|
||||||
res.send(JSON.stringify(overrideConfig));
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = overrideController;
|
|
|
@ -7,14 +7,9 @@ const {
|
||||||
convertMCPToolToPlugin,
|
convertMCPToolToPlugin,
|
||||||
convertMCPToolsToPlugins,
|
convertMCPToolsToPlugins,
|
||||||
} = require('@librechat/api');
|
} = require('@librechat/api');
|
||||||
const {
|
const { getCachedTools, setCachedTools, mergeUserTools } = require('~/server/services/Config');
|
||||||
getCachedTools,
|
|
||||||
setCachedTools,
|
|
||||||
mergeUserTools,
|
|
||||||
getCustomConfig,
|
|
||||||
} = require('~/server/services/Config');
|
|
||||||
const { loadAndFormatTools } = require('~/server/services/ToolService');
|
|
||||||
const { availableTools, toolkits } = require('~/app/clients/tools');
|
const { availableTools, toolkits } = require('~/app/clients/tools');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { getMCPManager } = require('~/config');
|
const { getMCPManager } = require('~/config');
|
||||||
const { getLogStores } = require('~/cache');
|
const { getLogStores } = require('~/cache');
|
||||||
|
|
||||||
|
@ -27,8 +22,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;
|
||||||
/** @type {import('@librechat/api').LCManifestTool[]} */
|
/** @type {import('@librechat/api').LCManifestTool[]} */
|
||||||
const pluginManifest = availableTools;
|
const pluginManifest = availableTools;
|
||||||
|
|
||||||
|
@ -74,13 +70,14 @@ const getAvailableTools = async (req, res) => {
|
||||||
logger.warn('[getAvailableTools] User ID not found in request');
|
logger.warn('[getAvailableTools] User ID not found in request');
|
||||||
return res.status(401).json({ message: 'Unauthorized' });
|
return res.status(401).json({ message: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
const customConfig = await getCustomConfig();
|
|
||||||
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 mcpManager = getMCPManager();
|
||||||
const userPlugins =
|
const userPlugins =
|
||||||
cachedUserTools != null
|
cachedUserTools != null
|
||||||
? convertMCPToolsToPlugins({ functionTools: cachedUserTools, customConfig })
|
? convertMCPToolsToPlugins({ functionTools: cachedUserTools, mcpManager })
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (cachedToolsArray != null && userPlugins != null) {
|
if (cachedToolsArray != null && userPlugins != null) {
|
||||||
|
@ -93,28 +90,19 @@ const getAvailableTools = async (req, res) => {
|
||||||
let toolDefinitions = await getCachedTools({ includeGlobal: true });
|
let toolDefinitions = await getCachedTools({ includeGlobal: true });
|
||||||
let prelimCachedTools;
|
let prelimCachedTools;
|
||||||
|
|
||||||
// TODO: this is a temp fix until app config is refactored
|
|
||||||
if (!toolDefinitions) {
|
|
||||||
toolDefinitions = loadAndFormatTools({
|
|
||||||
adminFilter: req.app.locals?.filteredTools,
|
|
||||||
adminIncluded: req.app.locals?.includedTools,
|
|
||||||
directory: req.app.locals?.paths.structuredTools,
|
|
||||||
});
|
|
||||||
prelimCachedTools = toolDefinitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {import('@librechat/api').LCManifestTool[]} */
|
/** @type {import('@librechat/api').LCManifestTool[]} */
|
||||||
let pluginManifest = availableTools;
|
let pluginManifest = availableTools;
|
||||||
if (customConfig?.mcpServers != null) {
|
|
||||||
|
const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
|
||||||
|
if (appConfig?.mcpConfig != null) {
|
||||||
try {
|
try {
|
||||||
const mcpManager = getMCPManager();
|
|
||||||
const mcpTools = await mcpManager.getAllToolFunctions(userId);
|
const mcpTools = await mcpManager.getAllToolFunctions(userId);
|
||||||
prelimCachedTools = prelimCachedTools ?? {};
|
prelimCachedTools = prelimCachedTools ?? {};
|
||||||
for (const [toolKey, toolData] of Object.entries(mcpTools)) {
|
for (const [toolKey, toolData] of Object.entries(mcpTools)) {
|
||||||
const plugin = convertMCPToolToPlugin({
|
const plugin = convertMCPToolToPlugin({
|
||||||
toolKey,
|
toolKey,
|
||||||
toolData,
|
toolData,
|
||||||
customConfig,
|
mcpManager,
|
||||||
});
|
});
|
||||||
if (plugin) {
|
if (plugin) {
|
||||||
pluginManifest.push(plugin);
|
pluginManifest.push(plugin);
|
||||||
|
@ -161,7 +149,7 @@ const getAvailableTools = async (req, res) => {
|
||||||
if (plugin.pluginKey.includes(Constants.mcp_delimiter)) {
|
if (plugin.pluginKey.includes(Constants.mcp_delimiter)) {
|
||||||
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) {
|
||||||
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
||||||
|
|
|
@ -1,30 +1,31 @@
|
||||||
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(),
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('~/server/services/Config', () => ({
|
jest.mock('~/server/services/Config', () => ({
|
||||||
getCustomConfig: jest.fn(),
|
|
||||||
getCachedTools: jest.fn(),
|
getCachedTools: jest.fn(),
|
||||||
|
getAppConfig: jest.fn().mockResolvedValue({
|
||||||
|
filteredTools: [],
|
||||||
|
includedTools: [],
|
||||||
|
}),
|
||||||
setCachedTools: jest.fn(),
|
setCachedTools: jest.fn(),
|
||||||
mergeUserTools: jest.fn(),
|
mergeUserTools: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('~/server/services/ToolService', () => ({
|
// loadAndFormatTools mock removed - no longer used in PluginController
|
||||||
getToolkitKey: jest.fn(),
|
|
||||||
loadAndFormatTools: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('~/config', () => ({
|
jest.mock('~/config', () => ({
|
||||||
getMCPManager: jest.fn(() => ({
|
getMCPManager: jest.fn(() => ({
|
||||||
loadAllManifestTools: jest.fn().mockResolvedValue([]),
|
getAllToolFunctions: jest.fn().mockResolvedValue({}),
|
||||||
|
getRawConfig: jest.fn().mockReturnValue({}),
|
||||||
})),
|
})),
|
||||||
getFlowStateManager: jest.fn(),
|
getFlowStateManager: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
@ -39,7 +40,6 @@ jest.mock('~/cache', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { getAvailableTools, getAvailablePluginsController } = require('./PluginController');
|
const { getAvailableTools, getAvailablePluginsController } = require('./PluginController');
|
||||||
const { loadAndFormatTools } = require('~/server/services/ToolService');
|
|
||||||
|
|
||||||
describe('PluginController', () => {
|
describe('PluginController', () => {
|
||||||
let mockReq, mockRes, mockCache;
|
let mockReq, mockRes, mockCache;
|
||||||
|
@ -48,12 +48,9 @@ describe('PluginController', () => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockReq = {
|
mockReq = {
|
||||||
user: { id: 'test-user-id' },
|
user: { id: 'test-user-id' },
|
||||||
app: {
|
config: {
|
||||||
locals: {
|
filteredTools: [],
|
||||||
paths: { structuredTools: '/mock/path' },
|
includedTools: [],
|
||||||
filteredTools: null,
|
|
||||||
includedTools: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
||||||
|
@ -63,13 +60,19 @@ describe('PluginController', () => {
|
||||||
// Clear availableTools and toolkits arrays before each test
|
// Clear availableTools and toolkits arrays before each test
|
||||||
require('~/app/clients/tools').availableTools.length = 0;
|
require('~/app/clients/tools').availableTools.length = 0;
|
||||||
require('~/app/clients/tools').toolkits.length = 0;
|
require('~/app/clients/tools').toolkits.length = 0;
|
||||||
|
|
||||||
|
// Reset getCachedTools mock to ensure clean state
|
||||||
|
getCachedTools.mockReset();
|
||||||
|
|
||||||
|
// Reset getAppConfig mock to ensure clean state with default values
|
||||||
|
getAppConfig.mockReset();
|
||||||
|
getAppConfig.mockResolvedValue({
|
||||||
|
filteredTools: [],
|
||||||
|
includedTools: [],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getAvailablePluginsController', () => {
|
describe('getAvailablePluginsController', () => {
|
||||||
beforeEach(() => {
|
|
||||||
mockReq.app = { locals: { filteredTools: [], includedTools: [] } };
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use filterUniquePlugins to remove duplicate plugins', async () => {
|
it('should use filterUniquePlugins to remove duplicate plugins', async () => {
|
||||||
// Add plugins with duplicates to availableTools
|
// Add plugins with duplicates to availableTools
|
||||||
const mockPlugins = [
|
const mockPlugins = [
|
||||||
|
@ -82,10 +85,17 @@ describe('PluginController', () => {
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Configure getAppConfig to return the expected config
|
||||||
|
getAppConfig.mockResolvedValueOnce({
|
||||||
|
filteredTools: [],
|
||||||
|
includedTools: [],
|
||||||
|
});
|
||||||
|
|
||||||
await getAvailablePluginsController(mockReq, mockRes);
|
await getAvailablePluginsController(mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||||
const responseData = mockRes.json.mock.calls[0][0];
|
const responseData = mockRes.json.mock.calls[0][0];
|
||||||
|
// The real filterUniquePlugins should have removed the duplicate
|
||||||
expect(responseData).toHaveLength(2);
|
expect(responseData).toHaveLength(2);
|
||||||
expect(responseData[0].pluginKey).toBe('key1');
|
expect(responseData[0].pluginKey).toBe('key1');
|
||||||
expect(responseData[1].pluginKey).toBe('key2');
|
expect(responseData[1].pluginKey).toBe('key2');
|
||||||
|
@ -99,10 +109,16 @@ describe('PluginController', () => {
|
||||||
require('~/app/clients/tools').availableTools.push(mockPlugin);
|
require('~/app/clients/tools').availableTools.push(mockPlugin);
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Configure getAppConfig to return the expected config
|
||||||
|
getAppConfig.mockResolvedValueOnce({
|
||||||
|
filteredTools: [],
|
||||||
|
includedTools: [],
|
||||||
|
});
|
||||||
|
|
||||||
await getAvailablePluginsController(mockReq, mockRes);
|
await getAvailablePluginsController(mockReq, mockRes);
|
||||||
|
|
||||||
const responseData = mockRes.json.mock.calls[0][0];
|
const responseData = mockRes.json.mock.calls[0][0];
|
||||||
// checkPluginAuth returns false, so authenticated property is not added
|
// The real checkPluginAuth returns false for plugins without authConfig, so authenticated property is not added
|
||||||
expect(responseData[0].authenticated).toBeUndefined();
|
expect(responseData[0].authenticated).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -126,9 +142,14 @@ describe('PluginController', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
require('~/app/clients/tools').availableTools.push(...mockPlugins);
|
require('~/app/clients/tools').availableTools.push(...mockPlugins);
|
||||||
mockReq.app.locals.includedTools = ['key1'];
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Configure getAppConfig to return config with includedTools
|
||||||
|
getAppConfig.mockResolvedValueOnce({
|
||||||
|
filteredTools: [],
|
||||||
|
includedTools: ['key1'],
|
||||||
|
});
|
||||||
|
|
||||||
await getAvailablePluginsController(mockReq, mockRes);
|
await getAvailablePluginsController(mockReq, mockRes);
|
||||||
|
|
||||||
const responseData = mockRes.json.mock.calls[0][0];
|
const responseData = mockRes.json.mock.calls[0][0];
|
||||||
|
@ -152,20 +173,26 @@ describe('PluginController', () => {
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
mockReq.config = {
|
||||||
|
mcpConfig: null,
|
||||||
|
paths: { structuredTools: '/mock/path' },
|
||||||
|
};
|
||||||
|
|
||||||
// Mock second call to return tool definitions
|
// Mock second call to return tool definitions (includeGlobal: true)
|
||||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||||
const responseData = mockRes.json.mock.calls[0][0];
|
const responseData = mockRes.json.mock.calls[0][0];
|
||||||
// convertMCPToolsToPlugins should have converted the tool
|
expect(responseData).toBeDefined();
|
||||||
|
expect(Array.isArray(responseData)).toBe(true);
|
||||||
expect(responseData.length).toBeGreaterThan(0);
|
expect(responseData.length).toBeGreaterThan(0);
|
||||||
const convertedTool = responseData.find(
|
const convertedTool = responseData.find(
|
||||||
(tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}server1`,
|
(tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}server1`,
|
||||||
);
|
);
|
||||||
expect(convertedTool).toBeDefined();
|
expect(convertedTool).toBeDefined();
|
||||||
|
// The real convertMCPToolsToPlugins extracts the name from the delimiter
|
||||||
expect(convertedTool.name).toBe('tool1');
|
expect(convertedTool.name).toBe('tool1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -188,15 +215,20 @@ describe('PluginController', () => {
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(mockCachedPlugins);
|
mockCache.get.mockResolvedValue(mockCachedPlugins);
|
||||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
mockReq.config = {
|
||||||
|
mcpConfig: null,
|
||||||
|
paths: { structuredTools: '/mock/path' },
|
||||||
|
};
|
||||||
|
|
||||||
// Mock second call to return tool definitions
|
// Mock second call to return tool definitions
|
||||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||||
const responseData = mockRes.json.mock.calls[0][0];
|
const responseData = mockRes.json.mock.calls[0][0];
|
||||||
// Should have deduplicated tools with same pluginKey
|
expect(Array.isArray(responseData)).toBe(true);
|
||||||
|
// The real filterUniquePlugins should have deduplicated tools with same pluginKey
|
||||||
const userToolCount = responseData.filter((tool) => tool.pluginKey === 'user-tool').length;
|
const userToolCount = responseData.filter((tool) => tool.pluginKey === 'user-tool').length;
|
||||||
expect(userToolCount).toBe(1);
|
expect(userToolCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
@ -213,11 +245,15 @@ describe('PluginController', () => {
|
||||||
require('~/app/clients/tools').availableTools.push(mockPlugin);
|
require('~/app/clients/tools').availableTools.push(mockPlugin);
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCachedTools.mockResolvedValue(null);
|
// First call returns null for user tools
|
||||||
getCustomConfig.mockResolvedValue(null);
|
getCachedTools.mockResolvedValueOnce(null);
|
||||||
|
mockReq.config = {
|
||||||
|
mcpConfig: null,
|
||||||
|
paths: { structuredTools: '/mock/path' },
|
||||||
|
};
|
||||||
|
|
||||||
// Mock loadAndFormatTools to return tool definitions including our tool
|
// Second call (with includeGlobal: true) returns the tool definitions
|
||||||
loadAndFormatTools.mockReturnValue({
|
getCachedTools.mockResolvedValueOnce({
|
||||||
tool1: {
|
tool1: {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
|
@ -235,7 +271,7 @@ describe('PluginController', () => {
|
||||||
expect(Array.isArray(responseData)).toBe(true);
|
expect(Array.isArray(responseData)).toBe(true);
|
||||||
const tool = responseData.find((t) => t.pluginKey === 'tool1');
|
const tool = responseData.find((t) => t.pluginKey === 'tool1');
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
// checkPluginAuth returns false, so authenticated property is not added
|
// The real checkPluginAuth returns false for plugins without authConfig, so authenticated property is not added
|
||||||
expect(tool.authenticated).toBeUndefined();
|
expect(tool.authenticated).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -257,11 +293,15 @@ describe('PluginController', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCachedTools.mockResolvedValue(null);
|
// First call returns null for user tools
|
||||||
getCustomConfig.mockResolvedValue(null);
|
getCachedTools.mockResolvedValueOnce(null);
|
||||||
|
mockReq.config = {
|
||||||
|
mcpConfig: null,
|
||||||
|
paths: { structuredTools: '/mock/path' },
|
||||||
|
};
|
||||||
|
|
||||||
// Mock loadAndFormatTools to return tool definitions
|
// Second call (with includeGlobal: true) returns the tool definitions
|
||||||
loadAndFormatTools.mockReturnValue({
|
getCachedTools.mockResolvedValueOnce({
|
||||||
toolkit1_function: {
|
toolkit1_function: {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
|
@ -283,9 +323,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`]: {
|
||||||
|
@ -298,17 +337,24 @@ describe('PluginController', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock the MCP manager to return tools
|
// Mock the MCP manager to return tools and server config
|
||||||
const mockMCPManager = {
|
const mockMCPManager = {
|
||||||
getAllToolFunctions: jest.fn().mockResolvedValue(functionTools),
|
getAllToolFunctions: jest.fn().mockResolvedValue(functionTools),
|
||||||
|
getRawConfig: jest.fn().mockReturnValue(serverConfig),
|
||||||
};
|
};
|
||||||
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
|
// First call returns empty user tools
|
||||||
getCachedTools.mockResolvedValueOnce({});
|
getCachedTools.mockResolvedValueOnce({});
|
||||||
|
|
||||||
// Mock loadAndFormatTools to return empty object since these are MCP tools
|
// Mock getAppConfig to return the mcpConfig
|
||||||
loadAndFormatTools.mockReturnValue({});
|
mockReq.config = {
|
||||||
|
mcpConfig: {
|
||||||
|
'test-server': serverConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Second call (with includeGlobal: true) returns the tool definitions
|
||||||
getCachedTools.mockResolvedValueOnce(functionTools);
|
getCachedTools.mockResolvedValueOnce(functionTools);
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
@ -319,28 +365,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' },
|
||||||
|
@ -364,24 +406,28 @@ describe('PluginController', () => {
|
||||||
// Mock the MCP manager to return tools
|
// Mock the MCP manager to return tools
|
||||||
const mockMCPManager = {
|
const mockMCPManager = {
|
||||||
getAllToolFunctions: jest.fn().mockResolvedValue(mcpToolFunctions),
|
getAllToolFunctions: jest.fn().mockResolvedValue(mcpToolFunctions),
|
||||||
|
getRawConfig: jest.fn().mockReturnValue({
|
||||||
|
customUserVars: {
|
||||||
|
API_KEY: { title: 'API Key', description: 'Your API key' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCustomConfig.mockResolvedValue(customConfig);
|
mockReq.config = appConfig;
|
||||||
|
|
||||||
// First call returns user tools (empty in this case)
|
// First call returns user tools (empty in this case)
|
||||||
getCachedTools.mockResolvedValueOnce({});
|
getCachedTools.mockResolvedValueOnce({});
|
||||||
|
|
||||||
// Mock loadAndFormatTools to return empty object for MCP tools
|
// Second call (with includeGlobal: true) returns tool definitions including our MCP tool
|
||||||
loadAndFormatTools.mockReturnValue({});
|
|
||||||
|
|
||||||
// Second call returns tool definitions including our MCP tool
|
|
||||||
getCachedTools.mockResolvedValueOnce(mcpToolFunctions);
|
getCachedTools.mockResolvedValueOnce(mcpToolFunctions);
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||||
const responseData = mockRes.json.mock.calls[0][0];
|
const responseData = mockRes.json.mock.calls[0][0];
|
||||||
|
expect(Array.isArray(responseData)).toBe(true);
|
||||||
|
|
||||||
// Find the MCP tool in the response
|
// Find the MCP tool in the response
|
||||||
const mcpTool = responseData.find(
|
const mcpTool = responseData.find(
|
||||||
|
@ -417,24 +463,36 @@ describe('PluginController', () => {
|
||||||
|
|
||||||
it('should handle null cachedTools and cachedUserTools', async () => {
|
it('should handle null cachedTools and cachedUserTools', async () => {
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCachedTools.mockResolvedValue(null);
|
// First call returns null for user tools
|
||||||
getCustomConfig.mockResolvedValue(null);
|
getCachedTools.mockResolvedValueOnce(null);
|
||||||
|
mockReq.config = {
|
||||||
|
mcpConfig: null,
|
||||||
|
paths: { structuredTools: '/mock/path' },
|
||||||
|
};
|
||||||
|
|
||||||
// Mock loadAndFormatTools to return empty object when getCachedTools returns null
|
// Mock MCP manager to return no tools
|
||||||
loadAndFormatTools.mockReturnValue({});
|
const mockMCPManager = {
|
||||||
|
getAllToolFunctions: jest.fn().mockResolvedValue({}),
|
||||||
|
getRawConfig: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
|
// Second call (with includeGlobal: true) returns empty object instead of null
|
||||||
|
getCachedTools.mockResolvedValueOnce({});
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
// Should handle null values gracefully
|
// Should handle null values gracefully
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle when getCachedTools returns undefined', async () => {
|
it('should handle when getCachedTools returns undefined', async () => {
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
mockReq.config = {
|
||||||
|
mcpConfig: null,
|
||||||
// Mock loadAndFormatTools to return empty object when getCachedTools returns undefined
|
paths: { structuredTools: '/mock/path' },
|
||||||
loadAndFormatTools.mockReturnValue({});
|
};
|
||||||
|
|
||||||
// Mock getCachedTools to return undefined for both calls
|
// Mock getCachedTools to return undefined for both calls
|
||||||
getCachedTools.mockReset();
|
getCachedTools.mockReset();
|
||||||
|
@ -444,6 +502,7 @@ describe('PluginController', () => {
|
||||||
|
|
||||||
// Should handle undefined values gracefully
|
// Should handle undefined values gracefully
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle cachedToolsArray and userPlugins both being defined', async () => {
|
it('should handle cachedToolsArray and userPlugins both being defined', async () => {
|
||||||
|
@ -461,8 +520,18 @@ describe('PluginController', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(cachedTools);
|
mockCache.get.mockResolvedValue(cachedTools);
|
||||||
getCachedTools.mockResolvedValue(userTools);
|
getCachedTools.mockResolvedValueOnce(userTools);
|
||||||
getCustomConfig.mockResolvedValue(null);
|
mockReq.config = {
|
||||||
|
mcpConfig: null,
|
||||||
|
paths: { structuredTools: '/mock/path' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// The controller expects a second call to getCachedTools
|
||||||
|
getCachedTools.mockResolvedValueOnce({
|
||||||
|
'cached-tool': { type: 'function', function: { name: 'cached-tool' } },
|
||||||
|
[`user-tool${Constants.mcp_delimiter}server1`]:
|
||||||
|
userTools[`user-tool${Constants.mcp_delimiter}server1`],
|
||||||
|
});
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
|
@ -474,8 +543,20 @@ describe('PluginController', () => {
|
||||||
|
|
||||||
it('should handle empty toolDefinitions object', async () => {
|
it('should handle empty toolDefinitions object', async () => {
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({});
|
// Reset getCachedTools to ensure clean state
|
||||||
getCustomConfig.mockResolvedValue(null);
|
getCachedTools.mockReset();
|
||||||
|
getCachedTools.mockResolvedValue({});
|
||||||
|
mockReq.config = {}; // No mcpConfig at all
|
||||||
|
|
||||||
|
// Ensure no plugins are available
|
||||||
|
require('~/app/clients/tools').availableTools.length = 0;
|
||||||
|
|
||||||
|
// Reset MCP manager to default state
|
||||||
|
const mockMCPManager = {
|
||||||
|
getAllToolFunctions: jest.fn().mockResolvedValue({}),
|
||||||
|
getRawConfig: jest.fn().mockReturnValue({}),
|
||||||
|
};
|
||||||
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
|
@ -484,8 +565,8 @@ describe('PluginController', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle MCP tools without customUserVars', async () => {
|
it('should handle MCP tools without customUserVars', async () => {
|
||||||
const customConfig = {
|
const appConfig = {
|
||||||
mcpServers: {
|
mcpConfig: {
|
||||||
'test-server': {
|
'test-server': {
|
||||||
// No customUserVars defined
|
// No customUserVars defined
|
||||||
},
|
},
|
||||||
|
@ -494,30 +575,60 @@ describe('PluginController', () => {
|
||||||
|
|
||||||
const mockUserTools = {
|
const mockUserTools = {
|
||||||
[`tool1${Constants.mcp_delimiter}test-server`]: {
|
[`tool1${Constants.mcp_delimiter}test-server`]: {
|
||||||
function: { name: 'tool1', description: 'Tool 1' },
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: `tool1${Constants.mcp_delimiter}test-server`,
|
||||||
|
description: 'Tool 1',
|
||||||
|
parameters: { type: 'object', properties: {} },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock the MCP manager to return the tools
|
||||||
|
const mockMCPManager = {
|
||||||
|
getAllToolFunctions: jest.fn().mockResolvedValue(mockUserTools),
|
||||||
|
getRawConfig: jest.fn().mockReturnValue({
|
||||||
|
// No customUserVars defined
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCustomConfig.mockResolvedValue(customConfig);
|
mockReq.config = appConfig;
|
||||||
|
// First call returns empty user tools
|
||||||
|
getCachedTools.mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
// Second call (with includeGlobal: true) returns the tool definitions
|
||||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||||
|
|
||||||
getCachedTools.mockResolvedValueOnce({
|
// Ensure no plugins in availableTools for clean test
|
||||||
[`tool1${Constants.mcp_delimiter}test-server`]: true,
|
require('~/app/clients/tools').availableTools.length = 0;
|
||||||
});
|
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||||
const responseData = mockRes.json.mock.calls[0][0];
|
const responseData = mockRes.json.mock.calls[0][0];
|
||||||
expect(responseData[0].authenticated).toBe(true);
|
expect(Array.isArray(responseData)).toBe(true);
|
||||||
// The actual implementation doesn't set authConfig on tools without customUserVars
|
expect(responseData.length).toBeGreaterThan(0);
|
||||||
expect(responseData[0].authConfig).toEqual([]);
|
|
||||||
|
const mcpTool = responseData.find(
|
||||||
|
(tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}test-server`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mcpTool).toBeDefined();
|
||||||
|
expect(mcpTool.authenticated).toBe(true);
|
||||||
|
// The actual implementation sets authConfig to empty array when no customUserVars
|
||||||
|
expect(mcpTool.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: {} };
|
mockReq.config = {};
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Configure getAppConfig to return config with undefined properties
|
||||||
|
// The controller will use default values [] for filteredTools and includedTools
|
||||||
|
getAppConfig.mockResolvedValueOnce({});
|
||||||
|
|
||||||
await getAvailablePluginsController(mockReq, mockRes);
|
await getAvailablePluginsController(mockReq, mockRes);
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||||
|
@ -532,27 +643,21 @@ describe('PluginController', () => {
|
||||||
toolkit: true,
|
toolkit: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure req.app.locals is properly mocked
|
// No need to mock app.locals anymore as it's not used
|
||||||
mockReq.app = {
|
|
||||||
locals: {
|
|
||||||
filteredTools: [],
|
|
||||||
includedTools: [],
|
|
||||||
paths: { structuredTools: '/mock/path' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add the toolkit to availableTools
|
// Add the toolkit to availableTools
|
||||||
require('~/app/clients/tools').availableTools.push(mockToolkit);
|
require('~/app/clients/tools').availableTools.push(mockToolkit);
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCachedTools.mockResolvedValue({});
|
// First call returns empty object
|
||||||
getCustomConfig.mockResolvedValue(null);
|
getCachedTools.mockResolvedValueOnce({});
|
||||||
|
mockReq.config = {
|
||||||
|
mcpConfig: null,
|
||||||
|
paths: { structuredTools: '/mock/path' },
|
||||||
|
};
|
||||||
|
|
||||||
// Mock loadAndFormatTools to return an empty object when toolDefinitions is null
|
// Second call (with includeGlobal: true) returns empty object to avoid null reference error
|
||||||
loadAndFormatTools.mockReturnValue({});
|
getCachedTools.mockResolvedValueOnce({});
|
||||||
|
|
||||||
// Mock getCachedTools second call to return null
|
|
||||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce(null);
|
|
||||||
|
|
||||||
await getAvailableTools(mockReq, mockRes);
|
await getAvailableTools(mockReq, mockRes);
|
||||||
|
|
||||||
|
|
|
@ -17,12 +17,14 @@ 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) => {
|
||||||
/** @type {MongoUser} */
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
||||||
|
/** @type {IUser} */
|
||||||
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
|
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
|
||||||
/**
|
/**
|
||||||
* These fields should not exist due to secure field selection, but deletion
|
* These fields should not exist due to secure field selection, but deletion
|
||||||
|
@ -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,
|
||||||
|
|
|
@ -246,6 +246,7 @@ function createToolEndCallback({ req, res, artifactPromises }) {
|
||||||
const attachment = await processFileCitations({
|
const attachment = await processFileCitations({
|
||||||
user,
|
user,
|
||||||
metadata,
|
metadata,
|
||||||
|
appConfig: req.config,
|
||||||
toolArtifact: output.artifact,
|
toolArtifact: output.artifact,
|
||||||
toolCallId: output.tool_call_id,
|
toolCallId: output.tool_call_id,
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,7 @@ const {
|
||||||
createRun,
|
createRun,
|
||||||
Tokenizer,
|
Tokenizer,
|
||||||
checkAccess,
|
checkAccess,
|
||||||
|
getBalanceConfig,
|
||||||
memoryInstructions,
|
memoryInstructions,
|
||||||
formatContentStrings,
|
formatContentStrings,
|
||||||
createMemoryProcessor,
|
createMemoryProcessor,
|
||||||
|
@ -446,8 +447,8 @@ class AgentClient extends BaseClient {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/** @type {TCustomConfig['memory']} */
|
const appConfig = this.options.req.config;
|
||||||
const memoryConfig = this.options.req?.app?.locals?.memory;
|
const memoryConfig = appConfig.memory;
|
||||||
if (!memoryConfig || memoryConfig.disabled === true) {
|
if (!memoryConfig || memoryConfig.disabled === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -455,7 +456,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) {
|
||||||
|
@ -577,8 +578,8 @@ class AgentClient extends BaseClient {
|
||||||
if (this.processMemory == null) {
|
if (this.processMemory == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/** @type {TCustomConfig['memory']} */
|
const appConfig = this.options.req.config;
|
||||||
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];
|
||||||
|
@ -620,9 +621,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;
|
||||||
}
|
}
|
||||||
|
@ -644,6 +651,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,
|
||||||
|
@ -761,8 +769,9 @@ class AgentClient extends BaseClient {
|
||||||
abortController = new AbortController();
|
abortController = new AbortController();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {TCustomConfig['endpoints']['agents']} */
|
const appConfig = this.options.req.config;
|
||||||
const agentsEConfig = this.options.req.app.locals[EModelEndpoint.agents];
|
/** @type {AppConfig['endpoints']['agents']} */
|
||||||
|
const agentsEConfig = appConfig.endpoints?.[EModelEndpoint.agents];
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
configurable: {
|
configurable: {
|
||||||
|
@ -1030,7 +1039,8 @@ class AgentClient extends BaseClient {
|
||||||
this.artifactPromises.push(...attachments);
|
this.artifactPromises.push(...attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.recordCollectedUsage({ context: 'message' });
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
|
await this.recordCollectedUsage({ context: 'message', balance: balanceConfig });
|
||||||
} 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',
|
||||||
|
@ -1071,6 +1081,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 = req.config;
|
||||||
let endpoint = agent.endpoint;
|
let endpoint = agent.endpoint;
|
||||||
|
|
||||||
/** @type {import('@librechat/agents').ClientOptions} */
|
/** @type {import('@librechat/agents').ClientOptions} */
|
||||||
|
@ -1078,11 +1089,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',
|
||||||
|
@ -1091,7 +1104,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(
|
||||||
|
@ -1100,7 +1116,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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1203,10 +1219,12 @@ class AgentClient extends BaseClient {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
await this.recordCollectedUsage({
|
await this.recordCollectedUsage({
|
||||||
model: clientOptions.model,
|
|
||||||
context: 'title',
|
|
||||||
collectedUsage,
|
collectedUsage,
|
||||||
|
context: 'title',
|
||||||
|
model: clientOptions.model,
|
||||||
|
balance: balanceConfig,
|
||||||
}).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',
|
||||||
|
@ -1225,17 +1243,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,
|
||||||
|
@ -1252,6 +1279,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,
|
||||||
|
|
|
@ -41,8 +41,16 @@ describe('AgentClient - titleConvo', () => {
|
||||||
|
|
||||||
// Mock request and response
|
// Mock request and response
|
||||||
mockReq = {
|
mockReq = {
|
||||||
app: {
|
user: {
|
||||||
locals: {
|
id: 'user-123',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
model: 'gpt-4',
|
||||||
|
endpoint: EModelEndpoint.openAI,
|
||||||
|
key: null,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
endpoints: {
|
||||||
[EModelEndpoint.openAI]: {
|
[EModelEndpoint.openAI]: {
|
||||||
// Match the agent endpoint
|
// Match the agent endpoint
|
||||||
titleModel: 'gpt-3.5-turbo',
|
titleModel: 'gpt-3.5-turbo',
|
||||||
|
@ -52,14 +60,6 @@ describe('AgentClient - titleConvo', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
user: {
|
|
||||||
id: 'user-123',
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
model: 'gpt-4',
|
|
||||||
endpoint: EModelEndpoint.openAI,
|
|
||||||
key: null,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mockRes = {};
|
mockRes = {};
|
||||||
|
@ -143,7 +143,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;
|
mockReq.config = { endpoints: {} };
|
||||||
|
|
||||||
const text = 'Test conversation text';
|
const text = 'Test conversation text';
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
@ -161,7 +161,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;
|
mockReq.config = {
|
||||||
|
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 +182,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;
|
mockReq.config = {
|
||||||
|
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();
|
||||||
|
@ -216,6 +234,9 @@ describe('AgentClient - titleConvo', () => {
|
||||||
model: 'gpt-3.5-turbo',
|
model: 'gpt-3.5-turbo',
|
||||||
context: 'title',
|
context: 'title',
|
||||||
collectedUsage: expect.any(Array),
|
collectedUsage: expect.any(Array),
|
||||||
|
balance: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -245,10 +266,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;
|
mockReq.config = {
|
||||||
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,18 +302,16 @@ 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;
|
mockReq.config = {
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI].titlePrompt;
|
endpoints: {
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI].titleMethod;
|
all: {
|
||||||
delete mockReq.app.locals[EModelEndpoint.openAI].titlePromptTemplate;
|
|
||||||
|
|
||||||
// Set 'all' config
|
|
||||||
mockReq.app.locals.all = {
|
|
||||||
titleModel: 'gpt-4o-mini',
|
titleModel: 'gpt-4o-mini',
|
||||||
titlePrompt: 'All config title prompt',
|
titlePrompt: 'All config title prompt',
|
||||||
titleMethod: 'completion',
|
titleMethod: 'completion',
|
||||||
titlePromptTemplate: 'All config template: {{content}}',
|
titlePromptTemplate: 'All config template: {{content}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const text = 'Test conversation text';
|
const text = 'Test conversation text';
|
||||||
|
@ -309,17 +335,21 @@ 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';
|
mockReq.config = {
|
||||||
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
|
||||||
|
},
|
||||||
|
all: {
|
||||||
titleModel: 'gpt-4o-mini',
|
titleModel: 'gpt-4o-mini',
|
||||||
titlePrompt: 'All config title prompt',
|
titlePrompt: 'All config title prompt',
|
||||||
titleMethod: 'completion',
|
titleMethod: 'completion',
|
||||||
titlePromptTemplate: 'All config template',
|
titlePromptTemplate: 'All config template',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const text = 'Test conversation text';
|
const text = 'Test conversation text';
|
||||||
|
@ -346,17 +376,18 @@ 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 = {
|
mockReq.config = {
|
||||||
|
endpoints: {
|
||||||
|
all: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: 'claude-3-haiku-20240307',
|
titleModel: 'claude-3-haiku-20240307',
|
||||||
titleMethod: 'completion', // Testing the new default method
|
titleMethod: 'completion', // Testing the new default method
|
||||||
titlePrompt: 'Generate a concise, descriptive title for this conversation',
|
titlePrompt: 'Generate a concise, descriptive title for this conversation',
|
||||||
titlePromptTemplate: 'Conversation summary: {{content}}',
|
titlePromptTemplate: 'Conversation summary: {{content}}',
|
||||||
titleEndpoint: EModelEndpoint.anthropic, // Should switch provider to Anthropic
|
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';
|
||||||
|
@ -402,15 +433,16 @@ 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 = {
|
mockReq.config = {
|
||||||
|
endpoints: {
|
||||||
|
all: {
|
||||||
titleModel: 'gpt-4o-mini',
|
titleModel: 'gpt-4o-mini',
|
||||||
titleMethod: method,
|
titleMethod: method,
|
||||||
titlePrompt: `Testing ${method} method`,
|
titlePrompt: `Testing ${method} method`,
|
||||||
titlePromptTemplate: `Template for ${method}: {{content}}`,
|
titlePromptTemplate: `Template for ${method}: {{content}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const text = `Test conversation for ${method} method`;
|
const text = `Test conversation for ${method} method`;
|
||||||
|
@ -455,7 +487,9 @@ 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] = {
|
mockReq.config = {
|
||||||
|
endpoints: {
|
||||||
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: 'grok-3',
|
titleModel: 'grok-3',
|
||||||
titleMethod: 'completion',
|
titleMethod: 'completion',
|
||||||
|
@ -480,6 +514,8 @@ describe('AgentClient - titleConvo', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.body.model = 'grok-3';
|
mockReq.body.model = 'grok-3';
|
||||||
|
@ -503,7 +539,9 @@ 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] = {
|
mockReq.config = {
|
||||||
|
endpoints: {
|
||||||
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: 'gpt-4o',
|
titleModel: 'gpt-4o',
|
||||||
titleMethod: 'structured',
|
titleMethod: 'structured',
|
||||||
|
@ -527,6 +565,8 @@ describe('AgentClient - titleConvo', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.body.model = 'gpt-4o';
|
mockReq.body.model = 'gpt-4o';
|
||||||
|
@ -551,7 +591,9 @@ 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] = {
|
mockReq.config = {
|
||||||
|
endpoints: {
|
||||||
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: Constants.CURRENT_MODEL,
|
titleModel: Constants.CURRENT_MODEL,
|
||||||
titleMethod: 'functions',
|
titleMethod: 'functions',
|
||||||
|
@ -576,6 +618,8 @@ describe('AgentClient - titleConvo', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.body.model = 'gpt-4o-latest';
|
mockReq.body.model = 'gpt-4o-latest';
|
||||||
|
@ -598,7 +642,9 @@ 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] = {
|
mockReq.config = {
|
||||||
|
endpoints: {
|
||||||
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: 'o1-mini',
|
titleModel: 'o1-mini',
|
||||||
titleMethod: 'completion',
|
titleMethod: 'completion',
|
||||||
|
@ -650,6 +696,8 @@ describe('AgentClient - titleConvo', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
|
||||||
mockReq.body.model = 'o1-mini';
|
mockReq.body.model = 'o1-mini';
|
||||||
|
@ -679,11 +727,10 @@ 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 = {
|
mockReq.config = {
|
||||||
|
endpoints: {
|
||||||
|
all: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: 'gpt-4',
|
titleModel: 'gpt-4',
|
||||||
titleMethod: 'structured',
|
titleMethod: 'structured',
|
||||||
|
@ -708,6 +755,8 @@ describe('AgentClient - titleConvo', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const text = 'Test Azure with all config fallback';
|
const text = 'Test Azure with all config fallback';
|
||||||
|
@ -982,13 +1031,6 @@ describe('AgentClient - titleConvo', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
mockReq = {
|
mockReq = {
|
||||||
app: {
|
|
||||||
locals: {
|
|
||||||
memory: {
|
|
||||||
messageWindowSize: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user: {
|
user: {
|
||||||
id: 'user-123',
|
id: 'user-123',
|
||||||
personalization: {
|
personalization: {
|
||||||
|
@ -997,6 +1039,13 @@ describe('AgentClient - titleConvo', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock getAppConfig for memory tests
|
||||||
|
mockReq.config = {
|
||||||
|
memory: {
|
||||||
|
messageWindowSize: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
mockRes = {};
|
mockRes = {};
|
||||||
|
|
||||||
mockOptions = {
|
mockOptions = {
|
||||||
|
|
|
@ -21,7 +21,7 @@ const getLogStores = require('~/cache/getLogStores');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ErrorHandlerDependencies
|
* @typedef {Object} ErrorHandlerDependencies
|
||||||
* @property {Express.Request} req - The Express request object
|
* @property {ServerRequest} req - The Express request object
|
||||||
* @property {Express.Response} res - The Express response object
|
* @property {Express.Response} res - The Express response object
|
||||||
* @property {() => ErrorHandlerContext} getContext - Function to get the current context
|
* @property {() => ErrorHandlerContext} getContext - Function to get the current context
|
||||||
* @property {string} [originPath] - The origin path for the error handler
|
* @property {string} [originPath] - The origin path for the error handler
|
||||||
|
|
|
@ -487,6 +487,7 @@ const getListAgentsHandler = async (req, res) => {
|
||||||
*/
|
*/
|
||||||
const uploadAgentAvatarHandler = async (req, res) => {
|
const uploadAgentAvatarHandler = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = req.config;
|
||||||
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,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const { v4 } = require('uuid');
|
const { v4 } = require('uuid');
|
||||||
const { sleep } = require('@librechat/agents');
|
const { sleep } = require('@librechat/agents');
|
||||||
const { sendEvent } = require('@librechat/api');
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { sendEvent, getBalanceConfig } = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
Time,
|
Time,
|
||||||
Constants,
|
Constants,
|
||||||
|
@ -47,6 +47,7 @@ const { getOpenAIClient } = require('./helpers');
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const chatV1 = async (req, res) => {
|
const chatV1 = async (req, res) => {
|
||||||
|
const appConfig = req.config;
|
||||||
logger.debug('[/assistants/chat/] req.body', req.body);
|
logger.debug('[/assistants/chat/] req.body', req.body);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -251,8 +252,8 @@ const chatV1 = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkBalanceBeforeRun = async () => {
|
const checkBalanceBeforeRun = async () => {
|
||||||
const balance = req.app?.locals?.balance;
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
if (!balance?.enabled) {
|
if (!balanceConfig?.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const transactions =
|
const transactions =
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const { v4 } = require('uuid');
|
const { v4 } = require('uuid');
|
||||||
const { sleep } = require('@librechat/agents');
|
const { sleep } = require('@librechat/agents');
|
||||||
const { sendEvent } = require('@librechat/api');
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { sendEvent, getBalanceConfig } = require('@librechat/api');
|
||||||
const {
|
const {
|
||||||
Time,
|
Time,
|
||||||
Constants,
|
Constants,
|
||||||
|
@ -38,12 +38,13 @@ const { getOpenAIClient } = require('./helpers');
|
||||||
* @route POST /
|
* @route POST /
|
||||||
* @desc Chat with an assistant
|
* @desc Chat with an assistant
|
||||||
* @access Public
|
* @access Public
|
||||||
* @param {Express.Request} req - The request object, containing the request data.
|
* @param {ServerRequest} req - The request object, containing the request data.
|
||||||
* @param {Express.Response} res - The response object, used to send back a response.
|
* @param {Express.Response} res - The response object, used to send back a response.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
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 = req.config;
|
||||||
|
|
||||||
/** @type {{files: MongoFile[]}} */
|
/** @type {{files: MongoFile[]}} */
|
||||||
const {
|
const {
|
||||||
|
@ -126,8 +127,8 @@ const chatV2 = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkBalanceBeforeRun = async () => {
|
const checkBalanceBeforeRun = async () => {
|
||||||
const balance = req.app?.locals?.balance;
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
if (!balance?.enabled) {
|
if (!balanceConfig?.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const transactions =
|
const transactions =
|
||||||
|
@ -374,9 +375,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,
|
||||||
|
|
|
@ -22,7 +22,7 @@ const getLogStores = require('~/cache/getLogStores');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ErrorHandlerDependencies
|
* @typedef {Object} ErrorHandlerDependencies
|
||||||
* @property {Express.Request} req - The Express request object
|
* @property {ServerRequest} req - The Express request object
|
||||||
* @property {Express.Response} res - The Express response object
|
* @property {Express.Response} res - The Express response object
|
||||||
* @property {() => ErrorHandlerContext} getContext - Function to get the current context
|
* @property {() => ErrorHandlerContext} getContext - Function to get the current context
|
||||||
* @property {string} [originPath] - The origin path for the error handler
|
* @property {string} [originPath] - The origin path for the error handler
|
||||||
|
|
|
@ -11,7 +11,7 @@ const { initializeClient } = require('~/server/services/Endpoints/assistants');
|
||||||
const { getEndpointsConfig } = require('~/server/services/Config');
|
const { getEndpointsConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Express.Request} req
|
* @param {ServerRequest} req
|
||||||
* @param {string} [endpoint]
|
* @param {string} [endpoint]
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
|
@ -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 = req.config;
|
||||||
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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -258,8 +258,9 @@ function filterAssistantDocs({ documents, userId, assistantsConfig = {} }) {
|
||||||
*/
|
*/
|
||||||
const getAssistantDocuments = async (req, res) => {
|
const getAssistantDocuments = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = req.config;
|
||||||
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 = req.config;
|
||||||
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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -94,7 +94,7 @@ const createAssistant = async (req, res) => {
|
||||||
/**
|
/**
|
||||||
* Modifies an assistant.
|
* Modifies an assistant.
|
||||||
* @param {object} params
|
* @param {object} params
|
||||||
* @param {Express.Request} params.req
|
* @param {ServerRequest} params.req
|
||||||
* @param {OpenAIClient} params.openai
|
* @param {OpenAIClient} params.openai
|
||||||
* @param {string} params.assistant_id
|
* @param {string} params.assistant_id
|
||||||
* @param {AssistantUpdateParams} params.updateData
|
* @param {AssistantUpdateParams} params.updateData
|
||||||
|
@ -199,7 +199,7 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
|
||||||
/**
|
/**
|
||||||
* Modifies an assistant with the resource file id.
|
* Modifies an assistant with the resource file id.
|
||||||
* @param {object} params
|
* @param {object} params
|
||||||
* @param {Express.Request} params.req
|
* @param {ServerRequest} params.req
|
||||||
* @param {OpenAIClient} params.openai
|
* @param {OpenAIClient} params.openai
|
||||||
* @param {string} params.assistant_id
|
* @param {string} params.assistant_id
|
||||||
* @param {string} params.tool_resource
|
* @param {string} params.tool_resource
|
||||||
|
@ -227,7 +227,7 @@ const addResourceFileId = async ({ req, openai, assistant_id, tool_resource, fil
|
||||||
/**
|
/**
|
||||||
* Deletes a file ID from an assistant's resource.
|
* Deletes a file ID from an assistant's resource.
|
||||||
* @param {object} params
|
* @param {object} params
|
||||||
* @param {Express.Request} params.req
|
* @param {ServerRequest} params.req
|
||||||
* @param {OpenAIClient} params.openai
|
* @param {OpenAIClient} params.openai
|
||||||
* @param {string} params.assistant_id
|
* @param {string} params.assistant_id
|
||||||
* @param {string} [params.tool_resource]
|
* @param {string} [params.tool_resource]
|
||||||
|
|
|
@ -35,9 +35,10 @@ const toolAccessPermType = {
|
||||||
*/
|
*/
|
||||||
const verifyWebSearchAuth = async (req, res) => {
|
const verifyWebSearchAuth = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = req.config;
|
||||||
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 +111,7 @@ const verifyToolAuth = async (req, res) => {
|
||||||
*/
|
*/
|
||||||
const callTool = async (req, res) => {
|
const callTool = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = req.config;
|
||||||
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 +157,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,27 @@ 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({})),
|
||||||
});
|
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 +49,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
|
||||||
|
|
|
@ -12,8 +12,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 = req.config;
|
||||||
/** @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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,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 = req.config;
|
||||||
/** @type {Partial<TAssistantEndpoint>} */
|
/** @type {Partial<TAssistantEndpoint>} */
|
||||||
const assistantsConfig = req.app.locals?.[endpoint];
|
const assistantsConfig = appConfig.endpoints?.[endpoint];
|
||||||
if (!assistantsConfig) {
|
if (!assistantsConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,9 +40,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 = req.config;
|
||||||
|
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 = await getAppConfig({
|
||||||
|
role: req?.user?.role,
|
||||||
|
});
|
||||||
|
if (email && !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 {
|
||||||
|
|
27
api/server/middleware/config/app.js
Normal file
27
api/server/middleware/config/app.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
|
|
||||||
|
const configMiddleware = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userRole = req.user?.role;
|
||||||
|
req.config = await getAppConfig({ role: userRole });
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Config middleware error:', {
|
||||||
|
error: error.message,
|
||||||
|
userRole: req.user?.role,
|
||||||
|
path: req.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
req.config = await getAppConfig();
|
||||||
|
next();
|
||||||
|
} catch (fallbackError) {
|
||||||
|
logger.error('Fallback config middleware error:', fallbackError);
|
||||||
|
next(fallbackError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = configMiddleware;
|
|
@ -82,7 +82,7 @@ const sendError = async (req, res, options, callback) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the response based on whether headers have been sent or not.
|
* Sends the response based on whether headers have been sent or not.
|
||||||
* @param {Express.Request} req - The server response.
|
* @param {ServerRequest} req - The server response.
|
||||||
* @param {Express.Response} res - The server response.
|
* @param {Express.Response} res - The server response.
|
||||||
* @param {Object} data - The data to be sent.
|
* @param {Object} data - The data to be sent.
|
||||||
* @param {string} [errorMessage] - The error message, if any.
|
* @param {string} [errorMessage] - The error message, if any.
|
||||||
|
|
|
@ -13,6 +13,7 @@ const requireLdapAuth = require('./requireLdapAuth');
|
||||||
const abortMiddleware = require('./abortMiddleware');
|
const abortMiddleware = require('./abortMiddleware');
|
||||||
const checkInviteUser = require('./checkInviteUser');
|
const checkInviteUser = require('./checkInviteUser');
|
||||||
const requireJwtAuth = require('./requireJwtAuth');
|
const requireJwtAuth = require('./requireJwtAuth');
|
||||||
|
const configMiddleware = require('./config/app');
|
||||||
const validateModel = require('./validateModel');
|
const validateModel = require('./validateModel');
|
||||||
const moderateText = require('./moderateText');
|
const moderateText = require('./moderateText');
|
||||||
const logHeaders = require('./logHeaders');
|
const logHeaders = require('./logHeaders');
|
||||||
|
@ -43,6 +44,7 @@ module.exports = {
|
||||||
requireLocalAuth,
|
requireLocalAuth,
|
||||||
canDeleteAccount,
|
canDeleteAccount,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
|
configMiddleware,
|
||||||
concurrentLimiter,
|
concurrentLimiter,
|
||||||
checkDomainAllowed,
|
checkDomainAllowed,
|
||||||
validateMessageReq,
|
validateMessageReq,
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ const { USE_REDIS, CONVO_ACCESS_VIOLATION_SCORE: score = 0 } = process.env ?? {}
|
||||||
* If the `cache` store is not available, the middleware will skip its logic.
|
* If the `cache` store is not available, the middleware will skip its logic.
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
* @param {Express.Request} req - Express request object containing user information.
|
* @param {ServerRequest} req - Express request object containing user information.
|
||||||
* @param {Express.Response} res - Express response object.
|
* @param {Express.Response} res - Express response object.
|
||||||
* @param {function} next - Express next middleware function.
|
* @param {function} next - Express next middleware function.
|
||||||
* @throws {Error} Throws an error if the user doesn't have access to the conversation.
|
* @throws {Error} Throws an error if the user doesn't have access to the conversation.
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ const { logViolation } = require('~/cache');
|
||||||
* Validates the model of the request.
|
* Validates the model of the request.
|
||||||
*
|
*
|
||||||
* @async
|
* @async
|
||||||
* @param {Express.Request} req - The Express request object.
|
* @param {ServerRequest} req - The Express request object.
|
||||||
* @param {Express.Response} res - The Express response object.
|
* @param {Express.Response} res - The Express response object.
|
||||||
* @param {Function} next - The Express next function.
|
* @param {Function} next - The Express next function.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -83,7 +83,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 = req.config;
|
||||||
|
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' });
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ const {
|
||||||
checkBan,
|
checkBan,
|
||||||
requireJwtAuth,
|
requireJwtAuth,
|
||||||
messageIpLimiter,
|
messageIpLimiter,
|
||||||
|
configMiddleware,
|
||||||
concurrentLimiter,
|
concurrentLimiter,
|
||||||
messageUserLimiter,
|
messageUserLimiter,
|
||||||
} = require('~/server/middleware');
|
} = require('~/server/middleware');
|
||||||
|
@ -22,6 +23,8 @@ router.use(uaParser);
|
||||||
router.use('/', v1);
|
router.use('/', v1);
|
||||||
|
|
||||||
const chatRouter = express.Router();
|
const chatRouter = express.Router();
|
||||||
|
chatRouter.use(configMiddleware);
|
||||||
|
|
||||||
if (isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
|
if (isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
|
||||||
chatRouter.use(concurrentLimiter);
|
chatRouter.use(concurrentLimiter);
|
||||||
}
|
}
|
||||||
|
@ -37,6 +40,4 @@ if (isEnabled(LIMIT_MESSAGE_USER)) {
|
||||||
chatRouter.use('/', chat);
|
chatRouter.use('/', chat);
|
||||||
router.use('/chat', chatRouter);
|
router.use('/chat', chatRouter);
|
||||||
|
|
||||||
// Add marketplace routes
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools');
|
const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools');
|
||||||
const { getAvailableTools } = require('~/server/controllers/PluginController');
|
const { getAvailableTools } = require('~/server/controllers/PluginController');
|
||||||
const { toolCallLimiter } = require('~/server/middleware/limiters');
|
const { toolCallLimiter } = require('~/server/middleware');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { generateCheckAccess } = require('@librechat/api');
|
const { generateCheckAccess } = require('@librechat/api');
|
||||||
const { PermissionTypes, Permissions, PermissionBits } = require('librechat-data-provider');
|
const { PermissionTypes, Permissions, PermissionBits } = require('librechat-data-provider');
|
||||||
const { requireJwtAuth, canAccessAgentResource } = require('~/server/middleware');
|
const { requireJwtAuth, configMiddleware, canAccessAgentResource } = require('~/server/middleware');
|
||||||
const v1 = require('~/server/controllers/agents/v1');
|
const v1 = require('~/server/controllers/agents/v1');
|
||||||
const { getRoleByName } = require('~/models/Role');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
const actions = require('./actions');
|
const actions = require('./actions');
|
||||||
|
@ -36,13 +36,13 @@ router.use(requireJwtAuth);
|
||||||
* Agent actions route.
|
* Agent actions route.
|
||||||
* @route GET|POST /agents/actions
|
* @route GET|POST /agents/actions
|
||||||
*/
|
*/
|
||||||
router.use('/actions', actions);
|
router.use('/actions', configMiddleware, actions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of available tools for agents.
|
* Get a list of available tools for agents.
|
||||||
* @route GET /agents/tools
|
* @route GET /agents/tools
|
||||||
*/
|
*/
|
||||||
router.use('/tools', tools);
|
router.use('/tools', configMiddleware, tools);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all agent categories with counts
|
* Get all agent categories with counts
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { actionDelimiter, EModelEndpoint, removeNullishValues } = require('librechat-data-provider');
|
const { actionDelimiter, EModelEndpoint, removeNullishValues } = require('librechat-data-provider');
|
||||||
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
||||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
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 { logger } = require('~/config');
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ const router = express.Router();
|
||||||
*/
|
*/
|
||||||
router.post('/:assistant_id', async (req, res) => {
|
router.post('/:assistant_id', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = req.config;
|
||||||
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 +31,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 +129,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,6 +1,6 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { uaParser, checkBan, requireJwtAuth, configMiddleware } = require('~/server/middleware');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { uaParser, checkBan, requireJwtAuth } = require('~/server/middleware');
|
|
||||||
|
|
||||||
const { v1 } = require('./v1');
|
const { v1 } = require('./v1');
|
||||||
const chatV1 = require('./chatV1');
|
const chatV1 = require('./chatV1');
|
||||||
|
@ -10,6 +10,7 @@ const chatV2 = require('./chatV2');
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
router.use(checkBan);
|
router.use(checkBan);
|
||||||
router.use(uaParser);
|
router.use(uaParser);
|
||||||
|
router.use(configMiddleware);
|
||||||
router.use('/v1/', v1);
|
router.use('/v1/', v1);
|
||||||
router.use('/v1/chat', chatV1);
|
router.use('/v1/chat', chatV1);
|
||||||
router.use('/v2/', v2);
|
router.use('/v2/', v2);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { configMiddleware } = require('~/server/middleware');
|
||||||
const v1 = require('~/server/controllers/assistants/v1');
|
const v1 = require('~/server/controllers/assistants/v1');
|
||||||
const v2 = require('~/server/controllers/assistants/v2');
|
const v2 = require('~/server/controllers/assistants/v2');
|
||||||
const documents = require('./documents');
|
const documents = require('./documents');
|
||||||
|
@ -6,6 +7,7 @@ const actions = require('./actions');
|
||||||
const tools = require('./tools');
|
const tools = require('./tools');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
router.use(configMiddleware);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assistant actions route.
|
* Assistant actions route.
|
||||||
|
|
|
@ -17,12 +17,12 @@ const {
|
||||||
const { verify2FAWithTempToken } = require('~/server/controllers/auth/TwoFactorAuthController');
|
const { verify2FAWithTempToken } = require('~/server/controllers/auth/TwoFactorAuthController');
|
||||||
const { logoutController } = require('~/server/controllers/auth/LogoutController');
|
const { logoutController } = require('~/server/controllers/auth/LogoutController');
|
||||||
const { loginController } = require('~/server/controllers/auth/LoginController');
|
const { loginController } = require('~/server/controllers/auth/LoginController');
|
||||||
const { getBalanceConfig } = require('~/server/services/Config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const middleware = require('~/server/middleware');
|
const middleware = require('~/server/middleware');
|
||||||
const { Balance } = require('~/db/models');
|
const { Balance } = require('~/db/models');
|
||||||
|
|
||||||
const setBalanceConfig = createSetBalanceConfig({
|
const setBalanceConfig = createSetBalanceConfig({
|
||||||
getBalanceConfig,
|
getAppConfig,
|
||||||
Balance,
|
Balance,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
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 { isEnabled, getBalanceConfig } = require('@librechat/api');
|
||||||
const { getCustomConfig } = require('~/server/services/Config/getCustomConfig');
|
const {
|
||||||
|
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 &&
|
||||||
|
@ -55,10 +62,12 @@ router.get('/', async function (req, res) {
|
||||||
!!process.env.SAML_CERT &&
|
!!process.env.SAML_CERT &&
|
||||||
!!process.env.SAML_SESSION_SECRET;
|
!!process.env.SAML_SESSION_SECRET;
|
||||||
|
|
||||||
|
const balanceConfig = getBalanceConfig(appConfig);
|
||||||
|
|
||||||
/** @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 +100,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: balanceConfig,
|
||||||
sharedLinksEnabled,
|
sharedLinksEnabled,
|
||||||
publicSharedLinksEnabled,
|
publicSharedLinksEnabled,
|
||||||
analyticsGtmId: process.env.ANALYTICS_GTM_ID,
|
analyticsGtmId: process.env.ANALYTICS_GTM_ID,
|
||||||
|
@ -109,27 +118,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 (error) {
|
||||||
|
logger.error('Error loading MCP servers', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Error loading MCP servers', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {TCustomConfig['webSearch']} */
|
getMCPServers();
|
||||||
const webSearchConfig = req.app.locals.webSearch;
|
const webSearchConfig = appConfig?.webSearch;
|
||||||
if (
|
if (
|
||||||
webSearchConfig != null &&
|
webSearchConfig != null &&
|
||||||
(webSearchConfig.searchProvider ||
|
(webSearchConfig.searchProvider ||
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
|
||||||
const endpointController = require('~/server/controllers/EndpointController');
|
const endpointController = require('~/server/controllers/EndpointController');
|
||||||
const overrideController = require('~/server/controllers/OverrideController');
|
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
router.get('/', endpointController);
|
router.get('/', endpointController);
|
||||||
router.get('/config/override', overrideController);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
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 { logger } = require('~/config');
|
const { filterFile } = require('~/server/services/Files/process');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = req.config;
|
||||||
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 +20,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 +40,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,8 +36,9 @@ const router = express.Router();
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const appConfig = req.config;
|
||||||
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 +115,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 = req.config;
|
||||||
|
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 });
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { isAgentsEndpoint } = require('librechat-data-provider');
|
const { isAgentsEndpoint } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
filterFile,
|
filterFile,
|
||||||
processImageFile,
|
processImageFile,
|
||||||
processAgentFileUpload,
|
processAgentFileUpload,
|
||||||
} = require('~/server/services/Files/process');
|
} = require('~/server/services/Files/process');
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
const metadata = req.body;
|
const metadata = req.body;
|
||||||
|
const appConfig = req.config;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
filterFile({ req, image: true });
|
filterFile({ req, image: true });
|
||||||
|
@ -30,7 +31,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 +44,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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { uaParser, checkBan, requireJwtAuth, createFileLimiters } = require('~/server/middleware');
|
const {
|
||||||
|
createFileLimiters,
|
||||||
|
configMiddleware,
|
||||||
|
requireJwtAuth,
|
||||||
|
uaParser,
|
||||||
|
checkBan,
|
||||||
|
} = require('~/server/middleware');
|
||||||
const { avatar: asstAvatarRouter } = require('~/server/routes/assistants/v1');
|
const { avatar: asstAvatarRouter } = require('~/server/routes/assistants/v1');
|
||||||
const { avatar: agentAvatarRouter } = require('~/server/routes/agents/v1');
|
const { avatar: agentAvatarRouter } = require('~/server/routes/agents/v1');
|
||||||
const { createMulterInstance } = require('./multer');
|
const { createMulterInstance } = require('./multer');
|
||||||
|
@ -12,6 +18,7 @@ const speech = require('./speech');
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
|
router.use(configMiddleware);
|
||||||
router.use(checkBan);
|
router.use(checkBan);
|
||||||
router.use(uaParser);
|
router.use(uaParser);
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,12 @@ 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);
|
const appConfig = req.config;
|
||||||
|
const outputPath = path.join(appConfig.paths.uploads, 'temp', req.user.id);
|
||||||
if (!fs.existsSync(outputPath)) {
|
if (!fs.existsSync(outputPath)) {
|
||||||
fs.mkdirSync(outputPath, { recursive: true });
|
fs.mkdirSync(outputPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
@ -68,8 +69,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,15 +22,13 @@ describe('Multer Configuration', () => {
|
||||||
|
|
||||||
mockReq = {
|
mockReq = {
|
||||||
user: { id: 'test-user-123' },
|
user: { id: 'test-user-123' },
|
||||||
app: {
|
body: {},
|
||||||
locals: {
|
originalUrl: '/api/files/upload',
|
||||||
|
config: {
|
||||||
paths: {
|
paths: {
|
||||||
uploads: tempDir,
|
uploads: tempDir,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
body: {},
|
|
||||||
originalUrl: '/api/files/upload',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFile = {
|
mockFile = {
|
||||||
|
@ -79,7 +63,7 @@ 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;
|
mockReq.config.paths.uploads = deepPath;
|
||||||
|
|
||||||
const cb = jest.fn((err, destination) => {
|
const cb = jest.fn((err, destination) => {
|
||||||
expect(err).toBeNull();
|
expect(err).toBeNull();
|
||||||
|
@ -331,11 +315,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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -462,26 +446,15 @@ describe('Multer Configuration', () => {
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle file system errors when directory creation fails', (done) => {
|
it('should handle file system errors when directory creation fails', () => {
|
||||||
// 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;
|
mockReq.config.paths.uploads = invalidPath;
|
||||||
|
|
||||||
try {
|
// The current implementation doesn't catch errors, so they're thrown synchronously
|
||||||
// Call getDestination which should fail due to permission/path issues
|
expect(() => {
|
||||||
storage.getDestination(mockReq, mockFile, (err, destination) => {
|
storage.getDestination(mockReq, mockFile, jest.fn());
|
||||||
// If callback is reached, we didn't get the expected error
|
}).toThrow();
|
||||||
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
|
|
||||||
// On Linux, this typically returns EACCES (permission denied)
|
|
||||||
// On macOS/Darwin, this returns ENOENT (no such file or directory)
|
|
||||||
expect(['EACCES', 'ENOENT']).toContain(error.code);
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle malformed filenames with real sanitization', (done) => {
|
it('should handle malformed filenames with real sanitization', (done) => {
|
||||||
|
@ -538,10 +511,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 +522,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,7 +8,7 @@ const {
|
||||||
deleteMemory,
|
deleteMemory,
|
||||||
setMemory,
|
setMemory,
|
||||||
} = require('~/models');
|
} = require('~/models');
|
||||||
const { requireJwtAuth } = require('~/server/middleware');
|
const { requireJwtAuth, configMiddleware } = require('~/server/middleware');
|
||||||
const { getRoleByName } = require('~/models/Role');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
@ -48,7 +48,7 @@ router.use(requireJwtAuth);
|
||||||
* Returns all memories for the authenticated user, sorted by updated_at (newest first).
|
* Returns all memories for the authenticated user, sorted by updated_at (newest first).
|
||||||
* Also includes memory usage percentage based on token limit.
|
* Also includes memory usage percentage based on token limit.
|
||||||
*/
|
*/
|
||||||
router.get('/', checkMemoryRead, async (req, res) => {
|
router.get('/', checkMemoryRead, configMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const memories = await getAllUserMemories(req.user.id);
|
const memories = await getAllUserMemories(req.user.id);
|
||||||
|
|
||||||
|
@ -60,7 +60,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 = req.config;
|
||||||
|
const memoryConfig = appConfig?.memory;
|
||||||
const tokenLimit = memoryConfig?.tokenLimit;
|
const tokenLimit = memoryConfig?.tokenLimit;
|
||||||
const charLimit = memoryConfig?.charLimit || 10000;
|
const charLimit = memoryConfig?.charLimit || 10000;
|
||||||
|
|
||||||
|
@ -87,7 +88,7 @@ router.get('/', checkMemoryRead, async (req, res) => {
|
||||||
* Body: { key: string, value: string }
|
* Body: { key: string, value: string }
|
||||||
* Returns 201 and { created: true, memory: <createdDoc> } when successful.
|
* Returns 201 and { created: true, memory: <createdDoc> } when successful.
|
||||||
*/
|
*/
|
||||||
router.post('/', memoryPayloadLimit, checkMemoryCreate, async (req, res) => {
|
router.post('/', memoryPayloadLimit, checkMemoryCreate, configMiddleware, async (req, res) => {
|
||||||
const { key, value } = req.body;
|
const { key, value } = req.body;
|
||||||
|
|
||||||
if (typeof key !== 'string' || key.trim() === '') {
|
if (typeof key !== 'string' || key.trim() === '') {
|
||||||
|
@ -98,7 +99,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 = req.config;
|
||||||
|
const memoryConfig = appConfig?.memory;
|
||||||
const charLimit = memoryConfig?.charLimit || 10000;
|
const charLimit = memoryConfig?.charLimit || 10000;
|
||||||
|
|
||||||
if (key.length > 1000) {
|
if (key.length > 1000) {
|
||||||
|
@ -117,6 +119,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 = req.config;
|
||||||
|
const memoryConfig = appConfig?.memory;
|
||||||
const tokenLimit = memoryConfig?.tokenLimit;
|
const tokenLimit = memoryConfig?.tokenLimit;
|
||||||
|
|
||||||
if (tokenLimit) {
|
if (tokenLimit) {
|
||||||
|
@ -191,7 +196,7 @@ router.patch('/preferences', checkMemoryOptOut, async (req, res) => {
|
||||||
* Body: { key?: string, value: string }
|
* Body: { key?: string, value: string }
|
||||||
* Returns 200 and { updated: true, memory: <updatedDoc> } when successful.
|
* Returns 200 and { updated: true, memory: <updatedDoc> } when successful.
|
||||||
*/
|
*/
|
||||||
router.patch('/:key', memoryPayloadLimit, checkMemoryUpdate, async (req, res) => {
|
router.patch('/:key', memoryPayloadLimit, checkMemoryUpdate, configMiddleware, async (req, res) => {
|
||||||
const { key: urlKey } = req.params;
|
const { key: urlKey } = req.params;
|
||||||
const { key: bodyKey, value } = req.body || {};
|
const { key: bodyKey, value } = req.body || {};
|
||||||
|
|
||||||
|
@ -200,8 +205,8 @@ router.patch('/:key', memoryPayloadLimit, checkMemoryUpdate, async (req, res) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
const newKey = bodyKey || urlKey;
|
const newKey = bodyKey || urlKey;
|
||||||
|
const appConfig = req.config;
|
||||||
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) {
|
||||||
|
|
|
@ -8,11 +8,11 @@ const { isEnabled, createSetBalanceConfig } = require('@librechat/api');
|
||||||
const { checkDomainAllowed, loginLimiter, logHeaders, checkBan } = require('~/server/middleware');
|
const { checkDomainAllowed, loginLimiter, logHeaders, checkBan } = require('~/server/middleware');
|
||||||
const { syncUserEntraGroupMemberships } = require('~/server/services/PermissionService');
|
const { syncUserEntraGroupMemberships } = require('~/server/services/PermissionService');
|
||||||
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
|
||||||
const { getBalanceConfig } = require('~/server/services/Config');
|
const { getAppConfig } = require('~/server/services/Config');
|
||||||
const { Balance } = require('~/db/models');
|
const { Balance } = require('~/db/models');
|
||||||
|
|
||||||
const setBalanceConfig = createSetBalanceConfig({
|
const setBalanceConfig = createSetBalanceConfig({
|
||||||
getBalanceConfig,
|
getAppConfig,
|
||||||
Balance,
|
Balance,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { getAvailablePluginsController } = require('../controllers/PluginController');
|
const { getAvailablePluginsController } = require('~/server/controllers/PluginController');
|
||||||
const requireJwtAuth = require('../middleware/requireJwtAuth');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
|
@ -424,7 +424,7 @@ router.get('/', async (req, res) => {
|
||||||
/**
|
/**
|
||||||
* Deletes a prompt
|
* Deletes a prompt
|
||||||
*
|
*
|
||||||
* @param {Express.Request} req - The request object.
|
* @param {ServerRequest} req - The request object.
|
||||||
* @param {TDeletePromptVariables} req.params - The request parameters
|
* @param {TDeletePromptVariables} req.params - The request parameters
|
||||||
* @param {import('mongoose').ObjectId} req.params.promptId - The prompt ID
|
* @param {import('mongoose').ObjectId} req.params.promptId - The prompt ID
|
||||||
* @param {Express.Response} res - The response object.
|
* @param {Express.Response} res - The response object.
|
||||||
|
|
|
@ -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,14 +1,14 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { requireJwtAuth, canDeleteAccount, verifyEmailLimiter } = require('~/server/middleware');
|
|
||||||
const {
|
const {
|
||||||
getUserController,
|
|
||||||
deleteUserController,
|
|
||||||
verifyEmailController,
|
|
||||||
updateUserPluginsController,
|
updateUserPluginsController,
|
||||||
resendVerificationController,
|
resendVerificationController,
|
||||||
getTermsStatusController,
|
getTermsStatusController,
|
||||||
acceptTermsController,
|
acceptTermsController,
|
||||||
|
verifyEmailController,
|
||||||
|
deleteUserController,
|
||||||
|
getUserController,
|
||||||
} = require('~/server/controllers/UserController');
|
} = require('~/server/controllers/UserController');
|
||||||
|
const { requireJwtAuth, canDeleteAccount, verifyEmailLimiter } = require('~/server/middleware');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
|
@ -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({
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
peoplePicker: expect.objectContaining({
|
||||||
users: true,
|
users: true,
|
||||||
groups: true,
|
groups: true,
|
||||||
roles: 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,
|
||||||
|
endpoints: {
|
||||||
[EModelEndpoint.agents]: agentsDefaults,
|
[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;
|
||||||
|
|
|
@ -10,10 +10,24 @@ const {
|
||||||
conflictingAzureVariables,
|
conflictingAzureVariables,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
|
|
||||||
|
jest.mock('@librechat/data-schemas', () => ({
|
||||||
|
...jest.requireActual('@librechat/data-schemas'),
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const AppService = require('./AppService');
|
const AppService = require('./AppService');
|
||||||
|
|
||||||
jest.mock('./Config/loadCustomConfig', () => {
|
jest.mock('./Files/Firebase/initialize', () => ({
|
||||||
return jest.fn(() =>
|
initializeFirebase: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./Config/loadCustomConfig', () =>
|
||||||
|
jest.fn(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
registration: { socialLogins: ['testLogin'] },
|
registration: { socialLogins: ['testLogin'] },
|
||||||
fileStrategy: 'testStrategy',
|
fileStrategy: 'testStrategy',
|
||||||
|
@ -21,40 +35,10 @@ jest.mock('./Config/loadCustomConfig', () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
jest.mock('./Files/Firebase/initialize', () => ({
|
|
||||||
initializeFirebase: jest.fn(),
|
jest.mock('./start/tools', () => ({
|
||||||
}));
|
|
||||||
jest.mock('~/models', () => ({
|
|
||||||
initializeRoles: jest.fn(),
|
|
||||||
seedDefaultRoles: jest.fn(),
|
|
||||||
ensureDefaultCategories: jest.fn(),
|
|
||||||
}));
|
|
||||||
jest.mock('~/models/Role', () => ({
|
|
||||||
updateAccessPermissions: jest.fn(),
|
|
||||||
getRoleByName: jest.fn().mockResolvedValue(null),
|
|
||||||
}));
|
|
||||||
jest.mock('./Config', () => ({
|
|
||||||
setCachedTools: jest.fn(),
|
|
||||||
getCachedTools: jest.fn().mockResolvedValue({
|
|
||||||
ExampleTool: {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
description: 'Example tool function',
|
|
||||||
name: 'exampleFunction',
|
|
||||||
parameters: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
param1: { type: 'string', description: 'An example parameter' },
|
|
||||||
},
|
|
||||||
required: ['param1'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
jest.mock('./ToolService', () => ({
|
|
||||||
loadAndFormatTools: jest.fn().mockReturnValue({
|
loadAndFormatTools: jest.fn().mockReturnValue({
|
||||||
ExampleTool: {
|
ExampleTool: {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
|
@ -116,28 +100,36 @@ const azureGroups = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
jest.mock('./start/checks', () => ({
|
||||||
|
...jest.requireActual('./start/checks'),
|
||||||
|
checkHealth: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('AppService', () => {
|
describe('AppService', () => {
|
||||||
let app;
|
|
||||||
const mockedTurnstileConfig = {
|
const mockedTurnstileConfig = {
|
||||||
siteKey: 'default-site-key',
|
siteKey: 'default-site-key',
|
||||||
options: {},
|
options: {},
|
||||||
};
|
};
|
||||||
|
const loadCustomConfig = require('./Config/loadCustomConfig');
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
app = { locals: {} };
|
|
||||||
process.env.CDN_PROVIDER = undefined;
|
process.env.CDN_PROVIDER = undefined;
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly assign process.env and app.locals based on custom config', async () => {
|
it('should correctly assign process.env and initialize app config based on custom config', async () => {
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(process.env.CDN_PROVIDER).toEqual('testStrategy');
|
expect(process.env.CDN_PROVIDER).toEqual('testStrategy');
|
||||||
|
|
||||||
expect(app.locals).toEqual({
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
config: expect.objectContaining({
|
config: expect.objectContaining({
|
||||||
fileStrategy: 'testStrategy',
|
fileStrategy: 'testStrategy',
|
||||||
}),
|
}),
|
||||||
|
registration: expect.objectContaining({
|
||||||
socialLogins: ['testLogin'],
|
socialLogins: ['testLogin'],
|
||||||
|
}),
|
||||||
fileStrategy: 'testStrategy',
|
fileStrategy: 'testStrategy',
|
||||||
interfaceConfig: expect.objectContaining({
|
interfaceConfig: expect.objectContaining({
|
||||||
endpointsMenu: true,
|
endpointsMenu: true,
|
||||||
|
@ -157,7 +149,7 @@ describe('AppService', () => {
|
||||||
balance: { enabled: true },
|
balance: { enabled: true },
|
||||||
filteredTools: undefined,
|
filteredTools: undefined,
|
||||||
includedTools: undefined,
|
includedTools: undefined,
|
||||||
webSearch: {
|
webSearch: expect.objectContaining({
|
||||||
safeSearch: 1,
|
safeSearch: 1,
|
||||||
jinaApiKey: '${JINA_API_KEY}',
|
jinaApiKey: '${JINA_API_KEY}',
|
||||||
cohereApiKey: '${COHERE_API_KEY}',
|
cohereApiKey: '${COHERE_API_KEY}',
|
||||||
|
@ -166,20 +158,23 @@ describe('AppService', () => {
|
||||||
firecrawlApiKey: '${FIRECRAWL_API_KEY}',
|
firecrawlApiKey: '${FIRECRAWL_API_KEY}',
|
||||||
firecrawlApiUrl: '${FIRECRAWL_API_URL}',
|
firecrawlApiUrl: '${FIRECRAWL_API_URL}',
|
||||||
searxngInstanceUrl: '${SEARXNG_INSTANCE_URL}',
|
searxngInstanceUrl: '${SEARXNG_INSTANCE_URL}',
|
||||||
},
|
}),
|
||||||
memory: undefined,
|
memory: undefined,
|
||||||
agents: {
|
endpoints: expect.objectContaining({
|
||||||
|
agents: expect.objectContaining({
|
||||||
disableBuilder: false,
|
disableBuilder: false,
|
||||||
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
|
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
|
||||||
maxCitations: 30,
|
maxCitations: 30,
|
||||||
maxCitationsPerFile: 7,
|
maxCitationsPerFile: 7,
|
||||||
minRelevanceScore: 0.45,
|
minRelevanceScore: 0.45,
|
||||||
},
|
}),
|
||||||
});
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log a warning if the config version is outdated', async () => {
|
it('should log a warning if the config version is outdated', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
version: '0.9.0', // An outdated version for this test
|
version: '0.9.0', // An outdated version for this test
|
||||||
registration: { socialLogins: ['testLogin'] },
|
registration: { socialLogins: ['testLogin'] },
|
||||||
|
@ -187,50 +182,62 @@ describe('AppService', () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
await AppService();
|
||||||
|
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Outdated Config version'));
|
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Outdated Config version'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change the `imageOutputType` based on config value', async () => {
|
it('should change the `imageOutputType` based on config value', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
version: '0.10.0',
|
version: '0.10.0',
|
||||||
imageOutputType: EImageOutputType.WEBP,
|
imageOutputType: EImageOutputType.WEBP,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
expect(app.locals.imageOutputType).toEqual(EImageOutputType.WEBP);
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
imageOutputType: EImageOutputType.WEBP,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to `PNG` `imageOutputType` with no provided type', async () => {
|
it('should default to `PNG` `imageOutputType` with no provided type', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
version: '0.10.0',
|
version: '0.10.0',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
expect(app.locals.imageOutputType).toEqual(EImageOutputType.PNG);
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
imageOutputType: EImageOutputType.PNG,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to `PNG` `imageOutputType` with no provided config', async () => {
|
it('should default to `PNG` `imageOutputType` with no provided config', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(undefined));
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(undefined));
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
expect(app.locals.imageOutputType).toEqual(EImageOutputType.PNG);
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
imageOutputType: EImageOutputType.PNG,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize Firebase when fileStrategy is firebase', async () => {
|
it('should initialize Firebase when fileStrategy is firebase', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
fileStrategy: FileSources.firebase,
|
fileStrategy: FileSources.firebase,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
await AppService();
|
||||||
|
|
||||||
const { initializeFirebase } = require('./Files/Firebase/initialize');
|
const { initializeFirebase } = require('./Files/Firebase/initialize');
|
||||||
expect(initializeFirebase).toHaveBeenCalled();
|
expect(initializeFirebase).toHaveBeenCalled();
|
||||||
|
@ -239,10 +246,9 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load and format tools accurately with defined structure', async () => {
|
it('should load and format tools accurately with defined structure', async () => {
|
||||||
const { loadAndFormatTools } = require('./ToolService');
|
const { loadAndFormatTools } = require('./start/tools');
|
||||||
const { setCachedTools, getCachedTools } = require('./Config');
|
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(loadAndFormatTools).toHaveBeenCalledWith({
|
expect(loadAndFormatTools).toHaveBeenCalledWith({
|
||||||
adminFilter: undefined,
|
adminFilter: undefined,
|
||||||
|
@ -250,31 +256,9 @@ describe('AppService', () => {
|
||||||
directory: expect.anything(),
|
directory: expect.anything(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify setCachedTools was called with the tools
|
// Verify tools are included in the returned config
|
||||||
expect(setCachedTools).toHaveBeenCalledWith(
|
expect(result.availableTools).toBeDefined();
|
||||||
{
|
expect(result.availableTools.ExampleTool).toEqual({
|
||||||
ExampleTool: {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
description: 'Example tool function',
|
|
||||||
name: 'exampleFunction',
|
|
||||||
parameters: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
param1: { type: 'string', description: 'An example parameter' },
|
|
||||||
},
|
|
||||||
required: ['param1'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ isGlobal: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify we can retrieve the tools from cache
|
|
||||||
const cachedTools = await getCachedTools({ includeGlobal: true });
|
|
||||||
expect(cachedTools.ExampleTool).toBeDefined();
|
|
||||||
expect(cachedTools.ExampleTool).toEqual({
|
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
description: 'Example tool function',
|
description: 'Example tool function',
|
||||||
|
@ -291,7 +275,7 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure Assistants endpoint based on custom config', async () => {
|
it('should correctly configure Assistants endpoint based on custom config', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.assistants]: {
|
[EModelEndpoint.assistants]: {
|
||||||
|
@ -305,22 +289,25 @@ describe('AppService', () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.assistants);
|
expect(result).toEqual(
|
||||||
expect(app.locals[EModelEndpoint.assistants]).toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
endpoints: expect.objectContaining({
|
||||||
|
[EModelEndpoint.assistants]: expect.objectContaining({
|
||||||
disableBuilder: true,
|
disableBuilder: true,
|
||||||
pollIntervalMs: 5000,
|
pollIntervalMs: 5000,
|
||||||
timeoutMs: 30000,
|
timeoutMs: 30000,
|
||||||
supportedIds: expect.arrayContaining(['id1', 'id2']),
|
supportedIds: expect.arrayContaining(['id1', 'id2']),
|
||||||
privateAssistants: false,
|
privateAssistants: false,
|
||||||
}),
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure Agents endpoint based on custom config', async () => {
|
it('should correctly configure Agents endpoint based on custom config', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.agents]: {
|
[EModelEndpoint.agents]: {
|
||||||
|
@ -334,36 +321,45 @@ describe('AppService', () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.agents);
|
expect(result).toEqual(
|
||||||
expect(app.locals[EModelEndpoint.agents]).toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
endpoints: expect.objectContaining({
|
||||||
|
[EModelEndpoint.agents]: expect.objectContaining({
|
||||||
disableBuilder: true,
|
disableBuilder: true,
|
||||||
recursionLimit: 10,
|
recursionLimit: 10,
|
||||||
maxRecursionLimit: 20,
|
maxRecursionLimit: 20,
|
||||||
allowedProviders: expect.arrayContaining(['openai', 'anthropic']),
|
allowedProviders: expect.arrayContaining(['openai', 'anthropic']),
|
||||||
capabilities: expect.arrayContaining([AgentCapabilities.tools, AgentCapabilities.actions]),
|
capabilities: expect.arrayContaining([
|
||||||
|
AgentCapabilities.tools,
|
||||||
|
AgentCapabilities.actions,
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should configure Agents endpoint with defaults when no config is provided', async () => {
|
it('should configure Agents endpoint with defaults when no config is provided', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve({}));
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve({}));
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.agents);
|
expect(result).toEqual(
|
||||||
expect(app.locals[EModelEndpoint.agents]).toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
endpoints: expect.objectContaining({
|
||||||
|
[EModelEndpoint.agents]: expect.objectContaining({
|
||||||
disableBuilder: false,
|
disableBuilder: false,
|
||||||
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
|
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
|
||||||
}),
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should configure Agents endpoint with defaults when endpoints exist but agents is not defined', async () => {
|
it('should configure Agents endpoint with defaults when endpoints exist but agents is not defined', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.openAI]: {
|
[EModelEndpoint.openAI]: {
|
||||||
|
@ -373,20 +369,26 @@ describe('AppService', () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.agents);
|
expect(result).toEqual(
|
||||||
expect(app.locals[EModelEndpoint.agents]).toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
endpoints: expect.objectContaining({
|
||||||
|
[EModelEndpoint.agents]: expect.objectContaining({
|
||||||
disableBuilder: false,
|
disableBuilder: false,
|
||||||
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
|
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
|
||||||
}),
|
}),
|
||||||
|
[EModelEndpoint.openAI]: expect.objectContaining({
|
||||||
|
titleConvo: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure minimum Azure OpenAI Assistant values', async () => {
|
it('should correctly configure minimum Azure OpenAI Assistant values', async () => {
|
||||||
const assistantGroups = [azureGroups[0], { ...azureGroups[1], assistants: true }];
|
const assistantGroups = [azureGroups[0], { ...azureGroups[1], assistants: true }];
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
|
@ -400,13 +402,24 @@ describe('AppService', () => {
|
||||||
process.env.WESTUS_API_KEY = 'westus-key';
|
process.env.WESTUS_API_KEY = 'westus-key';
|
||||||
process.env.EASTUS_API_KEY = 'eastus-key';
|
process.env.EASTUS_API_KEY = 'eastus-key';
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.azureAssistants);
|
expect(result).toEqual(
|
||||||
expect(app.locals[EModelEndpoint.azureAssistants].capabilities.length).toEqual(3);
|
expect.objectContaining({
|
||||||
|
endpoints: expect.objectContaining({
|
||||||
|
[EModelEndpoint.azureAssistants]: expect.objectContaining({
|
||||||
|
capabilities: expect.arrayContaining([
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(String),
|
||||||
|
expect.any(String),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure Azure OpenAI endpoint based on custom config', async () => {
|
it('should correctly configure Azure OpenAI endpoint based on custom config', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
|
@ -419,18 +432,20 @@ describe('AppService', () => {
|
||||||
process.env.WESTUS_API_KEY = 'westus-key';
|
process.env.WESTUS_API_KEY = 'westus-key';
|
||||||
process.env.EASTUS_API_KEY = 'eastus-key';
|
process.env.EASTUS_API_KEY = 'eastus-key';
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.azureOpenAI);
|
|
||||||
const azureConfig = app.locals[EModelEndpoint.azureOpenAI];
|
|
||||||
expect(azureConfig).toHaveProperty('modelNames');
|
|
||||||
expect(azureConfig).toHaveProperty('modelGroupMap');
|
|
||||||
expect(azureConfig).toHaveProperty('groupMap');
|
|
||||||
|
|
||||||
const { modelNames, modelGroupMap, groupMap } = validateAzureGroups(azureGroups);
|
const { modelNames, modelGroupMap, groupMap } = validateAzureGroups(azureGroups);
|
||||||
expect(azureConfig.modelNames).toEqual(modelNames);
|
expect(result).toEqual(
|
||||||
expect(azureConfig.modelGroupMap).toEqual(modelGroupMap);
|
expect.objectContaining({
|
||||||
expect(azureConfig.groupMap).toEqual(groupMap);
|
endpoints: expect.objectContaining({
|
||||||
|
[EModelEndpoint.azureOpenAI]: expect.objectContaining({
|
||||||
|
modelNames,
|
||||||
|
modelGroupMap,
|
||||||
|
groupMap,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not modify FILE_UPLOAD environment variables without rate limits', async () => {
|
it('should not modify FILE_UPLOAD environment variables without rate limits', async () => {
|
||||||
|
@ -442,7 +457,7 @@ describe('AppService', () => {
|
||||||
|
|
||||||
const initialEnv = { ...process.env };
|
const initialEnv = { ...process.env };
|
||||||
|
|
||||||
await AppService(app);
|
await AppService();
|
||||||
|
|
||||||
// Expect environment variables to remain unchanged
|
// Expect environment variables to remain unchanged
|
||||||
expect(process.env.FILE_UPLOAD_IP_MAX).toEqual(initialEnv.FILE_UPLOAD_IP_MAX);
|
expect(process.env.FILE_UPLOAD_IP_MAX).toEqual(initialEnv.FILE_UPLOAD_IP_MAX);
|
||||||
|
@ -464,11 +479,9 @@ describe('AppService', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(rateLimitsConfig));
|
||||||
Promise.resolve(rateLimitsConfig),
|
|
||||||
);
|
|
||||||
|
|
||||||
await AppService(app);
|
await AppService();
|
||||||
|
|
||||||
// Verify that process.env has been updated according to the rate limits config
|
// Verify that process.env has been updated according to the rate limits config
|
||||||
expect(process.env.FILE_UPLOAD_IP_MAX).toEqual('100');
|
expect(process.env.FILE_UPLOAD_IP_MAX).toEqual('100');
|
||||||
|
@ -484,7 +497,7 @@ describe('AppService', () => {
|
||||||
process.env.FILE_UPLOAD_USER_MAX = 'initialUserMax';
|
process.env.FILE_UPLOAD_USER_MAX = 'initialUserMax';
|
||||||
process.env.FILE_UPLOAD_USER_WINDOW = 'initialUserWindow';
|
process.env.FILE_UPLOAD_USER_WINDOW = 'initialUserWindow';
|
||||||
|
|
||||||
await AppService(app);
|
await AppService();
|
||||||
|
|
||||||
// Verify that process.env falls back to the initial values
|
// Verify that process.env falls back to the initial values
|
||||||
expect(process.env.FILE_UPLOAD_IP_MAX).toEqual('initialMax');
|
expect(process.env.FILE_UPLOAD_IP_MAX).toEqual('initialMax');
|
||||||
|
@ -502,7 +515,7 @@ describe('AppService', () => {
|
||||||
|
|
||||||
const initialEnv = { ...process.env };
|
const initialEnv = { ...process.env };
|
||||||
|
|
||||||
await AppService(app);
|
await AppService();
|
||||||
|
|
||||||
// Expect environment variables to remain unchanged
|
// Expect environment variables to remain unchanged
|
||||||
expect(process.env.IMPORT_IP_MAX).toEqual(initialEnv.IMPORT_IP_MAX);
|
expect(process.env.IMPORT_IP_MAX).toEqual(initialEnv.IMPORT_IP_MAX);
|
||||||
|
@ -524,11 +537,9 @@ describe('AppService', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(importLimitsConfig));
|
||||||
Promise.resolve(importLimitsConfig),
|
|
||||||
);
|
|
||||||
|
|
||||||
await AppService(app);
|
await AppService();
|
||||||
|
|
||||||
// Verify that process.env has been updated according to the rate limits config
|
// Verify that process.env has been updated according to the rate limits config
|
||||||
expect(process.env.IMPORT_IP_MAX).toEqual('150');
|
expect(process.env.IMPORT_IP_MAX).toEqual('150');
|
||||||
|
@ -544,7 +555,7 @@ describe('AppService', () => {
|
||||||
process.env.IMPORT_USER_MAX = 'initialUserMax';
|
process.env.IMPORT_USER_MAX = 'initialUserMax';
|
||||||
process.env.IMPORT_USER_WINDOW = 'initialUserWindow';
|
process.env.IMPORT_USER_WINDOW = 'initialUserWindow';
|
||||||
|
|
||||||
await AppService(app);
|
await AppService();
|
||||||
|
|
||||||
// Verify that process.env falls back to the initial values
|
// Verify that process.env falls back to the initial values
|
||||||
expect(process.env.IMPORT_IP_MAX).toEqual('initialMax');
|
expect(process.env.IMPORT_IP_MAX).toEqual('initialMax');
|
||||||
|
@ -554,7 +565,7 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure endpoint with titlePrompt, titleMethod, and titlePromptTemplate', async () => {
|
it('should correctly configure endpoint with titlePrompt, titleMethod, and titlePromptTemplate', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.openAI]: {
|
[EModelEndpoint.openAI]: {
|
||||||
|
@ -581,43 +592,40 @@ describe('AppService', () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
// Check OpenAI endpoint configuration
|
expect(result).toEqual(
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.openAI);
|
|
||||||
expect(app.locals[EModelEndpoint.openAI]).toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
endpoints: expect.objectContaining({
|
||||||
|
// Check OpenAI endpoint configuration
|
||||||
|
[EModelEndpoint.openAI]: expect.objectContaining({
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: 'gpt-3.5-turbo',
|
titleModel: 'gpt-3.5-turbo',
|
||||||
titleMethod: 'structured',
|
titleMethod: 'structured',
|
||||||
titlePrompt: 'Custom title prompt for conversation',
|
titlePrompt: 'Custom title prompt for conversation',
|
||||||
titlePromptTemplate: 'Summarize this conversation: {{conversation}}',
|
titlePromptTemplate: 'Summarize this conversation: {{conversation}}',
|
||||||
}),
|
}),
|
||||||
);
|
|
||||||
|
|
||||||
// Check Assistants endpoint configuration
|
// Check Assistants endpoint configuration
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.assistants);
|
[EModelEndpoint.assistants]: expect.objectContaining({
|
||||||
expect(app.locals[EModelEndpoint.assistants]).toMatchObject({
|
|
||||||
titleMethod: 'functions',
|
titleMethod: 'functions',
|
||||||
titlePrompt: 'Generate a title for this assistant conversation',
|
titlePrompt: 'Generate a title for this assistant conversation',
|
||||||
titlePromptTemplate: 'Assistant conversation template: {{messages}}',
|
titlePromptTemplate: 'Assistant conversation template: {{messages}}',
|
||||||
});
|
}),
|
||||||
|
|
||||||
// Check Azure OpenAI endpoint configuration
|
// Check Azure OpenAI endpoint configuration
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.azureOpenAI);
|
[EModelEndpoint.azureOpenAI]: expect.objectContaining({
|
||||||
expect(app.locals[EModelEndpoint.azureOpenAI]).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleMethod: 'completion',
|
titleMethod: 'completion',
|
||||||
titleModel: 'gpt-4',
|
titleModel: 'gpt-4',
|
||||||
titlePrompt: 'Azure title prompt',
|
titlePrompt: 'Azure title prompt',
|
||||||
titlePromptTemplate: 'Azure conversation: {{context}}',
|
titlePromptTemplate: 'Azure conversation: {{context}}',
|
||||||
}),
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should configure Agent endpoint with title generation settings', async () => {
|
it('should configure Agent endpoint with title generation settings', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.agents]: {
|
[EModelEndpoint.agents]: {
|
||||||
|
@ -634,10 +642,12 @@ describe('AppService', () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.agents);
|
expect(result).toEqual(
|
||||||
expect(app.locals[EModelEndpoint.agents]).toMatchObject({
|
expect.objectContaining({
|
||||||
|
endpoints: expect.objectContaining({
|
||||||
|
[EModelEndpoint.agents]: expect.objectContaining({
|
||||||
disableBuilder: false,
|
disableBuilder: false,
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: 'gpt-4',
|
titleModel: 'gpt-4',
|
||||||
|
@ -645,12 +655,18 @@ describe('AppService', () => {
|
||||||
titlePrompt: 'Generate a descriptive title for this agent conversation',
|
titlePrompt: 'Generate a descriptive title for this agent conversation',
|
||||||
titlePromptTemplate: 'Agent conversation summary: {{content}}',
|
titlePromptTemplate: 'Agent conversation summary: {{content}}',
|
||||||
recursionLimit: 15,
|
recursionLimit: 15,
|
||||||
capabilities: expect.arrayContaining([AgentCapabilities.tools, AgentCapabilities.actions]),
|
capabilities: expect.arrayContaining([
|
||||||
});
|
AgentCapabilities.tools,
|
||||||
|
AgentCapabilities.actions,
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle missing title configuration options with defaults', async () => {
|
it('should handle missing title configuration options with defaults', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.openAI]: {
|
[EModelEndpoint.openAI]: {
|
||||||
|
@ -661,20 +677,26 @@ describe('AppService', () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.openAI);
|
expect(result).toEqual(
|
||||||
expect(app.locals[EModelEndpoint.openAI]).toMatchObject({
|
expect.objectContaining({
|
||||||
|
endpoints: expect.objectContaining({
|
||||||
|
[EModelEndpoint.openAI]: expect.objectContaining({
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
});
|
}),
|
||||||
// Check that the optional fields are undefined when not provided
|
}),
|
||||||
expect(app.locals[EModelEndpoint.openAI].titlePrompt).toBeUndefined();
|
}),
|
||||||
expect(app.locals[EModelEndpoint.openAI].titlePromptTemplate).toBeUndefined();
|
);
|
||||||
expect(app.locals[EModelEndpoint.openAI].titleMethod).toBeUndefined();
|
|
||||||
|
// Verify that optional fields are not set when not provided
|
||||||
|
expect(result.endpoints[EModelEndpoint.openAI].titlePrompt).toBeUndefined();
|
||||||
|
expect(result.endpoints[EModelEndpoint.openAI].titlePromptTemplate).toBeUndefined();
|
||||||
|
expect(result.endpoints[EModelEndpoint.openAI].titleMethod).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure titleEndpoint when specified', async () => {
|
it('should correctly configure titleEndpoint when specified', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.openAI]: {
|
[EModelEndpoint.openAI]: {
|
||||||
|
@ -691,27 +713,30 @@ describe('AppService', () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
endpoints: expect.objectContaining({
|
||||||
// Check OpenAI endpoint has titleEndpoint
|
// Check OpenAI endpoint has titleEndpoint
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.openAI);
|
[EModelEndpoint.openAI]: expect.objectContaining({
|
||||||
expect(app.locals[EModelEndpoint.openAI]).toMatchObject({
|
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: 'gpt-3.5-turbo',
|
titleModel: 'gpt-3.5-turbo',
|
||||||
titleEndpoint: EModelEndpoint.anthropic,
|
titleEndpoint: EModelEndpoint.anthropic,
|
||||||
titlePrompt: 'Generate a concise title',
|
titlePrompt: 'Generate a concise title',
|
||||||
});
|
}),
|
||||||
|
|
||||||
// Check Agents endpoint has titleEndpoint
|
// Check Agents endpoint has titleEndpoint
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.agents);
|
[EModelEndpoint.agents]: expect.objectContaining({
|
||||||
expect(app.locals[EModelEndpoint.agents]).toMatchObject({
|
|
||||||
titleEndpoint: 'custom-provider',
|
titleEndpoint: 'custom-provider',
|
||||||
titleMethod: 'structured',
|
titleMethod: 'structured',
|
||||||
});
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure all endpoint when specified', async () => {
|
it('should correctly configure all endpoint when specified', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
all: {
|
all: {
|
||||||
|
@ -731,11 +756,13 @@ describe('AppService', () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
// Check that 'all' endpoint config is loaded
|
// Check that 'all' endpoint config is loaded
|
||||||
expect(app.locals).toHaveProperty('all');
|
endpoints: expect.objectContaining({
|
||||||
expect(app.locals.all).toMatchObject({
|
all: expect.objectContaining({
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: 'gpt-4o-mini',
|
titleModel: 'gpt-4o-mini',
|
||||||
titleMethod: 'structured',
|
titleMethod: 'structured',
|
||||||
|
@ -743,27 +770,28 @@ describe('AppService', () => {
|
||||||
titlePromptTemplate: 'Default template: {{conversation}}',
|
titlePromptTemplate: 'Default template: {{conversation}}',
|
||||||
titleEndpoint: EModelEndpoint.anthropic,
|
titleEndpoint: EModelEndpoint.anthropic,
|
||||||
streamRate: 50,
|
streamRate: 50,
|
||||||
});
|
}),
|
||||||
|
|
||||||
// Check that OpenAI endpoint has its own config
|
// Check that OpenAI endpoint has its own config
|
||||||
expect(app.locals).toHaveProperty(EModelEndpoint.openAI);
|
[EModelEndpoint.openAI]: expect.objectContaining({
|
||||||
expect(app.locals[EModelEndpoint.openAI]).toMatchObject({
|
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
titleModel: 'gpt-3.5-turbo',
|
titleModel: 'gpt-3.5-turbo',
|
||||||
});
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('AppService updating app.locals and issuing warnings', () => {
|
describe('AppService updating app config and issuing warnings', () => {
|
||||||
let app;
|
|
||||||
let initialEnv;
|
let initialEnv;
|
||||||
|
const loadCustomConfig = require('./Config/loadCustomConfig');
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Store initial environment variables to restore them after each test
|
// Store initial environment variables to restore them after each test
|
||||||
initialEnv = { ...process.env };
|
initialEnv = { ...process.env };
|
||||||
|
|
||||||
app = { locals: {} };
|
|
||||||
process.env.CDN_PROVIDER = undefined;
|
process.env.CDN_PROVIDER = undefined;
|
||||||
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -771,26 +799,29 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
process.env = { ...initialEnv };
|
process.env = { ...initialEnv };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update app.locals with default values if loadCustomConfig returns undefined', async () => {
|
it('should initialize app config with default values if loadCustomConfig returns undefined', async () => {
|
||||||
// Mock loadCustomConfig to return undefined
|
// Mock loadCustomConfig to return undefined
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(undefined));
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(undefined));
|
||||||
|
|
||||||
await AppService(app);
|
const result = await AppService();
|
||||||
|
|
||||||
expect(app.locals).toBeDefined();
|
expect(result).toEqual(
|
||||||
expect(app.locals.paths).toBeDefined();
|
|
||||||
expect(app.locals.config).toEqual({});
|
|
||||||
expect(app.locals.fileStrategy).toEqual(FileSources.local);
|
|
||||||
expect(app.locals.socialLogins).toEqual(defaultSocialLogins);
|
|
||||||
expect(app.locals.balance).toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
paths: expect.anything(),
|
||||||
|
config: {},
|
||||||
|
fileStrategy: FileSources.local,
|
||||||
|
registration: expect.objectContaining({
|
||||||
|
socialLogins: defaultSocialLogins,
|
||||||
|
}),
|
||||||
|
balance: expect.objectContaining({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
startBalance: undefined,
|
startBalance: undefined,
|
||||||
}),
|
}),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update app.locals with values from loadCustomConfig', async () => {
|
it('should initialize app config with values from loadCustomConfig', async () => {
|
||||||
// Mock loadCustomConfig to return a specific config object with a complete balance config
|
// Mock loadCustomConfig to return a specific config object with a complete balance config
|
||||||
const customConfig = {
|
const customConfig = {
|
||||||
fileStrategy: 'firebase',
|
fileStrategy: 'firebase',
|
||||||
|
@ -804,21 +835,24 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
refillAmount: 5000,
|
refillAmount: 5000,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(customConfig));
|
||||||
Promise.resolve(customConfig),
|
|
||||||
|
const result = await AppService();
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
paths: expect.anything(),
|
||||||
|
config: customConfig,
|
||||||
|
fileStrategy: customConfig.fileStrategy,
|
||||||
|
registration: expect.objectContaining({
|
||||||
|
socialLogins: customConfig.registration.socialLogins,
|
||||||
|
}),
|
||||||
|
balance: customConfig.balance,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await AppService(app);
|
|
||||||
|
|
||||||
expect(app.locals).toBeDefined();
|
|
||||||
expect(app.locals.paths).toBeDefined();
|
|
||||||
expect(app.locals.config).toEqual(customConfig);
|
|
||||||
expect(app.locals.fileStrategy).toEqual(customConfig.fileStrategy);
|
|
||||||
expect(app.locals.socialLogins).toEqual(customConfig.registration.socialLogins);
|
|
||||||
expect(app.locals.balance).toEqual(customConfig.balance);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply the assistants endpoint configuration correctly to app.locals', async () => {
|
it('should apply the assistants endpoint configuration correctly to app config', async () => {
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
endpoints: {
|
endpoints: {
|
||||||
assistants: {
|
assistants: {
|
||||||
|
@ -829,18 +863,25 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig));
|
||||||
|
|
||||||
const app = { locals: {} };
|
const result = await AppService();
|
||||||
await AppService(app);
|
|
||||||
|
|
||||||
expect(app.locals).toHaveProperty('assistants');
|
expect(result).toEqual(
|
||||||
const { assistants } = app.locals;
|
expect.objectContaining({
|
||||||
expect(assistants.disableBuilder).toBe(true);
|
endpoints: expect.objectContaining({
|
||||||
expect(assistants.pollIntervalMs).toBe(5000);
|
assistants: expect.objectContaining({
|
||||||
expect(assistants.timeoutMs).toBe(30000);
|
disableBuilder: true,
|
||||||
expect(assistants.supportedIds).toEqual(['id1', 'id2']);
|
pollIntervalMs: 5000,
|
||||||
expect(assistants.excludedIds).toBeUndefined();
|
timeoutMs: 30000,
|
||||||
|
supportedIds: ['id1', 'id2'],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify excludedIds is undefined when not provided
|
||||||
|
expect(result.endpoints.assistants.excludedIds).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log a warning when both supportedIds and excludedIds are provided', async () => {
|
it('should log a warning when both supportedIds and excludedIds are provided', async () => {
|
||||||
|
@ -855,12 +896,11 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig));
|
||||||
|
|
||||||
const app = { locals: {} };
|
await AppService();
|
||||||
await require('./AppService')(app);
|
|
||||||
|
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
expect(logger.warn).toHaveBeenCalledWith(
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
"The 'assistants' endpoint has both 'supportedIds' and 'excludedIds' defined.",
|
"The 'assistants' endpoint has both 'supportedIds' and 'excludedIds' defined.",
|
||||||
|
@ -877,12 +917,11 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig));
|
||||||
|
|
||||||
const app = { locals: {} };
|
await AppService();
|
||||||
await require('./AppService')(app);
|
|
||||||
|
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
expect(logger.warn).toHaveBeenCalledWith(
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
"The 'assistants' endpoint has both 'privateAssistants' and 'supportedIds' or 'excludedIds' defined.",
|
"The 'assistants' endpoint has both 'privateAssistants' and 'supportedIds' or 'excludedIds' defined.",
|
||||||
|
@ -891,7 +930,7 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should issue expected warnings when loading Azure Groups with deprecated Environment Variables', async () => {
|
it('should issue expected warnings when loading Azure Groups with deprecated Environment Variables', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
|
@ -905,10 +944,9 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
process.env[varInfo.key] = 'test';
|
process.env[varInfo.key] = 'test';
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = { locals: {} };
|
await AppService();
|
||||||
await require('./AppService')(app);
|
|
||||||
|
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
deprecatedAzureVariables.forEach(({ key, description }) => {
|
deprecatedAzureVariables.forEach(({ key, description }) => {
|
||||||
expect(logger.warn).toHaveBeenCalledWith(
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
`The \`${key}\` environment variable (related to ${description}) should not be used in combination with the \`azureOpenAI\` endpoint configuration, as you will experience conflicts and errors.`,
|
`The \`${key}\` environment variable (related to ${description}) should not be used in combination with the \`azureOpenAI\` endpoint configuration, as you will experience conflicts and errors.`,
|
||||||
|
@ -917,7 +955,7 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should issue expected warnings when loading conflicting Azure Envrionment Variables', async () => {
|
it('should issue expected warnings when loading conflicting Azure Envrionment Variables', async () => {
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
|
loadCustomConfig.mockImplementationOnce(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
|
@ -931,10 +969,9 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
process.env[varInfo.key] = 'test';
|
process.env[varInfo.key] = 'test';
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = { locals: {} };
|
await AppService();
|
||||||
await require('./AppService')(app);
|
|
||||||
|
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
conflictingAzureVariables.forEach(({ key }) => {
|
conflictingAzureVariables.forEach(({ key }) => {
|
||||||
expect(logger.warn).toHaveBeenCalledWith(
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
`The \`${key}\` environment variable should not be used in combination with the \`azureOpenAI\` endpoint configuration, as you may experience with the defined placeholders for mapping to the current model grouping using the same name.`,
|
`The \`${key}\` environment variable should not be used in combination with the \`azureOpenAI\` endpoint configuration, as you may experience with the defined placeholders for mapping to the current model grouping using the same name.`,
|
||||||
|
@ -953,22 +990,25 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig));
|
||||||
|
|
||||||
// Set actual environment variables with different values
|
// Set actual environment variables with different values
|
||||||
process.env.OCR_API_KEY_CUSTOM_VAR_NAME = 'actual-api-key';
|
process.env.OCR_API_KEY_CUSTOM_VAR_NAME = 'actual-api-key';
|
||||||
process.env.OCR_BASEURL_CUSTOM_VAR_NAME = 'https://actual-ocr-url.com';
|
process.env.OCR_BASEURL_CUSTOM_VAR_NAME = 'https://actual-ocr-url.com';
|
||||||
|
|
||||||
// Initialize app
|
const result = await AppService();
|
||||||
const app = { locals: {} };
|
|
||||||
await AppService(app);
|
|
||||||
|
|
||||||
// Verify that the raw string references were preserved and not interpolated
|
// Verify that the raw string references were preserved and not interpolated
|
||||||
expect(app.locals.ocr).toBeDefined();
|
expect(result).toEqual(
|
||||||
expect(app.locals.ocr.apiKey).toEqual('${OCR_API_KEY_CUSTOM_VAR_NAME}');
|
expect.objectContaining({
|
||||||
expect(app.locals.ocr.baseURL).toEqual('${OCR_BASEURL_CUSTOM_VAR_NAME}');
|
ocr: expect.objectContaining({
|
||||||
expect(app.locals.ocr.strategy).toEqual('mistral_ocr');
|
apiKey: '${OCR_API_KEY_CUSTOM_VAR_NAME}',
|
||||||
expect(app.locals.ocr.mistralModel).toEqual('mistral-medium');
|
baseURL: '${OCR_BASEURL_CUSTOM_VAR_NAME}',
|
||||||
|
strategy: 'mistral_ocr',
|
||||||
|
mistralModel: 'mistral-medium',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure peoplePicker permissions when specified', async () => {
|
it('should correctly configure peoplePicker permissions when specified', async () => {
|
||||||
|
@ -982,17 +1022,21 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
|
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig));
|
||||||
|
|
||||||
const app = { locals: {} };
|
const result = await AppService();
|
||||||
await AppService(app);
|
|
||||||
|
|
||||||
// Check that interface config includes the permissions
|
// Check that interface config includes the permissions
|
||||||
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
|
expect(result).toEqual(
|
||||||
expect(app.locals.interfaceConfig.peoplePicker).toMatchObject({
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
peoplePicker: expect.objectContaining({
|
||||||
users: true,
|
users: true,
|
||||||
groups: true,
|
groups: true,
|
||||||
roles: true,
|
roles: true,
|
||||||
});
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -350,6 +350,7 @@ async function runAssistant({
|
||||||
accumulatedMessages = [],
|
accumulatedMessages = [],
|
||||||
in_progress: inProgress,
|
in_progress: inProgress,
|
||||||
}) {
|
}) {
|
||||||
|
const appConfig = openai.req.config;
|
||||||
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 +397,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,
|
||||||
|
@ -78,7 +78,7 @@ const createTokenHash = () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Verification Email
|
* Send Verification Email
|
||||||
* @param {Partial<MongoUser> & { _id: ObjectId, email: string, name: string}} user
|
* @param {Partial<IUser>} user
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const sendVerificationEmail = async (user) => {
|
const sendVerificationEmail = async (user) => {
|
||||||
|
@ -112,7 +112,7 @@ const sendVerificationEmail = async (user) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify Email
|
* Verify Email
|
||||||
* @param {Express.Request} req
|
* @param {ServerRequest} req
|
||||||
*/
|
*/
|
||||||
const verifyEmail = async (req) => {
|
const verifyEmail = async (req) => {
|
||||||
const { email, token } = req.body;
|
const { email, token } = req.body;
|
||||||
|
@ -160,9 +160,9 @@ const verifyEmail = async (req) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user.
|
* Register a new user.
|
||||||
* @param {MongoUser} user <email, password, name, username>
|
* @param {IUser} user <email, password, name, username>
|
||||||
* @param {Partial<MongoUser>} [additionalData={}]
|
* @param {Partial<IUser>} [additionalData={}]
|
||||||
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
|
* @returns {Promise<{status: number, message: string, user?: IUser}>}
|
||||||
*/
|
*/
|
||||||
const registerUser = async (user, additionalData = {}) => {
|
const registerUser = async (user, additionalData = {}) => {
|
||||||
const { error } = registerSchema.safeParse(user);
|
const { error } = registerSchema.safeParse(user);
|
||||||
|
@ -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 (!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({
|
||||||
|
@ -248,7 +248,7 @@ const registerUser = async (user, additionalData = {}) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request password reset
|
* Request password reset
|
||||||
* @param {Express.Request} req
|
* @param {ServerRequest} req
|
||||||
*/
|
*/
|
||||||
const requestPasswordReset = async (req) => {
|
const requestPasswordReset = async (req) => {
|
||||||
const { email } = req.body;
|
const { email } = req.body;
|
||||||
|
|
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,69 +0,0 @@
|
||||||
const { isEnabled } = 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,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
async function hasCustomUserVars() {
|
|
||||||
const customConfig = await getCustomConfig();
|
|
||||||
const mcpServers = customConfig?.mcpServers;
|
|
||||||
return Object.values(mcpServers ?? {}).some((server) => server.customUserVars);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
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 = req.config ?? (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,12 +1,11 @@
|
||||||
|
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 mcpToolsCache = require('./mcpToolsCache');
|
const mcpToolsCache = require('./mcpToolsCache');
|
||||||
const loadCustomConfig = require('./loadCustomConfig');
|
const loadCustomConfig = require('./loadCustomConfig');
|
||||||
const loadConfigModels = require('./loadConfigModels');
|
const loadConfigModels = require('./loadConfigModels');
|
||||||
const loadDefaultModels = require('./loadDefaultModels');
|
const loadDefaultModels = require('./loadDefaultModels');
|
||||||
const getEndpointsConfig = require('./getEndpointsConfig');
|
const getEndpointsConfig = require('./getEndpointsConfig');
|
||||||
const loadOverrideConfig = require('./loadOverrideConfig');
|
|
||||||
const loadAsyncEndpoints = require('./loadAsyncEndpoints');
|
const loadAsyncEndpoints = require('./loadAsyncEndpoints');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -14,10 +13,9 @@ module.exports = {
|
||||||
loadCustomConfig,
|
loadCustomConfig,
|
||||||
loadConfigModels,
|
loadConfigModels,
|
||||||
loadDefaultModels,
|
loadDefaultModels,
|
||||||
loadOverrideConfig,
|
|
||||||
loadAsyncEndpoints,
|
loadAsyncEndpoints,
|
||||||
|
...appConfig,
|
||||||
...getCachedTools,
|
...getCachedTools,
|
||||||
...getCustomConfig,
|
|
||||||
...mcpToolsCache,
|
...mcpToolsCache,
|
||||||
...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,43 +1,39 @@
|
||||||
|
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
|
||||||
* @function loadConfigModels
|
* @function loadConfigModels
|
||||||
* @param {Express.Request} req - The Express request object.
|
* @param {ServerRequest} 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();
|
||||||
|
|
|
@ -11,7 +11,7 @@ const {
|
||||||
* Loads the default models for the application.
|
* Loads the default models for the application.
|
||||||
* @async
|
* @async
|
||||||
* @function
|
* @function
|
||||||
* @param {Express.Request} req - The Express request object.
|
* @param {ServerRequest} req - The Express request object.
|
||||||
*/
|
*/
|
||||||
async function loadDefaultModels(req) {
|
async function loadDefaultModels(req) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
// fetch some remote config
|
|
||||||
async function loadOverrideConfig() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = loadOverrideConfig;
|
|
|
@ -49,6 +49,7 @@ const initializeAgent = async ({
|
||||||
allowedProviders,
|
allowedProviders,
|
||||||
isInitialAgent = false,
|
isInitialAgent = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const appConfig = req.config;
|
||||||
if (
|
if (
|
||||||
isAgentsEndpoint(endpointOption?.endpoint) &&
|
isAgentsEndpoint(endpointOption?.endpoint) &&
|
||||||
allowedProviders.size > 0 &&
|
allowedProviders.size > 0 &&
|
||||||
|
@ -90,10 +91,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;
|
||||||
|
@ -112,7 +114,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;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue