diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 5773c3788d..255f178d98 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -7,6 +7,7 @@ const { createRun, Tokenizer, checkAccess, + hasCustomUserVars, memoryInstructions, formatContentStrings, createMemoryProcessor, @@ -39,12 +40,7 @@ const { deleteMemory, setMemory, } = require('~/models'); -const { - hasCustomUserVars, - checkCapability, - getMCPAuthMap, - getAppConfig, -} = require('~/server/services/Config'); +const { checkCapability, getMCPAuthMap, getAppConfig } = require('~/server/services/Config'); const { addCacheControl, createContextHandlers } = require('~/app/clients/prompts'); const { initializeAgent } = require('~/server/services/Endpoints/agents/agent'); const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens'); @@ -917,7 +913,7 @@ class AgentClient extends BaseClient { } try { - if (await hasCustomUserVars()) { + if (hasCustomUserVars(appConfig)) { config.configurable.userMCPAuthMap = await getMCPAuthMap({ tools: agent.tools, userId: this.options.req.user.id, @@ -1102,7 +1098,7 @@ class AgentClient extends BaseClient { model: agent.model || agent.model_parameters.model, }; - let titleProviderConfig = await getProviderConfig(endpoint); + let titleProviderConfig = getProviderConfig({ provider: endpoint, appConfig }); /** @type {TEndpoint | undefined} */ const endpointConfig = @@ -1117,7 +1113,10 @@ class AgentClient extends BaseClient { if (endpointConfig?.titleEndpoint && endpointConfig.titleEndpoint !== endpoint) { try { - titleProviderConfig = await getProviderConfig(endpointConfig.titleEndpoint); + titleProviderConfig = getProviderConfig({ + provider: endpointConfig.titleEndpoint, + appConfig, + }); endpoint = endpointConfig.titleEndpoint; } catch (error) { logger.warn( @@ -1126,7 +1125,7 @@ class AgentClient extends BaseClient { ); // Fall back to original provider config endpoint = agent.endpoint; - titleProviderConfig = await getProviderConfig(endpoint); + titleProviderConfig = getProviderConfig({ provider: endpoint, appConfig }); } } diff --git a/api/server/services/Config/getCustomConfig.js b/api/server/services/Config/getCustomConfig.js index 0b1b766641..10fa68d0c7 100644 --- a/api/server/services/Config/getCustomConfig.js +++ b/api/server/services/Config/getCustomConfig.js @@ -1,46 +1,5 @@ const { logger } = require('@librechat/data-schemas'); -const { EModelEndpoint } = require('librechat-data-provider'); -const { isEnabled, getUserMCPAuthMap, normalizeEndpointName } = require('@librechat/api'); -const { getAppConfig } = require('./app'); - -/** - * Retrieves the configuration object - * @function getBalanceConfig - * @param {Object} params - * @param {string} [params.role] - * @returns {Promise} - * */ -async function getBalanceConfig({ role }) { - const isLegacyEnabled = isEnabled(process.env.CHECK_BALANCE); - const startBalance = process.env.START_BALANCE; - /** @type {TCustomConfig['balance']} */ - const config = { - enabled: isLegacyEnabled, - startBalance: startBalance != null && startBalance ? parseInt(startBalance, 10) : undefined, - }; - const appConfig = await getAppConfig({ role }); - if (!appConfig) { - return config; - } - return { ...config, ...(appConfig?.['balance'] ?? {}) }; -} - -/** - * - * @param {string | EModelEndpoint} endpoint - * @returns {Promise} - */ -const getCustomEndpointConfig = async (endpoint) => { - const appConfig = await getAppConfig(); - if (!appConfig) { - throw new Error(`Config not found for the ${endpoint} custom endpoint.`); - } - - const customEndpoints = appConfig.endpoints?.[EModelEndpoint.custom] ?? []; - return customEndpoints.find( - (endpointConfig) => normalizeEndpointName(endpointConfig.name) === endpoint, - ); -}; +const { getUserMCPAuthMap } = require('@librechat/api'); /** * @param {Object} params @@ -67,18 +26,6 @@ async function getMCPAuthMap({ userId, tools, findPluginAuthsByKeys }) { } } -/** - * @returns {Promise} - */ -async function hasCustomUserVars() { - const customConfig = await getAppConfig(); - const mcpServers = customConfig?.mcpConfig; - return Object.values(mcpServers ?? {}).some((server) => server.customUserVars); -} - module.exports = { getMCPAuthMap, - getBalanceConfig, - hasCustomUserVars, - getCustomEndpointConfig, }; diff --git a/api/server/services/Endpoints/agents/agent.js b/api/server/services/Endpoints/agents/agent.js index a6ea4513cc..46da887d40 100644 --- a/api/server/services/Endpoints/agents/agent.js +++ b/api/server/services/Endpoints/agents/agent.js @@ -106,7 +106,7 @@ const initializeAgent = async ({ })) ?? {}; agent.endpoint = provider; - const { getOptions, overrideProvider } = await getProviderConfig(provider); + const { getOptions, overrideProvider } = getProviderConfig({ provider, appConfig }); if (overrideProvider !== agent.provider) { agent.provider = overrideProvider; } diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 3dbc0df609..10afcb57f6 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -1,6 +1,6 @@ const { logger } = require('@librechat/data-schemas'); -const { validateAgentModel } = require('@librechat/api'); const { createContentAggregator } = require('@librechat/agents'); +const { validateAgentModel, getCustomEndpointConfig } = require('@librechat/api'); const { Constants, EModelEndpoint, @@ -11,11 +11,11 @@ const { createToolEndCallback, getDefaultHandlers, } = require('~/server/controllers/agents/callbacks'); -const { getCustomEndpointConfig, getAppConfig } = require('~/server/services/Config'); const { initializeAgent } = require('~/server/services/Endpoints/agents/agent'); const { getModelsConfig } = require('~/server/controllers/ModelController'); const { loadAgentTools } = require('~/server/services/ToolService'); const AgentClient = require('~/server/controllers/agents/client'); +const { getAppConfig } = require('~/server/services/Config'); const { getAgent } = require('~/models/Agent'); const { logViolation } = require('~/cache'); @@ -147,7 +147,10 @@ const initializeClient = async ({ req, res, endpointOption }) => { let endpointConfig = appConfig.endpoints?.[primaryConfig.endpoint]; if (!isAgentsEndpoint(primaryConfig.endpoint) && !endpointConfig) { try { - endpointConfig = await getCustomEndpointConfig(primaryConfig.endpoint); + endpointConfig = getCustomEndpointConfig({ + endpoint: primaryConfig.endpoint, + appConfig, + }); } catch (err) { logger.error( '[api/server/controllers/agents/client.js #titleConvo] Error getting custom endpoint config', diff --git a/api/server/services/Endpoints/custom/initialize.js b/api/server/services/Endpoints/custom/initialize.js index 73d37e10c1..25cf867880 100644 --- a/api/server/services/Endpoints/custom/initialize.js +++ b/api/server/services/Endpoints/custom/initialize.js @@ -1,3 +1,6 @@ +const { Providers } = require('@librechat/agents'); +const { isUserProvided, getCustomEndpointConfig } = require('@librechat/api'); +const { getOpenAIConfig, createHandleLLMNewToken, resolveHeaders } = require('@librechat/api'); const { CacheKeys, ErrorTypes, @@ -5,13 +8,10 @@ const { FetchTokenConfig, extractEnvVariable, } = require('librechat-data-provider'); -const { Providers } = require('@librechat/agents'); -const { getOpenAIConfig, createHandleLLMNewToken, resolveHeaders } = require('@librechat/api'); const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService'); -const { getCustomEndpointConfig, getAppConfig } = require('~/server/services/Config'); const { fetchModels } = require('~/server/services/ModelService'); +const { getAppConfig } = require('~/server/services/Config'); const OpenAIClient = require('~/app/clients/OpenAIClient'); -const { isUserProvided } = require('~/server/utils'); const getLogStores = require('~/cache/getLogStores'); const { PROXY } = process.env; @@ -21,7 +21,10 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid const { key: expiresAt } = req.body; const endpoint = overrideEndpoint ?? req.body.endpoint; - const endpointConfig = await getCustomEndpointConfig(endpoint); + const endpointConfig = getCustomEndpointConfig({ + endpoint, + appConfig, + }); if (!endpointConfig) { throw new Error(`Config not found for the ${endpoint} custom endpoint.`); } diff --git a/api/server/services/Endpoints/custom/initialize.spec.js b/api/server/services/Endpoints/custom/initialize.spec.js index 8108ec8302..9fbd385f30 100644 --- a/api/server/services/Endpoints/custom/initialize.spec.js +++ b/api/server/services/Endpoints/custom/initialize.spec.js @@ -1,21 +1,16 @@ const initializeClient = require('./initialize'); jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), resolveHeaders: jest.fn(), getOpenAIConfig: jest.fn(), createHandleLLMNewToken: jest.fn(), -})); - -jest.mock('librechat-data-provider', () => ({ - CacheKeys: { TOKEN_CONFIG: 'token_config' }, - ErrorTypes: { NO_USER_KEY: 'NO_USER_KEY', NO_BASE_URL: 'NO_BASE_URL' }, - envVarRegex: /\$\{([^}]+)\}/, - FetchTokenConfig: {}, - extractEnvVariable: jest.fn((value) => value), -})); - -jest.mock('@librechat/agents', () => ({ - Providers: { OLLAMA: 'ollama' }, + getCustomEndpointConfig: jest.fn().mockReturnValue({ + apiKey: 'test-key', + baseURL: 'https://test.com', + headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' }, + models: { default: ['test-model'] }, + }), })); jest.mock('~/server/services/UserService', () => ({ @@ -24,12 +19,6 @@ jest.mock('~/server/services/UserService', () => ({ })); jest.mock('~/server/services/Config', () => ({ - getCustomEndpointConfig: jest.fn().mockResolvedValue({ - apiKey: 'test-key', - baseURL: 'https://test.com', - headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' }, - models: { default: ['test-model'] }, - }), getAppConfig: jest.fn().mockResolvedValue({ 'test-endpoint': { apiKey: 'test-key', @@ -48,10 +37,6 @@ jest.mock('~/app/clients/OpenAIClient', () => { })); }); -jest.mock('~/server/utils', () => ({ - isUserProvided: jest.fn().mockReturnValue(false), -})); - jest.mock('~/cache/getLogStores', () => jest.fn().mockReturnValue({ get: jest.fn(), @@ -61,13 +46,25 @@ jest.mock('~/cache/getLogStores', () => describe('custom/initializeClient', () => { const mockRequest = { body: { endpoint: 'test-endpoint' }, - user: { id: 'user-123', email: 'test@example.com' }, + user: { id: 'user-123', email: 'test@example.com', role: 'user' }, app: { locals: {} }, }; const mockResponse = {}; beforeEach(() => { jest.clearAllMocks(); + const { getCustomEndpointConfig, resolveHeaders, getOpenAIConfig } = require('@librechat/api'); + getCustomEndpointConfig.mockReturnValue({ + apiKey: 'test-key', + baseURL: 'https://test.com', + headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' }, + models: { default: ['test-model'] }, + }); + resolveHeaders.mockReturnValue({ 'x-user': 'user-123', 'x-email': 'test@example.com' }); + getOpenAIConfig.mockReturnValue({ + useLegacyContent: true, + endpointTokenConfig: null, + }); }); it('calls resolveHeaders with headers, user, and body for body placeholder support', async () => { @@ -75,14 +72,14 @@ describe('custom/initializeClient', () => { await initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true }); expect(resolveHeaders).toHaveBeenCalledWith({ headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' }, - user: { id: 'user-123', email: 'test@example.com' }, + user: { id: 'user-123', email: 'test@example.com', role: 'user' }, body: { endpoint: 'test-endpoint' }, // body - supports {{LIBRECHAT_BODY_*}} placeholders }); }); it('throws if endpoint config is missing', async () => { - const { getCustomEndpointConfig } = require('~/server/services/Config'); - getCustomEndpointConfig.mockResolvedValueOnce(null); + const { getCustomEndpointConfig } = require('@librechat/api'); + getCustomEndpointConfig.mockReturnValueOnce(null); await expect( initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true }), ).rejects.toThrow('Config not found for the test-endpoint custom endpoint.'); diff --git a/api/server/services/Endpoints/index.js b/api/server/services/Endpoints/index.js index 1847f0ca9c..b18dfb7979 100644 --- a/api/server/services/Endpoints/index.js +++ b/api/server/services/Endpoints/index.js @@ -1,11 +1,11 @@ const { Providers } = require('@librechat/agents'); const { EModelEndpoint } = require('librechat-data-provider'); +const { getCustomEndpointConfig } = require('@librechat/api'); const initAnthropic = require('~/server/services/Endpoints/anthropic/initialize'); const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options'); const initOpenAI = require('~/server/services/Endpoints/openAI/initialize'); const initCustom = require('~/server/services/Endpoints/custom/initialize'); const initGoogle = require('~/server/services/Endpoints/google/initialize'); -const { getCustomEndpointConfig } = require('~/server/services/Config'); /** Check if the provider is a known custom provider * @param {string | undefined} [provider] - The provider string @@ -31,14 +31,16 @@ const providerConfigMap = { /** * Get the provider configuration and override endpoint based on the provider string - * @param {string} provider - The provider string - * @returns {Promise<{ - * getOptions: Function, + * @param {Object} params + * @param {string} params.provider - The provider string + * @param {AppConfig} params.appConfig - The application configuration + * @returns {{ + * getOptions: (typeof providerConfigMap)[keyof typeof providerConfigMap], * overrideProvider: string, * customEndpointConfig?: TEndpoint - * }>} + * }} */ -async function getProviderConfig(provider) { +function getProviderConfig({ provider, appConfig }) { let getOptions = providerConfigMap[provider]; let overrideProvider = provider; /** @type {TEndpoint | undefined} */ @@ -48,7 +50,7 @@ async function getProviderConfig(provider) { overrideProvider = provider.toLowerCase(); getOptions = providerConfigMap[overrideProvider]; } else if (!getOptions) { - customEndpointConfig = await getCustomEndpointConfig(provider); + customEndpointConfig = getCustomEndpointConfig({ endpoint: provider, appConfig }); if (!customEndpointConfig) { throw new Error(`Provider ${provider} not supported`); } @@ -57,7 +59,7 @@ async function getProviderConfig(provider) { } if (isKnownCustomProvider(overrideProvider) && !customEndpointConfig) { - customEndpointConfig = await getCustomEndpointConfig(provider); + customEndpointConfig = getCustomEndpointConfig({ endpoint: provider, appConfig }); if (!customEndpointConfig) { throw new Error(`Provider ${provider} not supported`); } diff --git a/packages/api/src/app/config.ts b/packages/api/src/app/config.ts new file mode 100644 index 0000000000..d7307a1b3e --- /dev/null +++ b/packages/api/src/app/config.ts @@ -0,0 +1,43 @@ +import { EModelEndpoint, removeNullishValues } from 'librechat-data-provider'; +import type { TCustomConfig, TEndpoint } from 'librechat-data-provider'; +import type { AppConfig } from '~/types'; +import { isEnabled, normalizeEndpointName } from '~/utils'; + +/** + * Retrieves the balance configuration object + * */ +export function getBalanceConfig(appConfig?: AppConfig): Partial | null { + const isLegacyEnabled = isEnabled(process.env.CHECK_BALANCE); + const startBalance = process.env.START_BALANCE; + /** @type {} */ + const config: Partial = removeNullishValues({ + enabled: isLegacyEnabled, + startBalance: startBalance != null && startBalance ? parseInt(startBalance, 10) : undefined, + }); + if (!appConfig) { + return config; + } + return { ...config, ...(appConfig?.['balance'] ?? {}) }; +} + +export const getCustomEndpointConfig = ({ + endpoint, + appConfig, +}: { + endpoint: string | EModelEndpoint; + appConfig?: AppConfig; +}): Partial | undefined => { + if (!appConfig) { + throw new Error(`Config not found for the ${endpoint} custom endpoint.`); + } + + const customEndpoints = appConfig.endpoints?.[EModelEndpoint.custom] ?? []; + return customEndpoints.find( + (endpointConfig) => normalizeEndpointName(endpointConfig.name) === endpoint, + ); +}; + +export function hasCustomUserVars(appConfig?: AppConfig): boolean { + const mcpServers = appConfig?.mcpConfig; + return Object.values(mcpServers ?? {}).some((server) => server.customUserVars); +} diff --git a/packages/api/src/app/index.ts b/packages/api/src/app/index.ts new file mode 100644 index 0000000000..f03c2281a9 --- /dev/null +++ b/packages/api/src/app/index.ts @@ -0,0 +1 @@ +export * from './config'; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 27b84d05e8..7bc7e867eb 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,3 +1,4 @@ +export * from './app'; /* MCP */ export * from './mcp/MCPManager'; export * from './mcp/oauth';