mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-01 22:00:18 +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
24
packages/data-schemas/src/app/agents.ts
Normal file
24
packages/data-schemas/src/app/agents.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
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: Partial<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;
|
||||
}
|
||||
70
packages/data-schemas/src/app/assistants.ts
Normal file
70
packages/data-schemas/src/app/assistants.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import logger from '~/config/winston';
|
||||
import {
|
||||
Capabilities,
|
||||
EModelEndpoint,
|
||||
assistantEndpointSchema,
|
||||
defaultAssistantsVersion,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TAssistantEndpoint } from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
* Sets up the minimum, default Assistants configuration if Azure OpenAI Assistants option is enabled.
|
||||
* @returns The Assistants endpoint configuration.
|
||||
*/
|
||||
export function azureAssistantsDefaults(): {
|
||||
capabilities: TAssistantEndpoint['capabilities'];
|
||||
version: TAssistantEndpoint['version'];
|
||||
} {
|
||||
return {
|
||||
capabilities: [Capabilities.tools, Capabilities.actions, Capabilities.code_interpreter],
|
||||
version: defaultAssistantsVersion.azureAssistants,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the Assistants configuration from the config (`librechat.yaml`) file.
|
||||
* @param config - The loaded custom configuration.
|
||||
* @param assistantsEndpoint - The Assistants endpoint name.
|
||||
* - The previously loaded assistants configuration from Azure OpenAI Assistants option.
|
||||
* @param [prevConfig]
|
||||
* @returns The Assistants endpoint configuration.
|
||||
*/
|
||||
export function assistantsConfigSetup(
|
||||
config: Partial<TCustomConfig>,
|
||||
assistantsEndpoint: EModelEndpoint.assistants | EModelEndpoint.azureAssistants,
|
||||
prevConfig: Partial<TAssistantEndpoint> = {},
|
||||
): Partial<TAssistantEndpoint> {
|
||||
const assistantsConfig = config.endpoints?.[assistantsEndpoint];
|
||||
const parsedConfig = assistantEndpointSchema.parse(assistantsConfig);
|
||||
if (assistantsConfig?.supportedIds?.length && assistantsConfig.excludedIds?.length) {
|
||||
logger.warn(
|
||||
`Configuration conflict: The '${assistantsEndpoint}' endpoint has both 'supportedIds' and 'excludedIds' defined. The 'excludedIds' will be ignored.`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
assistantsConfig?.privateAssistants &&
|
||||
(assistantsConfig.supportedIds?.length || assistantsConfig.excludedIds?.length)
|
||||
) {
|
||||
logger.warn(
|
||||
`Configuration conflict: The '${assistantsEndpoint}' endpoint has both 'privateAssistants' and 'supportedIds' or 'excludedIds' defined. The 'supportedIds' and 'excludedIds' will be ignored.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...prevConfig,
|
||||
retrievalModels: parsedConfig.retrievalModels,
|
||||
disableBuilder: parsedConfig.disableBuilder,
|
||||
pollIntervalMs: parsedConfig.pollIntervalMs,
|
||||
supportedIds: parsedConfig.supportedIds,
|
||||
capabilities: parsedConfig.capabilities,
|
||||
excludedIds: parsedConfig.excludedIds,
|
||||
privateAssistants: parsedConfig.privateAssistants,
|
||||
timeoutMs: parsedConfig.timeoutMs,
|
||||
streamRate: parsedConfig.streamRate,
|
||||
titlePrompt: parsedConfig.titlePrompt,
|
||||
titleMethod: parsedConfig.titleMethod,
|
||||
titleModel: parsedConfig.titleModel,
|
||||
titleEndpoint: parsedConfig.titleEndpoint,
|
||||
titlePromptTemplate: parsedConfig.titlePromptTemplate,
|
||||
};
|
||||
}
|
||||
71
packages/data-schemas/src/app/azure.ts
Normal file
71
packages/data-schemas/src/app/azure.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import logger from '~/config/winston';
|
||||
import {
|
||||
EModelEndpoint,
|
||||
validateAzureGroups,
|
||||
mapModelToAzureConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TAzureConfig } from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
* Sets up the Azure OpenAI configuration from the config (`librechat.yaml`) file.
|
||||
* @param config - The loaded custom configuration.
|
||||
* @returns The Azure OpenAI configuration.
|
||||
*/
|
||||
export function azureConfigSetup(config: Partial<TCustomConfig>): TAzureConfig {
|
||||
const azureConfig = config.endpoints?.[EModelEndpoint.azureOpenAI];
|
||||
if (!azureConfig) {
|
||||
throw new Error('Azure OpenAI configuration is missing.');
|
||||
}
|
||||
const { groups, ...azureConfiguration } = azureConfig;
|
||||
const { isValid, modelNames, modelGroupMap, groupMap, errors } = validateAzureGroups(groups);
|
||||
|
||||
if (!isValid) {
|
||||
const errorString = errors.join('\n');
|
||||
const errorMessage = 'Invalid Azure OpenAI configuration:\n' + errorString;
|
||||
logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const assistantModels: string[] = [];
|
||||
const assistantGroups = new Set<string>();
|
||||
for (const modelName of modelNames) {
|
||||
mapModelToAzureConfig({ modelName, modelGroupMap, groupMap });
|
||||
const groupName = modelGroupMap?.[modelName]?.group;
|
||||
const modelGroup = groupMap?.[groupName];
|
||||
const supportsAssistants = modelGroup?.assistants || modelGroup?.[modelName]?.assistants;
|
||||
if (supportsAssistants) {
|
||||
assistantModels.push(modelName);
|
||||
if (!assistantGroups.has(groupName)) {
|
||||
assistantGroups.add(groupName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (azureConfiguration.assistants && assistantModels.length === 0) {
|
||||
throw new Error(
|
||||
'No Azure models are configured to support assistants. Please remove the `assistants` field or configure at least one model to support assistants.',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
azureConfiguration.assistants &&
|
||||
process.env.ENDPOINTS &&
|
||||
!process.env.ENDPOINTS.includes(EModelEndpoint.azureAssistants)
|
||||
) {
|
||||
logger.warn(
|
||||
`Azure Assistants are configured, but the endpoint will not be accessible as it's not included in the ENDPOINTS environment variable.
|
||||
Please add the value "${EModelEndpoint.azureAssistants}" to the ENDPOINTS list if expected.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
isValid,
|
||||
groupMap,
|
||||
modelNames,
|
||||
modelGroupMap,
|
||||
assistantModels,
|
||||
assistantGroups: Array.from(assistantGroups),
|
||||
...azureConfiguration,
|
||||
};
|
||||
}
|
||||
66
packages/data-schemas/src/app/endpoints.ts
Normal file
66
packages/data-schemas/src/app/endpoints.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TAgentsEndpoint } from 'librechat-data-provider';
|
||||
import type { AppConfig } from '~/types';
|
||||
import { azureAssistantsDefaults, assistantsConfigSetup } from './assistants';
|
||||
import { agentsConfigSetup } from './agents';
|
||||
import { azureConfigSetup } from './azure';
|
||||
|
||||
/**
|
||||
* Loads custom config endpoints
|
||||
* @param [config]
|
||||
* @param [agentsDefaults]
|
||||
*/
|
||||
export const loadEndpoints = (
|
||||
config: Partial<TCustomConfig>,
|
||||
agentsDefaults?: Partial<TAgentsEndpoint>,
|
||||
) => {
|
||||
const loadedEndpoints: AppConfig['endpoints'] = {};
|
||||
const endpoints = config?.endpoints;
|
||||
|
||||
if (endpoints?.[EModelEndpoint.azureOpenAI]) {
|
||||
loadedEndpoints[EModelEndpoint.azureOpenAI] = azureConfigSetup(config);
|
||||
}
|
||||
|
||||
if (endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) {
|
||||
loadedEndpoints[EModelEndpoint.azureAssistants] = azureAssistantsDefaults();
|
||||
}
|
||||
|
||||
if (endpoints?.[EModelEndpoint.azureAssistants]) {
|
||||
loadedEndpoints[EModelEndpoint.azureAssistants] = assistantsConfigSetup(
|
||||
config,
|
||||
EModelEndpoint.azureAssistants,
|
||||
loadedEndpoints[EModelEndpoint.azureAssistants],
|
||||
);
|
||||
}
|
||||
|
||||
if (endpoints?.[EModelEndpoint.assistants]) {
|
||||
loadedEndpoints[EModelEndpoint.assistants] = assistantsConfigSetup(
|
||||
config,
|
||||
EModelEndpoint.assistants,
|
||||
loadedEndpoints[EModelEndpoint.assistants],
|
||||
);
|
||||
}
|
||||
|
||||
loadedEndpoints[EModelEndpoint.agents] = agentsConfigSetup(config, agentsDefaults);
|
||||
|
||||
const endpointKeys = [
|
||||
EModelEndpoint.openAI,
|
||||
EModelEndpoint.google,
|
||||
EModelEndpoint.custom,
|
||||
EModelEndpoint.bedrock,
|
||||
EModelEndpoint.anthropic,
|
||||
];
|
||||
|
||||
endpointKeys.forEach((key) => {
|
||||
const currentKey = key as keyof typeof endpoints;
|
||||
if (endpoints?.[currentKey]) {
|
||||
loadedEndpoints[currentKey] = endpoints[currentKey];
|
||||
}
|
||||
});
|
||||
|
||||
if (endpoints?.all) {
|
||||
loadedEndpoints.all = endpoints.all;
|
||||
}
|
||||
|
||||
return loadedEndpoints;
|
||||
};
|
||||
6
packages/data-schemas/src/app/index.ts
Normal file
6
packages/data-schemas/src/app/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export * from './agents';
|
||||
export * from './interface';
|
||||
export * from './service';
|
||||
export * from './specs';
|
||||
export * from './turnstile';
|
||||
export * from './web';
|
||||
61
packages/data-schemas/src/app/interface.ts
Normal file
61
packages/data-schemas/src/app/interface.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { removeNullishValues } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TConfigDefaults } from 'librechat-data-provider';
|
||||
import type { AppConfig } from '~/types/app';
|
||||
import { isMemoryEnabled } from './memory';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
|
||||
return loadedInterface;
|
||||
}
|
||||
28
packages/data-schemas/src/app/memory.ts
Normal file
28
packages/data-schemas/src/app/memory.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { memorySchema } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TMemoryConfig } from 'librechat-data-provider';
|
||||
|
||||
const hasValidAgent = (agent: TMemoryConfig['agent']) =>
|
||||
!!agent &&
|
||||
(('id' in agent && !!agent.id) ||
|
||||
('provider' in agent && 'model' in agent && !!agent.provider && !!agent.model));
|
||||
|
||||
const isDisabled = (config?: TMemoryConfig | TCustomConfig['memory']) =>
|
||||
!config || config.disabled === true;
|
||||
|
||||
export function loadMemoryConfig(config: TCustomConfig['memory']): TMemoryConfig | undefined {
|
||||
if (!config) return undefined;
|
||||
if (isDisabled(config)) return config as TMemoryConfig;
|
||||
|
||||
if (!hasValidAgent(config.agent)) {
|
||||
return { ...config, disabled: true } as TMemoryConfig;
|
||||
}
|
||||
|
||||
const charLimit = memorySchema.shape.charLimit.safeParse(config.charLimit).data ?? 10000;
|
||||
|
||||
return { ...config, charLimit };
|
||||
}
|
||||
|
||||
export function isMemoryEnabled(config: TMemoryConfig | undefined): boolean {
|
||||
if (isDisabled(config)) return false;
|
||||
return hasValidAgent(config!.agent);
|
||||
}
|
||||
15
packages/data-schemas/src/app/ocr.ts
Normal file
15
packages/data-schemas/src/app/ocr.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { OCRStrategy } from 'librechat-data-provider';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
|
||||
export function loadOCRConfig(config?: TCustomConfig['ocr']): TCustomConfig['ocr'] | undefined {
|
||||
if (!config) return;
|
||||
const baseURL = config?.baseURL ?? '';
|
||||
const apiKey = config?.apiKey ?? '';
|
||||
const mistralModel = config?.mistralModel ?? '';
|
||||
return {
|
||||
apiKey,
|
||||
baseURL,
|
||||
mistralModel,
|
||||
strategy: config?.strategy ?? OCRStrategy.MISTRAL_OCR,
|
||||
};
|
||||
}
|
||||
113
packages/data-schemas/src/app/service.ts
Normal file
113
packages/data-schemas/src/app/service.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { EModelEndpoint, getConfigDefaults } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, FileSources, DeepPartial } from 'librechat-data-provider';
|
||||
import type { AppConfig, FunctionTool } from '~/types/app';
|
||||
import { loadDefaultInterface } from './interface';
|
||||
import { loadTurnstileConfig } from './turnstile';
|
||||
import { agentsConfigSetup } from './agents';
|
||||
import { loadWebSearchConfig } from './web';
|
||||
import { processModelSpecs } from './specs';
|
||||
import { loadMemoryConfig } from './memory';
|
||||
import { loadEndpoints } from './endpoints';
|
||||
import { loadOCRConfig } from './ocr';
|
||||
|
||||
export type Paths = {
|
||||
root: string;
|
||||
uploads: string;
|
||||
clientPath: string;
|
||||
dist: string;
|
||||
publicPath: string;
|
||||
fonts: string;
|
||||
assets: string;
|
||||
imageOutput: string;
|
||||
structuredTools: string;
|
||||
pluginManifest: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads custom config and initializes app-wide variables.
|
||||
* @function AppService
|
||||
*/
|
||||
export const AppService = async (params?: {
|
||||
config: DeepPartial<TCustomConfig>;
|
||||
paths?: Paths;
|
||||
systemTools?: Record<string, FunctionTool>;
|
||||
}): Promise<AppConfig> => {
|
||||
const { config, paths, systemTools } = params || {};
|
||||
if (!config) {
|
||||
throw new Error('Config is required');
|
||||
}
|
||||
const configDefaults = getConfigDefaults();
|
||||
|
||||
const ocr = loadOCRConfig(config.ocr);
|
||||
const webSearch = loadWebSearchConfig(config.webSearch);
|
||||
const memory = loadMemoryConfig(config.memory);
|
||||
const filteredTools = config.filteredTools;
|
||||
const includedTools = config.includedTools;
|
||||
const fileStrategy = (config.fileStrategy ?? configDefaults.fileStrategy) as
|
||||
| FileSources.local
|
||||
| FileSources.s3
|
||||
| FileSources.firebase
|
||||
| FileSources.azure_blob;
|
||||
const startBalance = process.env.START_BALANCE;
|
||||
const balance = config.balance ?? {
|
||||
enabled: process.env.CHECK_BALANCE?.toLowerCase().trim() === 'true',
|
||||
startBalance: startBalance ? parseInt(startBalance, 10) : undefined,
|
||||
};
|
||||
const transactions = config.transactions ?? configDefaults.transactions;
|
||||
const imageOutputType = config?.imageOutputType ?? configDefaults.imageOutputType;
|
||||
|
||||
process.env.CDN_PROVIDER = fileStrategy;
|
||||
|
||||
const availableTools = systemTools;
|
||||
|
||||
const mcpConfig = config.mcpServers || null;
|
||||
const registration = config.registration ?? configDefaults.registration;
|
||||
const interfaceConfig = await loadDefaultInterface({ config, configDefaults });
|
||||
const turnstileConfig = loadTurnstileConfig(config, configDefaults);
|
||||
const speech = config.speech;
|
||||
|
||||
const defaultConfig = {
|
||||
ocr,
|
||||
paths,
|
||||
config,
|
||||
memory,
|
||||
speech,
|
||||
balance,
|
||||
transactions,
|
||||
mcpConfig,
|
||||
webSearch,
|
||||
fileStrategy,
|
||||
registration,
|
||||
filteredTools,
|
||||
includedTools,
|
||||
availableTools,
|
||||
imageOutputType,
|
||||
interfaceConfig,
|
||||
turnstileConfig,
|
||||
fileStrategies: config.fileStrategies,
|
||||
};
|
||||
|
||||
const agentsDefaults = agentsConfigSetup(config);
|
||||
|
||||
if (!Object.keys(config).length) {
|
||||
const appConfig = {
|
||||
...defaultConfig,
|
||||
endpoints: {
|
||||
[EModelEndpoint.agents]: agentsDefaults,
|
||||
},
|
||||
};
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
const loadedEndpoints = loadEndpoints(config, agentsDefaults);
|
||||
|
||||
const appConfig = {
|
||||
...defaultConfig,
|
||||
fileConfig: config?.fileConfig,
|
||||
secureImageLinks: config?.secureImageLinks,
|
||||
modelSpecs: processModelSpecs(config?.endpoints, config.modelSpecs, interfaceConfig),
|
||||
endpoints: loadedEndpoints,
|
||||
};
|
||||
|
||||
return appConfig;
|
||||
};
|
||||
94
packages/data-schemas/src/app/specs.ts
Normal file
94
packages/data-schemas/src/app/specs.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import logger from '~/config/winston';
|
||||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
* Normalize the endpoint name to system-expected value.
|
||||
* @param name
|
||||
*/
|
||||
function normalizeEndpointName(name = ''): string {
|
||||
return name.toLowerCase() === 'ollama' ? 'ollama' : name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up Model Specs from the config (`librechat.yaml`) file.
|
||||
* @param [endpoints] - The loaded custom configuration for endpoints.
|
||||
* @param [modelSpecs] - The loaded custom configuration for model specs.
|
||||
* @param [interfaceConfig] - The loaded interface configuration.
|
||||
* @returns The processed model specs, if any.
|
||||
*/
|
||||
export function processModelSpecs(
|
||||
endpoints?: TCustomConfig['endpoints'],
|
||||
_modelSpecs?: TCustomConfig['modelSpecs'],
|
||||
interfaceConfig?: TCustomConfig['interface'],
|
||||
): TCustomConfig['modelSpecs'] | undefined {
|
||||
if (!_modelSpecs) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const list = _modelSpecs.list;
|
||||
const modelSpecs: typeof list = [];
|
||||
|
||||
const customEndpoints = endpoints?.[EModelEndpoint.custom] ?? [];
|
||||
|
||||
if (interfaceConfig?.modelSelect !== true && (_modelSpecs.addedEndpoints?.length ?? 0) > 0) {
|
||||
logger.warn(
|
||||
`To utilize \`addedEndpoints\`, which allows provider/model selections alongside model specs, set \`modelSelect: true\` in the interface configuration.
|
||||
|
||||
Example:
|
||||
\`\`\`yaml
|
||||
interface:
|
||||
modelSelect: true
|
||||
\`\`\`
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const spec of list) {
|
||||
const currentEndpoint = spec.preset?.endpoint as EModelEndpoint | undefined;
|
||||
if (!currentEndpoint) {
|
||||
logger.warn(
|
||||
'A model spec is missing the `endpoint` field within its `preset`. Skipping model spec...',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (EModelEndpoint[currentEndpoint] && currentEndpoint !== EModelEndpoint.custom) {
|
||||
modelSpecs.push(spec);
|
||||
continue;
|
||||
} else if (currentEndpoint === EModelEndpoint.custom) {
|
||||
logger.warn(
|
||||
`Model Spec with endpoint "${currentEndpoint}" is not supported. You must specify the name of the custom endpoint (case-sensitive, as defined in your config). Skipping model spec...`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedName = normalizeEndpointName(currentEndpoint);
|
||||
const endpoint = customEndpoints.find(
|
||||
(customEndpoint) => normalizedName === normalizeEndpointName(customEndpoint.name),
|
||||
);
|
||||
|
||||
if (!endpoint) {
|
||||
logger.warn(`Model spec with endpoint "${currentEndpoint}" was skipped: Endpoint not found in configuration. The \`endpoint\` value must exactly match either a system-defined endpoint or a custom endpoint defined by the user.
|
||||
|
||||
For more information, see the documentation at https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/model_specs#endpoint`);
|
||||
continue;
|
||||
}
|
||||
|
||||
modelSpecs.push({
|
||||
...spec,
|
||||
preset: {
|
||||
...spec.preset,
|
||||
endpoint: normalizedName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
..._modelSpecs,
|
||||
list: modelSpecs,
|
||||
};
|
||||
}
|
||||
45
packages/data-schemas/src/app/turnstile.ts
Normal file
45
packages/data-schemas/src/app/turnstile.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import logger from '~/config/winston';
|
||||
import { removeNullishValues } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TConfigDefaults } from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
* Loads and maps the Cloudflare Turnstile configuration.
|
||||
*
|
||||
* Expected config structure:
|
||||
*
|
||||
* turnstile:
|
||||
* siteKey: "your-site-key-here"
|
||||
* options:
|
||||
* language: "auto" // "auto" or an ISO 639-1 language code (e.g. en)
|
||||
* size: "normal" // Options: "normal", "compact", "flexible", or "invisible"
|
||||
*
|
||||
* @param config - The loaded custom configuration.
|
||||
* @param configDefaults - The custom configuration default values.
|
||||
* @returns The mapped Turnstile configuration.
|
||||
*/
|
||||
export function loadTurnstileConfig(
|
||||
config: Partial<TCustomConfig> | undefined,
|
||||
configDefaults: TConfigDefaults,
|
||||
): Partial<TCustomConfig['turnstile']> {
|
||||
const { turnstile: customTurnstile } = config ?? {};
|
||||
const { turnstile: defaults } = configDefaults;
|
||||
|
||||
const loadedTurnstile = removeNullishValues({
|
||||
siteKey:
|
||||
customTurnstile?.siteKey ?? (defaults as TCustomConfig['turnstile'] | undefined)?.siteKey,
|
||||
options:
|
||||
customTurnstile?.options ?? (defaults as TCustomConfig['turnstile'] | undefined)?.options,
|
||||
});
|
||||
|
||||
const enabled = Boolean(loadedTurnstile.siteKey);
|
||||
|
||||
if (enabled) {
|
||||
logger.debug(
|
||||
'Turnstile is ENABLED with configuration:\n' + JSON.stringify(loadedTurnstile, null, 2),
|
||||
);
|
||||
} else {
|
||||
logger.debug('Turnstile is DISABLED (no siteKey provided).');
|
||||
}
|
||||
|
||||
return loadedTurnstile;
|
||||
}
|
||||
84
packages/data-schemas/src/app/web.ts
Normal file
84
packages/data-schemas/src/app/web.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { SafeSearchTypes } from 'librechat-data-provider';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
import type { TWebSearchKeys, TWebSearchCategories } from '~/types/web';
|
||||
|
||||
export const webSearchAuth = {
|
||||
providers: {
|
||||
serper: {
|
||||
serperApiKey: 1 as const,
|
||||
},
|
||||
searxng: {
|
||||
searxngInstanceUrl: 1 as const,
|
||||
/** Optional (0) */
|
||||
searxngApiKey: 0 as const,
|
||||
},
|
||||
},
|
||||
scrapers: {
|
||||
firecrawl: {
|
||||
firecrawlApiKey: 1 as const,
|
||||
/** Optional (0) */
|
||||
firecrawlApiUrl: 0 as const,
|
||||
},
|
||||
},
|
||||
rerankers: {
|
||||
jina: {
|
||||
jinaApiKey: 1 as const,
|
||||
/** Optional (0) */
|
||||
jinaApiUrl: 0 as const,
|
||||
},
|
||||
cohere: { cohereApiKey: 1 as const },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts all API keys from the webSearchAuth configuration object
|
||||
*/
|
||||
export function getWebSearchKeys(): TWebSearchKeys[] {
|
||||
const keys: TWebSearchKeys[] = [];
|
||||
|
||||
// Iterate through each category (providers, scrapers, rerankers)
|
||||
for (const category of Object.keys(webSearchAuth)) {
|
||||
const categoryObj = webSearchAuth[category as TWebSearchCategories];
|
||||
|
||||
// Iterate through each service within the category
|
||||
for (const service of Object.keys(categoryObj)) {
|
||||
const serviceObj = categoryObj[service as keyof typeof categoryObj];
|
||||
|
||||
// Extract the API keys from the service
|
||||
for (const key of Object.keys(serviceObj)) {
|
||||
keys.push(key as TWebSearchKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
export const webSearchKeys: TWebSearchKeys[] = getWebSearchKeys();
|
||||
|
||||
export function loadWebSearchConfig(
|
||||
config: TCustomConfig['webSearch'],
|
||||
): TCustomConfig['webSearch'] {
|
||||
const serperApiKey = config?.serperApiKey ?? '${SERPER_API_KEY}';
|
||||
const searxngInstanceUrl = config?.searxngInstanceUrl ?? '${SEARXNG_INSTANCE_URL}';
|
||||
const searxngApiKey = config?.searxngApiKey ?? '${SEARXNG_API_KEY}';
|
||||
const firecrawlApiKey = config?.firecrawlApiKey ?? '${FIRECRAWL_API_KEY}';
|
||||
const firecrawlApiUrl = config?.firecrawlApiUrl ?? '${FIRECRAWL_API_URL}';
|
||||
const jinaApiKey = config?.jinaApiKey ?? '${JINA_API_KEY}';
|
||||
const jinaApiUrl = config?.jinaApiUrl ?? '${JINA_API_URL}';
|
||||
const cohereApiKey = config?.cohereApiKey ?? '${COHERE_API_KEY}';
|
||||
const safeSearch = config?.safeSearch ?? SafeSearchTypes.MODERATE;
|
||||
|
||||
return {
|
||||
...config,
|
||||
safeSearch,
|
||||
jinaApiKey,
|
||||
jinaApiUrl,
|
||||
cohereApiKey,
|
||||
serperApiKey,
|
||||
searxngInstanceUrl,
|
||||
searxngApiKey,
|
||||
firecrawlApiKey,
|
||||
firecrawlApiUrl,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './app';
|
||||
export * from './common';
|
||||
export * from './crypto';
|
||||
export * from './schema';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import pluginAuthSchema, { IPluginAuth } from '~/schema/pluginAuth';
|
||||
import pluginAuthSchema from '~/schema/pluginAuth';
|
||||
import type { IPluginAuth } from '~/types/pluginAuth';
|
||||
|
||||
/**
|
||||
* Creates or returns the PluginAuth model using the provided mongoose instance and schema
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import promptSchema, { IPrompt } from '~/schema/prompt';
|
||||
import promptSchema from '~/schema/prompt';
|
||||
import type { IPrompt } from '~/types/prompts';
|
||||
|
||||
/**
|
||||
* Creates or returns the Prompt model using the provided mongoose instance and schema
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import promptGroupSchema, { IPromptGroupDocument } from '~/schema/promptGroup';
|
||||
import promptGroupSchema from '~/schema/promptGroup';
|
||||
import type { IPromptGroupDocument } from '~/types/prompts';
|
||||
|
||||
/**
|
||||
* Creates or returns the PromptGroup model using the provided mongoose instance and schema
|
||||
|
|
|
|||
116
packages/data-schemas/src/types/app.ts
Normal file
116
packages/data-schemas/src/types/app.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import type {
|
||||
TEndpoint,
|
||||
FileSources,
|
||||
TAzureConfig,
|
||||
TCustomConfig,
|
||||
TMemoryConfig,
|
||||
EModelEndpoint,
|
||||
TAgentsEndpoint,
|
||||
TCustomEndpoints,
|
||||
TAssistantEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
|
||||
export type JsonSchemaType = {
|
||||
type: 'string' | 'number' | 'integer' | 'float' | 'boolean' | 'array' | 'object';
|
||||
enum?: string[];
|
||||
items?: JsonSchemaType;
|
||||
properties?: Record<string, JsonSchemaType>;
|
||||
required?: string[];
|
||||
description?: string;
|
||||
additionalProperties?: boolean | JsonSchemaType;
|
||||
};
|
||||
|
||||
export type ConvertJsonSchemaToZodOptions = {
|
||||
allowEmptyObject?: boolean;
|
||||
dropFields?: string[];
|
||||
transformOneOfAnyOf?: boolean;
|
||||
};
|
||||
|
||||
export interface FunctionTool {
|
||||
type: 'function';
|
||||
function: {
|
||||
description: string;
|
||||
name: string;
|
||||
parameters: JsonSchemaType;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Application configuration object
|
||||
* Based on the configuration defined in api/server/services/Config/getAppConfig.js
|
||||
*/
|
||||
export interface AppConfig {
|
||||
/** The main custom configuration */
|
||||
config: Partial<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?: Partial<TCustomConfig['turnstile']>;
|
||||
/** Balance configuration */
|
||||
balance?: Partial<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?: Partial<TEndpoint>;
|
||||
/** Google endpoint configuration */
|
||||
google?: Partial<TEndpoint>;
|
||||
/** Bedrock endpoint configuration */
|
||||
bedrock?: Partial<TEndpoint>;
|
||||
/** Anthropic endpoint configuration */
|
||||
anthropic?: Partial<TEndpoint>;
|
||||
/** GPT plugins endpoint configuration */
|
||||
gptPlugins?: Partial<TEndpoint>;
|
||||
/** Azure OpenAI endpoint configuration */
|
||||
azureOpenAI?: TAzureConfig;
|
||||
/** Assistants endpoint configuration */
|
||||
assistants?: Partial<TAssistantEndpoint>;
|
||||
/** Azure assistants endpoint configuration */
|
||||
azureAssistants?: Partial<TAssistantEndpoint>;
|
||||
/** Agents endpoint configuration */
|
||||
[EModelEndpoint.agents]?: Partial<TAgentsEndpoint>;
|
||||
/** Custom endpoints configuration */
|
||||
[EModelEndpoint.custom]?: TCustomEndpoints;
|
||||
/** Global endpoint configuration */
|
||||
all?: Partial<TEndpoint>;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Types } from 'mongoose';
|
||||
|
||||
export type ObjectId = Types.ObjectId;
|
||||
export * from './app';
|
||||
export * from './user';
|
||||
export * from './token';
|
||||
export * from './convo';
|
||||
|
|
@ -24,3 +25,5 @@ export * from './prompts';
|
|||
export * from './accessRole';
|
||||
export * from './aclEntry';
|
||||
export * from './group';
|
||||
/* Web */
|
||||
export * from './web';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { DeepPartial } from 'librechat-data-provider';
|
||||
import type { Document } from 'mongoose';
|
||||
import { CursorPaginationParams } from '~/common';
|
||||
|
||||
|
|
@ -54,9 +55,6 @@ export interface IRole extends Document {
|
|||
}
|
||||
|
||||
export type RolePermissions = IRole['permissions'];
|
||||
type DeepPartial<T> = {
|
||||
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
||||
};
|
||||
export type RolePermissionsInput = DeepPartial<RolePermissions>;
|
||||
|
||||
export interface CreateRoleRequest {
|
||||
|
|
|
|||
16
packages/data-schemas/src/types/web.ts
Normal file
16
packages/data-schemas/src/types/web.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { SearchCategories } from 'librechat-data-provider';
|
||||
|
||||
export type TWebSearchKeys =
|
||||
| 'serperApiKey'
|
||||
| 'searxngInstanceUrl'
|
||||
| 'searxngApiKey'
|
||||
| 'firecrawlApiKey'
|
||||
| 'firecrawlApiUrl'
|
||||
| 'jinaApiKey'
|
||||
| 'jinaApiUrl'
|
||||
| 'cohereApiKey';
|
||||
|
||||
export type TWebSearchCategories =
|
||||
| SearchCategories.PROVIDERS
|
||||
| SearchCategories.SCRAPERS
|
||||
| SearchCategories.RERANKERS;
|
||||
Loading…
Add table
Add a link
Reference in a new issue