mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🔃 refactor: Decouple Effects from AppService, move to data-schemas (#9974)
* chore: linting for `loadCustomConfig` * refactor: decouple CDN init and variable/health checks from AppService * refactor: move AppService to packages/data-schemas * chore: update AppConfig import path to use data-schemas * chore: update JsonSchemaType import path to use data-schemas * refactor: update UserController to import webSearchKeys and redefine FunctionTool typedef * chore: remove AppService.js * refactor: update AppConfig interface to use Partial<TCustomConfig> and make paths and fileStrategies optional * refactor: update checkConfig function to accept Partial<TCustomConfig> * chore: fix types * refactor: move handleRateLimits to startup checks as is an effect * test: remove outdated rate limit tests from AppService.spec and add new handleRateLimits tests in checks.spec
This commit is contained in:
parent
9ff608e6af
commit
838fb53208
73 changed files with 1383 additions and 1326 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import { EModelEndpoint, agentsEndpointSchema } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TAgentsEndpoint } from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
* Sets up the Agents configuration from the config (`librechat.yaml`) file.
|
||||
* If no agents config is defined, uses the provided defaults or parses empty object.
|
||||
*
|
||||
* @param config - The loaded custom configuration.
|
||||
* @param [defaultConfig] - Default configuration from getConfigDefaults.
|
||||
* @returns The Agents endpoint configuration.
|
||||
*/
|
||||
export function agentsConfigSetup(
|
||||
config: TCustomConfig,
|
||||
defaultConfig: Partial<TAgentsEndpoint>,
|
||||
): Partial<TAgentsEndpoint> {
|
||||
const agentsConfig = config?.endpoints?.[EModelEndpoint.agents];
|
||||
|
||||
if (!agentsConfig) {
|
||||
return defaultConfig || agentsEndpointSchema.parse({});
|
||||
}
|
||||
|
||||
const parsedConfig = agentsEndpointSchema.parse(agentsConfig);
|
||||
return parsedConfig;
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export * from './config';
|
||||
export * from './memory';
|
||||
export * from './migration';
|
||||
export * from './legacy';
|
||||
|
|
|
|||
|
|
@ -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', () => ({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
157
packages/api/src/app/AppService.interface.spec.ts
Normal file
157
packages/api/src/app/AppService.interface.spec.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
jest.mock('@librechat/data-schemas', () => ({
|
||||
...jest.requireActual('@librechat/data-schemas'),
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { AppService } from '@librechat/data-schemas';
|
||||
|
||||
describe('AppService interface configuration', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should set prompts to true when config specifies prompts as true', async () => {
|
||||
const config = {
|
||||
interface: {
|
||||
prompts: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await AppService({ config });
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
interfaceConfig: expect.objectContaining({
|
||||
prompts: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set prompts and bookmarks to false when config specifies them as false', async () => {
|
||||
const config = {
|
||||
interface: {
|
||||
prompts: false,
|
||||
bookmarks: false,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await AppService({ config });
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
interfaceConfig: expect.objectContaining({
|
||||
prompts: false,
|
||||
bookmarks: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not set prompts and bookmarks when not provided in config', async () => {
|
||||
const config = {};
|
||||
|
||||
const result = await AppService({ config });
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
interfaceConfig: expect.anything(),
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify that prompts and bookmarks are undefined when not provided
|
||||
expect(result.interfaceConfig?.prompts).toBeUndefined();
|
||||
expect(result.interfaceConfig?.bookmarks).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set prompts and bookmarks to different values when specified differently in config', async () => {
|
||||
const config = {
|
||||
interface: {
|
||||
prompts: true,
|
||||
bookmarks: false,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await AppService({ config });
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
interfaceConfig: expect.objectContaining({
|
||||
prompts: true,
|
||||
bookmarks: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly configure peoplePicker permissions including roles', async () => {
|
||||
const config = {
|
||||
interface: {
|
||||
peoplePicker: {
|
||||
users: true,
|
||||
groups: true,
|
||||
roles: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await AppService({ config });
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
interfaceConfig: expect.objectContaining({
|
||||
peoplePicker: expect.objectContaining({
|
||||
users: true,
|
||||
groups: true,
|
||||
roles: true,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle mixed peoplePicker permissions', async () => {
|
||||
const config = {
|
||||
interface: {
|
||||
peoplePicker: {
|
||||
users: true,
|
||||
groups: false,
|
||||
roles: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await AppService({ config });
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
interfaceConfig: expect.objectContaining({
|
||||
peoplePicker: expect.objectContaining({
|
||||
users: true,
|
||||
groups: false,
|
||||
roles: true,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not set peoplePicker when not provided in config', async () => {
|
||||
const config = {};
|
||||
|
||||
const result = await AppService({ config });
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
interfaceConfig: expect.anything(),
|
||||
}),
|
||||
);
|
||||
|
||||
// Verify that peoplePicker is undefined when not provided
|
||||
expect(result.interfaceConfig?.peoplePicker).toBeUndefined();
|
||||
});
|
||||
});
|
||||
814
packages/api/src/app/AppService.spec.ts
Normal file
814
packages/api/src/app/AppService.spec.ts
Normal 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,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
26
packages/api/src/app/cdn.ts
Normal file
26
packages/api/src/app/cdn.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import { FileSources } from 'librechat-data-provider';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import { initializeAzureBlobService } from '~/cdn/azure';
|
||||
import { initializeFirebase } from '~/cdn/firebase';
|
||||
import { initializeS3 } from '~/cdn/s3';
|
||||
|
||||
/**
|
||||
* Initializes file storage clients based on the configured file strategy.
|
||||
* This should be called after loading the app configuration.
|
||||
* @param {Object} options
|
||||
* @param {AppConfig} options.appConfig - The application configuration
|
||||
*/
|
||||
export function initializeFileStorage(appConfig: AppConfig) {
|
||||
const { fileStrategy } = appConfig;
|
||||
|
||||
if (fileStrategy === FileSources.firebase) {
|
||||
initializeFirebase();
|
||||
} else if (fileStrategy === FileSources.azure_blob) {
|
||||
initializeAzureBlobService().catch((error) => {
|
||||
logger.error('Error initializing Azure Blob Service:', error);
|
||||
});
|
||||
} else if (fileStrategy === FileSources.s3) {
|
||||
initializeS3();
|
||||
}
|
||||
}
|
||||
358
packages/api/src/app/checks.spec.ts
Normal file
358
packages/api/src/app/checks.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
307
packages/api/src/app/checks.ts
Normal file
307
packages/api/src/app/checks.ts
Normal 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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './config';
|
||||
export * from './interface';
|
||||
export * from './permissions';
|
||||
export * from './cdn';
|
||||
export * from './checks';
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
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';
|
||||
|
||||
/**
|
||||
* Loads the default interface object.
|
||||
* @param params - The loaded custom configuration.
|
||||
* @param params.config - The loaded custom configuration.
|
||||
* @param params.configDefaults - The custom configuration default values.
|
||||
* @returns default interface object.
|
||||
*/
|
||||
export async function loadDefaultInterface({
|
||||
config,
|
||||
configDefaults,
|
||||
}: {
|
||||
config?: Partial<TCustomConfig>;
|
||||
configDefaults: TConfigDefaults;
|
||||
}): Promise<AppConfig['interfaceConfig']> {
|
||||
const { interface: interfaceConfig } = config ?? {};
|
||||
const { interface: defaults } = configDefaults;
|
||||
const hasModelSpecs = (config?.modelSpecs?.list?.length ?? 0) > 0;
|
||||
const includesAddedEndpoints = (config?.modelSpecs?.addedEndpoints?.length ?? 0) > 0;
|
||||
|
||||
const memoryConfig = config?.memory;
|
||||
const memoryEnabled = isMemoryEnabled(memoryConfig);
|
||||
/** Only disable memories if memory config is present but disabled/invalid */
|
||||
const shouldDisableMemories = memoryConfig && !memoryEnabled;
|
||||
|
||||
const loadedInterface: AppConfig['interfaceConfig'] = removeNullishValues({
|
||||
// UI elements - use schema defaults
|
||||
endpointsMenu:
|
||||
interfaceConfig?.endpointsMenu ?? (hasModelSpecs ? false : defaults.endpointsMenu),
|
||||
modelSelect:
|
||||
interfaceConfig?.modelSelect ??
|
||||
(hasModelSpecs ? includesAddedEndpoints : defaults.modelSelect),
|
||||
parameters: interfaceConfig?.parameters ?? (hasModelSpecs ? false : defaults.parameters),
|
||||
presets: interfaceConfig?.presets ?? (hasModelSpecs ? false : defaults.presets),
|
||||
sidePanel: interfaceConfig?.sidePanel ?? defaults.sidePanel,
|
||||
privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy,
|
||||
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
|
||||
mcpServers: interfaceConfig?.mcpServers ?? defaults.mcpServers,
|
||||
customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome,
|
||||
|
||||
// Permissions - only include if explicitly configured
|
||||
bookmarks: interfaceConfig?.bookmarks,
|
||||
memories: shouldDisableMemories ? false : interfaceConfig?.memories,
|
||||
prompts: interfaceConfig?.prompts,
|
||||
multiConvo: interfaceConfig?.multiConvo,
|
||||
agents: interfaceConfig?.agents,
|
||||
temporaryChat: interfaceConfig?.temporaryChat,
|
||||
runCode: interfaceConfig?.runCode,
|
||||
webSearch: interfaceConfig?.webSearch,
|
||||
fileSearch: interfaceConfig?.fileSearch,
|
||||
fileCitations: interfaceConfig?.fileCitations,
|
||||
peoplePicker: interfaceConfig?.peoplePicker,
|
||||
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;
|
||||
}
|
||||
55
packages/api/src/app/limits.ts
Normal file
55
packages/api/src/app/limits.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { RateLimitPrefix } from 'librechat-data-provider';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param rateLimits
|
||||
*/
|
||||
export const handleRateLimits = (rateLimits?: TCustomConfig['rateLimits']) => {
|
||||
if (!rateLimits) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rateLimitKeys = {
|
||||
fileUploads: RateLimitPrefix.FILE_UPLOAD,
|
||||
conversationsImport: RateLimitPrefix.IMPORT,
|
||||
tts: RateLimitPrefix.TTS,
|
||||
stt: RateLimitPrefix.STT,
|
||||
};
|
||||
|
||||
Object.entries(rateLimitKeys).forEach(([key, prefix]) => {
|
||||
const rateLimit = rateLimits[key as keyof typeof rateLimitKeys];
|
||||
if (rateLimit) {
|
||||
setRateLimitEnvVars(prefix, rateLimit);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
type RateLimitConfig = {
|
||||
ipMax?: number | undefined;
|
||||
ipWindowInMinutes?: number | undefined;
|
||||
userMax?: number | undefined;
|
||||
userWindowInMinutes?: number | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set environment variables for rate limit configurations
|
||||
*
|
||||
* @param prefix - Prefix for environment variable names
|
||||
* @param rateLimit - Rate limit configuration object
|
||||
*/
|
||||
const setRateLimitEnvVars = (prefix: string, rateLimit: RateLimitConfig) => {
|
||||
const envVarsMapping = {
|
||||
ipMax: `${prefix}_IP_MAX`,
|
||||
ipWindowInMinutes: `${prefix}_IP_WINDOW`,
|
||||
userMax: `${prefix}_USER_MAX`,
|
||||
userWindowInMinutes: `${prefix}_USER_WINDOW`,
|
||||
};
|
||||
|
||||
Object.entries(envVarsMapping).forEach(([key, envVar]) => {
|
||||
const value = rateLimit[key as keyof RateLimitConfig];
|
||||
if (value !== undefined) {
|
||||
process.env[envVar] = value.toString();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { loadDefaultInterface } from '@librechat/data-schemas';
|
||||
import { SystemRoles, Permissions, PermissionTypes, roleDefaults } from 'librechat-data-provider';
|
||||
import 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();
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
|||
54
packages/api/src/cdn/azure.ts
Normal file
54
packages/api/src/cdn/azure.ts
Normal 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;
|
||||
};
|
||||
42
packages/api/src/cdn/firebase.ts
Normal file
42
packages/api/src/cdn/firebase.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import firebase from 'firebase/app';
|
||||
import { getStorage } from 'firebase/storage';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import type { FirebaseStorage } from 'firebase/storage';
|
||||
import type { FirebaseApp } from 'firebase/app';
|
||||
|
||||
let firebaseInitCount = 0;
|
||||
let firebaseApp: FirebaseApp | null = null;
|
||||
|
||||
export const initializeFirebase = () => {
|
||||
if (firebaseApp) {
|
||||
return firebaseApp;
|
||||
}
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: process.env.FIREBASE_API_KEY,
|
||||
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.FIREBASE_APP_ID,
|
||||
};
|
||||
|
||||
if (Object.values(firebaseConfig).some((value) => !value)) {
|
||||
if (firebaseInitCount === 0) {
|
||||
logger.info(
|
||||
'[Optional] Firebase CDN not initialized. To enable, set FIREBASE_API_KEY, FIREBASE_AUTH_DOMAIN, FIREBASE_PROJECT_ID, FIREBASE_STORAGE_BUCKET, FIREBASE_MESSAGING_SENDER_ID, and FIREBASE_APP_ID environment variables.',
|
||||
);
|
||||
}
|
||||
firebaseInitCount++;
|
||||
return null;
|
||||
}
|
||||
|
||||
firebaseApp = firebase.initializeApp(firebaseConfig);
|
||||
logger.info('Firebase CDN initialized');
|
||||
return firebaseApp;
|
||||
};
|
||||
|
||||
export const getFirebaseStorage = (): FirebaseStorage | null => {
|
||||
const app = initializeFirebase();
|
||||
return app ? getStorage(app) : null;
|
||||
};
|
||||
3
packages/api/src/cdn/index.ts
Normal file
3
packages/api/src/cdn/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './azure';
|
||||
export * from './firebase';
|
||||
export * from './s3';
|
||||
51
packages/api/src/cdn/s3.ts
Normal file
51
packages/api/src/cdn/s3.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export * from './app';
|
||||
export * from './cdn';
|
||||
/* Auth */
|
||||
export * from './auth';
|
||||
/* MCP */
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
import type {
|
||||
TEndpoint,
|
||||
FileSources,
|
||||
TAzureConfig,
|
||||
TCustomConfig,
|
||||
TMemoryConfig,
|
||||
EModelEndpoint,
|
||||
TAgentsEndpoint,
|
||||
TCustomEndpoints,
|
||||
TAssistantEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type { FunctionTool } from './tools';
|
||||
|
||||
/**
|
||||
* Application configuration object
|
||||
* Based on the configuration defined in api/server/services/Config/getAppConfig.js
|
||||
*/
|
||||
export interface AppConfig {
|
||||
/** The main custom configuration */
|
||||
config: TCustomConfig;
|
||||
/** OCR configuration */
|
||||
ocr?: TCustomConfig['ocr'];
|
||||
/** File paths configuration */
|
||||
paths: {
|
||||
uploads: string;
|
||||
imageOutput: string;
|
||||
publicPath: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
/** Memory configuration */
|
||||
memory?: TMemoryConfig;
|
||||
/** Web search configuration */
|
||||
webSearch?: TCustomConfig['webSearch'];
|
||||
/** File storage strategy ('local', 's3', 'firebase', 'azure_blob') */
|
||||
fileStrategy: FileSources.local | FileSources.s3 | FileSources.firebase | FileSources.azure_blob;
|
||||
/** File strategies configuration */
|
||||
fileStrategies: TCustomConfig['fileStrategies'];
|
||||
/** Registration configurations */
|
||||
registration?: TCustomConfig['registration'];
|
||||
/** Actions configurations */
|
||||
actions?: TCustomConfig['actions'];
|
||||
/** Admin-filtered tools */
|
||||
filteredTools?: string[];
|
||||
/** Admin-included tools */
|
||||
includedTools?: string[];
|
||||
/** Image output type configuration */
|
||||
imageOutputType: string;
|
||||
/** Interface configuration */
|
||||
interfaceConfig?: TCustomConfig['interface'];
|
||||
/** Turnstile configuration */
|
||||
turnstileConfig?: TCustomConfig['turnstile'];
|
||||
/** Balance configuration */
|
||||
balance?: TCustomConfig['balance'];
|
||||
/** Transactions configuration */
|
||||
transactions?: TCustomConfig['transactions'];
|
||||
/** Speech configuration */
|
||||
speech?: TCustomConfig['speech'];
|
||||
/** MCP server configuration */
|
||||
mcpConfig?: TCustomConfig['mcpServers'] | null;
|
||||
/** File configuration */
|
||||
fileConfig?: TCustomConfig['fileConfig'];
|
||||
/** Secure image links configuration */
|
||||
secureImageLinks?: TCustomConfig['secureImageLinks'];
|
||||
/** Processed model specifications */
|
||||
modelSpecs?: TCustomConfig['modelSpecs'];
|
||||
/** Available tools */
|
||||
availableTools?: Record<string, FunctionTool>;
|
||||
endpoints?: {
|
||||
/** OpenAI endpoint configuration */
|
||||
openAI?: TEndpoint;
|
||||
/** Google endpoint configuration */
|
||||
google?: TEndpoint;
|
||||
/** Bedrock endpoint configuration */
|
||||
bedrock?: TEndpoint;
|
||||
/** Anthropic endpoint configuration */
|
||||
anthropic?: TEndpoint;
|
||||
/** GPT plugins endpoint configuration */
|
||||
gptPlugins?: TEndpoint;
|
||||
/** Azure OpenAI endpoint configuration */
|
||||
azureOpenAI?: TAzureConfig;
|
||||
/** Assistants endpoint configuration */
|
||||
assistants?: TAssistantEndpoint;
|
||||
/** Azure assistants endpoint configuration */
|
||||
azureAssistants?: TAssistantEndpoint;
|
||||
/** Agents endpoint configuration */
|
||||
[EModelEndpoint.agents]?: TAgentsEndpoint;
|
||||
/** Custom endpoints configuration */
|
||||
[EModelEndpoint.custom]?: TCustomEndpoints;
|
||||
/** Global endpoint configuration */
|
||||
all?: TEndpoint;
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import type { JsonSchemaType } from './zod';
|
||||
|
||||
export interface FunctionTool {
|
||||
type: 'function';
|
||||
function: {
|
||||
description: string;
|
||||
name: string;
|
||||
parameters: JsonSchemaType;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
export type JsonSchemaType = {
|
||||
type: 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'array' | 'object';
|
||||
enum?: string[];
|
||||
items?: JsonSchemaType;
|
||||
properties?: Record<string, JsonSchemaType>;
|
||||
required?: string[];
|
||||
description?: string;
|
||||
additionalProperties?: boolean | JsonSchemaType;
|
||||
};
|
||||
|
||||
export type ConvertJsonSchemaToZodOptions = {
|
||||
allowEmptyObject?: boolean;
|
||||
dropFields?: string[];
|
||||
transformOneOfAnyOf?: boolean;
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { AppConfig } from '~/types';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import {
|
||||
createTempChatExpirationDate,
|
||||
getTempChatRetentionHours,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', () => ({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue