mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-28 21:26:13 +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
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,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue