🔃 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:
Danny Avila 2025-10-05 06:37:57 -04:00 committed by GitHub
parent 9ff608e6af
commit 838fb53208
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 1383 additions and 1326 deletions

View file

@ -74,6 +74,10 @@
"registry": "https://registry.npmjs.org/"
},
"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",
"@langchain/core": "^0.3.62",
"@librechat/agents": "^2.4.82",
@ -85,6 +89,7 @@
"eventsource": "^3.0.2",
"express": "^4.21.2",
"express-session": "^1.18.2",
"firebase": "^11.0.2",
"form-data": "^4.0.4",
"ioredis": "^5.3.2",
"js-yaml": "^4.1.0",

View file

@ -1,4 +1,3 @@
export * from './config';
export * from './memory';
export * from './migration';
export * from './legacy';

View file

@ -2,10 +2,9 @@ import { primeResources } from './resources';
import { logger } from '@librechat/data-schemas';
import { EModelEndpoint, EToolResources, AgentCapabilities } 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 { IUser } from '@librechat/data-schemas';
import type { TGetFiles } from './resources';
import type { AppConfig } from '~/types';
// Mock logger
jest.mock('@librechat/data-schemas', () => ({

View file

@ -1,10 +1,9 @@
import { logger } from '@librechat/data-schemas';
import { EModelEndpoint, EToolResources, AgentCapabilities } 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 { IMongoFile, IUser } from '@librechat/data-schemas';
import type { Request as ServerRequest } from 'express';
import type { AppConfig } from '~/types/';
/**
* Function type for retrieving files from the database

View 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();
});
});

View file

@ -0,0 +1,814 @@
import {
OCRStrategy,
FileSources,
EModelEndpoint,
EImageOutputType,
AgentCapabilities,
defaultSocialLogins,
validateAzureGroups,
defaultAgentCapabilities,
} from 'librechat-data-provider';
import type { TCustomConfig } from 'librechat-data-provider';
jest.mock('@librechat/data-schemas', () => ({
...jest.requireActual('@librechat/data-schemas'),
logger: {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
},
}));
import { AppService } from '@librechat/data-schemas';
const azureGroups = [
{
group: 'librechat-westus',
apiKey: '${WESTUS_API_KEY}',
instanceName: 'librechat-westus',
version: '2023-12-01-preview',
models: {
'gpt-4-vision-preview': {
deploymentName: 'gpt-4-vision-preview',
version: '2024-02-15-preview',
},
'gpt-3.5-turbo': {
deploymentName: 'gpt-35-turbo',
},
'gpt-3.5-turbo-1106': {
deploymentName: 'gpt-35-turbo-1106',
},
'gpt-4': {
deploymentName: 'gpt-4',
},
'gpt-4-1106-preview': {
deploymentName: 'gpt-4-1106-preview',
},
},
},
{
group: 'librechat-eastus',
apiKey: '${EASTUS_API_KEY}',
instanceName: 'librechat-eastus',
deploymentName: 'gpt-4-turbo',
version: '2024-02-15-preview',
models: {
'gpt-4-turbo': true,
},
} as const,
];
describe('AppService', () => {
const mockSystemTools = {
ExampleTool: {
type: 'function',
function: {
description: 'Example tool function',
name: 'exampleFunction',
parameters: {
type: 'object',
properties: {
param1: { type: 'string', description: 'An example parameter' },
},
required: ['param1'],
},
},
},
};
beforeEach(() => {
process.env.CDN_PROVIDER = undefined;
jest.clearAllMocks();
});
it('should correctly assign process.env and initialize app config based on custom config', async () => {
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(result).toEqual(
expect.objectContaining({
config: expect.objectContaining({
fileStrategy: 'testStrategy',
}),
registration: expect.objectContaining({
socialLogins: ['testLogin'],
}),
fileStrategy: 'testStrategy',
interfaceConfig: expect.objectContaining({
endpointsMenu: true,
modelSelect: true,
parameters: true,
sidePanel: true,
presets: true,
}),
mcpConfig: null,
imageOutputType: expect.any(String),
fileConfig: undefined,
secureImageLinks: undefined,
balance: { enabled: true },
filteredTools: undefined,
includedTools: undefined,
webSearch: expect.objectContaining({
safeSearch: 1,
jinaApiKey: '${JINA_API_KEY}',
jinaApiUrl: '${JINA_API_URL}',
cohereApiKey: '${COHERE_API_KEY}',
serperApiKey: '${SERPER_API_KEY}',
searxngApiKey: '${SEARXNG_API_KEY}',
firecrawlApiKey: '${FIRECRAWL_API_KEY}',
firecrawlApiUrl: '${FIRECRAWL_API_URL}',
searxngInstanceUrl: '${SEARXNG_INSTANCE_URL}',
}),
memory: undefined,
endpoints: expect.objectContaining({
agents: expect.objectContaining({
disableBuilder: false,
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
maxCitations: 30,
maxCitationsPerFile: 7,
minRelevanceScore: 0.45,
}),
}),
}),
);
});
it('should change the `imageOutputType` based on config value', async () => {
const config = {
version: '0.10.0',
imageOutputType: EImageOutputType.WEBP,
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
imageOutputType: EImageOutputType.WEBP,
}),
);
});
it('should default to `PNG` `imageOutputType` with no provided type', async () => {
const config = {
version: '0.10.0',
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
imageOutputType: EImageOutputType.PNG,
}),
);
});
it('should default to `PNG` `imageOutputType` with no provided config', async () => {
const config = {};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
imageOutputType: EImageOutputType.PNG,
}),
);
});
it('should load and format tools accurately with defined structure', async () => {
const config = {};
const result = await AppService({ config, systemTools: mockSystemTools });
// Verify tools are included in the returned config
expect(result.availableTools).toBeDefined();
expect(result.availableTools?.ExampleTool).toEqual({
type: 'function',
function: {
description: 'Example tool function',
name: 'exampleFunction',
parameters: {
type: 'object',
properties: {
param1: { type: 'string', description: 'An example parameter' },
},
required: ['param1'],
},
},
});
});
it('should correctly configure Assistants endpoint based on custom config', async () => {
const config: Partial<TCustomConfig> = {
endpoints: {
[EModelEndpoint.assistants]: {
disableBuilder: true,
pollIntervalMs: 5000,
timeoutMs: 30000,
supportedIds: ['id1', 'id2'],
privateAssistants: false,
},
},
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
[EModelEndpoint.assistants]: expect.objectContaining({
disableBuilder: true,
pollIntervalMs: 5000,
timeoutMs: 30000,
supportedIds: expect.arrayContaining(['id1', 'id2']),
privateAssistants: false,
}),
}),
}),
);
});
it('should correctly configure Agents endpoint based on custom config', async () => {
const config: Partial<TCustomConfig> = {
endpoints: {
[EModelEndpoint.agents]: {
disableBuilder: true,
recursionLimit: 10,
maxRecursionLimit: 20,
allowedProviders: ['openai', 'anthropic'],
capabilities: [AgentCapabilities.tools, AgentCapabilities.actions],
},
},
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
[EModelEndpoint.agents]: expect.objectContaining({
disableBuilder: true,
recursionLimit: 10,
maxRecursionLimit: 20,
allowedProviders: expect.arrayContaining(['openai', 'anthropic']),
capabilities: expect.arrayContaining([
AgentCapabilities.tools,
AgentCapabilities.actions,
]),
}),
}),
}),
);
});
it('should configure Agents endpoint with defaults when no config is provided', async () => {
const config = {};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
[EModelEndpoint.agents]: expect.objectContaining({
disableBuilder: false,
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
}),
}),
}),
);
});
it('should configure Agents endpoint with defaults when endpoints exist but agents is not defined', async () => {
const config = {
endpoints: {
[EModelEndpoint.openAI]: {
titleConvo: true,
},
},
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
[EModelEndpoint.agents]: expect.objectContaining({
disableBuilder: false,
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
}),
[EModelEndpoint.openAI]: expect.objectContaining({
titleConvo: true,
}),
}),
}),
);
});
it('should correctly configure minimum Azure OpenAI Assistant values', async () => {
const assistantGroups = [azureGroups[0], { ...azureGroups[1], assistants: true }];
const config = {
endpoints: {
[EModelEndpoint.azureOpenAI]: {
groups: assistantGroups,
assistants: true,
},
},
};
process.env.WESTUS_API_KEY = 'westus-key';
process.env.EASTUS_API_KEY = 'eastus-key';
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
[EModelEndpoint.azureAssistants]: expect.objectContaining({
capabilities: expect.arrayContaining([
expect.any(String),
expect.any(String),
expect.any(String),
]),
}),
}),
}),
);
});
it('should correctly configure Azure OpenAI endpoint based on custom config', async () => {
const config: Partial<TCustomConfig> = {
endpoints: {
[EModelEndpoint.azureOpenAI]: {
groups: azureGroups,
},
},
};
process.env.WESTUS_API_KEY = 'westus-key';
process.env.EASTUS_API_KEY = 'eastus-key';
const result = await AppService({ config });
const { modelNames, modelGroupMap, groupMap } = validateAzureGroups(azureGroups);
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
[EModelEndpoint.azureOpenAI]: expect.objectContaining({
modelNames,
modelGroupMap,
groupMap,
}),
}),
}),
);
});
it('should not modify FILE_UPLOAD environment variables without rate limits', async () => {
// 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 };
const config = {};
await AppService({ config });
// 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 fallback to default FILE_UPLOAD environment variables when rate limits are unspecified', async () => {
// Setup initial environment variables to non-default values
process.env.FILE_UPLOAD_IP_MAX = 'initialMax';
process.env.FILE_UPLOAD_IP_WINDOW = 'initialWindow';
process.env.FILE_UPLOAD_USER_MAX = 'initialUserMax';
process.env.FILE_UPLOAD_USER_WINDOW = 'initialUserWindow';
const config = {};
await AppService({ config });
// 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_WINDOW).toEqual('initialWindow');
expect(process.env.FILE_UPLOAD_USER_MAX).toEqual('initialUserMax');
expect(process.env.FILE_UPLOAD_USER_WINDOW).toEqual('initialUserWindow');
});
it('should not modify IMPORT environment variables without rate limits', async () => {
// 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 };
const config = {};
await AppService({ config });
// 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 fallback to default IMPORT environment variables when rate limits are unspecified', async () => {
// Setup initial environment variables to non-default values
process.env.IMPORT_IP_MAX = 'initialMax';
process.env.IMPORT_IP_WINDOW = 'initialWindow';
process.env.IMPORT_USER_MAX = 'initialUserMax';
process.env.IMPORT_USER_WINDOW = 'initialUserWindow';
const config = {};
await AppService({ config });
// Verify that process.env falls back to the initial values
expect(process.env.IMPORT_IP_MAX).toEqual('initialMax');
expect(process.env.IMPORT_IP_WINDOW).toEqual('initialWindow');
expect(process.env.IMPORT_USER_MAX).toEqual('initialUserMax');
expect(process.env.IMPORT_USER_WINDOW).toEqual('initialUserWindow');
});
it('should correctly configure endpoint with titlePrompt, titleMethod, and titlePromptTemplate', async () => {
const config: Partial<TCustomConfig> = {
endpoints: {
[EModelEndpoint.openAI]: {
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
titleMethod: 'structured',
titlePrompt: 'Custom title prompt for conversation',
titlePromptTemplate: 'Summarize this conversation: {{conversation}}',
},
[EModelEndpoint.assistants]: {
titleMethod: 'functions',
titlePrompt: 'Generate a title for this assistant conversation',
titlePromptTemplate: 'Assistant conversation template: {{messages}}',
},
[EModelEndpoint.azureOpenAI]: {
groups: azureGroups,
titleConvo: true,
titleMethod: 'completion',
titleModel: 'gpt-4',
titlePrompt: 'Azure title prompt',
titlePromptTemplate: 'Azure conversation: {{context}}',
},
},
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
// Check OpenAI endpoint configuration
[EModelEndpoint.openAI]: expect.objectContaining({
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
titleMethod: 'structured',
titlePrompt: 'Custom title prompt for conversation',
titlePromptTemplate: 'Summarize this conversation: {{conversation}}',
}),
// Check Assistants endpoint configuration
[EModelEndpoint.assistants]: expect.objectContaining({
titleMethod: 'functions',
titlePrompt: 'Generate a title for this assistant conversation',
titlePromptTemplate: 'Assistant conversation template: {{messages}}',
}),
// Check Azure OpenAI endpoint configuration
[EModelEndpoint.azureOpenAI]: expect.objectContaining({
titleConvo: true,
titleMethod: 'completion',
titleModel: 'gpt-4',
titlePrompt: 'Azure title prompt',
titlePromptTemplate: 'Azure conversation: {{context}}',
}),
}),
}),
);
});
it('should configure Agent endpoint with title generation settings', async () => {
const config: Partial<TCustomConfig> = {
endpoints: {
[EModelEndpoint.agents]: {
disableBuilder: false,
titleConvo: true,
titleModel: 'gpt-4',
titleMethod: 'structured',
titlePrompt: 'Generate a descriptive title for this agent conversation',
titlePromptTemplate: 'Agent conversation summary: {{content}}',
recursionLimit: 15,
capabilities: [AgentCapabilities.tools, AgentCapabilities.actions],
maxCitations: 30,
maxCitationsPerFile: 7,
minRelevanceScore: 0.45,
},
},
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
[EModelEndpoint.agents]: expect.objectContaining({
disableBuilder: false,
titleConvo: true,
titleModel: 'gpt-4',
titleMethod: 'structured',
titlePrompt: 'Generate a descriptive title for this agent conversation',
titlePromptTemplate: 'Agent conversation summary: {{content}}',
recursionLimit: 15,
capabilities: expect.arrayContaining([
AgentCapabilities.tools,
AgentCapabilities.actions,
]),
}),
}),
}),
);
});
it('should handle missing title configuration options with defaults', async () => {
const config = {
endpoints: {
[EModelEndpoint.openAI]: {
titleConvo: true,
// titlePrompt and titlePromptTemplate are not provided
},
},
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
[EModelEndpoint.openAI]: expect.objectContaining({
titleConvo: true,
}),
}),
}),
);
// Verify that optional fields are not set when not provided
expect(result.endpoints[EModelEndpoint.openAI].titlePrompt).toBeUndefined();
expect(result.endpoints[EModelEndpoint.openAI].titlePromptTemplate).toBeUndefined();
expect(result.endpoints[EModelEndpoint.openAI].titleMethod).toBeUndefined();
});
it('should correctly configure titleEndpoint when specified', async () => {
const config: Partial<TCustomConfig> = {
endpoints: {
[EModelEndpoint.openAI]: {
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
titleEndpoint: EModelEndpoint.anthropic,
titlePrompt: 'Generate a concise title',
},
[EModelEndpoint.agents]: {
disableBuilder: false,
capabilities: [AgentCapabilities.tools],
maxCitations: 30,
maxCitationsPerFile: 7,
minRelevanceScore: 0.45,
titleEndpoint: 'custom-provider',
titleMethod: 'structured',
},
},
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
// Check OpenAI endpoint has titleEndpoint
[EModelEndpoint.openAI]: expect.objectContaining({
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
titleEndpoint: EModelEndpoint.anthropic,
titlePrompt: 'Generate a concise title',
}),
// Check Agents endpoint has titleEndpoint
[EModelEndpoint.agents]: expect.objectContaining({
titleEndpoint: 'custom-provider',
titleMethod: 'structured',
}),
}),
}),
);
});
it('should correctly configure all endpoint when specified', async () => {
const config: Partial<TCustomConfig> = {
endpoints: {
all: {
titleConvo: true,
titleModel: 'gpt-4o-mini',
titleMethod: 'structured',
titlePrompt: 'Default title prompt for all endpoints',
titlePromptTemplate: 'Default template: {{conversation}}',
titleEndpoint: EModelEndpoint.anthropic,
streamRate: 50,
},
[EModelEndpoint.openAI]: {
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
},
},
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
// Check that 'all' endpoint config is loaded
endpoints: expect.objectContaining({
all: expect.objectContaining({
titleConvo: true,
titleModel: 'gpt-4o-mini',
titleMethod: 'structured',
titlePrompt: 'Default title prompt for all endpoints',
titlePromptTemplate: 'Default template: {{conversation}}',
titleEndpoint: EModelEndpoint.anthropic,
streamRate: 50,
}),
// Check that OpenAI endpoint has its own config
[EModelEndpoint.openAI]: expect.objectContaining({
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
}),
}),
}),
);
});
});
describe('AppService updating app config and issuing warnings', () => {
let initialEnv: NodeJS.ProcessEnv;
beforeEach(() => {
// Store initial environment variables to restore them after each test
initialEnv = { ...process.env };
process.env.CDN_PROVIDER = undefined;
jest.clearAllMocks();
});
afterEach(() => {
// Restore initial environment variables
process.env = { ...initialEnv };
});
it('should initialize app config with default values if config is empty', async () => {
const config = {};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
config: {},
fileStrategy: FileSources.local,
registration: expect.objectContaining({
socialLogins: defaultSocialLogins,
}),
balance: expect.objectContaining({
enabled: false,
startBalance: undefined,
}),
}),
);
});
it('should initialize app config with values from config', async () => {
// Mock loadCustomConfig to return a specific config object with a complete balance config
const config: Partial<TCustomConfig> = {
fileStrategy: FileSources.firebase,
registration: { socialLogins: ['testLogin'] },
balance: {
enabled: false,
startBalance: 5000,
autoRefillEnabled: true,
refillIntervalValue: 15,
refillIntervalUnit: 'hours',
refillAmount: 5000,
},
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
config,
fileStrategy: config.fileStrategy,
registration: expect.objectContaining({
socialLogins: config.registration?.socialLogins,
}),
balance: config.balance,
}),
);
});
it('should apply the assistants endpoint configuration correctly to app config', async () => {
const config: Partial<TCustomConfig> = {
endpoints: {
assistants: {
version: 'v2',
retrievalModels: ['gpt-4', 'gpt-3.5-turbo'],
capabilities: [],
disableBuilder: true,
pollIntervalMs: 5000,
timeoutMs: 30000,
supportedIds: ['id1', 'id2'],
},
},
};
const result = await AppService({ config });
expect(result).toEqual(
expect.objectContaining({
endpoints: expect.objectContaining({
assistants: expect.objectContaining({
disableBuilder: true,
pollIntervalMs: 5000,
timeoutMs: 30000,
supportedIds: ['id1', 'id2'],
}),
}),
}),
);
// Verify excludedIds is undefined when not provided
expect(result.endpoints.assistants.excludedIds).toBeUndefined();
});
it('should not parse environment variable references in OCR config', async () => {
// Mock custom configuration with env variable references in OCR config
const config: Partial<TCustomConfig> = {
ocr: {
apiKey: '${OCR_API_KEY_CUSTOM_VAR_NAME}',
baseURL: '${OCR_BASEURL_CUSTOM_VAR_NAME}',
strategy: OCRStrategy.MISTRAL_OCR,
mistralModel: 'mistral-medium',
},
};
// Set actual environment variables with different values
process.env.OCR_API_KEY_CUSTOM_VAR_NAME = 'actual-api-key';
process.env.OCR_BASEURL_CUSTOM_VAR_NAME = 'https://actual-ocr-url.com';
const result = await AppService({ config });
// Verify that the raw string references were preserved and not interpolated
expect(result).toEqual(
expect.objectContaining({
ocr: expect.objectContaining({
apiKey: '${OCR_API_KEY_CUSTOM_VAR_NAME}',
baseURL: '${OCR_BASEURL_CUSTOM_VAR_NAME}',
strategy: 'mistral_ocr',
mistralModel: 'mistral-medium',
}),
}),
);
});
it('should correctly configure peoplePicker permissions when specified', async () => {
const config = {
interface: {
peoplePicker: {
users: true,
groups: true,
roles: true,
},
},
};
const result = await AppService({ config });
// Check that interface config includes the permissions
expect(result).toEqual(
expect.objectContaining({
interfaceConfig: expect.objectContaining({
peoplePicker: expect.objectContaining({
users: true,
groups: true,
roles: true,
}),
}),
}),
);
});
});

View 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();
}
}

View file

@ -0,0 +1,358 @@
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(),
},
}));
import { handleRateLimits } from './limits';
import { checkWebSearchConfig } from './checks';
import { logger } from '@librechat/data-schemas';
import { extractVariableName as extract } from 'librechat-data-provider';
const extractVariableName = extract as jest.MockedFunction<typeof extract>;
describe('checkWebSearchConfig', () => {
let originalEnv: NodeJS.ProcessEnv;
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}',
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/** @ts-expect-error */
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-..."'),
);
});
});
});
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');
});
});

View file

@ -0,0 +1,307 @@
import { logger, webSearchKeys } from '@librechat/data-schemas';
import { Constants, extractVariableName } from 'librechat-data-provider';
import type { TCustomConfig } from 'librechat-data-provider';
import type { AppConfig } from '@librechat/data-schemas';
import { isEnabled, checkEmailConfig } from '~/utils';
import { handleRateLimits } from './limits';
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.',
},
];
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.
* Logs warnings for any default secret values being used and for usage of 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
*/
export 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.`);
if (!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.
*/
export 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.`,
);
}
});
}
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.
* @param config - The loaded custom configuration.
*/
export function checkConfig(config: Partial<TCustomConfig>) {
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`,
);
}
}
/**
* 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 webSearchConfig - The loaded web search configuration object.
*/
export function checkWebSearchConfig(webSearchConfig?: Partial<TCustomConfig['webSearch']> | null) {
if (!webSearchConfig) {
return;
}
webSearchKeys.forEach((key) => {
const value = webSearchConfig[key as keyof typeof webSearchConfig];
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`,
);
}
}
});
}

View file

@ -1,8 +1,8 @@
import { getTransactionsConfig, getBalanceConfig } from './config';
import { logger } from '@librechat/data-schemas';
import { FileSources } from 'librechat-data-provider';
import type { AppConfig } from '~/types';
import type { TCustomConfig } from 'librechat-data-provider';
import type { AppConfig } from '@librechat/data-schemas';
// Helper function to create a minimal AppConfig for testing
const createTestAppConfig = (overrides: Partial<AppConfig> = {}): AppConfig => {

View file

@ -1,8 +1,8 @@
import { logger } from '@librechat/data-schemas';
import { EModelEndpoint, removeNullishValues } 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 { logger } from '@librechat/data-schemas';
/**
* Retrieves the balance configuration object
@ -24,7 +24,7 @@ export function getBalanceConfig(appConfig?: AppConfig): Partial<TCustomConfig['
/**
* Retrieves the transactions configuration object
* */
export function getTransactionsConfig(appConfig?: AppConfig): TTransactionsConfig {
export function getTransactionsConfig(appConfig?: AppConfig): Partial<TTransactionsConfig> {
const defaultConfig: TTransactionsConfig = { enabled: true };
if (!appConfig) {
@ -66,5 +66,5 @@ export const getCustomEndpointConfig = ({
export function hasCustomUserVars(appConfig?: AppConfig): boolean {
const mcpServers = appConfig?.mcpConfig;
return Object.values(mcpServers ?? {}).some((server) => server.customUserVars);
return Object.values(mcpServers ?? {}).some((server) => server?.customUserVars);
}

View file

@ -1,3 +1,4 @@
export * from './config';
export * from './interface';
export * from './permissions';
export * from './cdn';
export * from './checks';

View 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();
}
});
};

View file

@ -1,8 +1,8 @@
import { loadDefaultInterface } from '@librechat/data-schemas';
import { SystemRoles, Permissions, PermissionTypes, roleDefaults } 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 { loadDefaultInterface } from './interface';
const mockUpdateAccessPermissions = jest.fn();
const mockGetRoleByName = jest.fn();

View file

@ -6,8 +6,7 @@ import {
PermissionTypes,
getConfigDefaults,
} from 'librechat-data-provider';
import type { IRole } from '@librechat/data-schemas';
import type { AppConfig } from '~/types/config';
import type { IRole, AppConfig } from '@librechat/data-schemas';
import { isMemoryEnabled } from '~/memory/config';
/**

View file

@ -0,0 +1,54 @@
import { logger } from '@librechat/data-schemas';
import { DefaultAzureCredential } from '@azure/identity';
import type { ContainerClient, BlobServiceClient } from '@azure/storage-blob';
let blobServiceClient: BlobServiceClient | null = 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 The initialized client, or null if the required configuration is missing.
*/
export const initializeAzureBlobService = async (): Promise<BlobServiceClient | null> => {
if (blobServiceClient) {
return blobServiceClient;
}
const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING;
if (connectionString) {
const { BlobServiceClient } = await import('@azure/storage-blob');
blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
logger.info('Azure Blob Service initialized using connection string');
} else {
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();
const { BlobServiceClient } = await import('@azure/storage-blob');
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 [containerName=process.env.AZURE_CONTAINER_NAME || 'files'] - The container name.
* @returns The Azure ContainerClient.
*/
export const getAzureContainerClient = async (
containerName = process.env.AZURE_CONTAINER_NAME || 'files',
): Promise<ContainerClient | null> => {
const serviceClient = await initializeAzureBlobService();
return serviceClient ? serviceClient.getContainerClient(containerName) : null;
};

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

View file

@ -0,0 +1,3 @@
export * from './azure';
export * from './firebase';
export * from './s3';

View file

@ -0,0 +1,51 @@
import { S3Client } from '@aws-sdk/client-s3';
import { logger } from '@librechat/data-schemas';
let s3: S3Client | null = 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 An instance of S3Client if the region is provided; otherwise, null.
*/
export const initializeS3 = (): S3Client | null => {
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;
};

View file

@ -1,4 +1,5 @@
export * from './app';
export * from './cdn';
/* Auth */
export * from './auth';
/* MCP */

View file

@ -1,8 +1,8 @@
import mapValues from 'lodash/mapValues';
import { logger } from '@librechat/data-schemas';
import { Constants } from 'librechat-data-provider';
import type { JsonSchemaType } from '@librechat/data-schemas';
import type { MCPConnection } from '~/mcp/connection';
import type { JsonSchemaType } from '~/types';
import type * as t from '~/mcp/types';
import { ConnectionsRepository } from '~/mcp/ConnectionsRepository';
import { detectOAuthRequirement } from '~/mcp/oauth';

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// zod.spec.ts
import { z } from 'zod';
import type { JsonSchemaType } from '~/types';
import type { JsonSchemaType } from '@librechat/data-schemas';
import { resolveJsonSchemaRefs, convertJsonSchemaToZod, convertWithResolvedRefs } from '../zod';
describe('convertJsonSchemaToZod', () => {

View file

@ -10,9 +10,8 @@ import {
} 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 { TokenMethods } from '@librechat/data-schemas';
import type { TokenMethods, JsonSchemaType } from '@librechat/data-schemas';
import type { FlowStateManager } from '~/flow/manager';
import type { JsonSchemaType } from '~/types/zod';
import type { RequestBody } from '~/types/http';
import type * as o from '~/mcp/oauth/types';

View file

@ -1,5 +1,5 @@
import { z } from 'zod';
import type { JsonSchemaType, ConvertJsonSchemaToZodOptions } from '~/types';
import type { JsonSchemaType, ConvertJsonSchemaToZodOptions } from '@librechat/data-schemas';
function isEmptyObjectSchema(jsonSchema?: JsonSchemaType): boolean {
return (

View file

@ -1,8 +1,8 @@
import { logger } from '@librechat/data-schemas';
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 { AppConfig, BalanceUpdateFields } from '~/types';
import type { BalanceUpdateFields } from '~/types';
import { getBalanceConfig } from '~/app/config';
export interface BalanceMiddlewareOptions {

View file

@ -1,6 +1,5 @@
import type { Request } from 'express';
import type { IUser } from '@librechat/data-schemas';
import type { AppConfig } from './config';
import type { IUser, AppConfig } from '@librechat/data-schemas';
/**
* LibreChat-specific request body type that extends Express Request body

View file

@ -1,4 +1,3 @@
export * from './config';
export * from './azure';
export * from './balance';
export * from './endpoints';
@ -11,6 +10,4 @@ export * from './mistral';
export * from './openai';
export * from './prompts';
export * from './run';
export * from './tools';
export * from './zod';
export * from './anthropic';

View file

@ -3,8 +3,8 @@ import { openAISchema, EModelEndpoint } 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 { OpenAIClientOptions, Providers } from '@librechat/agents';
import type { AppConfig } from '@librechat/data-schemas';
import type { AzureOptions } from './azure';
import type { AppConfig } from './config';
export type OpenAIParameters = z.infer<typeof openAISchema>;

View file

@ -1,10 +0,0 @@
import type { JsonSchemaType } from './zod';
export interface FunctionTool {
type: 'function';
function: {
description: string;
name: string;
parameters: JsonSchemaType;
};
}

View file

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

View file

@ -1,4 +1,4 @@
import type { AppConfig } from '~/types';
import type { AppConfig } from '@librechat/data-schemas';
import {
createTempChatExpirationDate,
getTempChatRetentionHours,

View file

@ -1,5 +1,5 @@
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

View file

@ -1,3 +1,5 @@
import { webSearchAuth } from '@librechat/data-schemas';
import { SafeSearchTypes, AuthType } from 'librechat-data-provider';
import type {
ScraperTypes,
TCustomConfig,
@ -5,8 +7,7 @@ import type {
SearchProviders,
TWebSearchConfig,
} from 'librechat-data-provider';
import { webSearchAuth, loadWebSearchAuth, extractWebSearchEnvVars } from './web';
import { SafeSearchTypes, AuthType } from 'librechat-data-provider';
import { loadWebSearchAuth, extractWebSearchEnvVars } from './web';
// Mock the extractVariableName function
jest.mock('../utils', () => ({

View file

@ -1,3 +1,9 @@
import {
AuthType,
SafeSearchTypes,
SearchCategories,
extractVariableName,
} from 'librechat-data-provider';
import type {
ScraperTypes,
RerankerTypes,
@ -5,108 +11,8 @@ import type {
SearchProviders,
TWebSearchConfig,
} from 'librechat-data-provider';
import {
SearchCategories,
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();
import { webSearchAuth } from '@librechat/data-schemas';
import type { TWebSearchKeys, TWebSearchCategories } from '@librechat/data-schemas';
export function extractWebSearchEnvVars({
keys,

View file

@ -10,44 +10,6 @@ import { extractEnvVariable, envVarRegex } from '../src/utils';
import { azureGroupConfigsSchema } from '../src/config';
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 {
let isValid = true;
const modelNames: string[] = [];
@ -239,13 +201,13 @@ export function mapModelToAzureConfig({
const { deploymentName = '', version = '' } =
typeof modelDetails === 'object'
? {
deploymentName: modelDetails.deploymentName ?? groupConfig.deploymentName,
version: modelDetails.version ?? groupConfig.version,
}
deploymentName: modelDetails.deploymentName ?? groupConfig.deploymentName,
version: modelDetails.version ?? groupConfig.version,
}
: {
deploymentName: groupConfig.deploymentName,
version: groupConfig.version,
};
deploymentName: groupConfig.deploymentName,
version: groupConfig.version,
};
if (!deploymentName || !version) {
throw new Error(
@ -335,13 +297,13 @@ export function mapGroupToAzureConfig({
const { deploymentName = '', version = '' } =
typeof modelDetails === 'object'
? {
deploymentName: modelDetails.deploymentName ?? groupConfig.deploymentName,
version: modelDetails.version ?? groupConfig.version,
}
deploymentName: modelDetails.deploymentName ?? groupConfig.deploymentName,
version: modelDetails.version ?? groupConfig.version,
}
: {
deploymentName: groupConfig.deploymentName,
version: groupConfig.version,
};
deploymentName: groupConfig.deploymentName,
version: groupConfig.version,
};
if (!deploymentName || !version) {
throw new Error(

View file

@ -155,8 +155,10 @@ export type TAzureGroupMap = Record<
export type TValidatedAzureConfig = {
modelNames: string[];
modelGroupMap: TAzureModelGroupMap;
groupMap: TAzureGroupMap;
assistantModels?: string[];
assistantGroups?: string[];
modelGroupMap: TAzureModelGroupMap;
};
export type TAzureConfigValidationResult = TValidatedAzureConfig & {
@ -752,7 +754,7 @@ export const webSearchSchema = z.object({
.optional(),
});
export type TWebSearchConfig = z.infer<typeof webSearchSchema>;
export type TWebSearchConfig = DeepPartial<z.infer<typeof webSearchSchema>>;
export const ocrSchema = z.object({
mistralModel: z.string().optional(),
@ -799,7 +801,7 @@ export const memorySchema = z.object({
.optional(),
});
export type TMemoryConfig = z.infer<typeof memorySchema>;
export type TMemoryConfig = DeepPartial<z.infer<typeof memorySchema>>;
const customEndpointsSchema = z.array(endpointSchema.partial()).optional();
@ -862,9 +864,27 @@ export const configSchema = z.object({
.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 TProviderSchema =

View file

@ -10,8 +10,8 @@ import type { TCustomConfig, TAgentsEndpoint } from 'librechat-data-provider';
* @returns The Agents endpoint configuration.
*/
export function agentsConfigSetup(
config: TCustomConfig,
defaultConfig: Partial<TAgentsEndpoint>,
config: Partial<TCustomConfig>,
defaultConfig?: Partial<TAgentsEndpoint>,
): Partial<TAgentsEndpoint> {
const agentsConfig = config?.endpoints?.[EModelEndpoint.agents];

View file

@ -0,0 +1,70 @@
import logger from '~/config/winston';
import {
Capabilities,
EModelEndpoint,
assistantEndpointSchema,
defaultAssistantsVersion,
} 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.
* @returns The Assistants endpoint configuration.
*/
export function azureAssistantsDefaults(): {
capabilities: TAssistantEndpoint['capabilities'];
version: TAssistantEndpoint['version'];
} {
return {
capabilities: [Capabilities.tools, Capabilities.actions, Capabilities.code_interpreter],
version: defaultAssistantsVersion.azureAssistants,
};
}
/**
* Sets up the Assistants configuration from the config (`librechat.yaml`) file.
* @param config - The loaded custom configuration.
* @param assistantsEndpoint - The Assistants endpoint name.
* - The previously loaded assistants configuration from Azure OpenAI Assistants option.
* @param [prevConfig]
* @returns The Assistants endpoint configuration.
*/
export function assistantsConfigSetup(
config: Partial<TCustomConfig>,
assistantsEndpoint: EModelEndpoint.assistants | EModelEndpoint.azureAssistants,
prevConfig: Partial<TAssistantEndpoint> = {},
): Partial<TAssistantEndpoint> {
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,
};
}

View file

@ -0,0 +1,71 @@
import logger from '~/config/winston';
import {
EModelEndpoint,
validateAzureGroups,
mapModelToAzureConfig,
} from 'librechat-data-provider';
import type { TCustomConfig, TAzureConfig } from 'librechat-data-provider';
/**
* Sets up the Azure OpenAI configuration from the config (`librechat.yaml`) file.
* @param config - The loaded custom configuration.
* @returns The Azure OpenAI configuration.
*/
export function azureConfigSetup(config: Partial<TCustomConfig>): TAzureConfig {
const azureConfig = config.endpoints?.[EModelEndpoint.azureOpenAI];
if (!azureConfig) {
throw new Error('Azure OpenAI configuration is missing.');
}
const { groups, ...azureConfiguration } = azureConfig;
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: string[] = [];
const assistantGroups = new Set<string>();
for (const modelName of modelNames) {
mapModelToAzureConfig({ modelName, modelGroupMap, groupMap });
const groupName = modelGroupMap?.[modelName]?.group;
const modelGroup = groupMap?.[groupName];
const supportsAssistants = modelGroup?.assistants || modelGroup?.[modelName]?.assistants;
if (supportsAssistants) {
assistantModels.push(modelName);
if (!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 {
errors,
isValid,
groupMap,
modelNames,
modelGroupMap,
assistantModels,
assistantGroups: Array.from(assistantGroups),
...azureConfiguration,
};
}

View file

@ -0,0 +1,66 @@
import { EModelEndpoint } from 'librechat-data-provider';
import type { TCustomConfig, TAgentsEndpoint } from 'librechat-data-provider';
import type { AppConfig } from '~/types';
import { azureAssistantsDefaults, assistantsConfigSetup } from './assistants';
import { agentsConfigSetup } from './agents';
import { azureConfigSetup } from './azure';
/**
* Loads custom config endpoints
* @param [config]
* @param [agentsDefaults]
*/
export const loadEndpoints = (
config: Partial<TCustomConfig>,
agentsDefaults?: Partial<TAgentsEndpoint>,
) => {
const loadedEndpoints: AppConfig['endpoints'] = {};
const endpoints = config?.endpoints;
if (endpoints?.[EModelEndpoint.azureOpenAI]) {
loadedEndpoints[EModelEndpoint.azureOpenAI] = azureConfigSetup(config);
}
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) => {
const currentKey = key as keyof typeof endpoints;
if (endpoints?.[currentKey]) {
loadedEndpoints[currentKey] = endpoints[currentKey];
}
});
if (endpoints?.all) {
loadedEndpoints.all = endpoints.all;
}
return loadedEndpoints;
};

View file

@ -0,0 +1,6 @@
export * from './agents';
export * from './interface';
export * from './service';
export * from './specs';
export * from './turnstile';
export * from './web';

View file

@ -1,8 +1,7 @@
import { logger } from '@librechat/data-schemas';
import { removeNullishValues } from 'librechat-data-provider';
import type { TCustomConfig, TConfigDefaults } from 'librechat-data-provider';
import type { AppConfig } from '~/types/config';
import { isMemoryEnabled } from '~/memory/config';
import type { AppConfig } from '~/types/app';
import { isMemoryEnabled } from './memory';
/**
* Loads the default interface object.
@ -58,51 +57,5 @@ export async function loadDefaultInterface({
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;
}

View 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);
}

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

View file

@ -0,0 +1,113 @@
import { EModelEndpoint, getConfigDefaults } from 'librechat-data-provider';
import type { TCustomConfig, FileSources, DeepPartial } from 'librechat-data-provider';
import type { AppConfig, FunctionTool } from '~/types/app';
import { loadDefaultInterface } from './interface';
import { loadTurnstileConfig } from './turnstile';
import { agentsConfigSetup } from './agents';
import { loadWebSearchConfig } from './web';
import { processModelSpecs } from './specs';
import { loadMemoryConfig } from './memory';
import { loadEndpoints } from './endpoints';
import { loadOCRConfig } from './ocr';
export type Paths = {
root: string;
uploads: string;
clientPath: string;
dist: string;
publicPath: string;
fonts: string;
assets: string;
imageOutput: string;
structuredTools: string;
pluginManifest: string;
};
/**
* Loads custom config and initializes app-wide variables.
* @function AppService
*/
export const AppService = async (params?: {
config: DeepPartial<TCustomConfig>;
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 ocr = loadOCRConfig(config.ocr);
const webSearch = loadWebSearchConfig(config.webSearch);
const memory = loadMemoryConfig(config.memory);
const filteredTools = config.filteredTools;
const includedTools = config.includedTools;
const fileStrategy = (config.fileStrategy ?? configDefaults.fileStrategy) as
| FileSources.local
| FileSources.s3
| FileSources.firebase
| FileSources.azure_blob;
const startBalance = process.env.START_BALANCE;
const balance = config.balance ?? {
enabled: process.env.CHECK_BALANCE?.toLowerCase().trim() === 'true',
startBalance: startBalance ? parseInt(startBalance, 10) : undefined,
};
const transactions = config.transactions ?? configDefaults.transactions;
const imageOutputType = config?.imageOutputType ?? configDefaults.imageOutputType;
process.env.CDN_PROVIDER = fileStrategy;
const availableTools = systemTools;
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;
}
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;
};

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

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

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

View file

@ -1,3 +1,4 @@
export * from './app';
export * from './common';
export * from './crypto';
export * from './schema';

View file

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

View file

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

View file

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

View file

@ -9,7 +9,31 @@ import type {
TCustomEndpoints,
TAssistantEndpoint,
} 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
@ -17,11 +41,11 @@ import type { FunctionTool } from './tools';
*/
export interface AppConfig {
/** The main custom configuration */
config: TCustomConfig;
config: Partial<TCustomConfig>;
/** OCR configuration */
ocr?: TCustomConfig['ocr'];
/** File paths configuration */
paths: {
paths?: {
uploads: string;
imageOutput: string;
publicPath: string;
@ -34,7 +58,7 @@ export interface AppConfig {
/** File storage strategy ('local', 's3', 'firebase', 'azure_blob') */
fileStrategy: FileSources.local | FileSources.s3 | FileSources.firebase | FileSources.azure_blob;
/** File strategies configuration */
fileStrategies: TCustomConfig['fileStrategies'];
fileStrategies?: TCustomConfig['fileStrategies'];
/** Registration configurations */
registration?: TCustomConfig['registration'];
/** Actions configurations */
@ -48,9 +72,9 @@ export interface AppConfig {
/** Interface configuration */
interfaceConfig?: TCustomConfig['interface'];
/** Turnstile configuration */
turnstileConfig?: TCustomConfig['turnstile'];
turnstileConfig?: Partial<TCustomConfig['turnstile']>;
/** Balance configuration */
balance?: TCustomConfig['balance'];
balance?: Partial<TCustomConfig['balance']>;
/** Transactions configuration */
transactions?: TCustomConfig['transactions'];
/** Speech configuration */
@ -67,26 +91,26 @@ export interface AppConfig {
availableTools?: Record<string, FunctionTool>;
endpoints?: {
/** OpenAI endpoint configuration */
openAI?: TEndpoint;
openAI?: Partial<TEndpoint>;
/** Google endpoint configuration */
google?: TEndpoint;
google?: Partial<TEndpoint>;
/** Bedrock endpoint configuration */
bedrock?: TEndpoint;
bedrock?: Partial<TEndpoint>;
/** Anthropic endpoint configuration */
anthropic?: TEndpoint;
anthropic?: Partial<TEndpoint>;
/** GPT plugins endpoint configuration */
gptPlugins?: TEndpoint;
gptPlugins?: Partial<TEndpoint>;
/** Azure OpenAI endpoint configuration */
azureOpenAI?: TAzureConfig;
/** Assistants endpoint configuration */
assistants?: TAssistantEndpoint;
assistants?: Partial<TAssistantEndpoint>;
/** Azure assistants endpoint configuration */
azureAssistants?: TAssistantEndpoint;
azureAssistants?: Partial<TAssistantEndpoint>;
/** Agents endpoint configuration */
[EModelEndpoint.agents]?: TAgentsEndpoint;
[EModelEndpoint.agents]?: Partial<TAgentsEndpoint>;
/** Custom endpoints configuration */
[EModelEndpoint.custom]?: TCustomEndpoints;
/** Global endpoint configuration */
all?: TEndpoint;
all?: Partial<TEndpoint>;
};
}

View file

@ -1,6 +1,7 @@
import type { Types } from 'mongoose';
export type ObjectId = Types.ObjectId;
export * from './app';
export * from './user';
export * from './token';
export * from './convo';
@ -24,3 +25,5 @@ export * from './prompts';
export * from './accessRole';
export * from './aclEntry';
export * from './group';
/* Web */
export * from './web';

View file

@ -1,4 +1,5 @@
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { DeepPartial } from 'librechat-data-provider';
import type { Document } from 'mongoose';
import { CursorPaginationParams } from '~/common';
@ -54,9 +55,6 @@ export interface IRole extends Document {
}
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 interface CreateRoleRequest {

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