diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index e444594b4a..c9c42a2a1a 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -1,29 +1,17 @@ const { - Constants, FileSources, - Capabilities, EModelEndpoint, EImageOutputType, defaultSocialLogins, - validateAzureGroups, - mapModelToAzureConfig, - assistantEndpointSchema, - deprecatedAzureVariables, - conflictingAzureVariables, } = require('librechat-data-provider'); +const { checkVariables, checkHealth, checkConfig, checkAzureVariables } = require('./start/checks'); const { initializeFirebase } = require('./Files/Firebase/initialize'); +const { assistantsConfigSetup } = require('./start/assistants'); const loadCustomConfig = require('./Config/loadCustomConfig'); const handleRateLimits = require('./Config/handleRateLimits'); +const { azureConfigSetup } = require('./start/azureOpenAI'); const { loadAndFormatTools } = require('./ToolService'); const paths = require('~/config/paths'); -const { logger } = require('~/config'); - -const secretDefaults = { - CREDS_KEY: 'f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0', - CREDS_IV: 'e2341419ec3dd3d19b13a1a87fafcbfb', - JWT_SECRET: '16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef', - JWT_REFRESH_SECRET: 'eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8418', -}; /** * @@ -39,6 +27,9 @@ const AppService = async (app) => { const imageOutputType = config?.imageOutputType ?? EImageOutputType.PNG; process.env.CDN_PROVIDER = fileStrategy; + checkVariables(); + await checkHealth(); + if (fileStrategy === FileSources.firebase) { initializeFirebase(); } @@ -59,161 +50,41 @@ const AppService = async (app) => { if (!Object.keys(config).length) { app.locals = { + paths, fileStrategy, socialLogins, availableTools, imageOutputType, - paths, }; return; } - if (config.version !== Constants.CONFIG_VERSION) { - logger.info( - `\nOutdated Config version: ${config.version}. Current version: ${Constants.CONFIG_VERSION}\n\nCheck out the latest config file guide for new options and features.\nhttps://docs.librechat.ai/install/configuration/custom_config.html\n\n`, - ); - } - + checkConfig(config); handleRateLimits(config?.rateLimits); const endpointLocals = {}; if (config?.endpoints?.[EModelEndpoint.azureOpenAI]) { - const { groups, ...azureConfiguration } = config.endpoints[EModelEndpoint.azureOpenAI]; - const { isValid, modelNames, modelGroupMap, groupMap, errors } = validateAzureGroups(groups); - - if (!isValid) { - const errorString = errors.join('\n'); - const errorMessage = 'Invalid Azure OpenAI configuration:\n' + errorString; - logger.error(errorMessage); - throw new Error(errorMessage); - } - - const assistantModels = []; - const assistantGroups = new Set(); - for (const modelName of modelNames) { - mapModelToAzureConfig({ modelName, modelGroupMap, groupMap }); - const groupName = modelGroupMap?.[modelName]?.group; - const modelGroup = groupMap?.[groupName]; - let supportsAssistants = modelGroup?.assistants || modelGroup?.[modelName]?.assistants; - if (supportsAssistants) { - assistantModels.push(modelName); - !assistantGroups.has(groupName) && assistantGroups.add(groupName); - } - } - - if (azureConfiguration.assistants && assistantModels.length === 0) { - throw new Error( - 'No Azure models are configured to support assistants. Please remove the `assistants` field or configure at least one model to support assistants.', - ); - } - - endpointLocals[EModelEndpoint.azureOpenAI] = { - modelNames, - modelGroupMap, - groupMap, - assistantModels, - assistantGroups: Array.from(assistantGroups), - ...azureConfiguration, - }; - - 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.`, - ); - } - }); - - if (azureConfiguration.assistants) { - endpointLocals[EModelEndpoint.assistants] = { - // Note: may need to add retrieval models here in the future - capabilities: [Capabilities.tools, Capabilities.actions, Capabilities.code_interpreter], - }; - } + endpointLocals[EModelEndpoint.azureOpenAI] = azureConfigSetup(config); + checkAzureVariables(); } if (config?.endpoints?.[EModelEndpoint.assistants]) { - const assistantsConfig = config.endpoints[EModelEndpoint.assistants]; - const parsedConfig = assistantEndpointSchema.parse(assistantsConfig); - if (assistantsConfig.supportedIds?.length && assistantsConfig.excludedIds?.length) { - logger.warn( - `Both \`supportedIds\` and \`excludedIds\` are defined for the ${EModelEndpoint.assistants} endpoint; \`excludedIds\` field will be ignored.`, - ); - } - - const prevConfig = endpointLocals[EModelEndpoint.assistants] ?? {}; - - /** @type {Partial} */ - endpointLocals[EModelEndpoint.assistants] = { - ...prevConfig, - retrievalModels: parsedConfig.retrievalModels, - disableBuilder: parsedConfig.disableBuilder, - pollIntervalMs: parsedConfig.pollIntervalMs, - supportedIds: parsedConfig.supportedIds, - capabilities: parsedConfig.capabilities, - excludedIds: parsedConfig.excludedIds, - timeoutMs: parsedConfig.timeoutMs, - }; - } - - 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 (error) { - logger.warn( - `RAG API is either not running or not reachable at ${process.env.RAG_API_URL}, you may experience errors with file uploads.`, - ); + endpointLocals[EModelEndpoint.assistants] = assistantsConfigSetup(config); } app.locals = { + paths, socialLogins, fileStrategy, availableTools, imageOutputType, - fileConfig: config?.fileConfig, interface: config?.interface, + fileConfig: config?.fileConfig, secureImageLinks: config?.secureImageLinks, - paths, ...endpointLocals, }; - - let hasDefaultSecrets = false; - for (const [key, value] of Object.entries(secretDefaults)) { - if (process.env[key] === value) { - logger.warn(`Default value for ${key} is being used.`); - !hasDefaultSecrets && (hasDefaultSecrets = true); - } - } - - if (hasDefaultSecrets) { - logger.info( - `Please replace any default secret values. - - For your conveninence, fork & run this replit to generate your own secret values: - - https://replit.com/@daavila/crypto#index.js - - `, - ); - } - - if (process.env.GOOGLE_API_KEY) { - logger.warn( - 'The `GOOGLE_API_KEY` environment variable is deprecated.\nPlease use the `GOOGLE_SEARCH_API_KEY` environment variable instead.', - ); - } }; module.exports = AppService; diff --git a/api/server/services/start/assistants.js b/api/server/services/start/assistants.js new file mode 100644 index 0000000000..c092318ce8 --- /dev/null +++ b/api/server/services/start/assistants.js @@ -0,0 +1,40 @@ +const { + Capabilities, + EModelEndpoint, + assistantEndpointSchema, +} = require('librechat-data-provider'); +const { logger } = require('~/config'); + +/** + * Sets up the Assistants configuration from the config (`librechat.yaml`) file. + * @param {TCustomConfig} config - The loaded custom configuration. + * @returns {Partial} The Assistants endpoint configuration. + */ +function assistantsConfigSetup(config) { + const assistantsConfig = config.endpoints[EModelEndpoint.assistants]; + const parsedConfig = assistantEndpointSchema.parse(assistantsConfig); + if (assistantsConfig.supportedIds?.length && assistantsConfig.excludedIds?.length) { + logger.warn( + `Both \`supportedIds\` and \`excludedIds\` are defined for the ${EModelEndpoint.assistants} endpoint; \`excludedIds\` field will be ignored.`, + ); + } + + const prevConfig = config.endpoints[EModelEndpoint.azureOpenAI]?.assistants + ? { + capabilities: [Capabilities.tools, Capabilities.actions, Capabilities.code_interpreter], + } + : {}; + + return { + ...prevConfig, + retrievalModels: parsedConfig.retrievalModels, + disableBuilder: parsedConfig.disableBuilder, + pollIntervalMs: parsedConfig.pollIntervalMs, + supportedIds: parsedConfig.supportedIds, + capabilities: parsedConfig.capabilities, + excludedIds: parsedConfig.excludedIds, + timeoutMs: parsedConfig.timeoutMs, + }; +} + +module.exports = { assistantsConfigSetup }; diff --git a/api/server/services/start/azureOpenAI.js b/api/server/services/start/azureOpenAI.js new file mode 100644 index 0000000000..3b5c446204 --- /dev/null +++ b/api/server/services/start/azureOpenAI.js @@ -0,0 +1,54 @@ +const { + EModelEndpoint, + validateAzureGroups, + mapModelToAzureConfig, +} = require('librechat-data-provider'); +const { logger } = require('~/config'); + +/** + * Sets up the Azure OpenAI configuration from the config (`librechat.yaml`) file. + * @param {TCustomConfig} config - The loaded custom configuration. + * @returns {TAzureConfig} The Azure OpenAI configuration. + */ +function azureConfigSetup(config) { + const { groups, ...azureConfiguration } = config.endpoints[EModelEndpoint.azureOpenAI]; + /** @type {TAzureConfigValidationResult} */ + const { isValid, modelNames, modelGroupMap, groupMap, errors } = validateAzureGroups(groups); + + if (!isValid) { + const errorString = errors.join('\n'); + const errorMessage = 'Invalid Azure OpenAI configuration:\n' + errorString; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + const assistantModels = []; + const assistantGroups = new Set(); + for (const modelName of modelNames) { + mapModelToAzureConfig({ modelName, modelGroupMap, groupMap }); + const groupName = modelGroupMap?.[modelName]?.group; + const modelGroup = groupMap?.[groupName]; + let supportsAssistants = modelGroup?.assistants || modelGroup?.[modelName]?.assistants; + if (supportsAssistants) { + assistantModels.push(modelName); + !assistantGroups.has(groupName) && assistantGroups.add(groupName); + } + } + + if (azureConfiguration.assistants && assistantModels.length === 0) { + throw new Error( + 'No Azure models are configured to support assistants. Please remove the `assistants` field or configure at least one model to support assistants.', + ); + } + + return { + modelNames, + modelGroupMap, + groupMap, + assistantModels, + assistantGroups: Array.from(assistantGroups), + ...azureConfiguration, + }; +} + +module.exports = { azureConfigSetup }; diff --git a/api/server/services/start/checks.js b/api/server/services/start/checks.js new file mode 100644 index 0000000000..3593a5bafb --- /dev/null +++ b/api/server/services/start/checks.js @@ -0,0 +1,107 @@ +const { + Constants, + deprecatedAzureVariables, + conflictingAzureVariables, +} = require('librechat-data-provider'); +const { logger } = require('~/config'); + +const secretDefaults = { + CREDS_KEY: 'f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0', + CREDS_IV: 'e2341419ec3dd3d19b13a1a87fafcbfb', + JWT_SECRET: '16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef', + JWT_REFRESH_SECRET: 'eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8418', +}; + +/** + * Checks environment variables for default secrets and deprecated variables. + * Logs warnings for any default secret values being used and for usage of deprecated `GOOGLE_API_KEY`. + * Advises on replacing default secrets and updating deprecated variables. + */ +function checkVariables() { + let hasDefaultSecrets = false; + for (const [key, value] of Object.entries(secretDefaults)) { + if (process.env[key] === value) { + logger.warn(`Default value for ${key} is being used.`); + !hasDefaultSecrets && (hasDefaultSecrets = true); + } + } + + if (hasDefaultSecrets) { + logger.info( + `Please replace any default secret values. + + For your conveninence, fork & run this replit to generate your own secret values: + + https://replit.com/@daavila/crypto#index.js + + `, + ); + } + + if (process.env.GOOGLE_API_KEY) { + logger.warn( + 'The `GOOGLE_API_KEY` environment variable is deprecated.\nPlease use the `GOOGLE_SEARCH_API_KEY` environment variable instead.', + ); + } + + if (process.env.OPENROUTER_API_KEY) { + logger.warn( + `The \`OPENROUTER_API_KEY\` environment variable is deprecated and its functionality will be removed soon. + Use of this environment variable is highly discouraged as it can lead to unexpected errors when using custom endpoints. + Please use the config (\`librechat.yaml\`) file for setting up OpenRouter, and use \`OPENROUTER_KEY\` or another environment variable instead.`, + ); + } +} + +/** + * Checks the health of auxiliary API's by attempting a fetch request to their respective `/health` endpoints. + * Logs information or warning based on the API's availability and response. + */ +async function checkHealth() { + try { + const response = await fetch(`${process.env.RAG_API_URL}/health`); + if (response?.ok && response?.status === 200) { + logger.info(`RAG API is running and reachable at ${process.env.RAG_API_URL}.`); + } + } catch (error) { + logger.warn( + `RAG API is either not running or not reachable at ${process.env.RAG_API_URL}, you may experience errors with file uploads.`, + ); + } +} + +/** + * Checks for the usage of deprecated and conflicting Azure variables. + * Logs warnings for any deprecated or conflicting environment variables found, indicating potential issues with `azureOpenAI` endpoint configuration. + */ +function checkAzureVariables() { + deprecatedAzureVariables.forEach(({ key, description }) => { + if (process.env[key]) { + logger.warn( + `The \`${key}\` environment variable (related to ${description}) should not be used in combination with the \`azureOpenAI\` endpoint configuration, as you will experience conflicts and errors.`, + ); + } + }); + + conflictingAzureVariables.forEach(({ key }) => { + if (process.env[key]) { + logger.warn( + `The \`${key}\` environment variable should not be used in combination with the \`azureOpenAI\` endpoint configuration, as you may experience with the defined placeholders for mapping to the current model grouping using the same name.`, + ); + } + }); +} + +/** + * Performs basic checks on the loaded config object. + * @param {TCustomConfig} config - The loaded custom configuration. + */ +function checkConfig(config) { + if (config.version !== Constants.CONFIG_VERSION) { + logger.info( + `\nOutdated Config version: ${config.version}. Current version: ${Constants.CONFIG_VERSION}\n\nCheck out the latest config file guide for new options and features.\nhttps://docs.librechat.ai/install/configuration/custom_config.html\n\n`, + ); + } +} + +module.exports = { checkVariables, checkHealth, checkConfig, checkAzureVariables }; diff --git a/api/typedefs.js b/api/typedefs.js index 3c5c08f49d..0399466ece 100644 --- a/api/typedefs.js +++ b/api/typedefs.js @@ -301,6 +301,12 @@ * @memberof typedefs */ +/** + * @exports TAzureConfigValidationResult + * @typedef {import('librechat-data-provider').TAzureConfigValidationResult} TAzureConfigValidationResult + * @memberof typedefs + */ + /** * @exports EImageOutputType * @typedef {import('librechat-data-provider').EImageOutputType} EImageOutputType diff --git a/librechat.example.yaml b/librechat.example.yaml index 177dbd7642..00f0f9fb50 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -2,7 +2,7 @@ # https://docs.librechat.ai/install/configuration/custom_config.html # Configuration version (required) -version: 1.0.5 +version: 1.0.6 # Cache settings: Set to true to enable caching cache: true diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index 6fdcfac4e0..28d728f4b2 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.5.4", + "version": "0.5.5", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/azure.ts b/packages/data-provider/src/azure.ts index 2df1a49f80..79382eef7b 100644 --- a/packages/data-provider/src/azure.ts +++ b/packages/data-provider/src/azure.ts @@ -4,6 +4,7 @@ import type { TAzureGroupMap, TAzureModelGroupMap, TValidatedAzureConfig, + TAzureConfigValidationResult, } from '../src/config'; import { errorsToString, extractEnvVariable, envVarRegex } from '../src/parsers'; import { azureGroupConfigsSchema } from '../src/config'; @@ -46,10 +47,7 @@ export const conflictingAzureVariables = [ }, ]; -export function validateAzureGroups(configs: TAzureGroups): TValidatedAzureConfig & { - isValid: boolean; - errors: (ZodError | string)[]; -} { +export function validateAzureGroups(configs: TAzureGroups): TAzureConfigValidationResult { let isValid = true; const modelNames: string[] = []; const modelGroupMap: TAzureModelGroupMap = {}; diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index bdec452d26..6f58412375 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1,5 +1,6 @@ /* eslint-disable max-len */ import { z } from 'zod'; +import type { ZodError } from 'zod'; import { EModelEndpoint, eModelEndpointSchema } from './schemas'; import { fileConfigSchema } from './file-config'; import { FileSources } from './types/files'; @@ -81,6 +82,11 @@ export type TValidatedAzureConfig = { groupMap: TAzureGroupMap; }; +export type TAzureConfigValidationResult = TValidatedAzureConfig & { + isValid: boolean; + errors: (ZodError | string)[]; +}; + export enum Capabilities { code_interpreter = 'code_interpreter', image_vision = 'image_vision', @@ -173,7 +179,7 @@ export const azureEndpointSchema = z ); export type TAzureConfig = Omit, 'groups'> & - TValidatedAzureConfig; + TAzureConfigValidationResult; export const rateLimitSchema = z.object({ fileUploads: z