mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🔃 refactor: Decouple Effects from AppService, move to data-schemas (#9974)
* chore: linting for `loadCustomConfig` * refactor: decouple CDN init and variable/health checks from AppService * refactor: move AppService to packages/data-schemas * chore: update AppConfig import path to use data-schemas * chore: update JsonSchemaType import path to use data-schemas * refactor: update UserController to import webSearchKeys and redefine FunctionTool typedef * chore: remove AppService.js * refactor: update AppConfig interface to use Partial<TCustomConfig> and make paths and fileStrategies optional * refactor: update checkConfig function to accept Partial<TCustomConfig> * chore: fix types * refactor: move handleRateLimits to startup checks as is an effect * test: remove outdated rate limit tests from AppService.spec and add new handleRateLimits tests in checks.spec
This commit is contained in:
parent
9ff608e6af
commit
838fb53208
73 changed files with 1383 additions and 1326 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger, webSearchKeys } = require('@librechat/data-schemas');
|
||||||
const { Tools, CacheKeys, Constants, FileSources } = require('librechat-data-provider');
|
const { Tools, CacheKeys, Constants, FileSources } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
webSearchKeys,
|
|
||||||
MCPOAuthHandler,
|
MCPOAuthHandler,
|
||||||
MCPTokenStorage,
|
MCPTokenStorage,
|
||||||
normalizeHttpError,
|
normalizeHttpError,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,12 @@ const compression = require('compression');
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const mongoSanitize = require('express-mongo-sanitize');
|
const mongoSanitize = require('express-mongo-sanitize');
|
||||||
const { isEnabled, ErrorController } = require('@librechat/api');
|
const {
|
||||||
|
isEnabled,
|
||||||
|
ErrorController,
|
||||||
|
performStartupChecks,
|
||||||
|
initializeFileStorage,
|
||||||
|
} = require('@librechat/api');
|
||||||
const { connectDb, indexSync } = require('~/db');
|
const { connectDb, indexSync } = require('~/db');
|
||||||
const initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager');
|
const initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager');
|
||||||
const createValidateImageRequest = require('./middleware/validateImageRequest');
|
const createValidateImageRequest = require('./middleware/validateImageRequest');
|
||||||
|
|
@ -49,9 +54,11 @@ const startServer = async () => {
|
||||||
app.set('trust proxy', trusted_proxy);
|
app.set('trust proxy', trusted_proxy);
|
||||||
|
|
||||||
await seedDatabase();
|
await seedDatabase();
|
||||||
|
|
||||||
const appConfig = await getAppConfig();
|
const appConfig = await getAppConfig();
|
||||||
|
initializeFileStorage(appConfig);
|
||||||
|
await performStartupChecks(appConfig);
|
||||||
await updateInterfacePermissions(appConfig);
|
await updateInterfacePermissions(appConfig);
|
||||||
|
|
||||||
const indexPath = path.join(appConfig.paths.dist, 'index.html');
|
const indexPath = path.join(appConfig.paths.dist, 'index.html');
|
||||||
let indexHTML = fs.readFileSync(indexPath, 'utf8');
|
let indexHTML = fs.readFileSync(indexPath, 'utf8');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
jest.mock('@librechat/data-schemas', () => ({
|
|
||||||
logger: {
|
|
||||||
info: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@librechat/api', () => ({
|
|
||||||
...jest.requireActual('@librechat/api'),
|
|
||||||
loadDefaultInterface: jest.fn(),
|
|
||||||
}));
|
|
||||||
jest.mock('./start/tools', () => ({
|
|
||||||
loadAndFormatTools: jest.fn().mockReturnValue({}),
|
|
||||||
}));
|
|
||||||
jest.mock('./start/checks', () => ({
|
|
||||||
checkVariables: jest.fn(),
|
|
||||||
checkHealth: jest.fn(),
|
|
||||||
checkConfig: jest.fn(),
|
|
||||||
checkAzureVariables: jest.fn(),
|
|
||||||
checkWebSearchConfig: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('./Config/loadCustomConfig', () => jest.fn());
|
|
||||||
|
|
||||||
const AppService = require('./AppService');
|
|
||||||
const { loadDefaultInterface } = require('@librechat/api');
|
|
||||||
|
|
||||||
describe('AppService interface configuration', () => {
|
|
||||||
let mockLoadCustomConfig;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetModules();
|
|
||||||
jest.clearAllMocks();
|
|
||||||
mockLoadCustomConfig = require('./Config/loadCustomConfig');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set prompts and bookmarks to true when loadDefaultInterface returns true for both', async () => {
|
|
||||||
mockLoadCustomConfig.mockResolvedValue({});
|
|
||||||
loadDefaultInterface.mockResolvedValue({ prompts: true, bookmarks: true });
|
|
||||||
|
|
||||||
const result = await AppService();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
interfaceConfig: expect.objectContaining({
|
|
||||||
prompts: true,
|
|
||||||
bookmarks: true,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(loadDefaultInterface).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set prompts and bookmarks to false when loadDefaultInterface returns false for both', async () => {
|
|
||||||
mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: false, bookmarks: false } });
|
|
||||||
loadDefaultInterface.mockResolvedValue({ prompts: false, bookmarks: false });
|
|
||||||
|
|
||||||
const result = await AppService();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
interfaceConfig: expect.objectContaining({
|
|
||||||
prompts: false,
|
|
||||||
bookmarks: false,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(loadDefaultInterface).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set prompts and bookmarks when loadDefaultInterface returns undefined for both', async () => {
|
|
||||||
mockLoadCustomConfig.mockResolvedValue({});
|
|
||||||
loadDefaultInterface.mockResolvedValue({});
|
|
||||||
|
|
||||||
const result = await AppService();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set prompts and bookmarks to different values when loadDefaultInterface returns different values', async () => {
|
|
||||||
mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: true, bookmarks: false } });
|
|
||||||
loadDefaultInterface.mockResolvedValue({ prompts: true, bookmarks: false });
|
|
||||||
|
|
||||||
const result = await AppService();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
interfaceConfig: expect.objectContaining({
|
|
||||||
prompts: true,
|
|
||||||
bookmarks: false,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(loadDefaultInterface).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should correctly configure peoplePicker permissions including roles', async () => {
|
|
||||||
mockLoadCustomConfig.mockResolvedValue({
|
|
||||||
interface: {
|
|
||||||
peoplePicker: {
|
|
||||||
users: true,
|
|
||||||
groups: true,
|
|
||||||
roles: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
loadDefaultInterface.mockResolvedValue({
|
|
||||||
peoplePicker: {
|
|
||||||
users: true,
|
|
||||||
groups: true,
|
|
||||||
roles: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await AppService();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
interfaceConfig: expect.objectContaining({
|
|
||||||
peoplePicker: expect.objectContaining({
|
|
||||||
users: true,
|
|
||||||
groups: true,
|
|
||||||
roles: true,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(loadDefaultInterface).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle mixed peoplePicker permissions', async () => {
|
|
||||||
mockLoadCustomConfig.mockResolvedValue({
|
|
||||||
interface: {
|
|
||||||
peoplePicker: {
|
|
||||||
users: true,
|
|
||||||
groups: false,
|
|
||||||
roles: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
loadDefaultInterface.mockResolvedValue({
|
|
||||||
peoplePicker: {
|
|
||||||
users: true,
|
|
||||||
groups: false,
|
|
||||||
roles: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await AppService();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
interfaceConfig: expect.objectContaining({
|
|
||||||
peoplePicker: expect.objectContaining({
|
|
||||||
users: true,
|
|
||||||
groups: false,
|
|
||||||
roles: true,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set default peoplePicker permissions when not provided', async () => {
|
|
||||||
mockLoadCustomConfig.mockResolvedValue({});
|
|
||||||
loadDefaultInterface.mockResolvedValue({
|
|
||||||
peoplePicker: {
|
|
||||||
users: true,
|
|
||||||
groups: true,
|
|
||||||
roles: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await AppService();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
interfaceConfig: expect.objectContaining({
|
|
||||||
peoplePicker: expect.objectContaining({
|
|
||||||
users: true,
|
|
||||||
groups: true,
|
|
||||||
roles: true,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,11 +1,25 @@
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
const { CacheKeys } = require('librechat-data-provider');
|
const { CacheKeys } = require('librechat-data-provider');
|
||||||
const AppService = require('~/server/services/AppService');
|
const { logger, AppService } = require('@librechat/data-schemas');
|
||||||
|
const { loadAndFormatTools } = require('~/server/services/start/tools');
|
||||||
|
const loadCustomConfig = require('./loadCustomConfig');
|
||||||
const { setCachedTools } = require('./getCachedTools');
|
const { setCachedTools } = require('./getCachedTools');
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
const getLogStores = require('~/cache/getLogStores');
|
||||||
|
const paths = require('~/config/paths');
|
||||||
|
|
||||||
const BASE_CONFIG_KEY = '_BASE_';
|
const BASE_CONFIG_KEY = '_BASE_';
|
||||||
|
|
||||||
|
const loadBaseConfig = async () => {
|
||||||
|
/** @type {TCustomConfig} */
|
||||||
|
const config = (await loadCustomConfig()) ?? {};
|
||||||
|
/** @type {Record<string, FunctionTool>} */
|
||||||
|
const systemTools = loadAndFormatTools({
|
||||||
|
adminFilter: config.filteredTools,
|
||||||
|
adminIncluded: config.includedTools,
|
||||||
|
directory: paths.structuredTools,
|
||||||
|
});
|
||||||
|
return AppService({ config, paths, systemTools });
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the app configuration based on user context
|
* Get the app configuration based on user context
|
||||||
* @param {Object} [options]
|
* @param {Object} [options]
|
||||||
|
|
@ -29,7 +43,7 @@ async function getAppConfig(options = {}) {
|
||||||
let baseConfig = await cache.get(BASE_CONFIG_KEY);
|
let baseConfig = await cache.get(BASE_CONFIG_KEY);
|
||||||
if (!baseConfig) {
|
if (!baseConfig) {
|
||||||
logger.info('[getAppConfig] App configuration not initialized. Initializing AppService...');
|
logger.info('[getAppConfig] App configuration not initialized. Initializing AppService...');
|
||||||
baseConfig = await AppService();
|
baseConfig = await loadBaseConfig();
|
||||||
|
|
||||||
if (!baseConfig) {
|
if (!baseConfig) {
|
||||||
throw new Error('Failed to initialize app configuration through AppService.');
|
throw new Error('Failed to initialize app configuration through AppService.');
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
const { RateLimitPrefix } = require('librechat-data-provider');
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {TCustomConfig['rateLimits'] | undefined} rateLimits
|
|
||||||
*/
|
|
||||||
const handleRateLimits = (rateLimits) => {
|
|
||||||
if (!rateLimits) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rateLimitKeys = {
|
|
||||||
fileUploads: RateLimitPrefix.FILE_UPLOAD,
|
|
||||||
conversationsImport: RateLimitPrefix.IMPORT,
|
|
||||||
tts: RateLimitPrefix.TTS,
|
|
||||||
stt: RateLimitPrefix.STT,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.entries(rateLimitKeys).forEach(([key, prefix]) => {
|
|
||||||
const rateLimit = rateLimits[key];
|
|
||||||
if (rateLimit) {
|
|
||||||
setRateLimitEnvVars(prefix, rateLimit);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set environment variables for rate limit configurations
|
|
||||||
*
|
|
||||||
* @param {string} prefix - Prefix for environment variable names
|
|
||||||
* @param {object} rateLimit - Rate limit configuration object
|
|
||||||
*/
|
|
||||||
const setRateLimitEnvVars = (prefix, rateLimit) => {
|
|
||||||
const envVarsMapping = {
|
|
||||||
ipMax: `${prefix}_IP_MAX`,
|
|
||||||
ipWindowInMinutes: `${prefix}_IP_WINDOW`,
|
|
||||||
userMax: `${prefix}_USER_MAX`,
|
|
||||||
userWindowInMinutes: `${prefix}_USER_WINDOW`,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.entries(envVarsMapping).forEach(([key, envVar]) => {
|
|
||||||
if (rateLimit[key] !== undefined) {
|
|
||||||
process.env[envVar] = rateLimit[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = handleRateLimits;
|
|
||||||
|
|
@ -5,14 +5,12 @@ const keyBy = require('lodash/keyBy');
|
||||||
const { loadYaml } = require('@librechat/api');
|
const { loadYaml } = require('@librechat/api');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const {
|
const {
|
||||||
CacheKeys,
|
|
||||||
configSchema,
|
configSchema,
|
||||||
paramSettings,
|
paramSettings,
|
||||||
EImageOutputType,
|
EImageOutputType,
|
||||||
agentParamSettings,
|
agentParamSettings,
|
||||||
validateSettingDefinitions,
|
validateSettingDefinitions,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const getLogStores = require('~/cache/getLogStores');
|
|
||||||
|
|
||||||
const projectRoot = path.resolve(__dirname, '..', '..', '..', '..');
|
const projectRoot = path.resolve(__dirname, '..', '..', '..', '..');
|
||||||
const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml');
|
const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml');
|
||||||
|
|
@ -119,7 +117,6 @@ https://www.librechat.ai/docs/configuration/stt_tts`);
|
||||||
.filter((endpoint) => endpoint.customParams)
|
.filter((endpoint) => endpoint.customParams)
|
||||||
.forEach((endpoint) => parseCustomParams(endpoint.name, endpoint.customParams));
|
.forEach((endpoint) => parseCustomParams(endpoint.name, endpoint.customParams));
|
||||||
|
|
||||||
|
|
||||||
if (result.data.modelSpecs) {
|
if (result.data.modelSpecs) {
|
||||||
customConfig.modelSpecs = result.data.modelSpecs;
|
customConfig.modelSpecs = result.data.modelSpecs;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const mime = require('mime');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { getAzureContainerClient } = require('./initialize');
|
const { getAzureContainerClient } = require('@librechat/api');
|
||||||
|
|
||||||
const defaultBasePath = 'images';
|
const defaultBasePath = 'images';
|
||||||
const { AZURE_STORAGE_PUBLIC_ACCESS = 'true', AZURE_CONTAINER_NAME = 'files' } = process.env;
|
const { AZURE_STORAGE_PUBLIC_ACCESS = 'true', AZURE_CONTAINER_NAME = 'files' } = process.env;
|
||||||
|
|
@ -30,7 +30,7 @@ async function saveBufferToAzure({
|
||||||
containerName,
|
containerName,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const containerClient = getAzureContainerClient(containerName);
|
const containerClient = await getAzureContainerClient(containerName);
|
||||||
const access = AZURE_STORAGE_PUBLIC_ACCESS?.toLowerCase() === 'true' ? 'blob' : undefined;
|
const access = AZURE_STORAGE_PUBLIC_ACCESS?.toLowerCase() === 'true' ? 'blob' : undefined;
|
||||||
// Create the container if it doesn't exist. This is done per operation.
|
// Create the container if it doesn't exist. This is done per operation.
|
||||||
await containerClient.createIfNotExists({ access });
|
await containerClient.createIfNotExists({ access });
|
||||||
|
|
@ -84,7 +84,7 @@ async function saveURLToAzure({
|
||||||
*/
|
*/
|
||||||
async function getAzureURL({ fileName, basePath = defaultBasePath, userId, containerName }) {
|
async function getAzureURL({ fileName, basePath = defaultBasePath, userId, containerName }) {
|
||||||
try {
|
try {
|
||||||
const containerClient = getAzureContainerClient(containerName);
|
const containerClient = await getAzureContainerClient(containerName);
|
||||||
const blobPath = userId ? `${basePath}/${userId}/${fileName}` : `${basePath}/${fileName}`;
|
const blobPath = userId ? `${basePath}/${userId}/${fileName}` : `${basePath}/${fileName}`;
|
||||||
const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
|
const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
|
||||||
return blockBlobClient.url;
|
return blockBlobClient.url;
|
||||||
|
|
@ -103,7 +103,7 @@ async function getAzureURL({ fileName, basePath = defaultBasePath, userId, conta
|
||||||
*/
|
*/
|
||||||
async function deleteFileFromAzure(req, file) {
|
async function deleteFileFromAzure(req, file) {
|
||||||
try {
|
try {
|
||||||
const containerClient = getAzureContainerClient(AZURE_CONTAINER_NAME);
|
const containerClient = await getAzureContainerClient(AZURE_CONTAINER_NAME);
|
||||||
const blobPath = file.filepath.split(`${AZURE_CONTAINER_NAME}/`)[1];
|
const blobPath = file.filepath.split(`${AZURE_CONTAINER_NAME}/`)[1];
|
||||||
if (!blobPath.includes(req.user.id)) {
|
if (!blobPath.includes(req.user.id)) {
|
||||||
throw new Error('User ID not found in blob path');
|
throw new Error('User ID not found in blob path');
|
||||||
|
|
@ -140,7 +140,7 @@ async function streamFileToAzure({
|
||||||
containerName,
|
containerName,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const containerClient = getAzureContainerClient(containerName);
|
const containerClient = await getAzureContainerClient(containerName);
|
||||||
const access = AZURE_STORAGE_PUBLIC_ACCESS?.toLowerCase() === 'true' ? 'blob' : undefined;
|
const access = AZURE_STORAGE_PUBLIC_ACCESS?.toLowerCase() === 'true' ? 'blob' : undefined;
|
||||||
|
|
||||||
// Create the container if it doesn't exist
|
// Create the container if it doesn't exist
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
const crud = require('./crud');
|
const crud = require('./crud');
|
||||||
const images = require('./images');
|
const images = require('./images');
|
||||||
const initialize = require('./initialize');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...crud,
|
...crud,
|
||||||
...images,
|
...images,
|
||||||
...initialize,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ const path = require('path');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { getFirebaseStorage } = require('@librechat/api');
|
||||||
const { ref, uploadBytes, getDownloadURL, deleteObject } = require('firebase/storage');
|
const { ref, uploadBytes, getDownloadURL, deleteObject } = require('firebase/storage');
|
||||||
const { getBufferMetadata } = require('~/server/utils');
|
const { getBufferMetadata } = require('~/server/utils');
|
||||||
const { getFirebaseStorage } = require('./initialize');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a file from Firebase Storage.
|
* Deletes a file from Firebase Storage.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
const crud = require('./crud');
|
const crud = require('./crud');
|
||||||
const images = require('./images');
|
const images = require('./images');
|
||||||
const initialize = require('./initialize');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...crud,
|
...crud,
|
||||||
...images,
|
...images,
|
||||||
...initialize,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
const firebase = require('firebase/app');
|
|
||||||
const { getStorage } = require('firebase/storage');
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
let firebaseApp = null;
|
|
||||||
|
|
||||||
const initializeFirebase = () => {
|
|
||||||
// Return existing instance if already initialized
|
|
||||||
if (firebaseApp) {
|
|
||||||
return firebaseApp;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firebaseConfig = {
|
|
||||||
apiKey: process.env.FIREBASE_API_KEY,
|
|
||||||
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
|
|
||||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
|
||||||
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
|
|
||||||
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
|
||||||
appId: process.env.FIREBASE_APP_ID,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Object.values(firebaseConfig).some((value) => !value)) {
|
|
||||||
i === 0 && logger.info('[Optional] CDN not initialized.');
|
|
||||||
i++;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
firebaseApp = firebase.initializeApp(firebaseConfig);
|
|
||||||
logger.info('Firebase CDN initialized');
|
|
||||||
return firebaseApp;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFirebaseStorage = () => {
|
|
||||||
const app = initializeFirebase();
|
|
||||||
return app ? getStorage(app) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = { initializeFirebase, getFirebaseStorage };
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
const { initializeS3 } = require('@librechat/api');
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { FileSources } = require('librechat-data-provider');
|
const { FileSources } = require('librechat-data-provider');
|
||||||
|
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
||||||
const {
|
const {
|
||||||
PutObjectCommand,
|
PutObjectCommand,
|
||||||
GetObjectCommand,
|
GetObjectCommand,
|
||||||
HeadObjectCommand,
|
HeadObjectCommand,
|
||||||
DeleteObjectCommand,
|
DeleteObjectCommand,
|
||||||
} = require('@aws-sdk/client-s3');
|
} = require('@aws-sdk/client-s3');
|
||||||
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
|
||||||
const { initializeS3 } = require('./initialize');
|
|
||||||
|
|
||||||
const bucketName = process.env.AWS_BUCKET_NAME;
|
const bucketName = process.env.AWS_BUCKET_NAME;
|
||||||
const defaultBasePath = 'images';
|
const defaultBasePath = 'images';
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
const crud = require('./crud');
|
const crud = require('./crud');
|
||||||
const images = require('./images');
|
const images = require('./images');
|
||||||
const initialize = require('./initialize');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...crud,
|
...crud,
|
||||||
...images,
|
...images,
|
||||||
...initialize,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
const { normalizeEndpointName } = require('@librechat/api');
|
|
||||||
const { EModelEndpoint } = require('librechat-data-provider');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up Model Specs from the config (`librechat.yaml`) file.
|
|
||||||
* @param {TCustomConfig['endpoints']} [endpoints] - The loaded custom configuration for endpoints.
|
|
||||||
* @param {TCustomConfig['modelSpecs'] | undefined} [modelSpecs] - The loaded custom configuration for model specs.
|
|
||||||
* @param {TCustomConfig['interface'] | undefined} [interfaceConfig] - The loaded interface configuration.
|
|
||||||
* @returns {TCustomConfig['modelSpecs'] | undefined} The processed model specs, if any.
|
|
||||||
*/
|
|
||||||
function processModelSpecs(endpoints, _modelSpecs, interfaceConfig) {
|
|
||||||
if (!_modelSpecs) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {TCustomConfig['modelSpecs']['list']} */
|
|
||||||
const modelSpecs = [];
|
|
||||||
/** @type {TCustomConfig['modelSpecs']['list']} */
|
|
||||||
const list = _modelSpecs.list;
|
|
||||||
|
|
||||||
const customEndpoints = endpoints?.[EModelEndpoint.custom] ?? [];
|
|
||||||
|
|
||||||
if (interfaceConfig.modelSelect !== true && (_modelSpecs.addedEndpoints?.length ?? 0) > 0) {
|
|
||||||
logger.warn(
|
|
||||||
`To utilize \`addedEndpoints\`, which allows provider/model selections alongside model specs, set \`modelSelect: true\` in the interface configuration.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
\`\`\`yaml
|
|
||||||
interface:
|
|
||||||
modelSelect: true
|
|
||||||
\`\`\`
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const spec of list) {
|
|
||||||
if (EModelEndpoint[spec.preset.endpoint] && spec.preset.endpoint !== EModelEndpoint.custom) {
|
|
||||||
modelSpecs.push(spec);
|
|
||||||
continue;
|
|
||||||
} else if (spec.preset.endpoint === EModelEndpoint.custom) {
|
|
||||||
logger.warn(
|
|
||||||
`Model Spec with endpoint "${spec.preset.endpoint}" is not supported. You must specify the name of the custom endpoint (case-sensitive, as defined in your config). Skipping model spec...`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedName = normalizeEndpointName(spec.preset.endpoint);
|
|
||||||
const endpoint = customEndpoints.find(
|
|
||||||
(customEndpoint) => normalizedName === normalizeEndpointName(customEndpoint.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!endpoint) {
|
|
||||||
logger.warn(`Model spec with endpoint "${spec.preset.endpoint}" was skipped: Endpoint not found in configuration. The \`endpoint\` value must exactly match either a system-defined endpoint or a custom endpoint defined by the user.
|
|
||||||
|
|
||||||
For more information, see the documentation at https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/model_specs#endpoint`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
modelSpecs.push({
|
|
||||||
...spec,
|
|
||||||
preset: {
|
|
||||||
...spec.preset,
|
|
||||||
endpoint: normalizedName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
..._modelSpecs,
|
|
||||||
list: modelSpecs,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { processModelSpecs };
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
const { removeNullishValues } = require('librechat-data-provider');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads and maps the Cloudflare Turnstile configuration.
|
|
||||||
*
|
|
||||||
* Expected config structure:
|
|
||||||
*
|
|
||||||
* turnstile:
|
|
||||||
* siteKey: "your-site-key-here"
|
|
||||||
* options:
|
|
||||||
* language: "auto" // "auto" or an ISO 639-1 language code (e.g. en)
|
|
||||||
* size: "normal" // Options: "normal", "compact", "flexible", or "invisible"
|
|
||||||
*
|
|
||||||
* @param {TCustomConfig | undefined} config - The loaded custom configuration.
|
|
||||||
* @param {TConfigDefaults} configDefaults - The custom configuration default values.
|
|
||||||
* @returns {TCustomConfig['turnstile']} The mapped Turnstile configuration.
|
|
||||||
*/
|
|
||||||
function loadTurnstileConfig(config, configDefaults) {
|
|
||||||
const { turnstile: customTurnstile = {} } = config ?? {};
|
|
||||||
const { turnstile: defaults = {} } = configDefaults;
|
|
||||||
|
|
||||||
/** @type {TCustomConfig['turnstile']} */
|
|
||||||
const loadedTurnstile = removeNullishValues({
|
|
||||||
siteKey: customTurnstile.siteKey ?? defaults.siteKey,
|
|
||||||
options: customTurnstile.options ?? defaults.options,
|
|
||||||
});
|
|
||||||
|
|
||||||
const enabled = Boolean(loadedTurnstile.siteKey);
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
logger.info(
|
|
||||||
'Turnstile is ENABLED with configuration:\n' + JSON.stringify(loadedTurnstile, null, 2),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.info('Turnstile is DISABLED (no siteKey provided).');
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadedTurnstile;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
loadTurnstileConfig,
|
|
||||||
};
|
|
||||||
|
|
@ -1103,13 +1103,13 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports AppConfig
|
* @exports AppConfig
|
||||||
* @typedef {import('@librechat/api').AppConfig} AppConfig
|
* @typedef {import('@librechat/data-schemas').AppConfig} AppConfig
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports JsonSchemaType
|
* @exports JsonSchemaType
|
||||||
* @typedef {import('@librechat/api').JsonSchemaType} JsonSchemaType
|
* @typedef {import('@librechat/data-schemas').JsonSchemaType} JsonSchemaType
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -1371,12 +1371,7 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports FunctionTool
|
* @exports FunctionTool
|
||||||
* @typedef {Object} FunctionTool
|
* @typedef {import('@librechat/data-schemas').FunctionTool} FunctionTool
|
||||||
* @property {'function'} type - The type of tool, 'function'.
|
|
||||||
* @property {Object} function - The function definition.
|
|
||||||
* @property {string} function.description - A description of what the function does.
|
|
||||||
* @property {string} function.name - The name of the function to be called.
|
|
||||||
* @property {Object} function.parameters - The parameters the function accepts, described as a JSON Schema object.
|
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
||||||
5
package-lock.json
generated
5
package-lock.json
generated
|
|
@ -51330,6 +51330,10 @@
|
||||||
"typescript": "^5.0.4"
|
"typescript": "^5.0.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
|
"@azure/identity": "^4.7.0",
|
||||||
|
"@azure/search-documents": "^12.0.0",
|
||||||
|
"@azure/storage-blob": "^12.27.0",
|
||||||
"@keyv/redis": "^4.3.3",
|
"@keyv/redis": "^4.3.3",
|
||||||
"@langchain/core": "^0.3.62",
|
"@langchain/core": "^0.3.62",
|
||||||
"@librechat/agents": "^2.4.82",
|
"@librechat/agents": "^2.4.82",
|
||||||
|
|
@ -51341,6 +51345,7 @@
|
||||||
"eventsource": "^3.0.2",
|
"eventsource": "^3.0.2",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
|
"firebase": "^11.0.2",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.4",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,10 @@
|
||||||
"registry": "https://registry.npmjs.org/"
|
"registry": "https://registry.npmjs.org/"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
|
"@azure/identity": "^4.7.0",
|
||||||
|
"@azure/search-documents": "^12.0.0",
|
||||||
|
"@azure/storage-blob": "^12.27.0",
|
||||||
"@keyv/redis": "^4.3.3",
|
"@keyv/redis": "^4.3.3",
|
||||||
"@langchain/core": "^0.3.62",
|
"@langchain/core": "^0.3.62",
|
||||||
"@librechat/agents": "^2.4.82",
|
"@librechat/agents": "^2.4.82",
|
||||||
|
|
@ -85,6 +89,7 @@
|
||||||
"eventsource": "^3.0.2",
|
"eventsource": "^3.0.2",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
|
"firebase": "^11.0.2",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.4",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
export * from './config';
|
|
||||||
export * from './memory';
|
export * from './memory';
|
||||||
export * from './migration';
|
export * from './migration';
|
||||||
export * from './legacy';
|
export * from './legacy';
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@ import { primeResources } from './resources';
|
||||||
import { logger } from '@librechat/data-schemas';
|
import { logger } from '@librechat/data-schemas';
|
||||||
import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-data-provider';
|
import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-data-provider';
|
||||||
import type { TAgentsEndpoint, TFile } from 'librechat-data-provider';
|
import type { TAgentsEndpoint, TFile } from 'librechat-data-provider';
|
||||||
|
import type { IUser, AppConfig } from '@librechat/data-schemas';
|
||||||
import type { Request as ServerRequest } from 'express';
|
import type { Request as ServerRequest } from 'express';
|
||||||
import type { IUser } from '@librechat/data-schemas';
|
|
||||||
import type { TGetFiles } from './resources';
|
import type { TGetFiles } from './resources';
|
||||||
import type { AppConfig } from '~/types';
|
|
||||||
|
|
||||||
// Mock logger
|
// Mock logger
|
||||||
jest.mock('@librechat/data-schemas', () => ({
|
jest.mock('@librechat/data-schemas', () => ({
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { logger } from '@librechat/data-schemas';
|
import { logger } from '@librechat/data-schemas';
|
||||||
import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-data-provider';
|
import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-data-provider';
|
||||||
import type { AgentToolResources, TFile, AgentBaseResource } from 'librechat-data-provider';
|
import type { AgentToolResources, TFile, AgentBaseResource } from 'librechat-data-provider';
|
||||||
|
import type { IMongoFile, AppConfig, IUser } from '@librechat/data-schemas';
|
||||||
import type { FilterQuery, QueryOptions, ProjectionType } from 'mongoose';
|
import type { FilterQuery, QueryOptions, ProjectionType } from 'mongoose';
|
||||||
import type { IMongoFile, IUser } from '@librechat/data-schemas';
|
|
||||||
import type { Request as ServerRequest } from 'express';
|
import type { Request as ServerRequest } from 'express';
|
||||||
import type { AppConfig } from '~/types/';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function type for retrieving files from the database
|
* Function type for retrieving files from the database
|
||||||
|
|
|
||||||
157
packages/api/src/app/AppService.interface.spec.ts
Normal file
157
packages/api/src/app/AppService.interface.spec.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
jest.mock('@librechat/data-schemas', () => ({
|
||||||
|
...jest.requireActual('@librechat/data-schemas'),
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { AppService } from '@librechat/data-schemas';
|
||||||
|
|
||||||
|
describe('AppService interface configuration', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set prompts to true when config specifies prompts as true', async () => {
|
||||||
|
const config = {
|
||||||
|
interface: {
|
||||||
|
prompts: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await AppService({ config });
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
prompts: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set prompts and bookmarks to false when config specifies them as false', async () => {
|
||||||
|
const config = {
|
||||||
|
interface: {
|
||||||
|
prompts: false,
|
||||||
|
bookmarks: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await AppService({ config });
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
prompts: false,
|
||||||
|
bookmarks: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set prompts and bookmarks when not provided in config', async () => {
|
||||||
|
const config = {};
|
||||||
|
|
||||||
|
const result = await AppService({ config });
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set prompts and bookmarks to different values when specified differently in config', async () => {
|
||||||
|
const config = {
|
||||||
|
interface: {
|
||||||
|
prompts: true,
|
||||||
|
bookmarks: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await AppService({ config });
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
prompts: true,
|
||||||
|
bookmarks: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly configure peoplePicker permissions including roles', async () => {
|
||||||
|
const config = {
|
||||||
|
interface: {
|
||||||
|
peoplePicker: {
|
||||||
|
users: true,
|
||||||
|
groups: true,
|
||||||
|
roles: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await AppService({ config });
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
peoplePicker: expect.objectContaining({
|
||||||
|
users: true,
|
||||||
|
groups: true,
|
||||||
|
roles: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed peoplePicker permissions', async () => {
|
||||||
|
const config = {
|
||||||
|
interface: {
|
||||||
|
peoplePicker: {
|
||||||
|
users: true,
|
||||||
|
groups: false,
|
||||||
|
roles: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await AppService({ config });
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.objectContaining({
|
||||||
|
peoplePicker: expect.objectContaining({
|
||||||
|
users: true,
|
||||||
|
groups: false,
|
||||||
|
roles: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set peoplePicker when not provided in config', async () => {
|
||||||
|
const config = {};
|
||||||
|
|
||||||
|
const result = await AppService({ config });
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
interfaceConfig: expect.anything(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that peoplePicker is undefined when not provided
|
||||||
|
expect(result.interfaceConfig?.peoplePicker).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const {
|
import {
|
||||||
|
OCRStrategy,
|
||||||
FileSources,
|
FileSources,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
EImageOutputType,
|
EImageOutputType,
|
||||||
|
|
@ -6,9 +7,8 @@ const {
|
||||||
defaultSocialLogins,
|
defaultSocialLogins,
|
||||||
validateAzureGroups,
|
validateAzureGroups,
|
||||||
defaultAgentCapabilities,
|
defaultAgentCapabilities,
|
||||||
deprecatedAzureVariables,
|
} from 'librechat-data-provider';
|
||||||
conflictingAzureVariables,
|
import type { TCustomConfig } from 'librechat-data-provider';
|
||||||
} = require('librechat-data-provider');
|
|
||||||
|
|
||||||
jest.mock('@librechat/data-schemas', () => ({
|
jest.mock('@librechat/data-schemas', () => ({
|
||||||
...jest.requireActual('@librechat/data-schemas'),
|
...jest.requireActual('@librechat/data-schemas'),
|
||||||
|
|
@ -20,48 +20,7 @@ jest.mock('@librechat/data-schemas', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const AppService = require('./AppService');
|
import { AppService } from '@librechat/data-schemas';
|
||||||
|
|
||||||
jest.mock('./Files/Firebase/initialize', () => ({
|
|
||||||
initializeFirebase: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('./Config/loadCustomConfig', () =>
|
|
||||||
jest.fn(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
registration: { socialLogins: ['testLogin'] },
|
|
||||||
fileStrategy: 'testStrategy',
|
|
||||||
balance: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.mock('./start/tools', () => ({
|
|
||||||
loadAndFormatTools: jest.fn().mockReturnValue({
|
|
||||||
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('./start/turnstile', () => ({
|
|
||||||
loadTurnstileConfig: jest.fn(() => ({
|
|
||||||
siteKey: 'default-site-key',
|
|
||||||
options: {},
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const azureGroups = [
|
const azureGroups = [
|
||||||
{
|
{
|
||||||
|
|
@ -97,20 +56,26 @@ const azureGroups = [
|
||||||
models: {
|
models: {
|
||||||
'gpt-4-turbo': true,
|
'gpt-4-turbo': true,
|
||||||
},
|
},
|
||||||
},
|
} as const,
|
||||||
];
|
];
|
||||||
|
|
||||||
jest.mock('./start/checks', () => ({
|
|
||||||
...jest.requireActual('./start/checks'),
|
|
||||||
checkHealth: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('AppService', () => {
|
describe('AppService', () => {
|
||||||
const mockedTurnstileConfig = {
|
const mockSystemTools = {
|
||||||
siteKey: 'default-site-key',
|
ExampleTool: {
|
||||||
options: {},
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
description: 'Example tool function',
|
||||||
|
name: 'exampleFunction',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
param1: { type: 'string', description: 'An example parameter' },
|
||||||
|
},
|
||||||
|
required: ['param1'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const loadCustomConfig = require('./Config/loadCustomConfig');
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env.CDN_PROVIDER = undefined;
|
process.env.CDN_PROVIDER = undefined;
|
||||||
|
|
@ -118,7 +83,15 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly assign process.env and initialize app config based on custom config', async () => {
|
it('should correctly assign process.env and initialize app config based on custom config', async () => {
|
||||||
const result = await AppService();
|
const config: Partial<TCustomConfig> = {
|
||||||
|
registration: { socialLogins: ['testLogin'] },
|
||||||
|
fileStrategy: 'testStrategy' as FileSources,
|
||||||
|
balance: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await AppService({ config, systemTools: mockSystemTools });
|
||||||
|
|
||||||
expect(process.env.CDN_PROVIDER).toEqual('testStrategy');
|
expect(process.env.CDN_PROVIDER).toEqual('testStrategy');
|
||||||
|
|
||||||
|
|
@ -139,9 +112,6 @@ describe('AppService', () => {
|
||||||
presets: true,
|
presets: true,
|
||||||
}),
|
}),
|
||||||
mcpConfig: null,
|
mcpConfig: null,
|
||||||
turnstileConfig: mockedTurnstileConfig,
|
|
||||||
modelSpecs: undefined,
|
|
||||||
paths: expect.anything(),
|
|
||||||
imageOutputType: expect.any(String),
|
imageOutputType: expect.any(String),
|
||||||
fileConfig: undefined,
|
fileConfig: undefined,
|
||||||
secureImageLinks: undefined,
|
secureImageLinks: undefined,
|
||||||
|
|
@ -173,30 +143,13 @@ describe('AppService', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log a warning if the config version is outdated', async () => {
|
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
version: '0.9.0', // An outdated version for this test
|
|
||||||
registration: { socialLogins: ['testLogin'] },
|
|
||||||
fileStrategy: 'testStrategy',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await AppService();
|
|
||||||
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
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 () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config = {
|
||||||
Promise.resolve({
|
|
||||||
version: '0.10.0',
|
version: '0.10.0',
|
||||||
imageOutputType: EImageOutputType.WEBP,
|
imageOutputType: EImageOutputType.WEBP,
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
imageOutputType: EImageOutputType.WEBP,
|
imageOutputType: EImageOutputType.WEBP,
|
||||||
|
|
@ -205,13 +158,11 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to `PNG` `imageOutputType` with no provided type', async () => {
|
it('should default to `PNG` `imageOutputType` with no provided type', async () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config = {
|
||||||
Promise.resolve({
|
|
||||||
version: '0.10.0',
|
version: '0.10.0',
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
imageOutputType: EImageOutputType.PNG,
|
imageOutputType: EImageOutputType.PNG,
|
||||||
|
|
@ -220,9 +171,9 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should default to `PNG` `imageOutputType` with no provided config', async () => {
|
it('should default to `PNG` `imageOutputType` with no provided config', async () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(undefined));
|
const config = {};
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
imageOutputType: EImageOutputType.PNG,
|
imageOutputType: EImageOutputType.PNG,
|
||||||
|
|
@ -230,35 +181,14 @@ describe('AppService', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize Firebase when fileStrategy is firebase', async () => {
|
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
fileStrategy: FileSources.firebase,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await AppService();
|
|
||||||
|
|
||||||
const { initializeFirebase } = require('./Files/Firebase/initialize');
|
|
||||||
expect(initializeFirebase).toHaveBeenCalled();
|
|
||||||
|
|
||||||
expect(process.env.CDN_PROVIDER).toEqual(FileSources.firebase);
|
|
||||||
});
|
|
||||||
|
|
||||||
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('./start/tools');
|
const config = {};
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config, systemTools: mockSystemTools });
|
||||||
|
|
||||||
expect(loadAndFormatTools).toHaveBeenCalledWith({
|
|
||||||
adminFilter: undefined,
|
|
||||||
adminIncluded: undefined,
|
|
||||||
directory: expect.anything(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify tools are included in the returned config
|
// Verify tools are included in the returned config
|
||||||
expect(result.availableTools).toBeDefined();
|
expect(result.availableTools).toBeDefined();
|
||||||
expect(result.availableTools.ExampleTool).toEqual({
|
expect(result.availableTools?.ExampleTool).toEqual({
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
description: 'Example tool function',
|
description: 'Example tool function',
|
||||||
|
|
@ -275,8 +205,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 () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config: Partial<TCustomConfig> = {
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.assistants]: {
|
[EModelEndpoint.assistants]: {
|
||||||
disableBuilder: true,
|
disableBuilder: true,
|
||||||
|
|
@ -286,10 +215,9 @@ describe('AppService', () => {
|
||||||
privateAssistants: false,
|
privateAssistants: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -307,8 +235,7 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure Agents endpoint based on custom config', async () => {
|
it('should correctly configure Agents endpoint based on custom config', async () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config: Partial<TCustomConfig> = {
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.agents]: {
|
[EModelEndpoint.agents]: {
|
||||||
disableBuilder: true,
|
disableBuilder: true,
|
||||||
|
|
@ -318,10 +245,9 @@ describe('AppService', () => {
|
||||||
capabilities: [AgentCapabilities.tools, AgentCapabilities.actions],
|
capabilities: [AgentCapabilities.tools, AgentCapabilities.actions],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -342,9 +268,9 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() => Promise.resolve({}));
|
const config = {};
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -359,17 +285,15 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config = {
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.openAI]: {
|
[EModelEndpoint.openAI]: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -388,21 +312,19 @@ describe('AppService', () => {
|
||||||
|
|
||||||
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 }];
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config = {
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
groups: assistantGroups,
|
groups: assistantGroups,
|
||||||
assistants: true,
|
assistants: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
endpoints: expect.objectContaining({
|
endpoints: expect.objectContaining({
|
||||||
|
|
@ -419,20 +341,18 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure Azure OpenAI endpoint based on custom config', async () => {
|
it('should correctly configure Azure OpenAI endpoint based on custom config', async () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config: Partial<TCustomConfig> = {
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
[EModelEndpoint.azureOpenAI]: {
|
||||||
groups: azureGroups,
|
groups: azureGroups,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
const { modelNames, modelGroupMap, groupMap } = validateAzureGroups(azureGroups);
|
const { modelNames, modelGroupMap, groupMap } = validateAzureGroups(azureGroups);
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
|
|
@ -456,8 +376,9 @@ describe('AppService', () => {
|
||||||
process.env.FILE_UPLOAD_USER_WINDOW = '20';
|
process.env.FILE_UPLOAD_USER_WINDOW = '20';
|
||||||
|
|
||||||
const initialEnv = { ...process.env };
|
const initialEnv = { ...process.env };
|
||||||
|
const config = {};
|
||||||
|
|
||||||
await AppService();
|
await AppService({ config });
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
@ -466,38 +387,15 @@ describe('AppService', () => {
|
||||||
expect(process.env.FILE_UPLOAD_USER_WINDOW).toEqual(initialEnv.FILE_UPLOAD_USER_WINDOW);
|
expect(process.env.FILE_UPLOAD_USER_WINDOW).toEqual(initialEnv.FILE_UPLOAD_USER_WINDOW);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly set FILE_UPLOAD environment variables based on rate limits', async () => {
|
|
||||||
// Define and mock a custom configuration with rate limits
|
|
||||||
const rateLimitsConfig = {
|
|
||||||
rateLimits: {
|
|
||||||
fileUploads: {
|
|
||||||
ipMax: '100',
|
|
||||||
ipWindowInMinutes: '60',
|
|
||||||
userMax: '50',
|
|
||||||
userWindowInMinutes: '30',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(rateLimitsConfig));
|
|
||||||
|
|
||||||
await AppService();
|
|
||||||
|
|
||||||
// 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_WINDOW).toEqual('60');
|
|
||||||
expect(process.env.FILE_UPLOAD_USER_MAX).toEqual('50');
|
|
||||||
expect(process.env.FILE_UPLOAD_USER_WINDOW).toEqual('30');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fallback to default FILE_UPLOAD environment variables when rate limits are unspecified', async () => {
|
it('should fallback to default FILE_UPLOAD environment variables when rate limits are unspecified', async () => {
|
||||||
// Setup initial environment variables to non-default values
|
// Setup initial environment variables to non-default values
|
||||||
process.env.FILE_UPLOAD_IP_MAX = 'initialMax';
|
process.env.FILE_UPLOAD_IP_MAX = 'initialMax';
|
||||||
process.env.FILE_UPLOAD_IP_WINDOW = 'initialWindow';
|
process.env.FILE_UPLOAD_IP_WINDOW = 'initialWindow';
|
||||||
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';
|
||||||
|
const config = {};
|
||||||
|
|
||||||
await AppService();
|
await AppService({ config });
|
||||||
|
|
||||||
// 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');
|
||||||
|
|
@ -514,8 +412,9 @@ describe('AppService', () => {
|
||||||
process.env.IMPORT_USER_WINDOW = '20';
|
process.env.IMPORT_USER_WINDOW = '20';
|
||||||
|
|
||||||
const initialEnv = { ...process.env };
|
const initialEnv = { ...process.env };
|
||||||
|
const config = {};
|
||||||
|
|
||||||
await AppService();
|
await AppService({ config });
|
||||||
|
|
||||||
// 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,38 +423,15 @@ describe('AppService', () => {
|
||||||
expect(process.env.IMPORT_USER_WINDOW).toEqual(initialEnv.IMPORT_USER_WINDOW);
|
expect(process.env.IMPORT_USER_WINDOW).toEqual(initialEnv.IMPORT_USER_WINDOW);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly set IMPORT environment variables based on rate limits', async () => {
|
|
||||||
// Define and mock a custom configuration with rate limits
|
|
||||||
const importLimitsConfig = {
|
|
||||||
rateLimits: {
|
|
||||||
conversationsImport: {
|
|
||||||
ipMax: '150',
|
|
||||||
ipWindowInMinutes: '60',
|
|
||||||
userMax: '50',
|
|
||||||
userWindowInMinutes: '30',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(importLimitsConfig));
|
|
||||||
|
|
||||||
await AppService();
|
|
||||||
|
|
||||||
// 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_WINDOW).toEqual('60');
|
|
||||||
expect(process.env.IMPORT_USER_MAX).toEqual('50');
|
|
||||||
expect(process.env.IMPORT_USER_WINDOW).toEqual('30');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fallback to default IMPORT environment variables when rate limits are unspecified', async () => {
|
it('should fallback to default IMPORT environment variables when rate limits are unspecified', async () => {
|
||||||
// Setup initial environment variables to non-default values
|
// Setup initial environment variables to non-default values
|
||||||
process.env.IMPORT_IP_MAX = 'initialMax';
|
process.env.IMPORT_IP_MAX = 'initialMax';
|
||||||
process.env.IMPORT_IP_WINDOW = 'initialWindow';
|
process.env.IMPORT_IP_WINDOW = 'initialWindow';
|
||||||
process.env.IMPORT_USER_MAX = 'initialUserMax';
|
process.env.IMPORT_USER_MAX = 'initialUserMax';
|
||||||
process.env.IMPORT_USER_WINDOW = 'initialUserWindow';
|
process.env.IMPORT_USER_WINDOW = 'initialUserWindow';
|
||||||
|
const config = {};
|
||||||
|
|
||||||
await AppService();
|
await AppService({ config });
|
||||||
|
|
||||||
// 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');
|
||||||
|
|
@ -565,8 +441,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 () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config: Partial<TCustomConfig> = {
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.openAI]: {
|
[EModelEndpoint.openAI]: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
|
|
@ -589,10 +464,9 @@ describe('AppService', () => {
|
||||||
titlePromptTemplate: 'Azure conversation: {{context}}',
|
titlePromptTemplate: 'Azure conversation: {{context}}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -625,8 +499,7 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should configure Agent endpoint with title generation settings', async () => {
|
it('should configure Agent endpoint with title generation settings', async () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config: Partial<TCustomConfig> = {
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.agents]: {
|
[EModelEndpoint.agents]: {
|
||||||
disableBuilder: false,
|
disableBuilder: false,
|
||||||
|
|
@ -637,12 +510,14 @@ describe('AppService', () => {
|
||||||
titlePromptTemplate: 'Agent conversation summary: {{content}}',
|
titlePromptTemplate: 'Agent conversation summary: {{content}}',
|
||||||
recursionLimit: 15,
|
recursionLimit: 15,
|
||||||
capabilities: [AgentCapabilities.tools, AgentCapabilities.actions],
|
capabilities: [AgentCapabilities.tools, AgentCapabilities.actions],
|
||||||
|
maxCitations: 30,
|
||||||
|
maxCitationsPerFile: 7,
|
||||||
|
minRelevanceScore: 0.45,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -666,18 +541,16 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle missing title configuration options with defaults', async () => {
|
it('should handle missing title configuration options with defaults', async () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config = {
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.openAI]: {
|
[EModelEndpoint.openAI]: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
// titlePrompt and titlePromptTemplate are not provided
|
// titlePrompt and titlePromptTemplate are not provided
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -696,8 +569,7 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure titleEndpoint when specified', async () => {
|
it('should correctly configure titleEndpoint when specified', async () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config: Partial<TCustomConfig> = {
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
[EModelEndpoint.openAI]: {
|
[EModelEndpoint.openAI]: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
|
|
@ -706,14 +578,18 @@ describe('AppService', () => {
|
||||||
titlePrompt: 'Generate a concise title',
|
titlePrompt: 'Generate a concise title',
|
||||||
},
|
},
|
||||||
[EModelEndpoint.agents]: {
|
[EModelEndpoint.agents]: {
|
||||||
|
disableBuilder: false,
|
||||||
|
capabilities: [AgentCapabilities.tools],
|
||||||
|
maxCitations: 30,
|
||||||
|
maxCitationsPerFile: 7,
|
||||||
|
minRelevanceScore: 0.45,
|
||||||
titleEndpoint: 'custom-provider',
|
titleEndpoint: 'custom-provider',
|
||||||
titleMethod: 'structured',
|
titleMethod: 'structured',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -736,8 +612,7 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure all endpoint when specified', async () => {
|
it('should correctly configure all endpoint when specified', async () => {
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
const config: Partial<TCustomConfig> = {
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
all: {
|
all: {
|
||||||
titleConvo: true,
|
titleConvo: true,
|
||||||
|
|
@ -753,10 +628,9 @@ describe('AppService', () => {
|
||||||
titleModel: 'gpt-3.5-turbo',
|
titleModel: 'gpt-3.5-turbo',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -783,8 +657,7 @@ describe('AppService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('AppService updating app config and issuing warnings', () => {
|
describe('AppService updating app config and issuing warnings', () => {
|
||||||
let initialEnv;
|
let initialEnv: NodeJS.ProcessEnv;
|
||||||
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
|
||||||
|
|
@ -799,15 +672,13 @@ describe('AppService updating app config and issuing warnings', () => {
|
||||||
process.env = { ...initialEnv };
|
process.env = { ...initialEnv };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize app config with default values if loadCustomConfig returns undefined', async () => {
|
it('should initialize app config with default values if config is empty', async () => {
|
||||||
// Mock loadCustomConfig to return undefined
|
const config = {};
|
||||||
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(undefined));
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
paths: expect.anything(),
|
|
||||||
config: {},
|
config: {},
|
||||||
fileStrategy: FileSources.local,
|
fileStrategy: FileSources.local,
|
||||||
registration: expect.objectContaining({
|
registration: expect.objectContaining({
|
||||||
|
|
@ -821,10 +692,10 @@ describe('AppService updating app config and issuing warnings', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize app config with values from loadCustomConfig', async () => {
|
it('should initialize app config with values from config', 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 config: Partial<TCustomConfig> = {
|
||||||
fileStrategy: 'firebase',
|
fileStrategy: FileSources.firebase,
|
||||||
registration: { socialLogins: ['testLogin'] },
|
registration: { socialLogins: ['testLogin'] },
|
||||||
balance: {
|
balance: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
@ -835,27 +706,28 @@ describe('AppService updating app config and issuing warnings', () => {
|
||||||
refillAmount: 5000,
|
refillAmount: 5000,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(customConfig));
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
paths: expect.anything(),
|
config,
|
||||||
config: customConfig,
|
fileStrategy: config.fileStrategy,
|
||||||
fileStrategy: customConfig.fileStrategy,
|
|
||||||
registration: expect.objectContaining({
|
registration: expect.objectContaining({
|
||||||
socialLogins: customConfig.registration.socialLogins,
|
socialLogins: config.registration?.socialLogins,
|
||||||
}),
|
}),
|
||||||
balance: customConfig.balance,
|
balance: config.balance,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply the assistants endpoint configuration correctly to app config', async () => {
|
it('should apply the assistants endpoint configuration correctly to app config', async () => {
|
||||||
const mockConfig = {
|
const config: Partial<TCustomConfig> = {
|
||||||
endpoints: {
|
endpoints: {
|
||||||
assistants: {
|
assistants: {
|
||||||
|
version: 'v2',
|
||||||
|
retrievalModels: ['gpt-4', 'gpt-3.5-turbo'],
|
||||||
|
capabilities: [],
|
||||||
disableBuilder: true,
|
disableBuilder: true,
|
||||||
pollIntervalMs: 5000,
|
pollIntervalMs: 5000,
|
||||||
timeoutMs: 30000,
|
timeoutMs: 30000,
|
||||||
|
|
@ -863,9 +735,8 @@ describe('AppService updating app config and issuing warnings', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig));
|
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -884,119 +755,22 @@ describe('AppService updating app config and issuing warnings', () => {
|
||||||
expect(result.endpoints.assistants.excludedIds).toBeUndefined();
|
expect(result.endpoints.assistants.excludedIds).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log a warning when both supportedIds and excludedIds are provided', async () => {
|
|
||||||
const mockConfig = {
|
|
||||||
endpoints: {
|
|
||||||
assistants: {
|
|
||||||
disableBuilder: false,
|
|
||||||
pollIntervalMs: 3000,
|
|
||||||
timeoutMs: 20000,
|
|
||||||
supportedIds: ['id1', 'id2'],
|
|
||||||
excludedIds: ['id3'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig));
|
|
||||||
|
|
||||||
await AppService();
|
|
||||||
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
expect(logger.warn).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining(
|
|
||||||
"The 'assistants' endpoint has both 'supportedIds' and 'excludedIds' defined.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should log a warning when privateAssistants and supportedIds or excludedIds are provided', async () => {
|
|
||||||
const mockConfig = {
|
|
||||||
endpoints: {
|
|
||||||
assistants: {
|
|
||||||
privateAssistants: true,
|
|
||||||
supportedIds: ['id1'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig));
|
|
||||||
|
|
||||||
await AppService();
|
|
||||||
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
expect(logger.warn).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining(
|
|
||||||
"The 'assistants' endpoint has both 'privateAssistants' and 'supportedIds' or 'excludedIds' defined.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should issue expected warnings when loading Azure Groups with deprecated Environment Variables', async () => {
|
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
|
||||||
groups: azureGroups,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
deprecatedAzureVariables.forEach((varInfo) => {
|
|
||||||
process.env[varInfo.key] = 'test';
|
|
||||||
});
|
|
||||||
|
|
||||||
await AppService();
|
|
||||||
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
deprecatedAzureVariables.forEach(({ key, description }) => {
|
|
||||||
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.`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should issue expected warnings when loading conflicting Azure Envrionment Variables', async () => {
|
|
||||||
loadCustomConfig.mockImplementationOnce(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
endpoints: {
|
|
||||||
[EModelEndpoint.azureOpenAI]: {
|
|
||||||
groups: azureGroups,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
conflictingAzureVariables.forEach((varInfo) => {
|
|
||||||
process.env[varInfo.key] = 'test';
|
|
||||||
});
|
|
||||||
|
|
||||||
await AppService();
|
|
||||||
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
conflictingAzureVariables.forEach(({ key }) => {
|
|
||||||
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.`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not parse environment variable references in OCR config', async () => {
|
it('should not parse environment variable references in OCR config', async () => {
|
||||||
// Mock custom configuration with env variable references in OCR config
|
// Mock custom configuration with env variable references in OCR config
|
||||||
const mockConfig = {
|
const config: Partial<TCustomConfig> = {
|
||||||
ocr: {
|
ocr: {
|
||||||
apiKey: '${OCR_API_KEY_CUSTOM_VAR_NAME}',
|
apiKey: '${OCR_API_KEY_CUSTOM_VAR_NAME}',
|
||||||
baseURL: '${OCR_BASEURL_CUSTOM_VAR_NAME}',
|
baseURL: '${OCR_BASEURL_CUSTOM_VAR_NAME}',
|
||||||
strategy: 'mistral_ocr',
|
strategy: OCRStrategy.MISTRAL_OCR,
|
||||||
mistralModel: 'mistral-medium',
|
mistralModel: 'mistral-medium',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
const result = await AppService();
|
const result = await AppService({ config });
|
||||||
|
|
||||||
// Verify that the raw string references were preserved and not interpolated
|
// Verify that the raw string references were preserved and not interpolated
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
|
|
@ -1012,7 +786,7 @@ describe('AppService updating app config and issuing warnings', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly configure peoplePicker permissions when specified', async () => {
|
it('should correctly configure peoplePicker permissions when specified', async () => {
|
||||||
const mockConfig = {
|
const config = {
|
||||||
interface: {
|
interface: {
|
||||||
peoplePicker: {
|
peoplePicker: {
|
||||||
users: true,
|
users: true,
|
||||||
|
|
@ -1022,9 +796,7 @@ describe('AppService updating app config and issuing warnings', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
loadCustomConfig.mockImplementationOnce(() => Promise.resolve(mockConfig));
|
const result = await AppService({ config });
|
||||||
|
|
||||||
const result = await AppService();
|
|
||||||
|
|
||||||
// Check that interface config includes the permissions
|
// Check that interface config includes the permissions
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
26
packages/api/src/app/cdn.ts
Normal file
26
packages/api/src/app/cdn.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
import { FileSources } from 'librechat-data-provider';
|
||||||
|
import type { AppConfig } from '@librechat/data-schemas';
|
||||||
|
import { initializeAzureBlobService } from '~/cdn/azure';
|
||||||
|
import { initializeFirebase } from '~/cdn/firebase';
|
||||||
|
import { initializeS3 } from '~/cdn/s3';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes file storage clients based on the configured file strategy.
|
||||||
|
* This should be called after loading the app configuration.
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {AppConfig} options.appConfig - The application configuration
|
||||||
|
*/
|
||||||
|
export function initializeFileStorage(appConfig: AppConfig) {
|
||||||
|
const { fileStrategy } = appConfig;
|
||||||
|
|
||||||
|
if (fileStrategy === FileSources.firebase) {
|
||||||
|
initializeFirebase();
|
||||||
|
} else if (fileStrategy === FileSources.azure_blob) {
|
||||||
|
initializeAzureBlobService().catch((error) => {
|
||||||
|
logger.error('Error initializing Azure Blob Service:', error);
|
||||||
|
});
|
||||||
|
} else if (fileStrategy === FileSources.s3) {
|
||||||
|
initializeS3();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,12 +11,15 @@ jest.mock('@librechat/data-schemas', () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { checkWebSearchConfig } = require('./checks');
|
import { handleRateLimits } from './limits';
|
||||||
const { logger } = require('@librechat/data-schemas');
|
import { checkWebSearchConfig } from './checks';
|
||||||
const { extractVariableName } = require('librechat-data-provider');
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
import { extractVariableName as extract } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
const extractVariableName = extract as jest.MockedFunction<typeof extract>;
|
||||||
|
|
||||||
describe('checkWebSearchConfig', () => {
|
describe('checkWebSearchConfig', () => {
|
||||||
let originalEnv;
|
let originalEnv: NodeJS.ProcessEnv;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Clear all mocks
|
// Clear all mocks
|
||||||
|
|
@ -178,6 +181,8 @@ describe('checkWebSearchConfig', () => {
|
||||||
anotherKey: '${SOME_VAR}',
|
anotherKey: '${SOME_VAR}',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
/** @ts-expect-error */
|
||||||
checkWebSearchConfig(config);
|
checkWebSearchConfig(config);
|
||||||
|
|
||||||
expect(extractVariableName).not.toHaveBeenCalled();
|
expect(extractVariableName).not.toHaveBeenCalled();
|
||||||
|
|
@ -200,3 +205,154 @@ describe('checkWebSearchConfig', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('handleRateLimits', () => {
|
||||||
|
let originalEnv: NodeJS.ProcessEnv;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Store original environment
|
||||||
|
originalEnv = process.env;
|
||||||
|
|
||||||
|
// Reset process.env
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original environment
|
||||||
|
process.env = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly set FILE_UPLOAD environment variables based on rate limits', () => {
|
||||||
|
const rateLimits = {
|
||||||
|
fileUploads: {
|
||||||
|
ipMax: 100,
|
||||||
|
ipWindowInMinutes: 60,
|
||||||
|
userMax: 50,
|
||||||
|
userWindowInMinutes: 30,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRateLimits(rateLimits);
|
||||||
|
|
||||||
|
// 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_WINDOW).toEqual('60');
|
||||||
|
expect(process.env.FILE_UPLOAD_USER_MAX).toEqual('50');
|
||||||
|
expect(process.env.FILE_UPLOAD_USER_WINDOW).toEqual('30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly set IMPORT environment variables based on rate limits', () => {
|
||||||
|
const rateLimits = {
|
||||||
|
conversationsImport: {
|
||||||
|
ipMax: 150,
|
||||||
|
ipWindowInMinutes: 60,
|
||||||
|
userMax: 50,
|
||||||
|
userWindowInMinutes: 30,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRateLimits(rateLimits);
|
||||||
|
|
||||||
|
// 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_WINDOW).toEqual('60');
|
||||||
|
expect(process.env.IMPORT_USER_MAX).toEqual('50');
|
||||||
|
expect(process.env.IMPORT_USER_WINDOW).toEqual('30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not modify FILE_UPLOAD environment variables without rate limits', () => {
|
||||||
|
// Setup initial environment variables
|
||||||
|
process.env.FILE_UPLOAD_IP_MAX = '10';
|
||||||
|
process.env.FILE_UPLOAD_IP_WINDOW = '15';
|
||||||
|
process.env.FILE_UPLOAD_USER_MAX = '5';
|
||||||
|
process.env.FILE_UPLOAD_USER_WINDOW = '20';
|
||||||
|
|
||||||
|
const initialEnv = { ...process.env };
|
||||||
|
|
||||||
|
handleRateLimits({});
|
||||||
|
|
||||||
|
// 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_WINDOW).toEqual(initialEnv.FILE_UPLOAD_IP_WINDOW);
|
||||||
|
expect(process.env.FILE_UPLOAD_USER_MAX).toEqual(initialEnv.FILE_UPLOAD_USER_MAX);
|
||||||
|
expect(process.env.FILE_UPLOAD_USER_WINDOW).toEqual(initialEnv.FILE_UPLOAD_USER_WINDOW);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not modify IMPORT environment variables without rate limits', () => {
|
||||||
|
// Setup initial environment variables
|
||||||
|
process.env.IMPORT_IP_MAX = '10';
|
||||||
|
process.env.IMPORT_IP_WINDOW = '15';
|
||||||
|
process.env.IMPORT_USER_MAX = '5';
|
||||||
|
process.env.IMPORT_USER_WINDOW = '20';
|
||||||
|
|
||||||
|
const initialEnv = { ...process.env };
|
||||||
|
|
||||||
|
handleRateLimits({});
|
||||||
|
|
||||||
|
// Expect environment variables to remain unchanged
|
||||||
|
expect(process.env.IMPORT_IP_MAX).toEqual(initialEnv.IMPORT_IP_MAX);
|
||||||
|
expect(process.env.IMPORT_IP_WINDOW).toEqual(initialEnv.IMPORT_IP_WINDOW);
|
||||||
|
expect(process.env.IMPORT_USER_MAX).toEqual(initialEnv.IMPORT_USER_MAX);
|
||||||
|
expect(process.env.IMPORT_USER_WINDOW).toEqual(initialEnv.IMPORT_USER_WINDOW);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle undefined rateLimits parameter', () => {
|
||||||
|
// Setup initial environment variables
|
||||||
|
process.env.FILE_UPLOAD_IP_MAX = 'initial';
|
||||||
|
process.env.IMPORT_IP_MAX = 'initial';
|
||||||
|
|
||||||
|
handleRateLimits(undefined);
|
||||||
|
|
||||||
|
// Should not modify any environment variables
|
||||||
|
expect(process.env.FILE_UPLOAD_IP_MAX).toEqual('initial');
|
||||||
|
expect(process.env.IMPORT_IP_MAX).toEqual('initial');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle partial rate limit configurations', () => {
|
||||||
|
const rateLimits = {
|
||||||
|
fileUploads: {
|
||||||
|
ipMax: 200,
|
||||||
|
// Only setting ipMax, other properties undefined
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRateLimits(rateLimits);
|
||||||
|
|
||||||
|
expect(process.env.FILE_UPLOAD_IP_MAX).toEqual('200');
|
||||||
|
// Other FILE_UPLOAD env vars should not be set
|
||||||
|
expect(process.env.FILE_UPLOAD_IP_WINDOW).toBeUndefined();
|
||||||
|
expect(process.env.FILE_UPLOAD_USER_MAX).toBeUndefined();
|
||||||
|
expect(process.env.FILE_UPLOAD_USER_WINDOW).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly set TTS and STT environment variables based on rate limits', () => {
|
||||||
|
const rateLimits = {
|
||||||
|
tts: {
|
||||||
|
ipMax: 75,
|
||||||
|
ipWindowInMinutes: 45,
|
||||||
|
userMax: 25,
|
||||||
|
userWindowInMinutes: 15,
|
||||||
|
},
|
||||||
|
stt: {
|
||||||
|
ipMax: 80,
|
||||||
|
ipWindowInMinutes: 50,
|
||||||
|
userMax: 30,
|
||||||
|
userWindowInMinutes: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRateLimits(rateLimits);
|
||||||
|
|
||||||
|
// Verify TTS environment variables
|
||||||
|
expect(process.env.TTS_IP_MAX).toEqual('75');
|
||||||
|
expect(process.env.TTS_IP_WINDOW).toEqual('45');
|
||||||
|
expect(process.env.TTS_USER_MAX).toEqual('25');
|
||||||
|
expect(process.env.TTS_USER_WINDOW).toEqual('15');
|
||||||
|
|
||||||
|
// Verify STT environment variables
|
||||||
|
expect(process.env.STT_IP_MAX).toEqual('80');
|
||||||
|
expect(process.env.STT_IP_WINDOW).toEqual('50');
|
||||||
|
expect(process.env.STT_USER_MAX).toEqual('30');
|
||||||
|
expect(process.env.STT_USER_WINDOW).toEqual('20');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
const { logger } = require('@librechat/data-schemas');
|
import { logger, webSearchKeys } from '@librechat/data-schemas';
|
||||||
const { isEnabled, webSearchKeys, checkEmailConfig } = require('@librechat/api');
|
import { Constants, extractVariableName } from 'librechat-data-provider';
|
||||||
const {
|
import type { TCustomConfig } from 'librechat-data-provider';
|
||||||
Constants,
|
import type { AppConfig } from '@librechat/data-schemas';
|
||||||
extractVariableName,
|
import { isEnabled, checkEmailConfig } from '~/utils';
|
||||||
deprecatedAzureVariables,
|
import { handleRateLimits } from './limits';
|
||||||
conflictingAzureVariables,
|
|
||||||
} = require('librechat-data-provider');
|
|
||||||
|
|
||||||
const secretDefaults = {
|
const secretDefaults = {
|
||||||
CREDS_KEY: 'f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0',
|
CREDS_KEY: 'f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0',
|
||||||
|
|
@ -32,17 +30,84 @@ const deprecatedVariables = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const deprecatedAzureVariables = [
|
||||||
|
/* "related to" precedes description text */
|
||||||
|
{ key: 'AZURE_OPENAI_DEFAULT_MODEL', description: 'setting a default model' },
|
||||||
|
{ key: 'AZURE_OPENAI_MODELS', description: 'setting models' },
|
||||||
|
{
|
||||||
|
key: 'AZURE_USE_MODEL_AS_DEPLOYMENT_NAME',
|
||||||
|
description: 'using model names as deployment names',
|
||||||
|
},
|
||||||
|
{ key: 'AZURE_API_KEY', description: 'setting a single Azure API key' },
|
||||||
|
{ key: 'AZURE_OPENAI_API_INSTANCE_NAME', description: 'setting a single Azure instance name' },
|
||||||
|
{
|
||||||
|
key: 'AZURE_OPENAI_API_DEPLOYMENT_NAME',
|
||||||
|
description: 'setting a single Azure deployment name',
|
||||||
|
},
|
||||||
|
{ key: 'AZURE_OPENAI_API_VERSION', description: 'setting a single Azure API version' },
|
||||||
|
{
|
||||||
|
key: 'AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME',
|
||||||
|
description: 'setting a single Azure completions deployment name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME',
|
||||||
|
description: 'setting a single Azure embeddings deployment name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'PLUGINS_USE_AZURE',
|
||||||
|
description: 'using Azure for Plugins',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const conflictingAzureVariables = [
|
||||||
|
{
|
||||||
|
key: 'INSTANCE_NAME',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'DEPLOYMENT_NAME',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the password reset configuration for security issues.
|
||||||
|
*/
|
||||||
|
function checkPasswordReset() {
|
||||||
|
const emailEnabled = checkEmailConfig();
|
||||||
|
const passwordResetAllowed = isEnabled(process.env.ALLOW_PASSWORD_RESET);
|
||||||
|
|
||||||
|
if (!emailEnabled && passwordResetAllowed) {
|
||||||
|
logger.warn(
|
||||||
|
`❗❗❗
|
||||||
|
|
||||||
|
Password reset is enabled with \`ALLOW_PASSWORD_RESET\` but email service is not configured.
|
||||||
|
|
||||||
|
This setup is insecure as password reset links will be issued with a recognized email.
|
||||||
|
|
||||||
|
Please configure email service for secure password reset functionality.
|
||||||
|
|
||||||
|
https://www.librechat.ai/docs/configuration/authentication/email
|
||||||
|
|
||||||
|
❗❗❗`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks environment variables for default secrets and deprecated variables.
|
* Checks environment variables for default secrets and deprecated variables.
|
||||||
* Logs warnings for any default secret values being used and for usage of deprecated `GOOGLE_API_KEY`.
|
* Logs warnings for any default secret values being used and for usage of deprecated variables.
|
||||||
* Advises on replacing default secrets and updating deprecated variables.
|
* Advises on replacing default secrets and updating deprecated variables.
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Function} options.isEnabled - Function to check if a feature is enabled
|
||||||
|
* @param {Function} options.checkEmailConfig - Function to check email configuration
|
||||||
*/
|
*/
|
||||||
function checkVariables() {
|
export function checkVariables() {
|
||||||
let hasDefaultSecrets = false;
|
let hasDefaultSecrets = false;
|
||||||
for (const [key, value] of Object.entries(secretDefaults)) {
|
for (const [key, value] of Object.entries(secretDefaults)) {
|
||||||
if (process.env[key] === value) {
|
if (process.env[key] === value) {
|
||||||
logger.warn(`Default value for ${key} is being used.`);
|
logger.warn(`Default value for ${key} is being used.`);
|
||||||
!hasDefaultSecrets && (hasDefaultSecrets = true);
|
if (!hasDefaultSecrets) {
|
||||||
|
hasDefaultSecrets = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,7 +134,7 @@ function checkVariables() {
|
||||||
* Checks the health of auxiliary API's by attempting a fetch request to their respective `/health` endpoints.
|
* Checks the health of auxiliary API's by attempting a fetch request to their respective `/health` endpoints.
|
||||||
* Logs information or warning based on the API's availability and response.
|
* Logs information or warning based on the API's availability and response.
|
||||||
*/
|
*/
|
||||||
async function checkHealth() {
|
export async function checkHealth() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${process.env.RAG_API_URL}/health`);
|
const response = await fetch(`${process.env.RAG_API_URL}/health`);
|
||||||
if (response?.ok && response?.status === 200) {
|
if (response?.ok && response?.status === 200) {
|
||||||
|
|
@ -104,11 +169,85 @@ function checkAzureVariables() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkInterfaceConfig(appConfig: AppConfig) {
|
||||||
|
const interfaceConfig = appConfig.interfaceConfig;
|
||||||
|
let i = 0;
|
||||||
|
const logSettings = () => {
|
||||||
|
// log interface object and model specs object (without list) for reference
|
||||||
|
logger.warn(`\`interface\` settings:\n${JSON.stringify(interfaceConfig, null, 2)}`);
|
||||||
|
logger.warn(
|
||||||
|
`\`modelSpecs\` settings:\n${JSON.stringify(
|
||||||
|
{ ...(appConfig?.modelSpecs ?? {}), list: undefined },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// warn about config.modelSpecs.prioritize if true and presets are enabled, that default presets will conflict with prioritizing model specs.
|
||||||
|
if (appConfig?.modelSpecs?.prioritize && interfaceConfig?.presets) {
|
||||||
|
logger.warn(
|
||||||
|
"Note: Prioritizing model specs can conflict with default presets if a default preset is set. It's recommended to disable presets from the interface or disable use of a default preset.",
|
||||||
|
);
|
||||||
|
if (i === 0) i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// warn about config.modelSpecs.enforce if true and if any of these, endpointsMenu, modelSelect, presets, or parameters are enabled, that enforcing model specs can conflict with these options.
|
||||||
|
if (
|
||||||
|
appConfig?.modelSpecs?.enforce &&
|
||||||
|
(interfaceConfig?.endpointsMenu ||
|
||||||
|
interfaceConfig?.modelSelect ||
|
||||||
|
interfaceConfig?.presets ||
|
||||||
|
interfaceConfig?.parameters)
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
"Note: Enforcing model specs can conflict with the interface options: endpointsMenu, modelSelect, presets, and parameters. It's recommended to disable these options from the interface or disable enforcing model specs.",
|
||||||
|
);
|
||||||
|
if (i === 0) i++;
|
||||||
|
}
|
||||||
|
// warn if enforce is true and prioritize is not, that enforcing model specs without prioritizing them can lead to unexpected behavior.
|
||||||
|
if (appConfig?.modelSpecs?.enforce && !appConfig?.modelSpecs?.prioritize) {
|
||||||
|
logger.warn(
|
||||||
|
"Note: Enforcing model specs without prioritizing them can lead to unexpected behavior. It's recommended to enable prioritizing model specs if enforcing them.",
|
||||||
|
);
|
||||||
|
if (i === 0) i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
logSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs startup checks including environment variable validation and health checks.
|
||||||
|
* This should be called during application startup before initializing services.
|
||||||
|
* @param [appConfig] - The application configuration object.
|
||||||
|
*/
|
||||||
|
export async function performStartupChecks(appConfig?: AppConfig) {
|
||||||
|
checkVariables();
|
||||||
|
if (appConfig?.endpoints?.azureOpenAI) {
|
||||||
|
checkAzureVariables();
|
||||||
|
}
|
||||||
|
if (appConfig) {
|
||||||
|
checkInterfaceConfig(appConfig);
|
||||||
|
}
|
||||||
|
if (appConfig?.config) {
|
||||||
|
checkConfig(appConfig.config);
|
||||||
|
}
|
||||||
|
if (appConfig?.config?.webSearch) {
|
||||||
|
checkWebSearchConfig(appConfig.config.webSearch);
|
||||||
|
}
|
||||||
|
if (appConfig?.config?.rateLimits) {
|
||||||
|
handleRateLimits(appConfig.config.rateLimits);
|
||||||
|
}
|
||||||
|
await checkHealth();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs basic checks on the loaded config object.
|
* Performs basic checks on the loaded config object.
|
||||||
* @param {TCustomConfig} config - The loaded custom configuration.
|
* @param config - The loaded custom configuration.
|
||||||
*/
|
*/
|
||||||
function checkConfig(config) {
|
export function checkConfig(config: Partial<TCustomConfig>) {
|
||||||
if (config.version !== Constants.CONFIG_VERSION) {
|
if (config.version !== Constants.CONFIG_VERSION) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`\nOutdated Config version: ${config.version}
|
`\nOutdated Config version: ${config.version}
|
||||||
|
|
@ -121,40 +260,19 @@ Latest version: ${Constants.CONFIG_VERSION}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPasswordReset() {
|
|
||||||
const emailEnabled = checkEmailConfig();
|
|
||||||
const passwordResetAllowed = isEnabled(process.env.ALLOW_PASSWORD_RESET);
|
|
||||||
|
|
||||||
if (!emailEnabled && passwordResetAllowed) {
|
|
||||||
logger.warn(
|
|
||||||
`❗❗❗
|
|
||||||
|
|
||||||
Password reset is enabled with \`ALLOW_PASSWORD_RESET\` but email service is not configured.
|
|
||||||
|
|
||||||
This setup is insecure as password reset links will be issued with a recognized email.
|
|
||||||
|
|
||||||
Please configure email service for secure password reset functionality.
|
|
||||||
|
|
||||||
https://www.librechat.ai/docs/configuration/authentication/email
|
|
||||||
|
|
||||||
❗❗❗`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks web search configuration values to ensure they are environment variable references.
|
* Checks web search configuration values to ensure they are environment variable references.
|
||||||
* Warns if actual API keys or URLs are used instead of environment variable references.
|
* Warns if actual API keys or URLs are used instead of environment variable references.
|
||||||
* Logs debug information for properly configured environment variable references.
|
* Logs debug information for properly configured environment variable references.
|
||||||
* @param {Object} webSearchConfig - The loaded web search configuration object.
|
* @param webSearchConfig - The loaded web search configuration object.
|
||||||
*/
|
*/
|
||||||
function checkWebSearchConfig(webSearchConfig) {
|
export function checkWebSearchConfig(webSearchConfig?: Partial<TCustomConfig['webSearch']> | null) {
|
||||||
if (!webSearchConfig) {
|
if (!webSearchConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
webSearchKeys.forEach((key) => {
|
webSearchKeys.forEach((key) => {
|
||||||
const value = webSearchConfig[key];
|
const value = webSearchConfig[key as keyof typeof webSearchConfig];
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
const varName = extractVariableName(value);
|
const varName = extractVariableName(value);
|
||||||
|
|
@ -187,11 +305,3 @@ function checkWebSearchConfig(webSearchConfig) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
checkHealth,
|
|
||||||
checkConfig,
|
|
||||||
checkVariables,
|
|
||||||
checkAzureVariables,
|
|
||||||
checkWebSearchConfig,
|
|
||||||
};
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { getTransactionsConfig, getBalanceConfig } from './config';
|
import { getTransactionsConfig, getBalanceConfig } from './config';
|
||||||
import { logger } from '@librechat/data-schemas';
|
import { logger } from '@librechat/data-schemas';
|
||||||
import { FileSources } from 'librechat-data-provider';
|
import { FileSources } from 'librechat-data-provider';
|
||||||
import type { AppConfig } from '~/types';
|
|
||||||
import type { TCustomConfig } from 'librechat-data-provider';
|
import type { TCustomConfig } from 'librechat-data-provider';
|
||||||
|
import type { AppConfig } from '@librechat/data-schemas';
|
||||||
|
|
||||||
// Helper function to create a minimal AppConfig for testing
|
// Helper function to create a minimal AppConfig for testing
|
||||||
const createTestAppConfig = (overrides: Partial<AppConfig> = {}): AppConfig => {
|
const createTestAppConfig = (overrides: Partial<AppConfig> = {}): AppConfig => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
import { EModelEndpoint, removeNullishValues } from 'librechat-data-provider';
|
import { EModelEndpoint, removeNullishValues } from 'librechat-data-provider';
|
||||||
import type { TCustomConfig, TEndpoint, TTransactionsConfig } from 'librechat-data-provider';
|
import type { TCustomConfig, TEndpoint, TTransactionsConfig } from 'librechat-data-provider';
|
||||||
import type { AppConfig } from '~/types';
|
import type { AppConfig } from '@librechat/data-schemas';
|
||||||
import { isEnabled, normalizeEndpointName } from '~/utils';
|
import { isEnabled, normalizeEndpointName } from '~/utils';
|
||||||
import { logger } from '@librechat/data-schemas';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the balance configuration object
|
* Retrieves the balance configuration object
|
||||||
|
|
@ -24,7 +24,7 @@ export function getBalanceConfig(appConfig?: AppConfig): Partial<TCustomConfig['
|
||||||
/**
|
/**
|
||||||
* Retrieves the transactions configuration object
|
* Retrieves the transactions configuration object
|
||||||
* */
|
* */
|
||||||
export function getTransactionsConfig(appConfig?: AppConfig): TTransactionsConfig {
|
export function getTransactionsConfig(appConfig?: AppConfig): Partial<TTransactionsConfig> {
|
||||||
const defaultConfig: TTransactionsConfig = { enabled: true };
|
const defaultConfig: TTransactionsConfig = { enabled: true };
|
||||||
|
|
||||||
if (!appConfig) {
|
if (!appConfig) {
|
||||||
|
|
@ -66,5 +66,5 @@ export const getCustomEndpointConfig = ({
|
||||||
|
|
||||||
export function hasCustomUserVars(appConfig?: AppConfig): boolean {
|
export function hasCustomUserVars(appConfig?: AppConfig): boolean {
|
||||||
const mcpServers = appConfig?.mcpConfig;
|
const mcpServers = appConfig?.mcpConfig;
|
||||||
return Object.values(mcpServers ?? {}).some((server) => server.customUserVars);
|
return Object.values(mcpServers ?? {}).some((server) => server?.customUserVars);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './config';
|
export * from './config';
|
||||||
export * from './interface';
|
|
||||||
export * from './permissions';
|
export * from './permissions';
|
||||||
|
export * from './cdn';
|
||||||
|
export * from './checks';
|
||||||
|
|
|
||||||
55
packages/api/src/app/limits.ts
Normal file
55
packages/api/src/app/limits.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { RateLimitPrefix } from 'librechat-data-provider';
|
||||||
|
import type { TCustomConfig } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param rateLimits
|
||||||
|
*/
|
||||||
|
export const handleRateLimits = (rateLimits?: TCustomConfig['rateLimits']) => {
|
||||||
|
if (!rateLimits) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rateLimitKeys = {
|
||||||
|
fileUploads: RateLimitPrefix.FILE_UPLOAD,
|
||||||
|
conversationsImport: RateLimitPrefix.IMPORT,
|
||||||
|
tts: RateLimitPrefix.TTS,
|
||||||
|
stt: RateLimitPrefix.STT,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(rateLimitKeys).forEach(([key, prefix]) => {
|
||||||
|
const rateLimit = rateLimits[key as keyof typeof rateLimitKeys];
|
||||||
|
if (rateLimit) {
|
||||||
|
setRateLimitEnvVars(prefix, rateLimit);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type RateLimitConfig = {
|
||||||
|
ipMax?: number | undefined;
|
||||||
|
ipWindowInMinutes?: number | undefined;
|
||||||
|
userMax?: number | undefined;
|
||||||
|
userWindowInMinutes?: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set environment variables for rate limit configurations
|
||||||
|
*
|
||||||
|
* @param prefix - Prefix for environment variable names
|
||||||
|
* @param rateLimit - Rate limit configuration object
|
||||||
|
*/
|
||||||
|
const setRateLimitEnvVars = (prefix: string, rateLimit: RateLimitConfig) => {
|
||||||
|
const envVarsMapping = {
|
||||||
|
ipMax: `${prefix}_IP_MAX`,
|
||||||
|
ipWindowInMinutes: `${prefix}_IP_WINDOW`,
|
||||||
|
userMax: `${prefix}_USER_MAX`,
|
||||||
|
userWindowInMinutes: `${prefix}_USER_WINDOW`,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(envVarsMapping).forEach(([key, envVar]) => {
|
||||||
|
const value = rateLimit[key as keyof RateLimitConfig];
|
||||||
|
if (value !== undefined) {
|
||||||
|
process.env[envVar] = value.toString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
import { loadDefaultInterface } from '@librechat/data-schemas';
|
||||||
import { SystemRoles, Permissions, PermissionTypes, roleDefaults } from 'librechat-data-provider';
|
import { SystemRoles, Permissions, PermissionTypes, roleDefaults } from 'librechat-data-provider';
|
||||||
import type { TConfigDefaults, TCustomConfig } from 'librechat-data-provider';
|
import type { TConfigDefaults, TCustomConfig } from 'librechat-data-provider';
|
||||||
import type { AppConfig } from '~/types/config';
|
import type { AppConfig } from '@librechat/data-schemas';
|
||||||
import { updateInterfacePermissions } from './permissions';
|
import { updateInterfacePermissions } from './permissions';
|
||||||
import { loadDefaultInterface } from './interface';
|
|
||||||
|
|
||||||
const mockUpdateAccessPermissions = jest.fn();
|
const mockUpdateAccessPermissions = jest.fn();
|
||||||
const mockGetRoleByName = jest.fn();
|
const mockGetRoleByName = jest.fn();
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@ import {
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
getConfigDefaults,
|
getConfigDefaults,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { IRole } from '@librechat/data-schemas';
|
import type { IRole, AppConfig } from '@librechat/data-schemas';
|
||||||
import type { AppConfig } from '~/types/config';
|
|
||||||
import { isMemoryEnabled } from '~/memory/config';
|
import { isMemoryEnabled } from '~/memory/config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
const { logger } = require('@librechat/data-schemas');
|
import { logger } from '@librechat/data-schemas';
|
||||||
const { BlobServiceClient } = require('@azure/storage-blob');
|
import { DefaultAzureCredential } from '@azure/identity';
|
||||||
|
import type { ContainerClient, BlobServiceClient } from '@azure/storage-blob';
|
||||||
|
|
||||||
let blobServiceClient = null;
|
let blobServiceClient: BlobServiceClient | null = null;
|
||||||
let azureWarningLogged = false;
|
let azureWarningLogged = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -9,18 +10,18 @@ let azureWarningLogged = false;
|
||||||
* This function establishes a connection by checking if a connection string is provided.
|
* This function establishes a connection by checking if a connection string is provided.
|
||||||
* If available, the connection string is used; otherwise, Managed Identity (via DefaultAzureCredential) is utilized.
|
* If available, the connection string is used; otherwise, Managed Identity (via DefaultAzureCredential) is utilized.
|
||||||
* Note: Container creation (and its public access settings) is handled later in the CRUD functions.
|
* Note: Container creation (and its public access settings) is handled later in the CRUD functions.
|
||||||
* @returns {BlobServiceClient|null} The initialized client, or null if the required configuration is missing.
|
* @returns The initialized client, or null if the required configuration is missing.
|
||||||
*/
|
*/
|
||||||
const initializeAzureBlobService = () => {
|
export const initializeAzureBlobService = async (): Promise<BlobServiceClient | null> => {
|
||||||
if (blobServiceClient) {
|
if (blobServiceClient) {
|
||||||
return blobServiceClient;
|
return blobServiceClient;
|
||||||
}
|
}
|
||||||
const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING;
|
const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING;
|
||||||
if (connectionString) {
|
if (connectionString) {
|
||||||
|
const { BlobServiceClient } = await import('@azure/storage-blob');
|
||||||
blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
|
blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
|
||||||
logger.info('Azure Blob Service initialized using connection string');
|
logger.info('Azure Blob Service initialized using connection string');
|
||||||
} else {
|
} else {
|
||||||
const { DefaultAzureCredential } = require('@azure/identity');
|
|
||||||
const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME;
|
const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME;
|
||||||
if (!accountName) {
|
if (!accountName) {
|
||||||
if (!azureWarningLogged) {
|
if (!azureWarningLogged) {
|
||||||
|
|
@ -33,6 +34,7 @@ const initializeAzureBlobService = () => {
|
||||||
}
|
}
|
||||||
const url = `https://${accountName}.blob.core.windows.net`;
|
const url = `https://${accountName}.blob.core.windows.net`;
|
||||||
const credential = new DefaultAzureCredential();
|
const credential = new DefaultAzureCredential();
|
||||||
|
const { BlobServiceClient } = await import('@azure/storage-blob');
|
||||||
blobServiceClient = new BlobServiceClient(url, credential);
|
blobServiceClient = new BlobServiceClient(url, credential);
|
||||||
logger.info('Azure Blob Service initialized using Managed Identity');
|
logger.info('Azure Blob Service initialized using Managed Identity');
|
||||||
}
|
}
|
||||||
|
|
@ -41,15 +43,12 @@ const initializeAzureBlobService = () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the Azure ContainerClient for the given container name.
|
* Retrieves the Azure ContainerClient for the given container name.
|
||||||
* @param {string} [containerName=process.env.AZURE_CONTAINER_NAME || 'files'] - The container name.
|
* @param [containerName=process.env.AZURE_CONTAINER_NAME || 'files'] - The container name.
|
||||||
* @returns {ContainerClient|null} The Azure ContainerClient.
|
* @returns The Azure ContainerClient.
|
||||||
*/
|
*/
|
||||||
const getAzureContainerClient = (containerName = process.env.AZURE_CONTAINER_NAME || 'files') => {
|
export const getAzureContainerClient = async (
|
||||||
const serviceClient = initializeAzureBlobService();
|
containerName = process.env.AZURE_CONTAINER_NAME || 'files',
|
||||||
|
): Promise<ContainerClient | null> => {
|
||||||
|
const serviceClient = await initializeAzureBlobService();
|
||||||
return serviceClient ? serviceClient.getContainerClient(containerName) : null;
|
return serviceClient ? serviceClient.getContainerClient(containerName) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
initializeAzureBlobService,
|
|
||||||
getAzureContainerClient,
|
|
||||||
};
|
|
||||||
42
packages/api/src/cdn/firebase.ts
Normal file
42
packages/api/src/cdn/firebase.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import firebase from 'firebase/app';
|
||||||
|
import { getStorage } from 'firebase/storage';
|
||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
import type { FirebaseStorage } from 'firebase/storage';
|
||||||
|
import type { FirebaseApp } from 'firebase/app';
|
||||||
|
|
||||||
|
let firebaseInitCount = 0;
|
||||||
|
let firebaseApp: FirebaseApp | null = null;
|
||||||
|
|
||||||
|
export const initializeFirebase = () => {
|
||||||
|
if (firebaseApp) {
|
||||||
|
return firebaseApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: process.env.FIREBASE_API_KEY,
|
||||||
|
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
|
||||||
|
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||||
|
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
|
||||||
|
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
||||||
|
appId: process.env.FIREBASE_APP_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.values(firebaseConfig).some((value) => !value)) {
|
||||||
|
if (firebaseInitCount === 0) {
|
||||||
|
logger.info(
|
||||||
|
'[Optional] Firebase CDN not initialized. To enable, set FIREBASE_API_KEY, FIREBASE_AUTH_DOMAIN, FIREBASE_PROJECT_ID, FIREBASE_STORAGE_BUCKET, FIREBASE_MESSAGING_SENDER_ID, and FIREBASE_APP_ID environment variables.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
firebaseInitCount++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
firebaseApp = firebase.initializeApp(firebaseConfig);
|
||||||
|
logger.info('Firebase CDN initialized');
|
||||||
|
return firebaseApp;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFirebaseStorage = (): FirebaseStorage | null => {
|
||||||
|
const app = initializeFirebase();
|
||||||
|
return app ? getStorage(app) : null;
|
||||||
|
};
|
||||||
3
packages/api/src/cdn/index.ts
Normal file
3
packages/api/src/cdn/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './azure';
|
||||||
|
export * from './firebase';
|
||||||
|
export * from './s3';
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const { S3Client } = require('@aws-sdk/client-s3');
|
import { S3Client } from '@aws-sdk/client-s3';
|
||||||
const { logger } = require('@librechat/data-schemas');
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
|
||||||
let s3 = null;
|
let s3: S3Client | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes and returns an instance of the AWS S3 client.
|
* Initializes and returns an instance of the AWS S3 client.
|
||||||
|
|
@ -11,9 +11,9 @@ let s3 = null;
|
||||||
*
|
*
|
||||||
* If AWS_ENDPOINT_URL is provided, it will be used as the endpoint.
|
* If AWS_ENDPOINT_URL is provided, it will be used as the endpoint.
|
||||||
*
|
*
|
||||||
* @returns {S3Client|null} An instance of S3Client if the region is provided; otherwise, null.
|
* @returns An instance of S3Client if the region is provided; otherwise, null.
|
||||||
*/
|
*/
|
||||||
const initializeS3 = () => {
|
export const initializeS3 = (): S3Client | null => {
|
||||||
if (s3) {
|
if (s3) {
|
||||||
return s3;
|
return s3;
|
||||||
}
|
}
|
||||||
|
|
@ -49,5 +49,3 @@ const initializeS3 = () => {
|
||||||
|
|
||||||
return s3;
|
return s3;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { initializeS3 };
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './app';
|
export * from './app';
|
||||||
|
export * from './cdn';
|
||||||
/* Auth */
|
/* Auth */
|
||||||
export * from './auth';
|
export * from './auth';
|
||||||
/* MCP */
|
/* MCP */
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import mapValues from 'lodash/mapValues';
|
import mapValues from 'lodash/mapValues';
|
||||||
import { logger } from '@librechat/data-schemas';
|
import { logger } from '@librechat/data-schemas';
|
||||||
import { Constants } from 'librechat-data-provider';
|
import { Constants } from 'librechat-data-provider';
|
||||||
|
import type { JsonSchemaType } from '@librechat/data-schemas';
|
||||||
import type { MCPConnection } from '~/mcp/connection';
|
import type { MCPConnection } from '~/mcp/connection';
|
||||||
import type { JsonSchemaType } from '~/types';
|
|
||||||
import type * as t from '~/mcp/types';
|
import type * as t from '~/mcp/types';
|
||||||
import { ConnectionsRepository } from '~/mcp/ConnectionsRepository';
|
import { ConnectionsRepository } from '~/mcp/ConnectionsRepository';
|
||||||
import { detectOAuthRequirement } from '~/mcp/oauth';
|
import { detectOAuthRequirement } from '~/mcp/oauth';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
// zod.spec.ts
|
// zod.spec.ts
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { JsonSchemaType } from '~/types';
|
import type { JsonSchemaType } from '@librechat/data-schemas';
|
||||||
import { resolveJsonSchemaRefs, convertJsonSchemaToZod, convertWithResolvedRefs } from '../zod';
|
import { resolveJsonSchemaRefs, convertJsonSchemaToZod, convertWithResolvedRefs } from '../zod';
|
||||||
|
|
||||||
describe('convertJsonSchemaToZod', () => {
|
describe('convertJsonSchemaToZod', () => {
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,8 @@ import {
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { SearchResultData, UIResource, TPlugin, TUser } from 'librechat-data-provider';
|
import type { SearchResultData, UIResource, TPlugin, TUser } from 'librechat-data-provider';
|
||||||
import type * as t from '@modelcontextprotocol/sdk/types.js';
|
import type * as t from '@modelcontextprotocol/sdk/types.js';
|
||||||
import type { TokenMethods } from '@librechat/data-schemas';
|
import type { TokenMethods, JsonSchemaType } from '@librechat/data-schemas';
|
||||||
import type { FlowStateManager } from '~/flow/manager';
|
import type { FlowStateManager } from '~/flow/manager';
|
||||||
import type { JsonSchemaType } from '~/types/zod';
|
|
||||||
import type { RequestBody } from '~/types/http';
|
import type { RequestBody } from '~/types/http';
|
||||||
import type * as o from '~/mcp/oauth/types';
|
import type * as o from '~/mcp/oauth/types';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { JsonSchemaType, ConvertJsonSchemaToZodOptions } from '~/types';
|
import type { JsonSchemaType, ConvertJsonSchemaToZodOptions } from '@librechat/data-schemas';
|
||||||
|
|
||||||
function isEmptyObjectSchema(jsonSchema?: JsonSchemaType): boolean {
|
function isEmptyObjectSchema(jsonSchema?: JsonSchemaType): boolean {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { logger } from '@librechat/data-schemas';
|
import { logger } from '@librechat/data-schemas';
|
||||||
import type { NextFunction, Request as ServerRequest, Response as ServerResponse } from 'express';
|
import type { NextFunction, Request as ServerRequest, Response as ServerResponse } from 'express';
|
||||||
import type { IBalance, IUser, BalanceConfig, ObjectId } from '@librechat/data-schemas';
|
import type { IBalance, IUser, BalanceConfig, ObjectId, AppConfig } from '@librechat/data-schemas';
|
||||||
import type { Model } from 'mongoose';
|
import type { Model } from 'mongoose';
|
||||||
import type { AppConfig, BalanceUpdateFields } from '~/types';
|
import type { BalanceUpdateFields } from '~/types';
|
||||||
import { getBalanceConfig } from '~/app/config';
|
import { getBalanceConfig } from '~/app/config';
|
||||||
|
|
||||||
export interface BalanceMiddlewareOptions {
|
export interface BalanceMiddlewareOptions {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
import type { IUser } from '@librechat/data-schemas';
|
import type { IUser, AppConfig } from '@librechat/data-schemas';
|
||||||
import type { AppConfig } from './config';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LibreChat-specific request body type that extends Express Request body
|
* LibreChat-specific request body type that extends Express Request body
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
export * from './config';
|
|
||||||
export * from './azure';
|
export * from './azure';
|
||||||
export * from './balance';
|
export * from './balance';
|
||||||
export * from './endpoints';
|
export * from './endpoints';
|
||||||
|
|
@ -11,6 +10,4 @@ export * from './mistral';
|
||||||
export * from './openai';
|
export * from './openai';
|
||||||
export * from './prompts';
|
export * from './prompts';
|
||||||
export * from './run';
|
export * from './run';
|
||||||
export * from './tools';
|
|
||||||
export * from './zod';
|
|
||||||
export * from './anthropic';
|
export * from './anthropic';
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { openAISchema, EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { TEndpointOption, TAzureConfig, TEndpoint, TConfig } from 'librechat-data-provider';
|
import type { TEndpointOption, TAzureConfig, TEndpoint, TConfig } from 'librechat-data-provider';
|
||||||
import type { BindToolsInput } from '@langchain/core/language_models/chat_models';
|
import type { BindToolsInput } from '@langchain/core/language_models/chat_models';
|
||||||
import type { OpenAIClientOptions, Providers } from '@librechat/agents';
|
import type { OpenAIClientOptions, Providers } from '@librechat/agents';
|
||||||
|
import type { AppConfig } from '@librechat/data-schemas';
|
||||||
import type { AzureOptions } from './azure';
|
import type { AzureOptions } from './azure';
|
||||||
import type { AppConfig } from './config';
|
|
||||||
|
|
||||||
export type OpenAIParameters = z.infer<typeof openAISchema>;
|
export type OpenAIParameters = z.infer<typeof openAISchema>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import type { JsonSchemaType } from './zod';
|
|
||||||
|
|
||||||
export interface FunctionTool {
|
|
||||||
type: 'function';
|
|
||||||
function: {
|
|
||||||
description: string;
|
|
||||||
name: string;
|
|
||||||
parameters: JsonSchemaType;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
export type JsonSchemaType = {
|
|
||||||
type: 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'array' | 'object';
|
|
||||||
enum?: string[];
|
|
||||||
items?: JsonSchemaType;
|
|
||||||
properties?: Record<string, JsonSchemaType>;
|
|
||||||
required?: string[];
|
|
||||||
description?: string;
|
|
||||||
additionalProperties?: boolean | JsonSchemaType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConvertJsonSchemaToZodOptions = {
|
|
||||||
allowEmptyObject?: boolean;
|
|
||||||
dropFields?: string[];
|
|
||||||
transformOneOfAnyOf?: boolean;
|
|
||||||
};
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AppConfig } from '~/types';
|
import type { AppConfig } from '@librechat/data-schemas';
|
||||||
import {
|
import {
|
||||||
createTempChatExpirationDate,
|
createTempChatExpirationDate,
|
||||||
getTempChatRetentionHours,
|
getTempChatRetentionHours,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { logger } from '@librechat/data-schemas';
|
import { logger } from '@librechat/data-schemas';
|
||||||
import type { AppConfig } from '~/types';
|
import type { AppConfig } from '@librechat/data-schemas';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default retention period for temporary chats in hours
|
* Default retention period for temporary chats in hours
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { webSearchAuth } from '@librechat/data-schemas';
|
||||||
|
import { SafeSearchTypes, AuthType } from 'librechat-data-provider';
|
||||||
import type {
|
import type {
|
||||||
ScraperTypes,
|
ScraperTypes,
|
||||||
TCustomConfig,
|
TCustomConfig,
|
||||||
|
|
@ -5,8 +7,7 @@ import type {
|
||||||
SearchProviders,
|
SearchProviders,
|
||||||
TWebSearchConfig,
|
TWebSearchConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { webSearchAuth, loadWebSearchAuth, extractWebSearchEnvVars } from './web';
|
import { loadWebSearchAuth, extractWebSearchEnvVars } from './web';
|
||||||
import { SafeSearchTypes, AuthType } from 'librechat-data-provider';
|
|
||||||
|
|
||||||
// Mock the extractVariableName function
|
// Mock the extractVariableName function
|
||||||
jest.mock('../utils', () => ({
|
jest.mock('../utils', () => ({
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
import {
|
||||||
|
AuthType,
|
||||||
|
SafeSearchTypes,
|
||||||
|
SearchCategories,
|
||||||
|
extractVariableName,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
import type {
|
import type {
|
||||||
ScraperTypes,
|
ScraperTypes,
|
||||||
RerankerTypes,
|
RerankerTypes,
|
||||||
|
|
@ -5,108 +11,8 @@ import type {
|
||||||
SearchProviders,
|
SearchProviders,
|
||||||
TWebSearchConfig,
|
TWebSearchConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import {
|
import { webSearchAuth } from '@librechat/data-schemas';
|
||||||
SearchCategories,
|
import type { TWebSearchKeys, TWebSearchCategories } from '@librechat/data-schemas';
|
||||||
SafeSearchTypes,
|
|
||||||
extractVariableName,
|
|
||||||
AuthType,
|
|
||||||
} from 'librechat-data-provider';
|
|
||||||
|
|
||||||
export function loadWebSearchConfig(
|
|
||||||
config: TCustomConfig['webSearch'],
|
|
||||||
): TCustomConfig['webSearch'] {
|
|
||||||
const serperApiKey = config?.serperApiKey ?? '${SERPER_API_KEY}';
|
|
||||||
const searxngInstanceUrl = config?.searxngInstanceUrl ?? '${SEARXNG_INSTANCE_URL}';
|
|
||||||
const searxngApiKey = config?.searxngApiKey ?? '${SEARXNG_API_KEY}';
|
|
||||||
const firecrawlApiKey = config?.firecrawlApiKey ?? '${FIRECRAWL_API_KEY}';
|
|
||||||
const firecrawlApiUrl = config?.firecrawlApiUrl ?? '${FIRECRAWL_API_URL}';
|
|
||||||
const jinaApiKey = config?.jinaApiKey ?? '${JINA_API_KEY}';
|
|
||||||
const jinaApiUrl = config?.jinaApiUrl ?? '${JINA_API_URL}';
|
|
||||||
const cohereApiKey = config?.cohereApiKey ?? '${COHERE_API_KEY}';
|
|
||||||
const safeSearch = config?.safeSearch ?? SafeSearchTypes.MODERATE;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
safeSearch,
|
|
||||||
jinaApiKey,
|
|
||||||
jinaApiUrl,
|
|
||||||
cohereApiKey,
|
|
||||||
serperApiKey,
|
|
||||||
searxngInstanceUrl,
|
|
||||||
searxngApiKey,
|
|
||||||
firecrawlApiKey,
|
|
||||||
firecrawlApiUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TWebSearchKeys =
|
|
||||||
| 'serperApiKey'
|
|
||||||
| 'searxngInstanceUrl'
|
|
||||||
| 'searxngApiKey'
|
|
||||||
| 'firecrawlApiKey'
|
|
||||||
| 'firecrawlApiUrl'
|
|
||||||
| 'jinaApiKey'
|
|
||||||
| 'jinaApiUrl'
|
|
||||||
| 'cohereApiKey';
|
|
||||||
|
|
||||||
export type TWebSearchCategories =
|
|
||||||
| SearchCategories.PROVIDERS
|
|
||||||
| SearchCategories.SCRAPERS
|
|
||||||
| SearchCategories.RERANKERS;
|
|
||||||
|
|
||||||
export const webSearchAuth = {
|
|
||||||
providers: {
|
|
||||||
serper: {
|
|
||||||
serperApiKey: 1 as const,
|
|
||||||
},
|
|
||||||
searxng: {
|
|
||||||
searxngInstanceUrl: 1 as const,
|
|
||||||
/** Optional (0) */
|
|
||||||
searxngApiKey: 0 as const,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scrapers: {
|
|
||||||
firecrawl: {
|
|
||||||
firecrawlApiKey: 1 as const,
|
|
||||||
/** Optional (0) */
|
|
||||||
firecrawlApiUrl: 0 as const,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rerankers: {
|
|
||||||
jina: {
|
|
||||||
jinaApiKey: 1 as const,
|
|
||||||
/** Optional (0) */
|
|
||||||
jinaApiUrl: 0 as const,
|
|
||||||
},
|
|
||||||
cohere: { cohereApiKey: 1 as const },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts all API keys from the webSearchAuth configuration object
|
|
||||||
*/
|
|
||||||
export function getWebSearchKeys(): TWebSearchKeys[] {
|
|
||||||
const keys: TWebSearchKeys[] = [];
|
|
||||||
|
|
||||||
// Iterate through each category (providers, scrapers, rerankers)
|
|
||||||
for (const category of Object.keys(webSearchAuth)) {
|
|
||||||
const categoryObj = webSearchAuth[category as TWebSearchCategories];
|
|
||||||
|
|
||||||
// Iterate through each service within the category
|
|
||||||
for (const service of Object.keys(categoryObj)) {
|
|
||||||
const serviceObj = categoryObj[service as keyof typeof categoryObj];
|
|
||||||
|
|
||||||
// Extract the API keys from the service
|
|
||||||
for (const key of Object.keys(serviceObj)) {
|
|
||||||
keys.push(key as TWebSearchKeys);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const webSearchKeys: TWebSearchKeys[] = getWebSearchKeys();
|
|
||||||
|
|
||||||
export function extractWebSearchEnvVars({
|
export function extractWebSearchEnvVars({
|
||||||
keys,
|
keys,
|
||||||
|
|
|
||||||
|
|
@ -10,44 +10,6 @@ import { extractEnvVariable, envVarRegex } from '../src/utils';
|
||||||
import { azureGroupConfigsSchema } from '../src/config';
|
import { azureGroupConfigsSchema } from '../src/config';
|
||||||
import { errorsToString } from '../src/parsers';
|
import { errorsToString } from '../src/parsers';
|
||||||
|
|
||||||
export const deprecatedAzureVariables = [
|
|
||||||
/* "related to" precedes description text */
|
|
||||||
{ key: 'AZURE_OPENAI_DEFAULT_MODEL', description: 'setting a default model' },
|
|
||||||
{ key: 'AZURE_OPENAI_MODELS', description: 'setting models' },
|
|
||||||
{
|
|
||||||
key: 'AZURE_USE_MODEL_AS_DEPLOYMENT_NAME',
|
|
||||||
description: 'using model names as deployment names',
|
|
||||||
},
|
|
||||||
{ key: 'AZURE_API_KEY', description: 'setting a single Azure API key' },
|
|
||||||
{ key: 'AZURE_OPENAI_API_INSTANCE_NAME', description: 'setting a single Azure instance name' },
|
|
||||||
{
|
|
||||||
key: 'AZURE_OPENAI_API_DEPLOYMENT_NAME',
|
|
||||||
description: 'setting a single Azure deployment name',
|
|
||||||
},
|
|
||||||
{ key: 'AZURE_OPENAI_API_VERSION', description: 'setting a single Azure API version' },
|
|
||||||
{
|
|
||||||
key: 'AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME',
|
|
||||||
description: 'setting a single Azure completions deployment name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME',
|
|
||||||
description: 'setting a single Azure embeddings deployment name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'PLUGINS_USE_AZURE',
|
|
||||||
description: 'using Azure for Plugins',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const conflictingAzureVariables = [
|
|
||||||
{
|
|
||||||
key: 'INSTANCE_NAME',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'DEPLOYMENT_NAME',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function validateAzureGroups(configs: TAzureGroups): TAzureConfigValidationResult {
|
export function validateAzureGroups(configs: TAzureGroups): TAzureConfigValidationResult {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
const modelNames: string[] = [];
|
const modelNames: string[] = [];
|
||||||
|
|
|
||||||
|
|
@ -155,8 +155,10 @@ export type TAzureGroupMap = Record<
|
||||||
|
|
||||||
export type TValidatedAzureConfig = {
|
export type TValidatedAzureConfig = {
|
||||||
modelNames: string[];
|
modelNames: string[];
|
||||||
modelGroupMap: TAzureModelGroupMap;
|
|
||||||
groupMap: TAzureGroupMap;
|
groupMap: TAzureGroupMap;
|
||||||
|
assistantModels?: string[];
|
||||||
|
assistantGroups?: string[];
|
||||||
|
modelGroupMap: TAzureModelGroupMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAzureConfigValidationResult = TValidatedAzureConfig & {
|
export type TAzureConfigValidationResult = TValidatedAzureConfig & {
|
||||||
|
|
@ -752,7 +754,7 @@ export const webSearchSchema = z.object({
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TWebSearchConfig = z.infer<typeof webSearchSchema>;
|
export type TWebSearchConfig = DeepPartial<z.infer<typeof webSearchSchema>>;
|
||||||
|
|
||||||
export const ocrSchema = z.object({
|
export const ocrSchema = z.object({
|
||||||
mistralModel: z.string().optional(),
|
mistralModel: z.string().optional(),
|
||||||
|
|
@ -799,7 +801,7 @@ export const memorySchema = z.object({
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TMemoryConfig = z.infer<typeof memorySchema>;
|
export type TMemoryConfig = DeepPartial<z.infer<typeof memorySchema>>;
|
||||||
|
|
||||||
const customEndpointsSchema = z.array(endpointSchema.partial()).optional();
|
const customEndpointsSchema = z.array(endpointSchema.partial()).optional();
|
||||||
|
|
||||||
|
|
@ -862,9 +864,27 @@ export const configSchema = z.object({
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getConfigDefaults = () => getSchemaDefaults(configSchema);
|
/**
|
||||||
|
* Recursively makes all properties of T optional, including nested objects.
|
||||||
|
* Handles arrays, primitives, functions, and Date objects correctly.
|
||||||
|
*/
|
||||||
|
export type DeepPartial<T> = T extends (infer U)[]
|
||||||
|
? DeepPartial<U>[]
|
||||||
|
: T extends ReadonlyArray<infer U>
|
||||||
|
? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
T extends Function
|
||||||
|
? T
|
||||||
|
: T extends Date
|
||||||
|
? T
|
||||||
|
: T extends object
|
||||||
|
? {
|
||||||
|
[P in keyof T]?: DeepPartial<T[P]>;
|
||||||
|
}
|
||||||
|
: T;
|
||||||
|
|
||||||
export type TCustomConfig = z.infer<typeof configSchema>;
|
export const getConfigDefaults = () => getSchemaDefaults(configSchema);
|
||||||
|
export type TCustomConfig = DeepPartial<z.infer<typeof configSchema>>;
|
||||||
export type TCustomEndpoints = z.infer<typeof customEndpointsSchema>;
|
export type TCustomEndpoints = z.infer<typeof customEndpointsSchema>;
|
||||||
|
|
||||||
export type TProviderSchema =
|
export type TProviderSchema =
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import type { TCustomConfig, TAgentsEndpoint } from 'librechat-data-provider';
|
||||||
* @returns The Agents endpoint configuration.
|
* @returns The Agents endpoint configuration.
|
||||||
*/
|
*/
|
||||||
export function agentsConfigSetup(
|
export function agentsConfigSetup(
|
||||||
config: TCustomConfig,
|
config: Partial<TCustomConfig>,
|
||||||
defaultConfig: Partial<TAgentsEndpoint>,
|
defaultConfig?: Partial<TAgentsEndpoint>,
|
||||||
): Partial<TAgentsEndpoint> {
|
): Partial<TAgentsEndpoint> {
|
||||||
const agentsConfig = config?.endpoints?.[EModelEndpoint.agents];
|
const agentsConfig = config?.endpoints?.[EModelEndpoint.agents];
|
||||||
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
const { logger } = require('@librechat/data-schemas');
|
import logger from '~/config/winston';
|
||||||
const {
|
import {
|
||||||
Capabilities,
|
Capabilities,
|
||||||
|
EModelEndpoint,
|
||||||
assistantEndpointSchema,
|
assistantEndpointSchema,
|
||||||
defaultAssistantsVersion,
|
defaultAssistantsVersion,
|
||||||
} = require('librechat-data-provider');
|
} from 'librechat-data-provider';
|
||||||
|
import type { TCustomConfig, TAssistantEndpoint } from 'librechat-data-provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the minimum, default Assistants configuration if Azure OpenAI Assistants option is enabled.
|
* Sets up the minimum, default Assistants configuration if Azure OpenAI Assistants option is enabled.
|
||||||
* @returns {Partial<TAssistantEndpoint>} The Assistants endpoint configuration.
|
* @returns The Assistants endpoint configuration.
|
||||||
*/
|
*/
|
||||||
function azureAssistantsDefaults() {
|
export function azureAssistantsDefaults(): {
|
||||||
|
capabilities: TAssistantEndpoint['capabilities'];
|
||||||
|
version: TAssistantEndpoint['version'];
|
||||||
|
} {
|
||||||
return {
|
return {
|
||||||
capabilities: [Capabilities.tools, Capabilities.actions, Capabilities.code_interpreter],
|
capabilities: [Capabilities.tools, Capabilities.actions, Capabilities.code_interpreter],
|
||||||
version: defaultAssistantsVersion.azureAssistants,
|
version: defaultAssistantsVersion.azureAssistants,
|
||||||
|
|
@ -18,22 +23,26 @@ function azureAssistantsDefaults() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the Assistants configuration from the config (`librechat.yaml`) file.
|
* Sets up the Assistants configuration from the config (`librechat.yaml`) file.
|
||||||
* @param {TCustomConfig} config - The loaded custom configuration.
|
* @param config - The loaded custom configuration.
|
||||||
* @param {EModelEndpoint.assistants|EModelEndpoint.azureAssistants} assistantsEndpoint - The Assistants endpoint name.
|
* @param assistantsEndpoint - The Assistants endpoint name.
|
||||||
* - The previously loaded assistants configuration from Azure OpenAI Assistants option.
|
* - The previously loaded assistants configuration from Azure OpenAI Assistants option.
|
||||||
* @param {Partial<TAssistantEndpoint>} [prevConfig]
|
* @param [prevConfig]
|
||||||
* @returns {Partial<TAssistantEndpoint>} The Assistants endpoint configuration.
|
* @returns The Assistants endpoint configuration.
|
||||||
*/
|
*/
|
||||||
function assistantsConfigSetup(config, assistantsEndpoint, prevConfig = {}) {
|
export function assistantsConfigSetup(
|
||||||
const assistantsConfig = config.endpoints[assistantsEndpoint];
|
config: Partial<TCustomConfig>,
|
||||||
|
assistantsEndpoint: EModelEndpoint.assistants | EModelEndpoint.azureAssistants,
|
||||||
|
prevConfig: Partial<TAssistantEndpoint> = {},
|
||||||
|
): Partial<TAssistantEndpoint> {
|
||||||
|
const assistantsConfig = config.endpoints?.[assistantsEndpoint];
|
||||||
const parsedConfig = assistantEndpointSchema.parse(assistantsConfig);
|
const parsedConfig = assistantEndpointSchema.parse(assistantsConfig);
|
||||||
if (assistantsConfig.supportedIds?.length && assistantsConfig.excludedIds?.length) {
|
if (assistantsConfig?.supportedIds?.length && assistantsConfig.excludedIds?.length) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Configuration conflict: The '${assistantsEndpoint}' endpoint has both 'supportedIds' and 'excludedIds' defined. The 'excludedIds' will be ignored.`,
|
`Configuration conflict: The '${assistantsEndpoint}' endpoint has both 'supportedIds' and 'excludedIds' defined. The 'excludedIds' will be ignored.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
assistantsConfig.privateAssistants &&
|
assistantsConfig?.privateAssistants &&
|
||||||
(assistantsConfig.supportedIds?.length || assistantsConfig.excludedIds?.length)
|
(assistantsConfig.supportedIds?.length || assistantsConfig.excludedIds?.length)
|
||||||
) {
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|
@ -59,5 +68,3 @@ function assistantsConfigSetup(config, assistantsEndpoint, prevConfig = {}) {
|
||||||
titlePromptTemplate: parsedConfig.titlePromptTemplate,
|
titlePromptTemplate: parsedConfig.titlePromptTemplate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { azureAssistantsDefaults, assistantsConfigSetup };
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
const { logger } = require('@librechat/data-schemas');
|
import logger from '~/config/winston';
|
||||||
const {
|
import {
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
validateAzureGroups,
|
validateAzureGroups,
|
||||||
mapModelToAzureConfig,
|
mapModelToAzureConfig,
|
||||||
} = require('librechat-data-provider');
|
} from 'librechat-data-provider';
|
||||||
|
import type { TCustomConfig, TAzureConfig } from 'librechat-data-provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the Azure OpenAI configuration from the config (`librechat.yaml`) file.
|
* Sets up the Azure OpenAI configuration from the config (`librechat.yaml`) file.
|
||||||
* @param {TCustomConfig} config - The loaded custom configuration.
|
* @param config - The loaded custom configuration.
|
||||||
* @returns {TAzureConfig} The Azure OpenAI configuration.
|
* @returns The Azure OpenAI configuration.
|
||||||
*/
|
*/
|
||||||
function azureConfigSetup(config) {
|
export function azureConfigSetup(config: Partial<TCustomConfig>): TAzureConfig {
|
||||||
const { groups, ...azureConfiguration } = config.endpoints[EModelEndpoint.azureOpenAI];
|
const azureConfig = config.endpoints?.[EModelEndpoint.azureOpenAI];
|
||||||
/** @type {TAzureConfigValidationResult} */
|
if (!azureConfig) {
|
||||||
|
throw new Error('Azure OpenAI configuration is missing.');
|
||||||
|
}
|
||||||
|
const { groups, ...azureConfiguration } = azureConfig;
|
||||||
const { isValid, modelNames, modelGroupMap, groupMap, errors } = validateAzureGroups(groups);
|
const { isValid, modelNames, modelGroupMap, groupMap, errors } = validateAzureGroups(groups);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
|
@ -22,16 +26,18 @@ function azureConfigSetup(config) {
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistantModels = [];
|
const assistantModels: string[] = [];
|
||||||
const assistantGroups = new Set();
|
const assistantGroups = new Set<string>();
|
||||||
for (const modelName of modelNames) {
|
for (const modelName of modelNames) {
|
||||||
mapModelToAzureConfig({ modelName, modelGroupMap, groupMap });
|
mapModelToAzureConfig({ modelName, modelGroupMap, groupMap });
|
||||||
const groupName = modelGroupMap?.[modelName]?.group;
|
const groupName = modelGroupMap?.[modelName]?.group;
|
||||||
const modelGroup = groupMap?.[groupName];
|
const modelGroup = groupMap?.[groupName];
|
||||||
let supportsAssistants = modelGroup?.assistants || modelGroup?.[modelName]?.assistants;
|
const supportsAssistants = modelGroup?.assistants || modelGroup?.[modelName]?.assistants;
|
||||||
if (supportsAssistants) {
|
if (supportsAssistants) {
|
||||||
assistantModels.push(modelName);
|
assistantModels.push(modelName);
|
||||||
!assistantGroups.has(groupName) && assistantGroups.add(groupName);
|
if (!assistantGroups.has(groupName)) {
|
||||||
|
assistantGroups.add(groupName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,13 +59,13 @@ function azureConfigSetup(config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
errors,
|
||||||
|
isValid,
|
||||||
|
groupMap,
|
||||||
modelNames,
|
modelNames,
|
||||||
modelGroupMap,
|
modelGroupMap,
|
||||||
groupMap,
|
|
||||||
assistantModels,
|
assistantModels,
|
||||||
assistantGroups: Array.from(assistantGroups),
|
assistantGroups: Array.from(assistantGroups),
|
||||||
...azureConfiguration,
|
...azureConfiguration,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { azureConfigSetup };
|
|
||||||
|
|
@ -1,22 +1,24 @@
|
||||||
const { agentsConfigSetup } = require('@librechat/api');
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
const { EModelEndpoint } = require('librechat-data-provider');
|
import type { TCustomConfig, TAgentsEndpoint } from 'librechat-data-provider';
|
||||||
const { azureAssistantsDefaults, assistantsConfigSetup } = require('./assistants');
|
import type { AppConfig } from '~/types';
|
||||||
const { azureConfigSetup } = require('./azureOpenAI');
|
import { azureAssistantsDefaults, assistantsConfigSetup } from './assistants';
|
||||||
const { checkAzureVariables } = require('./checks');
|
import { agentsConfigSetup } from './agents';
|
||||||
|
import { azureConfigSetup } from './azure';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads custom config endpoints
|
* Loads custom config endpoints
|
||||||
* @param {TCustomConfig} [config]
|
* @param [config]
|
||||||
* @param {TCustomConfig['endpoints']['agents']} [agentsDefaults]
|
* @param [agentsDefaults]
|
||||||
*/
|
*/
|
||||||
const loadEndpoints = (config, agentsDefaults) => {
|
export const loadEndpoints = (
|
||||||
/** @type {AppConfig['endpoints']} */
|
config: Partial<TCustomConfig>,
|
||||||
const loadedEndpoints = {};
|
agentsDefaults?: Partial<TAgentsEndpoint>,
|
||||||
|
) => {
|
||||||
|
const loadedEndpoints: AppConfig['endpoints'] = {};
|
||||||
const endpoints = config?.endpoints;
|
const endpoints = config?.endpoints;
|
||||||
|
|
||||||
if (endpoints?.[EModelEndpoint.azureOpenAI]) {
|
if (endpoints?.[EModelEndpoint.azureOpenAI]) {
|
||||||
loadedEndpoints[EModelEndpoint.azureOpenAI] = azureConfigSetup(config);
|
loadedEndpoints[EModelEndpoint.azureOpenAI] = azureConfigSetup(config);
|
||||||
checkAzureVariables();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) {
|
if (endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) {
|
||||||
|
|
@ -50,8 +52,9 @@ const loadEndpoints = (config, agentsDefaults) => {
|
||||||
];
|
];
|
||||||
|
|
||||||
endpointKeys.forEach((key) => {
|
endpointKeys.forEach((key) => {
|
||||||
if (endpoints?.[key]) {
|
const currentKey = key as keyof typeof endpoints;
|
||||||
loadedEndpoints[key] = endpoints[key];
|
if (endpoints?.[currentKey]) {
|
||||||
|
loadedEndpoints[currentKey] = endpoints[currentKey];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -61,7 +64,3 @@ const loadEndpoints = (config, agentsDefaults) => {
|
||||||
|
|
||||||
return loadedEndpoints;
|
return loadedEndpoints;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
loadEndpoints,
|
|
||||||
};
|
|
||||||
6
packages/data-schemas/src/app/index.ts
Normal file
6
packages/data-schemas/src/app/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export * from './agents';
|
||||||
|
export * from './interface';
|
||||||
|
export * from './service';
|
||||||
|
export * from './specs';
|
||||||
|
export * from './turnstile';
|
||||||
|
export * from './web';
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { logger } from '@librechat/data-schemas';
|
|
||||||
import { removeNullishValues } from 'librechat-data-provider';
|
import { removeNullishValues } from 'librechat-data-provider';
|
||||||
import type { TCustomConfig, TConfigDefaults } from 'librechat-data-provider';
|
import type { TCustomConfig, TConfigDefaults } from 'librechat-data-provider';
|
||||||
import type { AppConfig } from '~/types/config';
|
import type { AppConfig } from '~/types/app';
|
||||||
import { isMemoryEnabled } from '~/memory/config';
|
import { isMemoryEnabled } from './memory';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the default interface object.
|
* Loads the default interface object.
|
||||||
|
|
@ -58,51 +57,5 @@ export async function loadDefaultInterface({
|
||||||
marketplace: interfaceConfig?.marketplace,
|
marketplace: interfaceConfig?.marketplace,
|
||||||
});
|
});
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
const logSettings = () => {
|
|
||||||
// log interface object and model specs object (without list) for reference
|
|
||||||
logger.warn(`\`interface\` settings:\n${JSON.stringify(loadedInterface, null, 2)}`);
|
|
||||||
logger.warn(
|
|
||||||
`\`modelSpecs\` settings:\n${JSON.stringify(
|
|
||||||
{ ...(config?.modelSpecs ?? {}), list: undefined },
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// warn about config.modelSpecs.prioritize if true and presets are enabled, that default presets will conflict with prioritizing model specs.
|
|
||||||
if (config?.modelSpecs?.prioritize && loadedInterface.presets) {
|
|
||||||
logger.warn(
|
|
||||||
"Note: Prioritizing model specs can conflict with default presets if a default preset is set. It's recommended to disable presets from the interface or disable use of a default preset.",
|
|
||||||
);
|
|
||||||
if (i === 0) i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// warn about config.modelSpecs.enforce if true and if any of these, endpointsMenu, modelSelect, presets, or parameters are enabled, that enforcing model specs can conflict with these options.
|
|
||||||
if (
|
|
||||||
config?.modelSpecs?.enforce &&
|
|
||||||
(loadedInterface.endpointsMenu ||
|
|
||||||
loadedInterface.modelSelect ||
|
|
||||||
loadedInterface.presets ||
|
|
||||||
loadedInterface.parameters)
|
|
||||||
) {
|
|
||||||
logger.warn(
|
|
||||||
"Note: Enforcing model specs can conflict with the interface options: endpointsMenu, modelSelect, presets, and parameters. It's recommended to disable these options from the interface or disable enforcing model specs.",
|
|
||||||
);
|
|
||||||
if (i === 0) i++;
|
|
||||||
}
|
|
||||||
// warn if enforce is true and prioritize is not, that enforcing model specs without prioritizing them can lead to unexpected behavior.
|
|
||||||
if (config?.modelSpecs?.enforce && !config?.modelSpecs?.prioritize) {
|
|
||||||
logger.warn(
|
|
||||||
"Note: Enforcing model specs without prioritizing them can lead to unexpected behavior. It's recommended to enable prioritizing model specs if enforcing them.",
|
|
||||||
);
|
|
||||||
if (i === 0) i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i > 0) {
|
|
||||||
logSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadedInterface;
|
return loadedInterface;
|
||||||
}
|
}
|
||||||
28
packages/data-schemas/src/app/memory.ts
Normal file
28
packages/data-schemas/src/app/memory.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { memorySchema } from 'librechat-data-provider';
|
||||||
|
import type { TCustomConfig, TMemoryConfig } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
const hasValidAgent = (agent: TMemoryConfig['agent']) =>
|
||||||
|
!!agent &&
|
||||||
|
(('id' in agent && !!agent.id) ||
|
||||||
|
('provider' in agent && 'model' in agent && !!agent.provider && !!agent.model));
|
||||||
|
|
||||||
|
const isDisabled = (config?: TMemoryConfig | TCustomConfig['memory']) =>
|
||||||
|
!config || config.disabled === true;
|
||||||
|
|
||||||
|
export function loadMemoryConfig(config: TCustomConfig['memory']): TMemoryConfig | undefined {
|
||||||
|
if (!config) return undefined;
|
||||||
|
if (isDisabled(config)) return config as TMemoryConfig;
|
||||||
|
|
||||||
|
if (!hasValidAgent(config.agent)) {
|
||||||
|
return { ...config, disabled: true } as TMemoryConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const charLimit = memorySchema.shape.charLimit.safeParse(config.charLimit).data ?? 10000;
|
||||||
|
|
||||||
|
return { ...config, charLimit };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMemoryEnabled(config: TMemoryConfig | undefined): boolean {
|
||||||
|
if (isDisabled(config)) return false;
|
||||||
|
return hasValidAgent(config!.agent);
|
||||||
|
}
|
||||||
15
packages/data-schemas/src/app/ocr.ts
Normal file
15
packages/data-schemas/src/app/ocr.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { OCRStrategy } from 'librechat-data-provider';
|
||||||
|
import type { TCustomConfig } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
export function loadOCRConfig(config?: TCustomConfig['ocr']): TCustomConfig['ocr'] | undefined {
|
||||||
|
if (!config) return;
|
||||||
|
const baseURL = config?.baseURL ?? '';
|
||||||
|
const apiKey = config?.apiKey ?? '';
|
||||||
|
const mistralModel = config?.mistralModel ?? '';
|
||||||
|
return {
|
||||||
|
apiKey,
|
||||||
|
baseURL,
|
||||||
|
mistralModel,
|
||||||
|
strategy: config?.strategy ?? OCRStrategy.MISTRAL_OCR,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,48 +1,56 @@
|
||||||
const { FileSources, EModelEndpoint, getConfigDefaults } = require('librechat-data-provider');
|
import { EModelEndpoint, getConfigDefaults } from 'librechat-data-provider';
|
||||||
const {
|
import type { TCustomConfig, FileSources, DeepPartial } from 'librechat-data-provider';
|
||||||
isEnabled,
|
import type { AppConfig, FunctionTool } from '~/types/app';
|
||||||
loadOCRConfig,
|
import { loadDefaultInterface } from './interface';
|
||||||
loadMemoryConfig,
|
import { loadTurnstileConfig } from './turnstile';
|
||||||
agentsConfigSetup,
|
import { agentsConfigSetup } from './agents';
|
||||||
loadWebSearchConfig,
|
import { loadWebSearchConfig } from './web';
|
||||||
loadDefaultInterface,
|
import { processModelSpecs } from './specs';
|
||||||
} = require('@librechat/api');
|
import { loadMemoryConfig } from './memory';
|
||||||
const {
|
import { loadEndpoints } from './endpoints';
|
||||||
checkWebSearchConfig,
|
import { loadOCRConfig } from './ocr';
|
||||||
checkVariables,
|
|
||||||
checkHealth,
|
export type Paths = {
|
||||||
checkConfig,
|
root: string;
|
||||||
} = require('./start/checks');
|
uploads: string;
|
||||||
const { initializeAzureBlobService } = require('./Files/Azure/initialize');
|
clientPath: string;
|
||||||
const { initializeFirebase } = require('./Files/Firebase/initialize');
|
dist: string;
|
||||||
const handleRateLimits = require('./Config/handleRateLimits');
|
publicPath: string;
|
||||||
const loadCustomConfig = require('./Config/loadCustomConfig');
|
fonts: string;
|
||||||
const { loadTurnstileConfig } = require('./start/turnstile');
|
assets: string;
|
||||||
const { processModelSpecs } = require('./start/modelSpecs');
|
imageOutput: string;
|
||||||
const { initializeS3 } = require('./Files/S3/initialize');
|
structuredTools: string;
|
||||||
const { loadAndFormatTools } = require('./start/tools');
|
pluginManifest: string;
|
||||||
const { loadEndpoints } = require('./start/endpoints');
|
};
|
||||||
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
|
||||||
*/
|
*/
|
||||||
const AppService = async () => {
|
export const AppService = async (params?: {
|
||||||
/** @type {TCustomConfig} */
|
config: DeepPartial<TCustomConfig>;
|
||||||
const config = (await loadCustomConfig()) ?? {};
|
paths?: Paths;
|
||||||
|
systemTools?: Record<string, FunctionTool>;
|
||||||
|
}): Promise<AppConfig> => {
|
||||||
|
const { config, paths, systemTools } = params || {};
|
||||||
|
if (!config) {
|
||||||
|
throw new Error('Config is required');
|
||||||
|
}
|
||||||
const configDefaults = getConfigDefaults();
|
const configDefaults = getConfigDefaults();
|
||||||
|
|
||||||
const ocr = loadOCRConfig(config.ocr);
|
const ocr = loadOCRConfig(config.ocr);
|
||||||
const webSearch = loadWebSearchConfig(config.webSearch);
|
const webSearch = loadWebSearchConfig(config.webSearch);
|
||||||
checkWebSearchConfig(webSearch);
|
|
||||||
const memory = loadMemoryConfig(config.memory);
|
const memory = loadMemoryConfig(config.memory);
|
||||||
const filteredTools = config.filteredTools;
|
const filteredTools = config.filteredTools;
|
||||||
const includedTools = config.includedTools;
|
const includedTools = config.includedTools;
|
||||||
const fileStrategy = config.fileStrategy ?? configDefaults.fileStrategy;
|
const fileStrategy = (config.fileStrategy ?? configDefaults.fileStrategy) as
|
||||||
|
| FileSources.local
|
||||||
|
| FileSources.s3
|
||||||
|
| FileSources.firebase
|
||||||
|
| FileSources.azure_blob;
|
||||||
const startBalance = process.env.START_BALANCE;
|
const startBalance = process.env.START_BALANCE;
|
||||||
const balance = config.balance ?? {
|
const balance = config.balance ?? {
|
||||||
enabled: isEnabled(process.env.CHECK_BALANCE),
|
enabled: process.env.CHECK_BALANCE?.toLowerCase().trim() === 'true',
|
||||||
startBalance: startBalance ? parseInt(startBalance, 10) : undefined,
|
startBalance: startBalance ? parseInt(startBalance, 10) : undefined,
|
||||||
};
|
};
|
||||||
const transactions = config.transactions ?? configDefaults.transactions;
|
const transactions = config.transactions ?? configDefaults.transactions;
|
||||||
|
|
@ -50,23 +58,7 @@ const AppService = async () => {
|
||||||
|
|
||||||
process.env.CDN_PROVIDER = fileStrategy;
|
process.env.CDN_PROVIDER = fileStrategy;
|
||||||
|
|
||||||
checkVariables();
|
const availableTools = systemTools;
|
||||||
await checkHealth();
|
|
||||||
|
|
||||||
if (fileStrategy === FileSources.firebase) {
|
|
||||||
initializeFirebase();
|
|
||||||
} else if (fileStrategy === FileSources.azure_blob) {
|
|
||||||
initializeAzureBlobService();
|
|
||||||
} else if (fileStrategy === FileSources.s3) {
|
|
||||||
initializeS3();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {Record<string, FunctionTool>} */
|
|
||||||
const availableTools = loadAndFormatTools({
|
|
||||||
adminFilter: filteredTools,
|
|
||||||
adminIncluded: includedTools,
|
|
||||||
directory: paths.structuredTools,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mcpConfig = config.mcpServers || null;
|
const mcpConfig = config.mcpServers || null;
|
||||||
const registration = config.registration ?? configDefaults.registration;
|
const registration = config.registration ?? configDefaults.registration;
|
||||||
|
|
@ -107,8 +99,6 @@ const AppService = async () => {
|
||||||
return appConfig;
|
return appConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkConfig(config);
|
|
||||||
handleRateLimits(config?.rateLimits);
|
|
||||||
const loadedEndpoints = loadEndpoints(config, agentsDefaults);
|
const loadedEndpoints = loadEndpoints(config, agentsDefaults);
|
||||||
|
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
|
|
@ -121,5 +111,3 @@ const AppService = async () => {
|
||||||
|
|
||||||
return appConfig;
|
return appConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = AppService;
|
|
||||||
94
packages/data-schemas/src/app/specs.ts
Normal file
94
packages/data-schemas/src/app/specs.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import logger from '~/config/winston';
|
||||||
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
|
import type { TCustomConfig } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize the endpoint name to system-expected value.
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
function normalizeEndpointName(name = ''): string {
|
||||||
|
return name.toLowerCase() === 'ollama' ? 'ollama' : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up Model Specs from the config (`librechat.yaml`) file.
|
||||||
|
* @param [endpoints] - The loaded custom configuration for endpoints.
|
||||||
|
* @param [modelSpecs] - The loaded custom configuration for model specs.
|
||||||
|
* @param [interfaceConfig] - The loaded interface configuration.
|
||||||
|
* @returns The processed model specs, if any.
|
||||||
|
*/
|
||||||
|
export function processModelSpecs(
|
||||||
|
endpoints?: TCustomConfig['endpoints'],
|
||||||
|
_modelSpecs?: TCustomConfig['modelSpecs'],
|
||||||
|
interfaceConfig?: TCustomConfig['interface'],
|
||||||
|
): TCustomConfig['modelSpecs'] | undefined {
|
||||||
|
if (!_modelSpecs) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = _modelSpecs.list;
|
||||||
|
const modelSpecs: typeof list = [];
|
||||||
|
|
||||||
|
const customEndpoints = endpoints?.[EModelEndpoint.custom] ?? [];
|
||||||
|
|
||||||
|
if (interfaceConfig?.modelSelect !== true && (_modelSpecs.addedEndpoints?.length ?? 0) > 0) {
|
||||||
|
logger.warn(
|
||||||
|
`To utilize \`addedEndpoints\`, which allows provider/model selections alongside model specs, set \`modelSelect: true\` in the interface configuration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
\`\`\`yaml
|
||||||
|
interface:
|
||||||
|
modelSelect: true
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list || list.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const spec of list) {
|
||||||
|
const currentEndpoint = spec.preset?.endpoint as EModelEndpoint | undefined;
|
||||||
|
if (!currentEndpoint) {
|
||||||
|
logger.warn(
|
||||||
|
'A model spec is missing the `endpoint` field within its `preset`. Skipping model spec...',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (EModelEndpoint[currentEndpoint] && currentEndpoint !== EModelEndpoint.custom) {
|
||||||
|
modelSpecs.push(spec);
|
||||||
|
continue;
|
||||||
|
} else if (currentEndpoint === EModelEndpoint.custom) {
|
||||||
|
logger.warn(
|
||||||
|
`Model Spec with endpoint "${currentEndpoint}" is not supported. You must specify the name of the custom endpoint (case-sensitive, as defined in your config). Skipping model spec...`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedName = normalizeEndpointName(currentEndpoint);
|
||||||
|
const endpoint = customEndpoints.find(
|
||||||
|
(customEndpoint) => normalizedName === normalizeEndpointName(customEndpoint.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!endpoint) {
|
||||||
|
logger.warn(`Model spec with endpoint "${currentEndpoint}" was skipped: Endpoint not found in configuration. The \`endpoint\` value must exactly match either a system-defined endpoint or a custom endpoint defined by the user.
|
||||||
|
|
||||||
|
For more information, see the documentation at https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/model_specs#endpoint`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
modelSpecs.push({
|
||||||
|
...spec,
|
||||||
|
preset: {
|
||||||
|
...spec.preset,
|
||||||
|
endpoint: normalizedName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
..._modelSpecs,
|
||||||
|
list: modelSpecs,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
packages/data-schemas/src/app/turnstile.ts
Normal file
45
packages/data-schemas/src/app/turnstile.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import logger from '~/config/winston';
|
||||||
|
import { removeNullishValues } from 'librechat-data-provider';
|
||||||
|
import type { TCustomConfig, TConfigDefaults } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and maps the Cloudflare Turnstile configuration.
|
||||||
|
*
|
||||||
|
* Expected config structure:
|
||||||
|
*
|
||||||
|
* turnstile:
|
||||||
|
* siteKey: "your-site-key-here"
|
||||||
|
* options:
|
||||||
|
* language: "auto" // "auto" or an ISO 639-1 language code (e.g. en)
|
||||||
|
* size: "normal" // Options: "normal", "compact", "flexible", or "invisible"
|
||||||
|
*
|
||||||
|
* @param config - The loaded custom configuration.
|
||||||
|
* @param configDefaults - The custom configuration default values.
|
||||||
|
* @returns The mapped Turnstile configuration.
|
||||||
|
*/
|
||||||
|
export function loadTurnstileConfig(
|
||||||
|
config: Partial<TCustomConfig> | undefined,
|
||||||
|
configDefaults: TConfigDefaults,
|
||||||
|
): Partial<TCustomConfig['turnstile']> {
|
||||||
|
const { turnstile: customTurnstile } = config ?? {};
|
||||||
|
const { turnstile: defaults } = configDefaults;
|
||||||
|
|
||||||
|
const loadedTurnstile = removeNullishValues({
|
||||||
|
siteKey:
|
||||||
|
customTurnstile?.siteKey ?? (defaults as TCustomConfig['turnstile'] | undefined)?.siteKey,
|
||||||
|
options:
|
||||||
|
customTurnstile?.options ?? (defaults as TCustomConfig['turnstile'] | undefined)?.options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const enabled = Boolean(loadedTurnstile.siteKey);
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
logger.debug(
|
||||||
|
'Turnstile is ENABLED with configuration:\n' + JSON.stringify(loadedTurnstile, null, 2),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug('Turnstile is DISABLED (no siteKey provided).');
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadedTurnstile;
|
||||||
|
}
|
||||||
84
packages/data-schemas/src/app/web.ts
Normal file
84
packages/data-schemas/src/app/web.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { SafeSearchTypes } from 'librechat-data-provider';
|
||||||
|
import type { TCustomConfig } from 'librechat-data-provider';
|
||||||
|
import type { TWebSearchKeys, TWebSearchCategories } from '~/types/web';
|
||||||
|
|
||||||
|
export const webSearchAuth = {
|
||||||
|
providers: {
|
||||||
|
serper: {
|
||||||
|
serperApiKey: 1 as const,
|
||||||
|
},
|
||||||
|
searxng: {
|
||||||
|
searxngInstanceUrl: 1 as const,
|
||||||
|
/** Optional (0) */
|
||||||
|
searxngApiKey: 0 as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scrapers: {
|
||||||
|
firecrawl: {
|
||||||
|
firecrawlApiKey: 1 as const,
|
||||||
|
/** Optional (0) */
|
||||||
|
firecrawlApiUrl: 0 as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rerankers: {
|
||||||
|
jina: {
|
||||||
|
jinaApiKey: 1 as const,
|
||||||
|
/** Optional (0) */
|
||||||
|
jinaApiUrl: 0 as const,
|
||||||
|
},
|
||||||
|
cohere: { cohereApiKey: 1 as const },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all API keys from the webSearchAuth configuration object
|
||||||
|
*/
|
||||||
|
export function getWebSearchKeys(): TWebSearchKeys[] {
|
||||||
|
const keys: TWebSearchKeys[] = [];
|
||||||
|
|
||||||
|
// Iterate through each category (providers, scrapers, rerankers)
|
||||||
|
for (const category of Object.keys(webSearchAuth)) {
|
||||||
|
const categoryObj = webSearchAuth[category as TWebSearchCategories];
|
||||||
|
|
||||||
|
// Iterate through each service within the category
|
||||||
|
for (const service of Object.keys(categoryObj)) {
|
||||||
|
const serviceObj = categoryObj[service as keyof typeof categoryObj];
|
||||||
|
|
||||||
|
// Extract the API keys from the service
|
||||||
|
for (const key of Object.keys(serviceObj)) {
|
||||||
|
keys.push(key as TWebSearchKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const webSearchKeys: TWebSearchKeys[] = getWebSearchKeys();
|
||||||
|
|
||||||
|
export function loadWebSearchConfig(
|
||||||
|
config: TCustomConfig['webSearch'],
|
||||||
|
): TCustomConfig['webSearch'] {
|
||||||
|
const serperApiKey = config?.serperApiKey ?? '${SERPER_API_KEY}';
|
||||||
|
const searxngInstanceUrl = config?.searxngInstanceUrl ?? '${SEARXNG_INSTANCE_URL}';
|
||||||
|
const searxngApiKey = config?.searxngApiKey ?? '${SEARXNG_API_KEY}';
|
||||||
|
const firecrawlApiKey = config?.firecrawlApiKey ?? '${FIRECRAWL_API_KEY}';
|
||||||
|
const firecrawlApiUrl = config?.firecrawlApiUrl ?? '${FIRECRAWL_API_URL}';
|
||||||
|
const jinaApiKey = config?.jinaApiKey ?? '${JINA_API_KEY}';
|
||||||
|
const jinaApiUrl = config?.jinaApiUrl ?? '${JINA_API_URL}';
|
||||||
|
const cohereApiKey = config?.cohereApiKey ?? '${COHERE_API_KEY}';
|
||||||
|
const safeSearch = config?.safeSearch ?? SafeSearchTypes.MODERATE;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
safeSearch,
|
||||||
|
jinaApiKey,
|
||||||
|
jinaApiUrl,
|
||||||
|
cohereApiKey,
|
||||||
|
serperApiKey,
|
||||||
|
searxngInstanceUrl,
|
||||||
|
searxngApiKey,
|
||||||
|
firecrawlApiKey,
|
||||||
|
firecrawlApiUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './app';
|
||||||
export * from './common';
|
export * from './common';
|
||||||
export * from './crypto';
|
export * from './crypto';
|
||||||
export * from './schema';
|
export * from './schema';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import pluginAuthSchema, { IPluginAuth } from '~/schema/pluginAuth';
|
import pluginAuthSchema from '~/schema/pluginAuth';
|
||||||
|
import type { IPluginAuth } from '~/types/pluginAuth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates or returns the PluginAuth model using the provided mongoose instance and schema
|
* Creates or returns the PluginAuth model using the provided mongoose instance and schema
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import promptSchema, { IPrompt } from '~/schema/prompt';
|
import promptSchema from '~/schema/prompt';
|
||||||
|
import type { IPrompt } from '~/types/prompts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates or returns the Prompt model using the provided mongoose instance and schema
|
* Creates or returns the Prompt model using the provided mongoose instance and schema
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import promptGroupSchema, { IPromptGroupDocument } from '~/schema/promptGroup';
|
import promptGroupSchema from '~/schema/promptGroup';
|
||||||
|
import type { IPromptGroupDocument } from '~/types/prompts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates or returns the PromptGroup model using the provided mongoose instance and schema
|
* Creates or returns the PromptGroup model using the provided mongoose instance and schema
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,31 @@ import type {
|
||||||
TCustomEndpoints,
|
TCustomEndpoints,
|
||||||
TAssistantEndpoint,
|
TAssistantEndpoint,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import type { FunctionTool } from './tools';
|
|
||||||
|
export type JsonSchemaType = {
|
||||||
|
type: 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'array' | 'object';
|
||||||
|
enum?: string[];
|
||||||
|
items?: JsonSchemaType;
|
||||||
|
properties?: Record<string, JsonSchemaType>;
|
||||||
|
required?: string[];
|
||||||
|
description?: string;
|
||||||
|
additionalProperties?: boolean | JsonSchemaType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConvertJsonSchemaToZodOptions = {
|
||||||
|
allowEmptyObject?: boolean;
|
||||||
|
dropFields?: string[];
|
||||||
|
transformOneOfAnyOf?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FunctionTool {
|
||||||
|
type: 'function';
|
||||||
|
function: {
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
parameters: JsonSchemaType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application configuration object
|
* Application configuration object
|
||||||
|
|
@ -17,11 +41,11 @@ import type { FunctionTool } from './tools';
|
||||||
*/
|
*/
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
/** The main custom configuration */
|
/** The main custom configuration */
|
||||||
config: TCustomConfig;
|
config: Partial<TCustomConfig>;
|
||||||
/** OCR configuration */
|
/** OCR configuration */
|
||||||
ocr?: TCustomConfig['ocr'];
|
ocr?: TCustomConfig['ocr'];
|
||||||
/** File paths configuration */
|
/** File paths configuration */
|
||||||
paths: {
|
paths?: {
|
||||||
uploads: string;
|
uploads: string;
|
||||||
imageOutput: string;
|
imageOutput: string;
|
||||||
publicPath: string;
|
publicPath: string;
|
||||||
|
|
@ -34,7 +58,7 @@ export interface AppConfig {
|
||||||
/** File storage strategy ('local', 's3', 'firebase', 'azure_blob') */
|
/** File storage strategy ('local', 's3', 'firebase', 'azure_blob') */
|
||||||
fileStrategy: FileSources.local | FileSources.s3 | FileSources.firebase | FileSources.azure_blob;
|
fileStrategy: FileSources.local | FileSources.s3 | FileSources.firebase | FileSources.azure_blob;
|
||||||
/** File strategies configuration */
|
/** File strategies configuration */
|
||||||
fileStrategies: TCustomConfig['fileStrategies'];
|
fileStrategies?: TCustomConfig['fileStrategies'];
|
||||||
/** Registration configurations */
|
/** Registration configurations */
|
||||||
registration?: TCustomConfig['registration'];
|
registration?: TCustomConfig['registration'];
|
||||||
/** Actions configurations */
|
/** Actions configurations */
|
||||||
|
|
@ -48,9 +72,9 @@ export interface AppConfig {
|
||||||
/** Interface configuration */
|
/** Interface configuration */
|
||||||
interfaceConfig?: TCustomConfig['interface'];
|
interfaceConfig?: TCustomConfig['interface'];
|
||||||
/** Turnstile configuration */
|
/** Turnstile configuration */
|
||||||
turnstileConfig?: TCustomConfig['turnstile'];
|
turnstileConfig?: Partial<TCustomConfig['turnstile']>;
|
||||||
/** Balance configuration */
|
/** Balance configuration */
|
||||||
balance?: TCustomConfig['balance'];
|
balance?: Partial<TCustomConfig['balance']>;
|
||||||
/** Transactions configuration */
|
/** Transactions configuration */
|
||||||
transactions?: TCustomConfig['transactions'];
|
transactions?: TCustomConfig['transactions'];
|
||||||
/** Speech configuration */
|
/** Speech configuration */
|
||||||
|
|
@ -67,26 +91,26 @@ export interface AppConfig {
|
||||||
availableTools?: Record<string, FunctionTool>;
|
availableTools?: Record<string, FunctionTool>;
|
||||||
endpoints?: {
|
endpoints?: {
|
||||||
/** OpenAI endpoint configuration */
|
/** OpenAI endpoint configuration */
|
||||||
openAI?: TEndpoint;
|
openAI?: Partial<TEndpoint>;
|
||||||
/** Google endpoint configuration */
|
/** Google endpoint configuration */
|
||||||
google?: TEndpoint;
|
google?: Partial<TEndpoint>;
|
||||||
/** Bedrock endpoint configuration */
|
/** Bedrock endpoint configuration */
|
||||||
bedrock?: TEndpoint;
|
bedrock?: Partial<TEndpoint>;
|
||||||
/** Anthropic endpoint configuration */
|
/** Anthropic endpoint configuration */
|
||||||
anthropic?: TEndpoint;
|
anthropic?: Partial<TEndpoint>;
|
||||||
/** GPT plugins endpoint configuration */
|
/** GPT plugins endpoint configuration */
|
||||||
gptPlugins?: TEndpoint;
|
gptPlugins?: Partial<TEndpoint>;
|
||||||
/** Azure OpenAI endpoint configuration */
|
/** Azure OpenAI endpoint configuration */
|
||||||
azureOpenAI?: TAzureConfig;
|
azureOpenAI?: TAzureConfig;
|
||||||
/** Assistants endpoint configuration */
|
/** Assistants endpoint configuration */
|
||||||
assistants?: TAssistantEndpoint;
|
assistants?: Partial<TAssistantEndpoint>;
|
||||||
/** Azure assistants endpoint configuration */
|
/** Azure assistants endpoint configuration */
|
||||||
azureAssistants?: TAssistantEndpoint;
|
azureAssistants?: Partial<TAssistantEndpoint>;
|
||||||
/** Agents endpoint configuration */
|
/** Agents endpoint configuration */
|
||||||
[EModelEndpoint.agents]?: TAgentsEndpoint;
|
[EModelEndpoint.agents]?: Partial<TAgentsEndpoint>;
|
||||||
/** Custom endpoints configuration */
|
/** Custom endpoints configuration */
|
||||||
[EModelEndpoint.custom]?: TCustomEndpoints;
|
[EModelEndpoint.custom]?: TCustomEndpoints;
|
||||||
/** Global endpoint configuration */
|
/** Global endpoint configuration */
|
||||||
all?: TEndpoint;
|
all?: Partial<TEndpoint>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Types } from 'mongoose';
|
import type { Types } from 'mongoose';
|
||||||
|
|
||||||
export type ObjectId = Types.ObjectId;
|
export type ObjectId = Types.ObjectId;
|
||||||
|
export * from './app';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './token';
|
export * from './token';
|
||||||
export * from './convo';
|
export * from './convo';
|
||||||
|
|
@ -24,3 +25,5 @@ export * from './prompts';
|
||||||
export * from './accessRole';
|
export * from './accessRole';
|
||||||
export * from './aclEntry';
|
export * from './aclEntry';
|
||||||
export * from './group';
|
export * from './group';
|
||||||
|
/* Web */
|
||||||
|
export * from './web';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||||
|
import type { DeepPartial } from 'librechat-data-provider';
|
||||||
import type { Document } from 'mongoose';
|
import type { Document } from 'mongoose';
|
||||||
import { CursorPaginationParams } from '~/common';
|
import { CursorPaginationParams } from '~/common';
|
||||||
|
|
||||||
|
|
@ -54,9 +55,6 @@ export interface IRole extends Document {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RolePermissions = IRole['permissions'];
|
export type RolePermissions = IRole['permissions'];
|
||||||
type DeepPartial<T> = {
|
|
||||||
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
||||||
};
|
|
||||||
export type RolePermissionsInput = DeepPartial<RolePermissions>;
|
export type RolePermissionsInput = DeepPartial<RolePermissions>;
|
||||||
|
|
||||||
export interface CreateRoleRequest {
|
export interface CreateRoleRequest {
|
||||||
|
|
|
||||||
16
packages/data-schemas/src/types/web.ts
Normal file
16
packages/data-schemas/src/types/web.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { SearchCategories } from 'librechat-data-provider';
|
||||||
|
|
||||||
|
export type TWebSearchKeys =
|
||||||
|
| 'serperApiKey'
|
||||||
|
| 'searxngInstanceUrl'
|
||||||
|
| 'searxngApiKey'
|
||||||
|
| 'firecrawlApiKey'
|
||||||
|
| 'firecrawlApiUrl'
|
||||||
|
| 'jinaApiKey'
|
||||||
|
| 'jinaApiUrl'
|
||||||
|
| 'cohereApiKey';
|
||||||
|
|
||||||
|
export type TWebSearchCategories =
|
||||||
|
| SearchCategories.PROVIDERS
|
||||||
|
| SearchCategories.SCRAPERS
|
||||||
|
| SearchCategories.RERANKERS;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue