Merge branch 'main' into feature/entra-id-azure-integration

This commit is contained in:
victorbjor 2025-10-07 08:34:44 +02:00 committed by GitHub
commit be58d8e4f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
244 changed files with 6722 additions and 3399 deletions

View file

@ -1,7 +1,7 @@
const { logger } = require('@librechat/data-schemas');
const { CacheKeys } = require('librechat-data-provider');
const { loadDefaultModels, loadConfigModels } = require('~/server/services/Config');
const { getLogStores } = require('~/cache');
const { logger } = require('~/config');
/**
* @param {ServerRequest} req

View file

@ -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 {
webSearchKeys,
MCPOAuthHandler,
MCPTokenStorage,
normalizeHttpError,

View file

@ -257,7 +257,7 @@ class AgentClient extends BaseClient {
};
}
const files = await this.addImageURLs(
const files = await this.processAttachments(
orderedMessages[orderedMessages.length - 1],
attachments,
);
@ -1116,8 +1116,8 @@ class AgentClient extends BaseClient {
appConfig.endpoints?.[endpoint] ??
titleProviderConfig.customEndpointConfig;
if (!endpointConfig) {
logger.warn(
'[api/server/controllers/agents/client.js #titleConvo] Error getting endpoint config',
logger.debug(
`[api/server/controllers/agents/client.js #titleConvo] No endpoint config for "${endpoint}"`,
);
}

View file

@ -1,6 +1,6 @@
const { logger } = require('@librechat/data-schemas');
const { generate2FATempToken } = require('~/server/services/twoFactorService');
const { setAuthTokens } = require('~/server/services/AuthService');
const { logger } = require('~/config');
const loginController = async (req, res) => {
try {

View file

@ -1,8 +1,8 @@
const cookies = require('cookie');
const { getOpenIdConfig } = require('~/strategies');
const { isEnabled } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { logoutUser } = require('~/server/services/AuthService');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
const { getOpenIdConfig } = require('~/strategies');
const logoutController = async (req, res) => {
const refreshToken = req.headers.cookie ? cookies.parse(req.headers.cookie).refreshToken : null;

View file

@ -10,7 +10,12 @@ const compression = require('compression');
const cookieParser = require('cookie-parser');
const { logger } = require('@librechat/data-schemas');
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 initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager');
const createValidateImageRequest = require('./middleware/validateImageRequest');
@ -49,9 +54,11 @@ const startServer = async () => {
app.set('trust proxy', trusted_proxy);
await seedDatabase();
const appConfig = await getAppConfig();
initializeFileStorage(appConfig);
await performStartupChecks(appConfig);
await updateInterfacePermissions(appConfig);
const indexPath = path.join(appConfig.paths.dist, 'index.html');
let indexHTML = fs.readFileSync(indexPath, 'utf8');

View file

@ -1,6 +1,6 @@
const { isEnabled } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { SystemRoles } = require('librechat-data-provider');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
/**
* Checks if the user can delete their account

View file

@ -1,9 +1,9 @@
const { Keyv } = require('keyv');
const uap = require('ua-parser-js');
const { logger } = require('@librechat/data-schemas');
const { isEnabled, keyvMongo } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { isEnabled, removePorts } = require('~/server/utils');
const keyvMongo = require('~/cache/keyvMongo');
const { removePorts } = require('~/server/utils');
const denyRequest = require('./denyRequest');
const { getLogStores } = require('~/cache');
const { findUser } = require('~/models');

View file

@ -1,6 +1,6 @@
const { logger } = require('@librechat/data-schemas');
const { PrincipalType, PermissionTypes, Permissions } = require('librechat-data-provider');
const { getRoleByName } = require('~/models/Role');
const { logger } = require('~/config');
/**
* Middleware to check if user has permission to access people picker functionality

View file

@ -1,10 +1,11 @@
const { logger } = require('@librechat/data-schemas');
const { PrincipalType, PermissionTypes, Permissions } = require('librechat-data-provider');
const { checkPeoplePickerAccess } = require('./checkPeoplePickerAccess');
const { getRoleByName } = require('~/models/Role');
const { logger } = require('~/config');
jest.mock('~/models/Role');
jest.mock('~/config', () => ({
jest.mock('@librechat/data-schemas', () => ({
...jest.requireActual('@librechat/data-schemas'),
logger: {
error: jest.fn(),
},

View file

@ -1,7 +1,7 @@
const { isEnabled } = require('@librechat/api');
const { Time, CacheKeys, ViolationTypes } = require('librechat-data-provider');
const clearPendingReq = require('~/cache/clearPendingReq');
const { logViolation, getLogStores } = require('~/cache');
const { isEnabled } = require('~/server/utils');
const denyRequest = require('./denyRequest');
const {

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache } = require('~/cache/cacheFactory');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache } = require('~/cache/cacheFactory');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {

View file

@ -1,7 +1,7 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const { limiterCache } = require('~/cache/cacheFactory');
const { logViolation } = require('~/cache');
const { LOGIN_WINDOW = 5, LOGIN_MAX = 7, LOGIN_VIOLATION_SCORE: score } = process.env;

View file

@ -1,7 +1,7 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const denyRequest = require('~/server/middleware/denyRequest');
const { limiterCache } = require('~/cache/cacheFactory');
const { logViolation } = require('~/cache');
const {

View file

@ -1,7 +1,7 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const { limiterCache } = require('~/cache/cacheFactory');
const { logViolation } = require('~/cache');
const { REGISTER_WINDOW = 60, REGISTER_MAX = 5, REGISTRATION_VIOLATION_SCORE: score } = process.env;

View file

@ -1,7 +1,7 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const { limiterCache } = require('~/cache/cacheFactory');
const { logViolation } = require('~/cache');
const {

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache } = require('~/cache/cacheFactory');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache } = require('~/cache/cacheFactory');
const logViolation = require('~/cache/logViolation');
const { TOOL_CALL_VIOLATION_SCORE: score } = process.env;

View file

@ -1,7 +1,7 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const logViolation = require('~/cache/logViolation');
const { limiterCache } = require('~/cache/cacheFactory');
const getEnvironmentVariables = () => {
const TTS_IP_MAX = parseInt(process.env.TTS_IP_MAX) || 100;

View file

@ -1,6 +1,6 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { limiterCache } = require('~/cache/cacheFactory');
const logViolation = require('~/cache/logViolation');
const getEnvironmentVariables = () => {

View file

@ -1,7 +1,7 @@
const rateLimit = require('express-rate-limit');
const { limiterCache } = require('@librechat/api');
const { ViolationTypes } = require('librechat-data-provider');
const { removePorts } = require('~/server/utils');
const { limiterCache } = require('~/cache/cacheFactory');
const { logViolation } = require('~/cache');
const {

View file

@ -1,4 +1,4 @@
const { logger } = require('~/config');
const { logger } = require('@librechat/data-schemas');
/**
* Middleware to log Forwarded Headers

View file

@ -1,8 +1,8 @@
const axios = require('axios');
const { isEnabled } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { ErrorTypes } = require('librechat-data-provider');
const { isEnabled } = require('~/server/utils');
const denyRequest = require('./denyRequest');
const { logger } = require('~/config');
async function moderateText(req, res, next) {
if (!isEnabled(process.env.OPENAI_MODERATION)) {

View file

@ -1,6 +1,6 @@
const cookies = require('cookie');
const { isEnabled } = require('~/server/utils');
const passport = require('passport');
const { isEnabled } = require('@librechat/api');
// This middleware does not require authentication,
// but if the user is authenticated, it will set the user object.

View file

@ -1,6 +1,6 @@
const passport = require('passport');
const cookies = require('cookie');
const { isEnabled } = require('~/server/utils');
const passport = require('passport');
const { isEnabled } = require('@librechat/api');
/**
* Custom Middleware to handle JWT authentication, with support for OpenID token reuse

View file

@ -1,5 +1,5 @@
const passport = require('passport');
const { logger } = require('~/config');
const { logger } = require('@librechat/data-schemas');
const requireLocalAuth = (req, res, next) => {
passport.authenticate('local', (err, user, info) => {

View file

@ -1,5 +1,5 @@
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
const { isEnabled } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
function validatePasswordReset(req, res, next) {
if (isEnabled(process.env.ALLOW_PASSWORD_RESET)) {

View file

@ -1,4 +1,4 @@
const { isEnabled } = require('~/server/utils');
const { isEnabled } = require('@librechat/api');
function validateRegistration(req, res, next) {
if (req.invite) {

View file

@ -1,10 +1,13 @@
const request = require('supertest');
const express = require('express');
const request = require('supertest');
const { isEnabled } = require('@librechat/api');
const { getLdapConfig } = require('~/server/services/Config/ldap');
const { isEnabled } = require('~/server/utils');
jest.mock('~/server/services/Config/ldap');
jest.mock('~/server/utils');
jest.mock('@librechat/api', () => ({
...jest.requireActual('@librechat/api'),
isEnabled: jest.fn(),
}));
const app = express();

View file

@ -1,4 +1,5 @@
const express = require('express');
const { isEnabled } = require('@librechat/api');
const {
uaParser,
checkBan,
@ -8,7 +9,6 @@ const {
concurrentLimiter,
messageUserLimiter,
} = require('~/server/middleware');
const { isEnabled } = require('~/server/utils');
const { v1 } = require('./v1');
const chat = require('./chat');

View file

@ -156,7 +156,7 @@ router.get('/', async function (req, res) {
if (
webSearchConfig != null &&
(webSearchConfig.searchProvider ||
webSearchConfig.scraperType ||
webSearchConfig.scraperProvider ||
webSearchConfig.rerankerType)
) {
payload.webSearch = {};
@ -165,8 +165,8 @@ router.get('/', async function (req, res) {
if (webSearchConfig?.searchProvider) {
payload.webSearch.searchProvider = webSearchConfig.searchProvider;
}
if (webSearchConfig?.scraperType) {
payload.webSearch.scraperType = webSearchConfig.scraperType;
if (webSearchConfig?.scraperProvider) {
payload.webSearch.scraperProvider = webSearchConfig.scraperProvider;
}
if (webSearchConfig?.rerankerType) {
payload.webSearch.rerankerType = webSearchConfig.rerankerType;

View file

@ -1,19 +1,19 @@
const { isEnabled } = require('@librechat/api');
const { EModelEndpoint } = require('librechat-data-provider');
const {
validateConvoAccess,
messageUserLimiter,
concurrentLimiter,
messageIpLimiter,
requireJwtAuth,
checkBan,
uaParser,
} = require('~/server/middleware');
const anthropic = require('./anthropic');
const express = require('express');
const openAI = require('./openAI');
const custom = require('./custom');
const google = require('./google');
const anthropic = require('./anthropic');
const { isEnabled } = require('~/server/utils');
const { EModelEndpoint } = require('librechat-data-provider');
const {
checkBan,
uaParser,
requireJwtAuth,
messageIpLimiter,
concurrentLimiter,
messageUserLimiter,
validateConvoAccess,
} = require('~/server/middleware');
const { LIMIT_CONCURRENT_MESSAGES, LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {};

View file

@ -1,6 +1,7 @@
const fs = require('fs').promises;
const express = require('express');
const { EnvVar } = require('@librechat/agents');
const { logger } = require('@librechat/data-schemas');
const {
Time,
isUUID,
@ -30,7 +31,6 @@ const { cleanFileName } = require('~/server/utils/files');
const { getAssistant } = require('~/models/Assistant');
const { getAgent } = require('~/models/Agent');
const { getLogStores } = require('~/cache');
const { logger } = require('~/config');
const { Readable } = require('stream');
const router = express.Router();

View file

@ -1,9 +1,9 @@
const multer = require('multer');
const express = require('express');
const { logger } = require('@librechat/data-schemas');
const { CacheKeys } = require('librechat-data-provider');
const { getVoices, streamAudio, textToSpeech } = require('~/server/services/Files/Audio');
const { getLogStores } = require('~/cache');
const { logger } = require('~/config');
const router = express.Router();
const upload = multer();

View file

@ -1,8 +1,8 @@
const express = require('express');
const crypto = require('crypto');
const express = require('express');
const { logger } = require('@librechat/data-schemas');
const { getPresets, savePreset, deletePresets } = require('~/models');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const { logger } = require('~/config');
const router = express.Router();
router.use(requireJwtAuth);

View file

@ -1,7 +1,7 @@
const express = require('express');
const { MeiliSearch } = require('meilisearch');
const { isEnabled } = require('@librechat/api');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const { isEnabled } = require('~/server/utils');
const router = express.Router();

View file

@ -1,7 +1,7 @@
const express = require('express');
const { isEnabled } = require('@librechat/api');
const staticCache = require('../utils/staticCache');
const paths = require('~/config/paths');
const { isEnabled } = require('~/server/utils');
const skipGzipScan = !isEnabled(process.env.ENABLE_IMAGE_OUTPUT_GZIP_SCAN);

View file

@ -1,8 +1,9 @@
const express = require('express');
const router = express.Router();
const { logger } = require('@librechat/data-schemas');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const { countTokens } = require('~/server/utils');
const { logger } = require('~/config');
const router = express.Router();
router.post('/', requireJwtAuth, async (req, res) => {
try {

View file

@ -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,
}),
}),
}),
);
});
});

View file

@ -1,125 +0,0 @@
const { FileSources, EModelEndpoint, getConfigDefaults } = require('librechat-data-provider');
const {
isEnabled,
loadOCRConfig,
loadMemoryConfig,
agentsConfigSetup,
loadWebSearchConfig,
loadDefaultInterface,
} = require('@librechat/api');
const {
checkWebSearchConfig,
checkVariables,
checkHealth,
checkConfig,
} = require('./start/checks');
const { initializeAzureBlobService } = require('./Files/Azure/initialize');
const { initializeFirebase } = require('./Files/Firebase/initialize');
const handleRateLimits = require('./Config/handleRateLimits');
const loadCustomConfig = require('./Config/loadCustomConfig');
const { loadTurnstileConfig } = require('./start/turnstile');
const { processModelSpecs } = require('./start/modelSpecs');
const { initializeS3 } = require('./Files/S3/initialize');
const { loadAndFormatTools } = require('./start/tools');
const { loadEndpoints } = require('./start/endpoints');
const paths = require('~/config/paths');
/**
* Loads custom config and initializes app-wide variables.
* @function AppService
*/
const AppService = async () => {
/** @type {TCustomConfig} */
const config = (await loadCustomConfig()) ?? {};
const configDefaults = getConfigDefaults();
const ocr = loadOCRConfig(config.ocr);
const webSearch = loadWebSearchConfig(config.webSearch);
checkWebSearchConfig(webSearch);
const memory = loadMemoryConfig(config.memory);
const filteredTools = config.filteredTools;
const includedTools = config.includedTools;
const fileStrategy = config.fileStrategy ?? configDefaults.fileStrategy;
const startBalance = process.env.START_BALANCE;
const balance = config.balance ?? {
enabled: isEnabled(process.env.CHECK_BALANCE),
startBalance: startBalance ? parseInt(startBalance, 10) : undefined,
};
const transactions = config.transactions ?? configDefaults.transactions;
const imageOutputType = config?.imageOutputType ?? configDefaults.imageOutputType;
process.env.CDN_PROVIDER = fileStrategy;
checkVariables();
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 registration = config.registration ?? configDefaults.registration;
const interfaceConfig = await loadDefaultInterface({ config, configDefaults });
const turnstileConfig = loadTurnstileConfig(config, configDefaults);
const speech = config.speech;
const defaultConfig = {
ocr,
paths,
config,
memory,
speech,
balance,
transactions,
mcpConfig,
webSearch,
fileStrategy,
registration,
filteredTools,
includedTools,
availableTools,
imageOutputType,
interfaceConfig,
turnstileConfig,
fileStrategies: config.fileStrategies,
};
const agentsDefaults = agentsConfigSetup(config);
if (!Object.keys(config).length) {
const appConfig = {
...defaultConfig,
endpoints: {
[EModelEndpoint.agents]: agentsDefaults,
},
};
return appConfig;
}
checkConfig(config);
handleRateLimits(config?.rateLimits);
const loadedEndpoints = loadEndpoints(config, agentsDefaults);
const appConfig = {
...defaultConfig,
fileConfig: config?.fileConfig,
secureImageLinks: config?.secureImageLinks,
modelSpecs: processModelSpecs(config?.endpoints, config.modelSpecs, interfaceConfig),
endpoints: loadedEndpoints,
};
return appConfig;
};
module.exports = AppService;

File diff suppressed because it is too large Load diff

View file

@ -129,7 +129,7 @@ const verifyEmail = async (req) => {
return { message: 'Email already verified', status: 'success' };
}
let emailVerificationData = await findToken({ email: decodedEmail });
let emailVerificationData = await findToken({ email: decodedEmail }, { sort: { createdAt: -1 } });
if (!emailVerificationData) {
logger.warn(`[verifyEmail] [No email verification data found] [Email: ${decodedEmail}]`);
@ -319,9 +319,12 @@ const requestPasswordReset = async (req) => {
* @returns
*/
const resetPassword = async (userId, token, password) => {
let passwordResetToken = await findToken({
userId,
});
let passwordResetToken = await findToken(
{
userId,
},
{ sort: { createdAt: -1 } },
);
if (!passwordResetToken) {
return new Error('Invalid or expired password reset token');

View file

@ -1,11 +1,25 @@
const { logger } = require('@librechat/data-schemas');
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 getLogStores = require('~/cache/getLogStores');
const paths = require('~/config/paths');
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
* @param {Object} [options]
@ -29,7 +43,7 @@ async function getAppConfig(options = {}) {
let baseConfig = await cache.get(BASE_CONFIG_KEY);
if (!baseConfig) {
logger.info('[getAppConfig] App configuration not initialized. Initializing AppService...');
baseConfig = await AppService();
baseConfig = await loadBaseConfig();
if (!baseConfig) {
throw new Error('Failed to initialize app configuration through AppService.');

View file

@ -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;

View file

@ -1,4 +1,4 @@
const { isEnabled } = require('~/server/utils');
const { isEnabled } = require('@librechat/api');
/** @returns {TStartupConfig['ldap'] | undefined} */
const getLdapConfig = () => {

View file

@ -5,14 +5,12 @@ const keyBy = require('lodash/keyBy');
const { loadYaml } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const {
CacheKeys,
configSchema,
paramSettings,
EImageOutputType,
agentParamSettings,
validateSettingDefinitions,
} = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const projectRoot = path.resolve(__dirname, '..', '..', '..', '..');
const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml');
@ -119,7 +117,6 @@ https://www.librechat.ai/docs/configuration/stt_tts`);
.filter((endpoint) => endpoint.customParams)
.forEach((endpoint) => parseCustomParams(endpoint.name, endpoint.customParams));
if (result.data.modelSpecs) {
customConfig.modelSpecs = result.data.modelSpecs;
}

View file

@ -1,6 +1,6 @@
const { isEnabled } = require('@librechat/api');
const { CacheKeys } = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const { isEnabled } = require('~/server/utils');
const { saveConvo } = require('~/models');
const addTitle = async (req, { text, response, client }) => {

View file

@ -1,7 +1,7 @@
const { isEnabled } = require('@librechat/api');
const { CacheKeys } = require('librechat-data-provider');
const { saveConvo } = require('~/models/Conversation');
const getLogStores = require('~/cache/getLogStores');
const { isEnabled } = require('~/server/utils');
const addTitle = async (req, { text, responseText, conversationId, client }) => {
const { TITLE_CONVO = 'true' } = process.env ?? {};

View file

@ -1,6 +1,6 @@
const { isEnabled } = require('@librechat/api');
const { CacheKeys } = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const { isEnabled } = require('~/server/utils');
const { saveConvo } = require('~/models');
const addTitle = async (req, { text, response, client }) => {

View file

@ -3,8 +3,8 @@ const path = require('path');
const mime = require('mime');
const axios = require('axios');
const fetch = require('node-fetch');
const { logger } = require('~/config');
const { getAzureContainerClient } = require('./initialize');
const { logger } = require('@librechat/data-schemas');
const { getAzureContainerClient } = require('@librechat/api');
const defaultBasePath = 'images';
const { AZURE_STORAGE_PUBLIC_ACCESS = 'true', AZURE_CONTAINER_NAME = 'files' } = process.env;
@ -30,7 +30,7 @@ async function saveBufferToAzure({
containerName,
}) {
try {
const containerClient = getAzureContainerClient(containerName);
const containerClient = await getAzureContainerClient(containerName);
const access = AZURE_STORAGE_PUBLIC_ACCESS?.toLowerCase() === 'true' ? 'blob' : undefined;
// Create the container if it doesn't exist. This is done per operation.
await containerClient.createIfNotExists({ access });
@ -84,7 +84,7 @@ async function saveURLToAzure({
*/
async function getAzureURL({ fileName, basePath = defaultBasePath, userId, containerName }) {
try {
const containerClient = getAzureContainerClient(containerName);
const containerClient = await getAzureContainerClient(containerName);
const blobPath = userId ? `${basePath}/${userId}/${fileName}` : `${basePath}/${fileName}`;
const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
return blockBlobClient.url;
@ -103,7 +103,7 @@ async function getAzureURL({ fileName, basePath = defaultBasePath, userId, conta
*/
async function deleteFileFromAzure(req, file) {
try {
const containerClient = getAzureContainerClient(AZURE_CONTAINER_NAME);
const containerClient = await getAzureContainerClient(AZURE_CONTAINER_NAME);
const blobPath = file.filepath.split(`${AZURE_CONTAINER_NAME}/`)[1];
if (!blobPath.includes(req.user.id)) {
throw new Error('User ID not found in blob path');
@ -140,7 +140,7 @@ async function streamFileToAzure({
containerName,
}) {
try {
const containerClient = getAzureContainerClient(containerName);
const containerClient = await getAzureContainerClient(containerName);
const access = AZURE_STORAGE_PUBLIC_ACCESS?.toLowerCase() === 'true' ? 'blob' : undefined;
// Create the container if it doesn't exist

View file

@ -1,9 +1,7 @@
const crud = require('./crud');
const images = require('./images');
const initialize = require('./initialize');
module.exports = {
...crud,
...images,
...initialize,
};

View file

@ -1,55 +0,0 @@
const { BlobServiceClient } = require('@azure/storage-blob');
const { logger } = require('~/config');
let blobServiceClient = null;
let azureWarningLogged = false;
/**
* Initializes the Azure Blob Service client.
* 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.
* 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.
*/
const initializeAzureBlobService = () => {
if (blobServiceClient) {
return blobServiceClient;
}
const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING;
if (connectionString) {
blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
logger.info('Azure Blob Service initialized using connection string');
} else {
const { DefaultAzureCredential } = require('@azure/identity');
const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME;
if (!accountName) {
if (!azureWarningLogged) {
logger.error(
'[initializeAzureBlobService] Azure Blob Service not initialized. Connection string missing and AZURE_STORAGE_ACCOUNT_NAME not provided.',
);
azureWarningLogged = true;
}
return null;
}
const url = `https://${accountName}.blob.core.windows.net`;
const credential = new DefaultAzureCredential();
blobServiceClient = new BlobServiceClient(url, credential);
logger.info('Azure Blob Service initialized using Managed Identity');
}
return blobServiceClient;
};
/**
* Retrieves the Azure ContainerClient for the given container name.
* @param {string} [containerName=process.env.AZURE_CONTAINER_NAME || 'files'] - The container name.
* @returns {ContainerClient|null} The Azure ContainerClient.
*/
const getAzureContainerClient = (containerName = process.env.AZURE_CONTAINER_NAME || 'files') => {
const serviceClient = initializeAzureBlobService();
return serviceClient ? serviceClient.getContainerClient(containerName) : null;
};
module.exports = {
initializeAzureBlobService,
getAzureContainerClient,
};

View file

@ -3,9 +3,9 @@ const path = require('path');
const axios = require('axios');
const fetch = require('node-fetch');
const { logger } = require('@librechat/data-schemas');
const { getFirebaseStorage } = require('@librechat/api');
const { ref, uploadBytes, getDownloadURL, deleteObject } = require('firebase/storage');
const { getBufferMetadata } = require('~/server/utils');
const { getFirebaseStorage } = require('./initialize');
/**
* Deletes a file from Firebase Storage.

View file

@ -1,9 +1,7 @@
const crud = require('./crud');
const images = require('./images');
const initialize = require('./initialize');
module.exports = {
...crud,
...images,
...initialize,
};

View file

@ -1,39 +0,0 @@
const firebase = require('firebase/app');
const { getStorage } = require('firebase/storage');
const { logger } = require('~/config');
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 };

View file

@ -4,6 +4,7 @@ const axios = require('axios');
const { logger } = require('@librechat/data-schemas');
const { EModelEndpoint } = require('librechat-data-provider');
const { generateShortLivedToken } = require('@librechat/api');
const { resizeImageBuffer } = require('~/server/services/Files/images/resize');
const { getBufferMetadata } = require('~/server/utils');
const paths = require('~/config/paths');
@ -286,7 +287,18 @@ async function uploadLocalFile({ req, file, file_id }) {
await fs.promises.writeFile(newPath, inputBuffer);
const filepath = path.posix.join('/', 'uploads', req.user.id, path.basename(newPath));
return { filepath, bytes };
let height, width;
if (file.mimetype && file.mimetype.startsWith('image/')) {
try {
const { width: imgWidth, height: imgHeight } = await resizeImageBuffer(inputBuffer, 'high');
height = imgHeight;
width = imgWidth;
} catch (error) {
logger.warn('[uploadLocalFile] Could not get image dimensions:', error.message);
}
}
return { filepath, bytes, height, width };
}
/**

View file

@ -1,7 +1,7 @@
const fs = require('fs');
const { sleep } = require('@librechat/agents');
const { logger } = require('@librechat/data-schemas');
const { FilePurpose } = require('librechat-data-provider');
const { sleep } = require('~/server/utils');
const { logger } = require('~/config');
/**
* Uploads a file that can be used across various OpenAI services.

View file

@ -1,15 +1,15 @@
const fs = require('fs');
const fetch = require('node-fetch');
const { initializeS3 } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { FileSources } = require('librechat-data-provider');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const {
PutObjectCommand,
GetObjectCommand,
HeadObjectCommand,
DeleteObjectCommand,
} = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const { initializeS3 } = require('./initialize');
const { logger } = require('~/config');
const bucketName = process.env.AWS_BUCKET_NAME;
const defaultBasePath = 'images';

View file

@ -1,9 +1,7 @@
const crud = require('./crud');
const images = require('./images');
const initialize = require('./initialize');
module.exports = {
...crud,
...images,
...initialize,
};

View file

@ -1,53 +0,0 @@
const { S3Client } = require('@aws-sdk/client-s3');
const { logger } = require('~/config');
let s3 = null;
/**
* Initializes and returns an instance of the AWS S3 client.
*
* If AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are provided, they will be used.
* Otherwise, the AWS SDK's default credentials chain (including IRSA) is used.
*
* 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.
*/
const initializeS3 = () => {
if (s3) {
return s3;
}
const region = process.env.AWS_REGION;
if (!region) {
logger.error('[initializeS3] AWS_REGION is not set. Cannot initialize S3.');
return null;
}
// Read the custom endpoint if provided.
const endpoint = process.env.AWS_ENDPOINT_URL;
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
const config = {
region,
// Conditionally add the endpoint if it is provided
...(endpoint ? { endpoint } : {}),
};
if (accessKeyId && secretAccessKey) {
s3 = new S3Client({
...config,
credentials: { accessKeyId, secretAccessKey },
});
logger.info('[initializeS3] S3 initialized with provided credentials.');
} else {
// When using IRSA, credentials are automatically provided via the IAM Role attached to the ServiceAccount.
s3 = new S3Client(config);
logger.info('[initializeS3] S3 initialized using default credentials (IRSA).');
}
return s3;
};
module.exports = { initializeS3 };

View file

@ -1,9 +1,9 @@
const sharp = require('sharp');
const fs = require('fs').promises;
const fetch = require('node-fetch');
const { logger } = require('@librechat/data-schemas');
const { EImageOutputType } = require('librechat-data-provider');
const { resizeAndConvert } = require('./resize');
const { logger } = require('~/config');
/**
* Uploads an avatar image for a user. This function can handle various types of input (URL, Buffer, or File object),

View file

@ -1,9 +1,9 @@
const fs = require('fs');
const path = require('path');
const sharp = require('sharp');
const { logger } = require('@librechat/data-schemas');
const { getStrategyFunctions } = require('../strategies');
const { resizeImageBuffer } = require('./resize');
const { logger } = require('~/config');
/**
* Converts an image file or buffer to target output type with specified resolution.

View file

@ -522,11 +522,6 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
}
const isImage = file.mimetype.startsWith('image');
if (!isImage && !tool_resource) {
/** Note: this needs to be removed when we can support files to providers */
throw new Error('No tool resource provided for non-image agent file upload');
}
let fileInfoMetadata;
const entity_id = messageAttachment === true ? undefined : agent_id;
const basePath = mime.getType(file.originalname)?.startsWith('image') ? 'images' : 'uploads';

View file

@ -1,8 +1,8 @@
const { getOpenIdConfig } = require('~/strategies/openidStrategy');
const { logger } = require('~/config');
const { CacheKeys } = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const client = require('openid-client');
const { logger } = require('@librechat/data-schemas');
const { CacheKeys } = require('librechat-data-provider');
const { getOpenIdConfig } = require('~/strategies/openidStrategy');
const getLogStores = require('~/cache/getLogStores');
/**
* Get Microsoft Graph API token using existing token exchange mechanism

View file

@ -450,7 +450,7 @@ async function getMCPSetupData(userId) {
logger.error(`[MCP][User: ${userId}] Error getting app connections:`, error);
}
const userConnections = mcpManager.getUserConnections(userId) || new Map();
const oauthServers = mcpManager.getOAuthServers() || new Set();
const oauthServers = mcpManager.getOAuthServers();
return {
mcpConfig,

View file

@ -170,7 +170,7 @@ describe('tests for the new helper functions used by the MCP connection status e
const mockMCPManager = {
appConnections: { getAll: jest.fn(() => null) },
getUserConnections: jest.fn(() => null),
getOAuthServers: jest.fn(() => null),
getOAuthServers: jest.fn(() => new Set()),
};
mockGetMCPManager.mockReturnValue(mockMCPManager);

View file

@ -1,5 +1,5 @@
const { logger } = require('@librechat/data-schemas');
const { ToolCallTypes } = require('librechat-data-provider');
const { logger } = require('~/config');
/**
* @typedef {import('openai').OpenAI} OpenAI

View file

@ -1,9 +1,9 @@
const { sleep } = require('@librechat/agents');
const { logger } = require('@librechat/data-schemas');
const { RunStatus, defaultOrderQuery, CacheKeys } = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const { retrieveRun } = require('./methods');
const { sleep } = require('~/server/utils');
const RunManager = require('./RunManager');
const { logger } = require('~/config');
async function withTimeout(promise, timeoutMs, timeoutMessage) {
let timeoutHandle;

View file

@ -1,5 +1,6 @@
const { logger } = require('~/config');
const { logger } = require('@librechat/data-schemas');
const { deleteNullOrEmptyConversations } = require('~/models/Conversation');
const cleanup = async () => {
try {
await deleteNullOrEmptyConversations();

View file

@ -1,63 +0,0 @@
const { logger } = require('@librechat/data-schemas');
const {
Capabilities,
assistantEndpointSchema,
defaultAssistantsVersion,
} = require('librechat-data-provider');
/**
* Sets up the minimum, default Assistants configuration if Azure OpenAI Assistants option is enabled.
* @returns {Partial<TAssistantEndpoint>} The Assistants endpoint configuration.
*/
function azureAssistantsDefaults() {
return {
capabilities: [Capabilities.tools, Capabilities.actions, Capabilities.code_interpreter],
version: defaultAssistantsVersion.azureAssistants,
};
}
/**
* Sets up the Assistants configuration from the config (`librechat.yaml`) file.
* @param {TCustomConfig} config - The loaded custom configuration.
* @param {EModelEndpoint.assistants|EModelEndpoint.azureAssistants} assistantsEndpoint - The Assistants endpoint name.
* - The previously loaded assistants configuration from Azure OpenAI Assistants option.
* @param {Partial<TAssistantEndpoint>} [prevConfig]
* @returns {Partial<TAssistantEndpoint>} The Assistants endpoint configuration.
*/
function assistantsConfigSetup(config, assistantsEndpoint, prevConfig = {}) {
const assistantsConfig = config.endpoints[assistantsEndpoint];
const parsedConfig = assistantEndpointSchema.parse(assistantsConfig);
if (assistantsConfig.supportedIds?.length && assistantsConfig.excludedIds?.length) {
logger.warn(
`Configuration conflict: The '${assistantsEndpoint}' endpoint has both 'supportedIds' and 'excludedIds' defined. The 'excludedIds' will be ignored.`,
);
}
if (
assistantsConfig.privateAssistants &&
(assistantsConfig.supportedIds?.length || assistantsConfig.excludedIds?.length)
) {
logger.warn(
`Configuration conflict: The '${assistantsEndpoint}' endpoint has both 'privateAssistants' and 'supportedIds' or 'excludedIds' defined. The 'supportedIds' and 'excludedIds' will be ignored.`,
);
}
return {
...prevConfig,
retrievalModels: parsedConfig.retrievalModels,
disableBuilder: parsedConfig.disableBuilder,
pollIntervalMs: parsedConfig.pollIntervalMs,
supportedIds: parsedConfig.supportedIds,
capabilities: parsedConfig.capabilities,
excludedIds: parsedConfig.excludedIds,
privateAssistants: parsedConfig.privateAssistants,
timeoutMs: parsedConfig.timeoutMs,
streamRate: parsedConfig.streamRate,
titlePrompt: parsedConfig.titlePrompt,
titleMethod: parsedConfig.titleMethod,
titleModel: parsedConfig.titleModel,
titleEndpoint: parsedConfig.titleEndpoint,
titlePromptTemplate: parsedConfig.titlePromptTemplate,
};
}
module.exports = { azureAssistantsDefaults, assistantsConfigSetup };

View file

@ -1,65 +0,0 @@
const { logger } = require('@librechat/data-schemas');
const {
EModelEndpoint,
validateAzureGroups,
mapModelToAzureConfig,
} = require('librechat-data-provider');
/**
* Sets up the Azure OpenAI configuration from the config (`librechat.yaml`) file.
* @param {TCustomConfig} config - The loaded custom configuration.
* @returns {TAzureConfig} The Azure OpenAI configuration.
*/
function azureConfigSetup(config) {
const { groups, ...azureConfiguration } = config.endpoints[EModelEndpoint.azureOpenAI];
/** @type {TAzureConfigValidationResult} */
const { isValid, modelNames, modelGroupMap, groupMap, errors } = validateAzureGroups(groups);
if (!isValid) {
const errorString = errors.join('\n');
const errorMessage = 'Invalid Azure OpenAI configuration:\n' + errorString;
logger.error(errorMessage);
throw new Error(errorMessage);
}
const assistantModels = [];
const assistantGroups = new Set();
for (const modelName of modelNames) {
mapModelToAzureConfig({ modelName, modelGroupMap, groupMap });
const groupName = modelGroupMap?.[modelName]?.group;
const modelGroup = groupMap?.[groupName];
let supportsAssistants = modelGroup?.assistants || modelGroup?.[modelName]?.assistants;
if (supportsAssistants) {
assistantModels.push(modelName);
!assistantGroups.has(groupName) && assistantGroups.add(groupName);
}
}
if (azureConfiguration.assistants && assistantModels.length === 0) {
throw new Error(
'No Azure models are configured to support assistants. Please remove the `assistants` field or configure at least one model to support assistants.',
);
}
if (
azureConfiguration.assistants &&
process.env.ENDPOINTS &&
!process.env.ENDPOINTS.includes(EModelEndpoint.azureAssistants)
) {
logger.warn(
`Azure Assistants are configured, but the endpoint will not be accessible as it's not included in the ENDPOINTS environment variable.
Please add the value "${EModelEndpoint.azureAssistants}" to the ENDPOINTS list if expected.`,
);
}
return {
modelNames,
modelGroupMap,
groupMap,
assistantModels,
assistantGroups: Array.from(assistantGroups),
...azureConfiguration,
};
}
module.exports = { azureConfigSetup };

View file

@ -1,197 +0,0 @@
const { logger } = require('@librechat/data-schemas');
const { isEnabled, webSearchKeys, checkEmailConfig } = require('@librechat/api');
const {
Constants,
extractVariableName,
deprecatedAzureVariables,
conflictingAzureVariables,
} = require('librechat-data-provider');
const secretDefaults = {
CREDS_KEY: 'f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0',
CREDS_IV: 'e2341419ec3dd3d19b13a1a87fafcbfb',
JWT_SECRET: '16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef',
JWT_REFRESH_SECRET: 'eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8418',
};
const deprecatedVariables = [
{
key: 'CHECK_BALANCE',
description:
'Please use the `balance` field in the `librechat.yaml` config file instead.\nMore info: https://librechat.ai/docs/configuration/librechat_yaml/object_structure/balance#overview',
},
{
key: 'START_BALANCE',
description:
'Please use the `balance` field in the `librechat.yaml` config file instead.\nMore info: https://librechat.ai/docs/configuration/librechat_yaml/object_structure/balance#overview',
},
{
key: 'GOOGLE_API_KEY',
description:
'Please use the `GOOGLE_SEARCH_API_KEY` environment variable for the Google Search Tool instead.',
},
];
/**
* 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`.
* Advises on replacing default secrets and updating deprecated variables.
*/
function checkVariables() {
let hasDefaultSecrets = false;
for (const [key, value] of Object.entries(secretDefaults)) {
if (process.env[key] === value) {
logger.warn(`Default value for ${key} is being used.`);
!hasDefaultSecrets && (hasDefaultSecrets = true);
}
}
if (hasDefaultSecrets) {
logger.info('Please replace any default secret values.');
logger.info(`\u200B
For your convenience, use this tool to generate your own secret values:
https://www.librechat.ai/toolkit/creds_generator
\u200B`);
}
deprecatedVariables.forEach(({ key, description }) => {
if (process.env[key]) {
logger.warn(`The \`${key}\` environment variable is deprecated. ${description}`);
}
});
checkPasswordReset();
}
/**
* 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.
*/
async function checkHealth() {
try {
const response = await fetch(`${process.env.RAG_API_URL}/health`);
if (response?.ok && response?.status === 200) {
logger.info(`RAG API is running and reachable at ${process.env.RAG_API_URL}.`);
}
} catch {
logger.warn(
`RAG API is either not running or not reachable at ${process.env.RAG_API_URL}, you may experience errors with file uploads.`,
);
}
}
/**
* Checks for the usage of deprecated and conflicting Azure variables.
* Logs warnings for any deprecated or conflicting environment variables found, indicating potential issues with `azureOpenAI` endpoint configuration.
*/
function checkAzureVariables() {
deprecatedAzureVariables.forEach(({ key, description }) => {
if (process.env[key]) {
logger.warn(
`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.`,
);
}
});
conflictingAzureVariables.forEach(({ key }) => {
if (process.env[key]) {
logger.warn(
`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.`,
);
}
});
}
/**
* Performs basic checks on the loaded config object.
* @param {TCustomConfig} config - The loaded custom configuration.
*/
function checkConfig(config) {
if (config.version !== Constants.CONFIG_VERSION) {
logger.info(
`\nOutdated Config version: ${config.version}
Latest version: ${Constants.CONFIG_VERSION}
Check out the Config changelogs for the latest options and features added.
https://www.librechat.ai/changelog\n\n`,
);
}
}
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.
* Warns if actual API keys or URLs are used instead of environment variable references.
* Logs debug information for properly configured environment variable references.
* @param {Object} webSearchConfig - The loaded web search configuration object.
*/
function checkWebSearchConfig(webSearchConfig) {
if (!webSearchConfig) {
return;
}
webSearchKeys.forEach((key) => {
const value = webSearchConfig[key];
if (typeof value === 'string') {
const varName = extractVariableName(value);
if (varName) {
// This is a proper environment variable reference
const actualValue = process.env[varName];
if (actualValue) {
logger.debug(`Web search ${key}: Using environment variable ${varName} with value set`);
} else {
logger.debug(
`Web search ${key}: Using environment variable ${varName} (not set in environment, user provided value)`,
);
}
} else {
// This is not an environment variable reference - warn user
logger.warn(
`❗ Web search configuration error: ${key} contains an actual value instead of an environment variable reference.
Current value: "${value.substring(0, 10)}..."
This is incorrect! You should use environment variable references in your librechat.yaml file, such as:
${key}: "\${YOUR_ENV_VAR_NAME}"
Then set the actual API key in your .env file or environment variables.
More info: https://www.librechat.ai/docs/configuration/librechat_yaml/web_search`,
);
}
}
});
}
module.exports = {
checkHealth,
checkConfig,
checkVariables,
checkAzureVariables,
checkWebSearchConfig,
};

View file

@ -1,202 +0,0 @@
jest.mock('librechat-data-provider', () => ({
...jest.requireActual('librechat-data-provider'),
extractVariableName: jest.fn(),
}));
jest.mock('@librechat/data-schemas', () => ({
...jest.requireActual('@librechat/data-schemas'),
logger: {
debug: jest.fn(),
warn: jest.fn(),
},
}));
const { checkWebSearchConfig } = require('./checks');
const { logger } = require('@librechat/data-schemas');
const { extractVariableName } = require('librechat-data-provider');
describe('checkWebSearchConfig', () => {
let originalEnv;
beforeEach(() => {
// Clear all mocks
jest.clearAllMocks();
// Store original environment
originalEnv = process.env;
// Reset process.env
process.env = { ...originalEnv };
});
afterEach(() => {
// Restore original environment
process.env = originalEnv;
});
describe('when webSearchConfig is undefined or null', () => {
it('should return early without logging when config is undefined', () => {
checkWebSearchConfig(undefined);
expect(logger.debug).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});
it('should return early without logging when config is null', () => {
checkWebSearchConfig(null);
expect(logger.debug).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});
});
describe('when config values are proper environment variable references', () => {
it('should log debug message for each valid environment variable with value set', () => {
const config = {
serperApiKey: '${SERPER_API_KEY}',
jinaApiKey: '${JINA_API_KEY}',
};
extractVariableName.mockReturnValueOnce('SERPER_API_KEY').mockReturnValueOnce('JINA_API_KEY');
process.env.SERPER_API_KEY = 'test-serper-key';
process.env.JINA_API_KEY = 'test-jina-key';
checkWebSearchConfig(config);
expect(extractVariableName).toHaveBeenCalledWith('${SERPER_API_KEY}');
expect(extractVariableName).toHaveBeenCalledWith('${JINA_API_KEY}');
expect(logger.debug).toHaveBeenCalledWith(
'Web search serperApiKey: Using environment variable SERPER_API_KEY with value set',
);
expect(logger.debug).toHaveBeenCalledWith(
'Web search jinaApiKey: Using environment variable JINA_API_KEY with value set',
);
expect(logger.warn).not.toHaveBeenCalled();
});
it('should log debug message for environment variables not set in environment', () => {
const config = {
cohereApiKey: '${COHERE_API_KEY}',
};
extractVariableName.mockReturnValue('COHERE_API_KEY');
delete process.env.COHERE_API_KEY;
checkWebSearchConfig(config);
expect(logger.debug).toHaveBeenCalledWith(
'Web search cohereApiKey: Using environment variable COHERE_API_KEY (not set in environment, user provided value)',
);
expect(logger.warn).not.toHaveBeenCalled();
});
});
describe('when config values are actual values instead of environment variable references', () => {
it('should warn when serperApiKey contains actual API key', () => {
const config = {
serperApiKey: 'sk-1234567890abcdef',
};
extractVariableName.mockReturnValue(null);
checkWebSearchConfig(config);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
'❗ Web search configuration error: serperApiKey contains an actual value',
),
);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Current value: "sk-1234567..."'),
);
expect(logger.debug).not.toHaveBeenCalled();
});
it('should warn when firecrawlApiUrl contains actual URL', () => {
const config = {
firecrawlApiUrl: 'https://api.firecrawl.dev',
};
extractVariableName.mockReturnValue(null);
checkWebSearchConfig(config);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
'❗ Web search configuration error: firecrawlApiUrl contains an actual value',
),
);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Current value: "https://ap..."'),
);
});
it('should include documentation link in warning message', () => {
const config = {
firecrawlApiKey: 'fc-actual-key',
};
extractVariableName.mockReturnValue(null);
checkWebSearchConfig(config);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
'More info: https://www.librechat.ai/docs/configuration/librechat_yaml/web_search',
),
);
});
});
describe('when config contains mixed value types', () => {
it('should only process string values and ignore non-string values', () => {
const config = {
serperApiKey: '${SERPER_API_KEY}',
safeSearch: 1,
scraperTimeout: 7500,
jinaApiKey: 'actual-key',
};
extractVariableName.mockReturnValueOnce('SERPER_API_KEY').mockReturnValueOnce(null);
process.env.SERPER_API_KEY = 'test-key';
checkWebSearchConfig(config);
expect(extractVariableName).toHaveBeenCalledTimes(2);
expect(logger.debug).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledTimes(1);
});
});
describe('edge cases', () => {
it('should handle config with no web search keys', () => {
const config = {
someOtherKey: 'value',
anotherKey: '${SOME_VAR}',
};
checkWebSearchConfig(config);
expect(extractVariableName).not.toHaveBeenCalled();
expect(logger.debug).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});
it('should truncate long values in warning messages', () => {
const config = {
serperApiKey: 'this-is-a-very-long-api-key-that-should-be-truncated-in-the-warning-message',
};
extractVariableName.mockReturnValue(null);
checkWebSearchConfig(config);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Current value: "this-is-a-..."'),
);
});
});
});

View file

@ -1,67 +0,0 @@
const { agentsConfigSetup } = require('@librechat/api');
const { EModelEndpoint } = require('librechat-data-provider');
const { azureAssistantsDefaults, assistantsConfigSetup } = require('./assistants');
const { azureConfigSetup } = require('./azureOpenAI');
const { checkAzureVariables } = require('./checks');
/**
* Loads custom config endpoints
* @param {TCustomConfig} [config]
* @param {TCustomConfig['endpoints']['agents']} [agentsDefaults]
*/
const loadEndpoints = (config, agentsDefaults) => {
/** @type {AppConfig['endpoints']} */
const loadedEndpoints = {};
const endpoints = config?.endpoints;
if (endpoints?.[EModelEndpoint.azureOpenAI]) {
loadedEndpoints[EModelEndpoint.azureOpenAI] = azureConfigSetup(config);
checkAzureVariables();
}
if (endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) {
loadedEndpoints[EModelEndpoint.azureAssistants] = azureAssistantsDefaults();
}
if (endpoints?.[EModelEndpoint.azureAssistants]) {
loadedEndpoints[EModelEndpoint.azureAssistants] = assistantsConfigSetup(
config,
EModelEndpoint.azureAssistants,
loadedEndpoints[EModelEndpoint.azureAssistants],
);
}
if (endpoints?.[EModelEndpoint.assistants]) {
loadedEndpoints[EModelEndpoint.assistants] = assistantsConfigSetup(
config,
EModelEndpoint.assistants,
loadedEndpoints[EModelEndpoint.assistants],
);
}
loadedEndpoints[EModelEndpoint.agents] = agentsConfigSetup(config, agentsDefaults);
const endpointKeys = [
EModelEndpoint.openAI,
EModelEndpoint.google,
EModelEndpoint.custom,
EModelEndpoint.bedrock,
EModelEndpoint.anthropic,
];
endpointKeys.forEach((key) => {
if (endpoints?.[key]) {
loadedEndpoints[key] = endpoints[key];
}
});
if (endpoints?.all) {
loadedEndpoints.all = endpoints.all;
}
return loadedEndpoints;
};
module.exports = {
loadEndpoints,
};

View file

@ -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 };

View file

@ -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,
};

View file

@ -1,7 +1,7 @@
const { Tiktoken } = require('tiktoken/lite');
const { logger } = require('@librechat/data-schemas');
const p50k_base = require('tiktoken/encoders/p50k_base.json');
const cl100k_base = require('tiktoken/encoders/cl100k_base.json');
const logger = require('~/config/winston');
/**
* Counts the number of tokens in a given text using a specified encoding model.

View file

@ -125,34 +125,6 @@ function formatAction(action) {
return formattedAction;
}
/**
* Checks if the given value is truthy by being either the boolean `true` or a string
* that case-insensitively matches 'true'.
*
* @function
* @param {string|boolean|null|undefined} value - The value to check.
* @returns {boolean} Returns `true` if the value is the boolean `true` or a case-insensitive
* match for the string 'true', otherwise returns `false`.
* @example
*
* isEnabled("True"); // returns true
* isEnabled("TRUE"); // returns true
* isEnabled(true); // returns true
* isEnabled("false"); // returns false
* isEnabled(false); // returns false
* isEnabled(null); // returns false
* isEnabled(); // returns false
*/
function isEnabled(value) {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
return value.toLowerCase().trim() === 'true';
}
return false;
}
/**
* Checks if the provided value is 'user_provided'.
*
@ -207,7 +179,6 @@ function generateConfig(key, baseURL, endpoint) {
}
module.exports = {
isEnabled,
handleText,
formatSteps,
escapeRegExp,

View file

@ -1,10 +1,10 @@
const { v4: uuidv4 } = require('uuid');
const { logger } = require('@librechat/data-schemas');
const { EModelEndpoint, Constants, ForkOptions } = require('librechat-data-provider');
const { createImportBatchBuilder } = require('./importBatchBuilder');
const BaseClient = require('~/app/clients/BaseClient');
const { getConvo } = require('~/models/Conversation');
const { getMessages } = require('~/models/Message');
const logger = require('~/config/winston');
/**
* Helper function to clone messages with proper parent-child relationships and timestamps

View file

@ -1,9 +1,9 @@
const { v4: uuidv4 } = require('uuid');
const { logger } = require('@librechat/data-schemas');
const { EModelEndpoint, Constants, openAISettings } = require('librechat-data-provider');
const { bulkIncrementTagCounts } = require('~/models/ConversationTag');
const { bulkSaveConvos } = require('~/models/Conversation');
const { bulkSaveMessages } = require('~/models/Message');
const { logger } = require('~/config');
/**
* Factory function for creating an instance of ImportBatchBuilder.

View file

@ -53,17 +53,6 @@ function LB_QueueAsyncCall(asyncFunc, args, callback) {
}
}
/**
* Delays the execution for a specified number of milliseconds.
*
* @param {number} ms - The number of milliseconds to delay.
* @return {Promise<void>} A promise that resolves after the specified delay.
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
module.exports = {
sleep,
LB_QueueAsyncCall,
};