| | jest.mock('axios'); |
| | jest.mock('~/cache/getLogStores'); |
| | jest.mock('@librechat/api', () => ({ |
| | ...jest.requireActual('@librechat/api'), |
| | loadYaml: jest.fn(), |
| | })); |
| | jest.mock('librechat-data-provider', () => { |
| | const actual = jest.requireActual('librechat-data-provider'); |
| | return { |
| | ...actual, |
| | paramSettings: { foo: {}, bar: {}, custom: {} }, |
| | agentParamSettings: { |
| | custom: [], |
| | google: [ |
| | { |
| | key: 'pressure', |
| | type: 'string', |
| | component: 'input', |
| | }, |
| | { |
| | key: 'temperature', |
| | type: 'number', |
| | component: 'slider', |
| | default: 0.5, |
| | range: { |
| | min: 0, |
| | max: 2, |
| | step: 0.01, |
| | }, |
| | }, |
| | ], |
| | }, |
| | }; |
| | }); |
| |
|
| | jest.mock('@librechat/data-schemas', () => { |
| | return { |
| | logger: { |
| | info: jest.fn(), |
| | warn: jest.fn(), |
| | debug: jest.fn(), |
| | error: jest.fn(), |
| | }, |
| | }; |
| | }); |
| |
|
| | const axios = require('axios'); |
| | const { loadYaml } = require('@librechat/api'); |
| | const { logger } = require('@librechat/data-schemas'); |
| | const loadCustomConfig = require('./loadCustomConfig'); |
| |
|
| | describe('loadCustomConfig', () => { |
| | beforeEach(() => { |
| | jest.resetAllMocks(); |
| | delete process.env.CONFIG_PATH; |
| | }); |
| |
|
| | it('should return null and log error if remote config fetch fails', async () => { |
| | process.env.CONFIG_PATH = 'http://example.com/config.yaml'; |
| | axios.get.mockRejectedValue(new Error('Network error')); |
| | const result = await loadCustomConfig(); |
| | expect(logger.error).toHaveBeenCalledTimes(1); |
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | it('should return null for an invalid local config file', async () => { |
| | process.env.CONFIG_PATH = 'localConfig.yaml'; |
| | loadYaml.mockReturnValueOnce(null); |
| | const result = await loadCustomConfig(); |
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | it('should parse, validate, and cache a valid local configuration', async () => { |
| | const mockConfig = { |
| | version: '1.0', |
| | cache: true, |
| | endpoints: { |
| | custom: [ |
| | { |
| | name: 'mistral', |
| | apiKey: 'user_provided', |
| | baseURL: 'https://api.mistral.ai/v1', |
| | }, |
| | ], |
| | }, |
| | }; |
| | process.env.CONFIG_PATH = 'validConfig.yaml'; |
| | loadYaml.mockReturnValueOnce(mockConfig); |
| | const result = await loadCustomConfig(); |
| |
|
| | expect(result).toEqual(mockConfig); |
| | }); |
| |
|
| | it('should return null and log if config schema validation fails', async () => { |
| | const invalidConfig = { invalidField: true }; |
| | process.env.CONFIG_PATH = 'invalidConfig.yaml'; |
| | loadYaml.mockReturnValueOnce(invalidConfig); |
| |
|
| | const result = await loadCustomConfig(); |
| |
|
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | it('should handle and return null on YAML parse error for a string response from remote', async () => { |
| | process.env.CONFIG_PATH = 'http://example.com/config.yaml'; |
| | axios.get.mockResolvedValue({ data: 'invalidYAMLContent' }); |
| |
|
| | const result = await loadCustomConfig(); |
| |
|
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | it('should return the custom config object for a valid remote config file', async () => { |
| | const mockConfig = { |
| | version: '1.0', |
| | cache: true, |
| | endpoints: { |
| | custom: [ |
| | { |
| | name: 'mistral', |
| | apiKey: 'user_provided', |
| | baseURL: 'https://api.mistral.ai/v1', |
| | }, |
| | ], |
| | }, |
| | }; |
| | process.env.CONFIG_PATH = 'http://example.com/config.yaml'; |
| | axios.get.mockResolvedValue({ data: mockConfig }); |
| | const result = await loadCustomConfig(); |
| | expect(result).toEqual(mockConfig); |
| | }); |
| |
|
| | it('should return null if the remote config file is not found', async () => { |
| | process.env.CONFIG_PATH = 'http://example.com/config.yaml'; |
| | axios.get.mockRejectedValue({ response: { status: 404 } }); |
| | const result = await loadCustomConfig(); |
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | it('should return null if the local config file is not found', async () => { |
| | process.env.CONFIG_PATH = 'nonExistentConfig.yaml'; |
| | loadYaml.mockReturnValueOnce(null); |
| | const result = await loadCustomConfig(); |
| | expect(result).toBeNull(); |
| | }); |
| |
|
| | it('should not cache the config if cache is set to false', async () => { |
| | const mockConfig = { |
| | version: '1.0', |
| | cache: false, |
| | endpoints: { |
| | custom: [ |
| | { |
| | name: 'mistral', |
| | apiKey: 'user_provided', |
| | baseURL: 'https://api.mistral.ai/v1', |
| | }, |
| | ], |
| | }, |
| | }; |
| | process.env.CONFIG_PATH = 'validConfig.yaml'; |
| | loadYaml.mockReturnValueOnce(mockConfig); |
| | await loadCustomConfig(); |
| | }); |
| |
|
| | it('should log the loaded custom config', async () => { |
| | const mockConfig = { |
| | version: '1.0', |
| | cache: true, |
| | endpoints: { |
| | custom: [ |
| | { |
| | name: 'mistral', |
| | apiKey: 'user_provided', |
| | baseURL: 'https://api.mistral.ai/v1', |
| | }, |
| | ], |
| | }, |
| | }; |
| | process.env.CONFIG_PATH = 'validConfig.yaml'; |
| | loadYaml.mockReturnValueOnce(mockConfig); |
| | await loadCustomConfig(); |
| | expect(logger.info).toHaveBeenCalledWith('Custom config file loaded:'); |
| | expect(logger.info).toHaveBeenCalledWith(JSON.stringify(mockConfig, null, 2)); |
| | expect(logger.debug).toHaveBeenCalledWith('Custom config:', mockConfig); |
| | }); |
| |
|
| | describe('parseCustomParams', () => { |
| | const mockConfig = { |
| | version: '1.0', |
| | cache: false, |
| | endpoints: { |
| | custom: [ |
| | { |
| | name: 'Google', |
| | apiKey: 'user_provided', |
| | customParams: {}, |
| | }, |
| | ], |
| | }, |
| | }; |
| |
|
| | async function loadCustomParams(customParams) { |
| | mockConfig.endpoints.custom[0].customParams = customParams; |
| | loadYaml.mockReturnValue(mockConfig); |
| | return await loadCustomConfig(); |
| | } |
| |
|
| | beforeEach(() => { |
| | jest.resetAllMocks(); |
| | process.env.CONFIG_PATH = 'validConfig.yaml'; |
| | }); |
| |
|
| | it('returns no error when customParams is undefined', async () => { |
| | const result = await loadCustomParams(undefined); |
| | expect(result).toEqual(mockConfig); |
| | }); |
| |
|
| | it('returns no error when customParams is valid', async () => { |
| | const result = await loadCustomParams({ |
| | defaultParamsEndpoint: 'google', |
| | paramDefinitions: [ |
| | { |
| | key: 'temperature', |
| | default: 0.5, |
| | }, |
| | ], |
| | }); |
| | expect(result).toEqual(mockConfig); |
| | }); |
| |
|
| | it('throws an error when paramDefinitions contain unsupported keys', async () => { |
| | const malformedCustomParams = { |
| | defaultParamsEndpoint: 'google', |
| | paramDefinitions: [ |
| | { key: 'temperature', default: 0.5 }, |
| | { key: 'unsupportedKey', range: 0.5 }, |
| | ], |
| | }; |
| | await expect(loadCustomParams(malformedCustomParams)).rejects.toThrow( |
| | 'paramDefinitions of "Google" endpoint contains invalid key(s). Valid parameter keys are pressure, temperature', |
| | ); |
| | }); |
| |
|
| | it('throws an error when paramDefinitions is malformed', async () => { |
| | const malformedCustomParams = { |
| | defaultParamsEndpoint: 'google', |
| | paramDefinitions: [ |
| | { |
| | key: 'temperature', |
| | type: 'noomba', |
| | component: 'inpoot', |
| | optionType: 'custom', |
| | }, |
| | ], |
| | }; |
| | await expect(loadCustomParams(malformedCustomParams)).rejects.toThrow( |
| | /Custom parameter definitions for "Google" endpoint is malformed:/, |
| | ); |
| | }); |
| |
|
| | it('throws an error when defaultParamsEndpoint is not provided', async () => { |
| | const malformedCustomParams = { defaultParamsEndpoint: undefined }; |
| | await expect(loadCustomParams(malformedCustomParams)).rejects.toThrow( |
| | 'defaultParamsEndpoint of "Google" endpoint is invalid. Valid options are foo, bar, custom, google', |
| | ); |
| | }); |
| |
|
| | it('fills the paramDefinitions with missing values', async () => { |
| | const customParams = { |
| | defaultParamsEndpoint: 'google', |
| | paramDefinitions: [ |
| | { key: 'temperature', default: 0.7, range: { min: 0.1, max: 0.9, step: 0.1 } }, |
| | { key: 'pressure', component: 'textarea' }, |
| | ], |
| | }; |
| |
|
| | const parsedConfig = await loadCustomParams(customParams); |
| | const paramDefinitions = parsedConfig.endpoints.custom[0].customParams.paramDefinitions; |
| | expect(paramDefinitions).toEqual([ |
| | { |
| | columnSpan: 1, |
| | component: 'slider', |
| | default: 0.7, |
| | includeInput: true, |
| | key: 'temperature', |
| | label: 'temperature', |
| | optionType: 'custom', |
| | range: { |
| | |
| | max: 0.9, |
| | min: 0.1, |
| | step: 0.1, |
| | }, |
| | type: 'number', |
| | }, |
| | { |
| | columnSpan: 1, |
| | component: 'textarea', |
| | key: 'pressure', |
| | label: 'pressure', |
| | optionType: 'custom', |
| | placeholder: '', |
| | type: 'string', |
| | }, |
| | ]); |
| | }); |
| | }); |
| | }); |
| |
|