🧵 refactor: Migrate Endpoint Initialization to TypeScript (#10794)
* refactor: move endpoint initialization methods to typescript
* refactor: move agent init to packages/api
- Introduced `initialize.ts` for agent initialization, including file processing and tool loading.
- Updated `resources.ts` to allow optional appConfig parameter.
- Enhanced endpoint configuration handling in various initialization files to support model parameters.
- Added new artifacts and prompts for React component generation.
- Refactored existing code to improve type safety and maintainability.
* refactor: streamline endpoint initialization and enhance type safety
- Updated initialization functions across various endpoints to use a consistent request structure, replacing `unknown` types with `ServerResponse`.
- Simplified request handling by directly extracting keys from the request body.
- Improved type safety by ensuring user IDs are safely accessed with optional chaining.
- Removed unnecessary parameters and streamlined model options handling for better clarity and maintainability.
* refactor: moved ModelService and extractBaseURL to packages/api
- Added comprehensive tests for the models fetching functionality, covering scenarios for OpenAI, Anthropic, Google, and Ollama models.
- Updated existing endpoint index to include the new models module.
- Enhanced utility functions for URL extraction and model data processing.
- Improved type safety and error handling across the models fetching logic.
* refactor: consolidate utility functions and remove unused files
- Merged `deriveBaseURL` and `extractBaseURL` into the `@librechat/api` module for better organization.
- Removed redundant utility files and their associated tests to streamline the codebase.
- Updated imports across various client files to utilize the new consolidated functions.
- Enhanced overall maintainability by reducing the number of utility modules.
* refactor: replace ModelService references with direct imports from @librechat/api and remove ModelService file
* refactor: move encrypt/decrypt methods and key db methods to data-schemas, use `getProviderConfig` from `@librechat/api`
* chore: remove unused 'res' from options in AgentClient
* refactor: file model imports and methods
- Updated imports in various controllers and services to use the unified file model from '~/models' instead of '~/models/File'.
- Consolidated file-related methods into a new file methods module in the data-schemas package.
- Added comprehensive tests for file methods including creation, retrieval, updating, and deletion.
- Enhanced the initializeAgent function to accept dependency injection for file-related methods.
- Improved error handling and logging in file methods.
* refactor: streamline database method references in agent initialization
* refactor: enhance file method tests and update type references to IMongoFile
* refactor: consolidate database method imports in agent client and initialization
* chore: remove redundant import of initializeAgent from @librechat/api
* refactor: move checkUserKeyExpiry utility to @librechat/api and update references across endpoints
* refactor: move updateUserPlugins logic to user.ts and simplify UserController
* refactor: update imports for user key management and remove UserService
* refactor: remove unused Anthropics and Bedrock endpoint files and clean up imports
* refactor: consolidate and update encryption imports across various files to use @librechat/data-schemas
* chore: update file model mock to use unified import from '~/models'
* chore: import order
* refactor: remove migrated to TS agent.js file and its associated logic from the endpoints
* chore: add reusable function to extract imports from source code in unused-packages workflow
* chore: enhance unused-packages workflow to include @librechat/api dependencies and improve dependency extraction
* chore: improve dependency extraction in unused-packages workflow with enhanced error handling and debugging output
* chore: add detailed debugging output to unused-packages workflow for better visibility into unused dependencies and exclusion lists
* chore: refine subpath handling in unused-packages workflow to correctly process scoped and non-scoped package imports
* chore: clean up unused debug output in unused-packages workflow and reorganize type imports in initialize.ts
2025-12-03 17:21:41 -05:00
|
|
|
const { fetchModels } = require('@librechat/api');
|
2024-03-05 15:42:19 -05:00
|
|
|
const loadConfigModels = require('./loadConfigModels');
|
2025-08-26 12:10:18 -04:00
|
|
|
const { getAppConfig } = require('./app');
|
2024-03-05 15:42:19 -05:00
|
|
|
|
🧵 refactor: Migrate Endpoint Initialization to TypeScript (#10794)
* refactor: move endpoint initialization methods to typescript
* refactor: move agent init to packages/api
- Introduced `initialize.ts` for agent initialization, including file processing and tool loading.
- Updated `resources.ts` to allow optional appConfig parameter.
- Enhanced endpoint configuration handling in various initialization files to support model parameters.
- Added new artifacts and prompts for React component generation.
- Refactored existing code to improve type safety and maintainability.
* refactor: streamline endpoint initialization and enhance type safety
- Updated initialization functions across various endpoints to use a consistent request structure, replacing `unknown` types with `ServerResponse`.
- Simplified request handling by directly extracting keys from the request body.
- Improved type safety by ensuring user IDs are safely accessed with optional chaining.
- Removed unnecessary parameters and streamlined model options handling for better clarity and maintainability.
* refactor: moved ModelService and extractBaseURL to packages/api
- Added comprehensive tests for the models fetching functionality, covering scenarios for OpenAI, Anthropic, Google, and Ollama models.
- Updated existing endpoint index to include the new models module.
- Enhanced utility functions for URL extraction and model data processing.
- Improved type safety and error handling across the models fetching logic.
* refactor: consolidate utility functions and remove unused files
- Merged `deriveBaseURL` and `extractBaseURL` into the `@librechat/api` module for better organization.
- Removed redundant utility files and their associated tests to streamline the codebase.
- Updated imports across various client files to utilize the new consolidated functions.
- Enhanced overall maintainability by reducing the number of utility modules.
* refactor: replace ModelService references with direct imports from @librechat/api and remove ModelService file
* refactor: move encrypt/decrypt methods and key db methods to data-schemas, use `getProviderConfig` from `@librechat/api`
* chore: remove unused 'res' from options in AgentClient
* refactor: file model imports and methods
- Updated imports in various controllers and services to use the unified file model from '~/models' instead of '~/models/File'.
- Consolidated file-related methods into a new file methods module in the data-schemas package.
- Added comprehensive tests for file methods including creation, retrieval, updating, and deletion.
- Enhanced the initializeAgent function to accept dependency injection for file-related methods.
- Improved error handling and logging in file methods.
* refactor: streamline database method references in agent initialization
* refactor: enhance file method tests and update type references to IMongoFile
* refactor: consolidate database method imports in agent client and initialization
* chore: remove redundant import of initializeAgent from @librechat/api
* refactor: move checkUserKeyExpiry utility to @librechat/api and update references across endpoints
* refactor: move updateUserPlugins logic to user.ts and simplify UserController
* refactor: update imports for user key management and remove UserService
* refactor: remove unused Anthropics and Bedrock endpoint files and clean up imports
* refactor: consolidate and update encryption imports across various files to use @librechat/data-schemas
* chore: update file model mock to use unified import from '~/models'
* chore: import order
* refactor: remove migrated to TS agent.js file and its associated logic from the endpoints
* chore: add reusable function to extract imports from source code in unused-packages workflow
* chore: enhance unused-packages workflow to include @librechat/api dependencies and improve dependency extraction
* chore: improve dependency extraction in unused-packages workflow with enhanced error handling and debugging output
* chore: add detailed debugging output to unused-packages workflow for better visibility into unused dependencies and exclusion lists
* chore: refine subpath handling in unused-packages workflow to correctly process scoped and non-scoped package imports
* chore: clean up unused debug output in unused-packages workflow and reorganize type imports in initialize.ts
2025-12-03 17:21:41 -05:00
|
|
|
jest.mock('@librechat/api', () => ({
|
|
|
|
|
...jest.requireActual('@librechat/api'),
|
|
|
|
|
fetchModels: jest.fn(),
|
|
|
|
|
}));
|
2025-08-26 12:10:18 -04:00
|
|
|
jest.mock('./app');
|
🏗️ refactor: Remove Redundant Caching, Migrate Config Services to TypeScript (#12466)
* ♻️ refactor: Remove redundant scopedCacheKey caching, support user-provided key model fetching
Remove redundant cache layers that used `scopedCacheKey()` (tenant-only scoping)
on top of `getAppConfig()` which already caches per-principal (role+user+tenant).
This caused config overrides for different principals within the same tenant to
be invisible due to stale cached data.
Changes:
- Add `requireJwtAuth` to `/api/endpoints` route for proper user context
- Remove ENDPOINT_CONFIG, STARTUP_CONFIG, PLUGINS, TOOLS, and MODELS_CONFIG
cache layers — all derive from `getAppConfig()` with cheap computation
- Enhance MODEL_QUERIES cache: hash(baseURL+apiKey) keys, 2-minute TTL,
caching centralized in `fetchModels()` base function
- Support fetching models with user-provided API keys in `loadConfigModels`
via `getUserKeyValues` lookup (no caching for user keys)
- Update all affected tests
Closes #1028
* ♻️ refactor: Migrate config services to TypeScript in packages/api
Move core config logic from CJS /api wrappers to typed TypeScript in
packages/api using dependency injection factories:
- `createEndpointsConfigService` — endpoint config merging + checkCapability
- `createLoadConfigModels` — custom endpoint model loading with user key support
- `createMCPToolCacheService` — MCP tool cache operations (update, merge, cache)
/api files become thin wrappers that wire dependencies (getAppConfig,
loadDefaultEndpointsConfig, getUserKeyValues, getCachedTools, etc.)
into the typed factories.
Also moves existing `endpoints/config.ts` → `endpoints/config/providers.ts`
to accommodate the new `config/` directory structure.
* 🔄 fix: Invalidate models query when user API key is set or revoked
Without this, users had to refresh the page after entering their API key
to see the updated model list fetched with their credentials.
- Invalidate QueryKeys.models in useUpdateUserKeysMutation onSuccess
- Invalidate QueryKeys.models in useRevokeUserKeyMutation onSuccess
- Invalidate QueryKeys.models in useRevokeAllUserKeysMutation onSuccess
* 🗺️ fix: Remap YAML-level override keys to AppConfig equivalents in mergeConfigOverrides
Config overrides stored in the DB use YAML-level keys (TCustomConfig),
but they're merged into the already-processed AppConfig where some fields
have been renamed by AppService. This caused mcpServers overrides to land
on a nonexistent key instead of mcpConfig, so config-override MCP servers
never appeared in the UI.
- Add OVERRIDE_KEY_MAP to remap mcpServers→mcpConfig, interface→interfaceConfig
- Apply remapping before deep merge in mergeConfigOverrides
- Add test for YAML-level key remapping behavior
- Update existing tests to use AppConfig field names in assertions
* 🧪 test: Update service.spec to use AppConfig field names after override key remapping
* 🛡️ fix: Address code review findings — reliability, types, tests, and performance
- Pass tenant context (getTenantId) in importers.js getEndpointsConfig call
- Add 5 tests for user-provided API key model fetching (key found, no key,
DB error, missing userId, apiKey-only with fixed baseURL)
- Distinguish NO_USER_KEY (debug) from infrastructure errors (warn) in catch
- Switch fetchPromisesMap from Promise.all to Promise.allSettled so one
failing provider doesn't kill the entire model config
- Parallelize getUserKeyValues DB lookups via batched Promise.allSettled
instead of sequential awaits in the loop
- Hoist standardCache instance in fetchModels to avoid double instantiation
- Replace Record<string, unknown> types with Partial<TConfig>-based types;
remove as unknown as T double-cast in endpoints config
- Narrow Bedrock availableRegions to typed destructure
- Narrow version field from string|number|undefined to string|undefined
- Fix import ordering in mcp/tools.ts and config/models.ts per AGENTS.md
- Add JSDoc to getModelsConfig alias clarifying caching semantics
* fix: Guard against null getCachedTools in mergeAppTools
* 🔍 fix: Address follow-up review — deduplicate extractEnvVariable, fix error discrimination, add log-level tests
- Deduplicate extractEnvVariable calls: resolve apiKey/baseURL once, reuse
for both the entry and isUserProvided checks (Finding A)
- Move ResolvedEndpoint interface from function closure to module scope (Finding B)
- Replace fragile msg.includes('NO_USER_KEY') with ErrorTypes.NO_USER_KEY
enum check against actual error message format (Finding C). Also handle
ErrorTypes.INVALID_USER_KEY as an expected "no key" case.
- Add test asserting logger.warn is called for infra errors (not debug)
- Add test asserting logger.debug is called for NO_USER_KEY errors (not warn)
* fix: Preserve numeric assistants version via String() coercion
* 🐛 fix: Address secondary review — Ollama cache bypass, cache tests, type safety
- Fix Ollama success path bypassing cache write in fetchModels (CRITICAL):
store result before returning so Ollama models benefit from 2-minute TTL
- Add 4 fetchModels cache behavior tests: cache write with TTL, cache hit
short-circuits HTTP, skipCache bypasses read+write, empty results not cached
- Type-safe OVERRIDE_KEY_MAP: Partial<Record<keyof TCustomConfig, keyof AppConfig>>
so compiler catches future field rename mismatches
- Fix import ordering in config/models.ts (package types longest→shortest)
- Rename ToolCacheDeps → MCPToolCacheDeps for naming consistency
- Expand getModelsConfig JSDoc to explain caching granularity
* fix: Narrow OVERRIDE_KEY_MAP index to satisfy strict tsconfig
* 🧩 fix: Add allowedProviders to TConfig, remove Record<string, unknown> from PartialEndpointEntry
The agents endpoint config includes allowedProviders (used by the frontend
AgentPanel to filter available providers), but it was missing from TConfig.
This forced PartialEndpointEntry to use & Record<string, unknown> as an
escape hatch, violating AGENTS.md type policy.
- Add allowedProviders?: (string | EModelEndpoint)[] to TConfig
- Remove Record<string, unknown> from PartialEndpointEntry — now just Partial<TConfig>
* 🛡️ fix: Isolate Ollama cache write from fetch try-catch, add Ollama cache tests
- Separate Ollama fetch and cache write into distinct scopes so a cache
failure (e.g., Redis down) doesn't misattribute the error as an Ollama
API failure and fall through to the OpenAI-compatible path (Issue A)
- Add 2 Ollama-specific cache tests: models written with TTL on fetch,
cached models returned without hitting server (Issue B)
- Replace hardcoded 120000 with Time.TWO_MINUTES constant in cache TTL
test assertion (Issue C)
- Fix OVERRIDE_KEY_MAP JSDoc to accurately describe runtime vs compile-time
type enforcement (Issue D)
- Add global beforeEach for cache mock reset to prevent cross-test leakage
* 🧪 fix: Address third review — DI consistency, cache key width, MCP tests
- Inject loadCustomEndpointsConfig via EndpointsConfigDeps with default
fallback, matching loadDefaultEndpointsConfig DI pattern (Finding 3)
- Widen modelsCacheKey from 64-bit (.slice(0,16)) to 128-bit (.slice(0,32))
for collision-sensitive cross-credential cache key (Finding 4)
- Add fetchModels.mockReset() in loadConfigModels.spec beforeEach to
prevent mock implementation leaks across tests (Finding 5)
- Add 11 unit tests for createMCPToolCacheService covering all three
functions: null/empty input, successful ops, error propagation,
cold-cache merge (Finding 2)
- Simplify getModelsConfig JSDoc to @see reference (Finding 10)
* ♻️ refactor: Address remaining follow-ups from reviews
OVERRIDE_KEY_MAP completeness:
- Add missing turnstile→turnstileConfig mapping
- Add exhaustiveness test verifying all three renamed keys are remapped
and original YAML keys don't leak through
Import role context:
- Pass userRole through importConversations job → importLibreChatConvo
so role-based endpoint overrides are honored during conversation import
- Update convos.js route to include req.user.role in the job payload
createEndpointsConfigService unit tests:
- Add 8 tests covering: default+custom merge, Azure/AzureAssistants/
Anthropic Vertex/Bedrock config enrichment, assistants version
coercion, agents allowedProviders, req.config bypass
Plugins/tools efficiency:
- Use Set for includedTools/filteredTools lookups (O(1) vs O(n) per plugin)
- Combine auth check + filter into single pass (eliminates intermediate array)
- Pre-compute toolDefKeys Set for O(1) tool definition lookups
* fix: Scope model query cache by user when userIdQuery is enabled
* fix: Skip model cache for userIdQuery endpoints, fix endpoints test types
- When userIdQuery is true, skip caching entirely (like user_provided keys)
to avoid cross-user model list leakage without duplicating cache data
- Fix AgentCapabilities type error in endpoints.spec.ts — use enum values
and appConfig() helper for partial mock typing
* 🐛 fix: Restore filteredTools+includedTools composition, add checkCapability tests
- Fix filteredTools regression: whitelist and blacklist are now applied
independently (two flat guards), matching original behavior where
includedTools=['a','b'] + filteredTools=['b'] produces ['a'] (Finding A)
- Fix Set spread in toolkit loop: pre-compute toolDefKeysList array once
alongside the Set, reuse for .some() without per-plugin allocation (Finding B)
- Add 2 filteredTools tests: blacklist-only path and combined
whitelist+blacklist composition (Finding C)
- Add 3 checkCapability tests: capability present, capability absent,
fallback to defaultAgentCapabilities for non-agents endpoints (Finding D)
* 🔑 fix: Include config-override MCP servers in filterAuthorizedTools
Config-override MCP servers (defined via admin config overrides for
roles/groups) were rejected by filterAuthorizedTools because it called
getAllServerConfigs(userId) without the configServers parameter. Only
YAML and DB-backed user servers were included in the access check.
- Add configServers parameter to filterAuthorizedTools
- Resolve config servers via resolveConfigServers(req) at all 4 callsites
(create, update, duplicate, revert) using parallel Promise.all
- Pass configServers through to getAllServerConfigs(userId, configServers)
so the registry merges config-source servers into the access check
- Update filterAuthorizedTools.spec.js mock for resolveConfigServers
* fix: Skip model cache for userIdQuery endpoints, fix endpoints test types
For user-provided key endpoints (userProvide: true), skip the full model
list re-fetch during message validation — the user already selected from
a list we served them, and re-fetching with skipCache:true on every
message send is both slow and fragile (5s provider timeout = rejected model).
Instead, validate the model string format only:
- Must be a string, max 256 chars
- Must match [a-zA-Z0-9][a-zA-Z0-9_.:\-/@+ ]* (covers all known provider
model ID formats while rejecting injection attempts)
System-configured endpoints still get full model list validation as before.
* 🧪 test: Add regression tests for filterAuthorizedTools configServers and validateModel
filterAuthorizedTools:
- Add test verifying configServers is passed to getAllServerConfigs and
config-override server tools are allowed through
- Guard resolveConfigServers in createAgentHandler to only run when
MCP tools are present (skip for tool-free agent creates)
validateModel (12 new tests):
- Format validation: missing model, non-string, length overflow, leading
special char, script injection, standard model ID acceptance
- userProvide early-return: next() called immediately, getModelsConfig
not invoked (regression guard for the exact bug this fixes)
- System endpoint list validation: reject unknown model, accept known
model, handle null/missing models config
Also fix unnecessary backslash escape in MODEL_PATTERN regex.
* 🧹 fix: Remove space from MODEL_PATTERN, trim input, clean up nits
- Remove space character from MODEL_PATTERN regex — no real model ID
uses spaces; prevents spurious violation logs from whitespace artifacts
- Add model.trim() before validation to handle accidental whitespace
- Remove redundant filterUniquePlugins call on already-deduplicated output
- Add comment documenting intentional whitelist+blacklist composition
- Add getUserKeyValues.mockReset() in loadConfigModels.spec beforeEach
- Remove narrating JSDoc from getModelsConfig one-liner
- Add 2 tests: trim whitespace handling, reject spaces in model ID
* fix: Match startup tool loader semantics — includedTools takes precedence over filteredTools
The startup tool loader (loadAndFormatTools) explicitly ignores
filteredTools when includedTools is set, with a warning log. The
PluginController was applying both independently, creating inconsistent
behavior where the same config produced different results at startup
vs plugin listing time.
Restored mutually exclusive semantics: when includedTools is non-empty,
filteredTools is not evaluated.
* 🧹 chore: Simplify validateModel flow, note auth requirement on endpoints route
- Separate missing-model from invalid-model checks cleanly: type+presence
guard first, then trim+format guard (reviewer NIT)
- Add route comment noting auth is required for role/tenant scoping
* fix: Write trimmed model back to req.body.model for downstream consumers
2026-03-30 16:49:48 -04:00
|
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|
|
|
|
...jest.requireActual('@librechat/data-schemas'),
|
|
|
|
|
logger: { debug: jest.fn(), error: jest.fn(), warn: jest.fn() },
|
|
|
|
|
}));
|
|
|
|
|
jest.mock('~/models', () => ({
|
|
|
|
|
getUserKeyValues: jest.fn(),
|
|
|
|
|
}));
|
2024-03-05 15:42:19 -05:00
|
|
|
|
|
|
|
|
const exampleConfig = {
|
|
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
|
|
|
|
{
|
|
|
|
|
name: 'Mistral',
|
|
|
|
|
apiKey: '${MY_PRECIOUS_MISTRAL_KEY}',
|
|
|
|
|
baseURL: 'https://api.mistral.ai/v1',
|
|
|
|
|
models: {
|
|
|
|
|
default: ['mistral-tiny', 'mistral-small', 'mistral-medium', 'mistral-large-latest'],
|
|
|
|
|
fetch: true,
|
|
|
|
|
},
|
|
|
|
|
dropParams: ['stop', 'user', 'frequency_penalty', 'presence_penalty'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'OpenRouter',
|
|
|
|
|
apiKey: '${MY_OPENROUTER_API_KEY}',
|
|
|
|
|
baseURL: 'https://openrouter.ai/api/v1',
|
|
|
|
|
models: {
|
|
|
|
|
default: ['gpt-3.5-turbo'],
|
|
|
|
|
fetch: true,
|
|
|
|
|
},
|
|
|
|
|
dropParams: ['stop'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'groq',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'https://api.groq.com/openai/v1/',
|
|
|
|
|
models: {
|
|
|
|
|
default: ['llama2-70b-4096', 'mixtral-8x7b-32768'],
|
|
|
|
|
fetch: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Ollama',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'http://localhost:11434/v1/',
|
|
|
|
|
models: {
|
|
|
|
|
default: ['mistral', 'llama2:13b'],
|
|
|
|
|
fetch: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-05-01 09:27:02 +02:00
|
|
|
{
|
|
|
|
|
name: 'MLX',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'http://localhost:8080/v1/',
|
|
|
|
|
models: {
|
|
|
|
|
default: ['Meta-Llama-3-8B-Instruct-4bit'],
|
|
|
|
|
fetch: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-03-05 15:42:19 -05:00
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe('loadConfigModels', () => {
|
2025-08-26 12:10:18 -04:00
|
|
|
const mockRequest = { user: { id: 'testUserId' } };
|
2024-03-05 15:42:19 -05:00
|
|
|
|
|
|
|
|
const originalEnv = process.env;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
🏗️ refactor: Remove Redundant Caching, Migrate Config Services to TypeScript (#12466)
* ♻️ refactor: Remove redundant scopedCacheKey caching, support user-provided key model fetching
Remove redundant cache layers that used `scopedCacheKey()` (tenant-only scoping)
on top of `getAppConfig()` which already caches per-principal (role+user+tenant).
This caused config overrides for different principals within the same tenant to
be invisible due to stale cached data.
Changes:
- Add `requireJwtAuth` to `/api/endpoints` route for proper user context
- Remove ENDPOINT_CONFIG, STARTUP_CONFIG, PLUGINS, TOOLS, and MODELS_CONFIG
cache layers — all derive from `getAppConfig()` with cheap computation
- Enhance MODEL_QUERIES cache: hash(baseURL+apiKey) keys, 2-minute TTL,
caching centralized in `fetchModels()` base function
- Support fetching models with user-provided API keys in `loadConfigModels`
via `getUserKeyValues` lookup (no caching for user keys)
- Update all affected tests
Closes #1028
* ♻️ refactor: Migrate config services to TypeScript in packages/api
Move core config logic from CJS /api wrappers to typed TypeScript in
packages/api using dependency injection factories:
- `createEndpointsConfigService` — endpoint config merging + checkCapability
- `createLoadConfigModels` — custom endpoint model loading with user key support
- `createMCPToolCacheService` — MCP tool cache operations (update, merge, cache)
/api files become thin wrappers that wire dependencies (getAppConfig,
loadDefaultEndpointsConfig, getUserKeyValues, getCachedTools, etc.)
into the typed factories.
Also moves existing `endpoints/config.ts` → `endpoints/config/providers.ts`
to accommodate the new `config/` directory structure.
* 🔄 fix: Invalidate models query when user API key is set or revoked
Without this, users had to refresh the page after entering their API key
to see the updated model list fetched with their credentials.
- Invalidate QueryKeys.models in useUpdateUserKeysMutation onSuccess
- Invalidate QueryKeys.models in useRevokeUserKeyMutation onSuccess
- Invalidate QueryKeys.models in useRevokeAllUserKeysMutation onSuccess
* 🗺️ fix: Remap YAML-level override keys to AppConfig equivalents in mergeConfigOverrides
Config overrides stored in the DB use YAML-level keys (TCustomConfig),
but they're merged into the already-processed AppConfig where some fields
have been renamed by AppService. This caused mcpServers overrides to land
on a nonexistent key instead of mcpConfig, so config-override MCP servers
never appeared in the UI.
- Add OVERRIDE_KEY_MAP to remap mcpServers→mcpConfig, interface→interfaceConfig
- Apply remapping before deep merge in mergeConfigOverrides
- Add test for YAML-level key remapping behavior
- Update existing tests to use AppConfig field names in assertions
* 🧪 test: Update service.spec to use AppConfig field names after override key remapping
* 🛡️ fix: Address code review findings — reliability, types, tests, and performance
- Pass tenant context (getTenantId) in importers.js getEndpointsConfig call
- Add 5 tests for user-provided API key model fetching (key found, no key,
DB error, missing userId, apiKey-only with fixed baseURL)
- Distinguish NO_USER_KEY (debug) from infrastructure errors (warn) in catch
- Switch fetchPromisesMap from Promise.all to Promise.allSettled so one
failing provider doesn't kill the entire model config
- Parallelize getUserKeyValues DB lookups via batched Promise.allSettled
instead of sequential awaits in the loop
- Hoist standardCache instance in fetchModels to avoid double instantiation
- Replace Record<string, unknown> types with Partial<TConfig>-based types;
remove as unknown as T double-cast in endpoints config
- Narrow Bedrock availableRegions to typed destructure
- Narrow version field from string|number|undefined to string|undefined
- Fix import ordering in mcp/tools.ts and config/models.ts per AGENTS.md
- Add JSDoc to getModelsConfig alias clarifying caching semantics
* fix: Guard against null getCachedTools in mergeAppTools
* 🔍 fix: Address follow-up review — deduplicate extractEnvVariable, fix error discrimination, add log-level tests
- Deduplicate extractEnvVariable calls: resolve apiKey/baseURL once, reuse
for both the entry and isUserProvided checks (Finding A)
- Move ResolvedEndpoint interface from function closure to module scope (Finding B)
- Replace fragile msg.includes('NO_USER_KEY') with ErrorTypes.NO_USER_KEY
enum check against actual error message format (Finding C). Also handle
ErrorTypes.INVALID_USER_KEY as an expected "no key" case.
- Add test asserting logger.warn is called for infra errors (not debug)
- Add test asserting logger.debug is called for NO_USER_KEY errors (not warn)
* fix: Preserve numeric assistants version via String() coercion
* 🐛 fix: Address secondary review — Ollama cache bypass, cache tests, type safety
- Fix Ollama success path bypassing cache write in fetchModels (CRITICAL):
store result before returning so Ollama models benefit from 2-minute TTL
- Add 4 fetchModels cache behavior tests: cache write with TTL, cache hit
short-circuits HTTP, skipCache bypasses read+write, empty results not cached
- Type-safe OVERRIDE_KEY_MAP: Partial<Record<keyof TCustomConfig, keyof AppConfig>>
so compiler catches future field rename mismatches
- Fix import ordering in config/models.ts (package types longest→shortest)
- Rename ToolCacheDeps → MCPToolCacheDeps for naming consistency
- Expand getModelsConfig JSDoc to explain caching granularity
* fix: Narrow OVERRIDE_KEY_MAP index to satisfy strict tsconfig
* 🧩 fix: Add allowedProviders to TConfig, remove Record<string, unknown> from PartialEndpointEntry
The agents endpoint config includes allowedProviders (used by the frontend
AgentPanel to filter available providers), but it was missing from TConfig.
This forced PartialEndpointEntry to use & Record<string, unknown> as an
escape hatch, violating AGENTS.md type policy.
- Add allowedProviders?: (string | EModelEndpoint)[] to TConfig
- Remove Record<string, unknown> from PartialEndpointEntry — now just Partial<TConfig>
* 🛡️ fix: Isolate Ollama cache write from fetch try-catch, add Ollama cache tests
- Separate Ollama fetch and cache write into distinct scopes so a cache
failure (e.g., Redis down) doesn't misattribute the error as an Ollama
API failure and fall through to the OpenAI-compatible path (Issue A)
- Add 2 Ollama-specific cache tests: models written with TTL on fetch,
cached models returned without hitting server (Issue B)
- Replace hardcoded 120000 with Time.TWO_MINUTES constant in cache TTL
test assertion (Issue C)
- Fix OVERRIDE_KEY_MAP JSDoc to accurately describe runtime vs compile-time
type enforcement (Issue D)
- Add global beforeEach for cache mock reset to prevent cross-test leakage
* 🧪 fix: Address third review — DI consistency, cache key width, MCP tests
- Inject loadCustomEndpointsConfig via EndpointsConfigDeps with default
fallback, matching loadDefaultEndpointsConfig DI pattern (Finding 3)
- Widen modelsCacheKey from 64-bit (.slice(0,16)) to 128-bit (.slice(0,32))
for collision-sensitive cross-credential cache key (Finding 4)
- Add fetchModels.mockReset() in loadConfigModels.spec beforeEach to
prevent mock implementation leaks across tests (Finding 5)
- Add 11 unit tests for createMCPToolCacheService covering all three
functions: null/empty input, successful ops, error propagation,
cold-cache merge (Finding 2)
- Simplify getModelsConfig JSDoc to @see reference (Finding 10)
* ♻️ refactor: Address remaining follow-ups from reviews
OVERRIDE_KEY_MAP completeness:
- Add missing turnstile→turnstileConfig mapping
- Add exhaustiveness test verifying all three renamed keys are remapped
and original YAML keys don't leak through
Import role context:
- Pass userRole through importConversations job → importLibreChatConvo
so role-based endpoint overrides are honored during conversation import
- Update convos.js route to include req.user.role in the job payload
createEndpointsConfigService unit tests:
- Add 8 tests covering: default+custom merge, Azure/AzureAssistants/
Anthropic Vertex/Bedrock config enrichment, assistants version
coercion, agents allowedProviders, req.config bypass
Plugins/tools efficiency:
- Use Set for includedTools/filteredTools lookups (O(1) vs O(n) per plugin)
- Combine auth check + filter into single pass (eliminates intermediate array)
- Pre-compute toolDefKeys Set for O(1) tool definition lookups
* fix: Scope model query cache by user when userIdQuery is enabled
* fix: Skip model cache for userIdQuery endpoints, fix endpoints test types
- When userIdQuery is true, skip caching entirely (like user_provided keys)
to avoid cross-user model list leakage without duplicating cache data
- Fix AgentCapabilities type error in endpoints.spec.ts — use enum values
and appConfig() helper for partial mock typing
* 🐛 fix: Restore filteredTools+includedTools composition, add checkCapability tests
- Fix filteredTools regression: whitelist and blacklist are now applied
independently (two flat guards), matching original behavior where
includedTools=['a','b'] + filteredTools=['b'] produces ['a'] (Finding A)
- Fix Set spread in toolkit loop: pre-compute toolDefKeysList array once
alongside the Set, reuse for .some() without per-plugin allocation (Finding B)
- Add 2 filteredTools tests: blacklist-only path and combined
whitelist+blacklist composition (Finding C)
- Add 3 checkCapability tests: capability present, capability absent,
fallback to defaultAgentCapabilities for non-agents endpoints (Finding D)
* 🔑 fix: Include config-override MCP servers in filterAuthorizedTools
Config-override MCP servers (defined via admin config overrides for
roles/groups) were rejected by filterAuthorizedTools because it called
getAllServerConfigs(userId) without the configServers parameter. Only
YAML and DB-backed user servers were included in the access check.
- Add configServers parameter to filterAuthorizedTools
- Resolve config servers via resolveConfigServers(req) at all 4 callsites
(create, update, duplicate, revert) using parallel Promise.all
- Pass configServers through to getAllServerConfigs(userId, configServers)
so the registry merges config-source servers into the access check
- Update filterAuthorizedTools.spec.js mock for resolveConfigServers
* fix: Skip model cache for userIdQuery endpoints, fix endpoints test types
For user-provided key endpoints (userProvide: true), skip the full model
list re-fetch during message validation — the user already selected from
a list we served them, and re-fetching with skipCache:true on every
message send is both slow and fragile (5s provider timeout = rejected model).
Instead, validate the model string format only:
- Must be a string, max 256 chars
- Must match [a-zA-Z0-9][a-zA-Z0-9_.:\-/@+ ]* (covers all known provider
model ID formats while rejecting injection attempts)
System-configured endpoints still get full model list validation as before.
* 🧪 test: Add regression tests for filterAuthorizedTools configServers and validateModel
filterAuthorizedTools:
- Add test verifying configServers is passed to getAllServerConfigs and
config-override server tools are allowed through
- Guard resolveConfigServers in createAgentHandler to only run when
MCP tools are present (skip for tool-free agent creates)
validateModel (12 new tests):
- Format validation: missing model, non-string, length overflow, leading
special char, script injection, standard model ID acceptance
- userProvide early-return: next() called immediately, getModelsConfig
not invoked (regression guard for the exact bug this fixes)
- System endpoint list validation: reject unknown model, accept known
model, handle null/missing models config
Also fix unnecessary backslash escape in MODEL_PATTERN regex.
* 🧹 fix: Remove space from MODEL_PATTERN, trim input, clean up nits
- Remove space character from MODEL_PATTERN regex — no real model ID
uses spaces; prevents spurious violation logs from whitespace artifacts
- Add model.trim() before validation to handle accidental whitespace
- Remove redundant filterUniquePlugins call on already-deduplicated output
- Add comment documenting intentional whitelist+blacklist composition
- Add getUserKeyValues.mockReset() in loadConfigModels.spec beforeEach
- Remove narrating JSDoc from getModelsConfig one-liner
- Add 2 tests: trim whitespace handling, reject spaces in model ID
* fix: Match startup tool loader semantics — includedTools takes precedence over filteredTools
The startup tool loader (loadAndFormatTools) explicitly ignores
filteredTools when includedTools is set, with a warning log. The
PluginController was applying both independently, creating inconsistent
behavior where the same config produced different results at startup
vs plugin listing time.
Restored mutually exclusive semantics: when includedTools is non-empty,
filteredTools is not evaluated.
* 🧹 chore: Simplify validateModel flow, note auth requirement on endpoints route
- Separate missing-model from invalid-model checks cleanly: type+presence
guard first, then trim+format guard (reviewer NIT)
- Add route comment noting auth is required for role/tenant scoping
* fix: Write trimmed model back to req.body.model for downstream consumers
2026-03-30 16:49:48 -04:00
|
|
|
jest.clearAllMocks();
|
|
|
|
|
fetchModels.mockReset();
|
|
|
|
|
require('~/models').getUserKeyValues.mockReset();
|
2024-03-05 15:42:19 -05:00
|
|
|
process.env = { ...originalEnv };
|
2025-08-26 12:10:18 -04:00
|
|
|
|
|
|
|
|
getAppConfig.mockResolvedValue({});
|
2024-03-05 15:42:19 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
process.env = originalEnv;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return an empty object if customConfig is null', async () => {
|
2025-08-26 12:10:18 -04:00
|
|
|
getAppConfig.mockResolvedValue(null);
|
2024-03-05 15:42:19 -05:00
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
expect(result).toEqual({});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles azure models and endpoint correctly', async () => {
|
2025-08-26 12:10:18 -04:00
|
|
|
getAppConfig.mockResolvedValue({
|
2024-03-05 15:42:19 -05:00
|
|
|
endpoints: {
|
2025-08-26 12:10:18 -04:00
|
|
|
azureOpenAI: { modelNames: ['model1', 'model2'] },
|
2024-03-05 15:42:19 -05:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
expect(result.azureOpenAI).toEqual(['model1', 'model2']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('fetches custom models based on the unique key', async () => {
|
|
|
|
|
process.env.BASE_URL = 'http://example.com';
|
|
|
|
|
process.env.API_KEY = 'some-api-key';
|
2025-08-26 12:10:18 -04:00
|
|
|
const customEndpoints = [
|
|
|
|
|
{
|
|
|
|
|
baseURL: '${BASE_URL}',
|
|
|
|
|
apiKey: '${API_KEY}',
|
|
|
|
|
name: 'CustomModel',
|
|
|
|
|
models: { fetch: true },
|
|
|
|
|
},
|
|
|
|
|
];
|
2024-03-05 15:42:19 -05:00
|
|
|
|
2025-08-26 12:10:18 -04:00
|
|
|
getAppConfig.mockResolvedValue({ endpoints: { custom: customEndpoints } });
|
2024-03-05 15:42:19 -05:00
|
|
|
fetchModels.mockResolvedValue(['customModel1', 'customModel2']);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
expect(fetchModels).toHaveBeenCalled();
|
|
|
|
|
expect(result.CustomModel).toEqual(['customModel1', 'customModel2']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('correctly associates models to names using unique keys', async () => {
|
2025-08-26 12:10:18 -04:00
|
|
|
getAppConfig.mockResolvedValue({
|
2024-03-05 15:42:19 -05:00
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
|
|
|
|
{
|
|
|
|
|
baseURL: 'http://example.com',
|
|
|
|
|
apiKey: 'API_KEY1',
|
|
|
|
|
name: 'Model1',
|
|
|
|
|
models: { fetch: true },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
baseURL: 'http://example.com',
|
|
|
|
|
apiKey: 'API_KEY2',
|
|
|
|
|
name: 'Model2',
|
|
|
|
|
models: { fetch: true },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
fetchModels.mockImplementation(({ apiKey }) =>
|
|
|
|
|
Promise.resolve(apiKey === 'API_KEY1' ? ['model1Data'] : ['model2Data']),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
expect(result.Model1).toEqual(['model1Data']);
|
|
|
|
|
expect(result.Model2).toEqual(['model2Data']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('correctly handles multiple endpoints with the same baseURL but different apiKeys', async () => {
|
|
|
|
|
// Mock the custom configuration to simulate the user's scenario
|
2025-08-26 12:10:18 -04:00
|
|
|
getAppConfig.mockResolvedValue({
|
2024-03-05 15:42:19 -05:00
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
|
|
|
|
{
|
|
|
|
|
name: 'LiteLLM',
|
|
|
|
|
apiKey: '${LITELLM_ALL_MODELS}',
|
|
|
|
|
baseURL: '${LITELLM_HOST}',
|
|
|
|
|
models: { fetch: true },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'OpenAI',
|
|
|
|
|
apiKey: '${LITELLM_OPENAI_MODELS}',
|
|
|
|
|
baseURL: '${LITELLM_SECOND_HOST}',
|
|
|
|
|
models: { fetch: true },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Google',
|
|
|
|
|
apiKey: '${LITELLM_GOOGLE_MODELS}',
|
|
|
|
|
baseURL: '${LITELLM_SECOND_HOST}',
|
|
|
|
|
models: { fetch: true },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Mock `fetchModels` to return different models based on the apiKey
|
|
|
|
|
fetchModels.mockImplementation(({ apiKey }) => {
|
|
|
|
|
switch (apiKey) {
|
|
|
|
|
case '${LITELLM_ALL_MODELS}':
|
|
|
|
|
return Promise.resolve(['AllModel1', 'AllModel2']);
|
|
|
|
|
case '${LITELLM_OPENAI_MODELS}':
|
|
|
|
|
return Promise.resolve(['OpenAIModel']);
|
|
|
|
|
case '${LITELLM_GOOGLE_MODELS}':
|
|
|
|
|
return Promise.resolve(['GoogleModel']);
|
|
|
|
|
default:
|
|
|
|
|
return Promise.resolve([]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
|
|
|
|
|
// Assert that the models are correctly fetched and mapped based on unique keys
|
|
|
|
|
expect(result.LiteLLM).toEqual(['AllModel1', 'AllModel2']);
|
|
|
|
|
expect(result.OpenAI).toEqual(['OpenAIModel']);
|
|
|
|
|
expect(result.Google).toEqual(['GoogleModel']);
|
|
|
|
|
|
|
|
|
|
// Ensure that fetchModels was called with correct parameters
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledTimes(3);
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ apiKey: '${LITELLM_ALL_MODELS}' }),
|
|
|
|
|
);
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ apiKey: '${LITELLM_OPENAI_MODELS}' }),
|
|
|
|
|
);
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ apiKey: '${LITELLM_GOOGLE_MODELS}' }),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('loads models based on custom endpoint configuration respecting fetch rules', async () => {
|
|
|
|
|
process.env.MY_PRECIOUS_MISTRAL_KEY = 'actual_mistral_api_key';
|
|
|
|
|
process.env.MY_OPENROUTER_API_KEY = 'actual_openrouter_api_key';
|
|
|
|
|
// Setup custom configuration with specific API keys for Mistral and OpenRouter
|
|
|
|
|
// and "user_provided" for groq and Ollama, indicating no fetch for the latter two
|
2025-08-26 12:10:18 -04:00
|
|
|
getAppConfig.mockResolvedValue(exampleConfig);
|
2024-03-05 15:42:19 -05:00
|
|
|
|
|
|
|
|
// Assuming fetchModels would be called only for Mistral and OpenRouter
|
|
|
|
|
fetchModels.mockImplementation(({ name }) => {
|
|
|
|
|
switch (name) {
|
|
|
|
|
case 'Mistral':
|
|
|
|
|
return Promise.resolve([
|
|
|
|
|
'mistral-tiny',
|
|
|
|
|
'mistral-small',
|
|
|
|
|
'mistral-medium',
|
|
|
|
|
'mistral-large-latest',
|
|
|
|
|
]);
|
|
|
|
|
case 'OpenRouter':
|
|
|
|
|
return Promise.resolve(['gpt-3.5-turbo']);
|
|
|
|
|
default:
|
|
|
|
|
return Promise.resolve([]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
|
|
|
|
|
// Since fetch is true and apiKey is not "user_provided", fetching occurs for Mistral and OpenRouter
|
|
|
|
|
expect(result.Mistral).toEqual([
|
|
|
|
|
'mistral-tiny',
|
|
|
|
|
'mistral-small',
|
|
|
|
|
'mistral-medium',
|
|
|
|
|
'mistral-large-latest',
|
|
|
|
|
]);
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
name: 'Mistral',
|
|
|
|
|
apiKey: process.env.MY_PRECIOUS_MISTRAL_KEY,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result.OpenRouter).toEqual(['gpt-3.5-turbo']);
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
name: 'OpenRouter',
|
|
|
|
|
apiKey: process.env.MY_OPENROUTER_API_KEY,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
2024-11-04 12:59:04 -05:00
|
|
|
// For groq and ollama, since the apiKey is "user_provided", models should not be fetched
|
2024-03-05 15:42:19 -05:00
|
|
|
// Depending on your implementation's behavior regarding "default" models without fetching,
|
|
|
|
|
// you may need to adjust the following assertions:
|
2025-10-11 06:55:06 -05:00
|
|
|
expect(result.groq).toEqual(exampleConfig.endpoints.custom[2].models.default);
|
|
|
|
|
expect(result.ollama).toEqual(exampleConfig.endpoints.custom[3].models.default);
|
2024-03-05 15:42:19 -05:00
|
|
|
|
2024-11-04 12:59:04 -05:00
|
|
|
// Verifying fetchModels was not called for groq and ollama
|
2024-03-05 15:42:19 -05:00
|
|
|
expect(fetchModels).not.toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
name: 'groq',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(fetchModels).not.toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
2024-11-04 12:59:04 -05:00
|
|
|
name: 'ollama',
|
2024-03-05 15:42:19 -05:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
2024-03-29 10:43:36 -04:00
|
|
|
|
|
|
|
|
it('falls back to default models if fetching returns an empty array', async () => {
|
2025-08-26 12:10:18 -04:00
|
|
|
getAppConfig.mockResolvedValue({
|
2024-03-29 10:43:36 -04:00
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
2024-03-29 11:49:38 -04:00
|
|
|
{
|
|
|
|
|
name: 'EndpointWithSameFetchKey',
|
|
|
|
|
apiKey: 'API_KEY',
|
|
|
|
|
baseURL: 'http://example.com',
|
|
|
|
|
models: {
|
|
|
|
|
fetch: true,
|
|
|
|
|
default: ['defaultModel1'],
|
|
|
|
|
},
|
|
|
|
|
},
|
2024-03-29 10:43:36 -04:00
|
|
|
{
|
|
|
|
|
name: 'EmptyFetchModel',
|
|
|
|
|
apiKey: 'API_KEY',
|
|
|
|
|
baseURL: 'http://example.com',
|
|
|
|
|
models: {
|
|
|
|
|
fetch: true,
|
|
|
|
|
default: ['defaultModel1', 'defaultModel2'],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fetchModels.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
2024-03-29 11:49:38 -04:00
|
|
|
expect(fetchModels).toHaveBeenCalledTimes(1);
|
2024-03-29 10:43:36 -04:00
|
|
|
expect(result.EmptyFetchModel).toEqual(['defaultModel1', 'defaultModel2']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('falls back to default models if fetching returns a falsy value', async () => {
|
2025-08-26 12:10:18 -04:00
|
|
|
getAppConfig.mockResolvedValue({
|
2024-03-29 10:43:36 -04:00
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
|
|
|
|
{
|
|
|
|
|
name: 'FalsyFetchModel',
|
|
|
|
|
apiKey: 'API_KEY',
|
|
|
|
|
baseURL: 'http://example.com',
|
|
|
|
|
models: {
|
|
|
|
|
fetch: true,
|
|
|
|
|
default: ['defaultModel1', 'defaultModel2'],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fetchModels.mockResolvedValue(false);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
name: 'FalsyFetchModel',
|
|
|
|
|
apiKey: 'API_KEY',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result.FalsyFetchModel).toEqual(['defaultModel1', 'defaultModel2']);
|
|
|
|
|
});
|
2024-11-04 12:59:04 -05:00
|
|
|
|
🏗️ refactor: Remove Redundant Caching, Migrate Config Services to TypeScript (#12466)
* ♻️ refactor: Remove redundant scopedCacheKey caching, support user-provided key model fetching
Remove redundant cache layers that used `scopedCacheKey()` (tenant-only scoping)
on top of `getAppConfig()` which already caches per-principal (role+user+tenant).
This caused config overrides for different principals within the same tenant to
be invisible due to stale cached data.
Changes:
- Add `requireJwtAuth` to `/api/endpoints` route for proper user context
- Remove ENDPOINT_CONFIG, STARTUP_CONFIG, PLUGINS, TOOLS, and MODELS_CONFIG
cache layers — all derive from `getAppConfig()` with cheap computation
- Enhance MODEL_QUERIES cache: hash(baseURL+apiKey) keys, 2-minute TTL,
caching centralized in `fetchModels()` base function
- Support fetching models with user-provided API keys in `loadConfigModels`
via `getUserKeyValues` lookup (no caching for user keys)
- Update all affected tests
Closes #1028
* ♻️ refactor: Migrate config services to TypeScript in packages/api
Move core config logic from CJS /api wrappers to typed TypeScript in
packages/api using dependency injection factories:
- `createEndpointsConfigService` — endpoint config merging + checkCapability
- `createLoadConfigModels` — custom endpoint model loading with user key support
- `createMCPToolCacheService` — MCP tool cache operations (update, merge, cache)
/api files become thin wrappers that wire dependencies (getAppConfig,
loadDefaultEndpointsConfig, getUserKeyValues, getCachedTools, etc.)
into the typed factories.
Also moves existing `endpoints/config.ts` → `endpoints/config/providers.ts`
to accommodate the new `config/` directory structure.
* 🔄 fix: Invalidate models query when user API key is set or revoked
Without this, users had to refresh the page after entering their API key
to see the updated model list fetched with their credentials.
- Invalidate QueryKeys.models in useUpdateUserKeysMutation onSuccess
- Invalidate QueryKeys.models in useRevokeUserKeyMutation onSuccess
- Invalidate QueryKeys.models in useRevokeAllUserKeysMutation onSuccess
* 🗺️ fix: Remap YAML-level override keys to AppConfig equivalents in mergeConfigOverrides
Config overrides stored in the DB use YAML-level keys (TCustomConfig),
but they're merged into the already-processed AppConfig where some fields
have been renamed by AppService. This caused mcpServers overrides to land
on a nonexistent key instead of mcpConfig, so config-override MCP servers
never appeared in the UI.
- Add OVERRIDE_KEY_MAP to remap mcpServers→mcpConfig, interface→interfaceConfig
- Apply remapping before deep merge in mergeConfigOverrides
- Add test for YAML-level key remapping behavior
- Update existing tests to use AppConfig field names in assertions
* 🧪 test: Update service.spec to use AppConfig field names after override key remapping
* 🛡️ fix: Address code review findings — reliability, types, tests, and performance
- Pass tenant context (getTenantId) in importers.js getEndpointsConfig call
- Add 5 tests for user-provided API key model fetching (key found, no key,
DB error, missing userId, apiKey-only with fixed baseURL)
- Distinguish NO_USER_KEY (debug) from infrastructure errors (warn) in catch
- Switch fetchPromisesMap from Promise.all to Promise.allSettled so one
failing provider doesn't kill the entire model config
- Parallelize getUserKeyValues DB lookups via batched Promise.allSettled
instead of sequential awaits in the loop
- Hoist standardCache instance in fetchModels to avoid double instantiation
- Replace Record<string, unknown> types with Partial<TConfig>-based types;
remove as unknown as T double-cast in endpoints config
- Narrow Bedrock availableRegions to typed destructure
- Narrow version field from string|number|undefined to string|undefined
- Fix import ordering in mcp/tools.ts and config/models.ts per AGENTS.md
- Add JSDoc to getModelsConfig alias clarifying caching semantics
* fix: Guard against null getCachedTools in mergeAppTools
* 🔍 fix: Address follow-up review — deduplicate extractEnvVariable, fix error discrimination, add log-level tests
- Deduplicate extractEnvVariable calls: resolve apiKey/baseURL once, reuse
for both the entry and isUserProvided checks (Finding A)
- Move ResolvedEndpoint interface from function closure to module scope (Finding B)
- Replace fragile msg.includes('NO_USER_KEY') with ErrorTypes.NO_USER_KEY
enum check against actual error message format (Finding C). Also handle
ErrorTypes.INVALID_USER_KEY as an expected "no key" case.
- Add test asserting logger.warn is called for infra errors (not debug)
- Add test asserting logger.debug is called for NO_USER_KEY errors (not warn)
* fix: Preserve numeric assistants version via String() coercion
* 🐛 fix: Address secondary review — Ollama cache bypass, cache tests, type safety
- Fix Ollama success path bypassing cache write in fetchModels (CRITICAL):
store result before returning so Ollama models benefit from 2-minute TTL
- Add 4 fetchModels cache behavior tests: cache write with TTL, cache hit
short-circuits HTTP, skipCache bypasses read+write, empty results not cached
- Type-safe OVERRIDE_KEY_MAP: Partial<Record<keyof TCustomConfig, keyof AppConfig>>
so compiler catches future field rename mismatches
- Fix import ordering in config/models.ts (package types longest→shortest)
- Rename ToolCacheDeps → MCPToolCacheDeps for naming consistency
- Expand getModelsConfig JSDoc to explain caching granularity
* fix: Narrow OVERRIDE_KEY_MAP index to satisfy strict tsconfig
* 🧩 fix: Add allowedProviders to TConfig, remove Record<string, unknown> from PartialEndpointEntry
The agents endpoint config includes allowedProviders (used by the frontend
AgentPanel to filter available providers), but it was missing from TConfig.
This forced PartialEndpointEntry to use & Record<string, unknown> as an
escape hatch, violating AGENTS.md type policy.
- Add allowedProviders?: (string | EModelEndpoint)[] to TConfig
- Remove Record<string, unknown> from PartialEndpointEntry — now just Partial<TConfig>
* 🛡️ fix: Isolate Ollama cache write from fetch try-catch, add Ollama cache tests
- Separate Ollama fetch and cache write into distinct scopes so a cache
failure (e.g., Redis down) doesn't misattribute the error as an Ollama
API failure and fall through to the OpenAI-compatible path (Issue A)
- Add 2 Ollama-specific cache tests: models written with TTL on fetch,
cached models returned without hitting server (Issue B)
- Replace hardcoded 120000 with Time.TWO_MINUTES constant in cache TTL
test assertion (Issue C)
- Fix OVERRIDE_KEY_MAP JSDoc to accurately describe runtime vs compile-time
type enforcement (Issue D)
- Add global beforeEach for cache mock reset to prevent cross-test leakage
* 🧪 fix: Address third review — DI consistency, cache key width, MCP tests
- Inject loadCustomEndpointsConfig via EndpointsConfigDeps with default
fallback, matching loadDefaultEndpointsConfig DI pattern (Finding 3)
- Widen modelsCacheKey from 64-bit (.slice(0,16)) to 128-bit (.slice(0,32))
for collision-sensitive cross-credential cache key (Finding 4)
- Add fetchModels.mockReset() in loadConfigModels.spec beforeEach to
prevent mock implementation leaks across tests (Finding 5)
- Add 11 unit tests for createMCPToolCacheService covering all three
functions: null/empty input, successful ops, error propagation,
cold-cache merge (Finding 2)
- Simplify getModelsConfig JSDoc to @see reference (Finding 10)
* ♻️ refactor: Address remaining follow-ups from reviews
OVERRIDE_KEY_MAP completeness:
- Add missing turnstile→turnstileConfig mapping
- Add exhaustiveness test verifying all three renamed keys are remapped
and original YAML keys don't leak through
Import role context:
- Pass userRole through importConversations job → importLibreChatConvo
so role-based endpoint overrides are honored during conversation import
- Update convos.js route to include req.user.role in the job payload
createEndpointsConfigService unit tests:
- Add 8 tests covering: default+custom merge, Azure/AzureAssistants/
Anthropic Vertex/Bedrock config enrichment, assistants version
coercion, agents allowedProviders, req.config bypass
Plugins/tools efficiency:
- Use Set for includedTools/filteredTools lookups (O(1) vs O(n) per plugin)
- Combine auth check + filter into single pass (eliminates intermediate array)
- Pre-compute toolDefKeys Set for O(1) tool definition lookups
* fix: Scope model query cache by user when userIdQuery is enabled
* fix: Skip model cache for userIdQuery endpoints, fix endpoints test types
- When userIdQuery is true, skip caching entirely (like user_provided keys)
to avoid cross-user model list leakage without duplicating cache data
- Fix AgentCapabilities type error in endpoints.spec.ts — use enum values
and appConfig() helper for partial mock typing
* 🐛 fix: Restore filteredTools+includedTools composition, add checkCapability tests
- Fix filteredTools regression: whitelist and blacklist are now applied
independently (two flat guards), matching original behavior where
includedTools=['a','b'] + filteredTools=['b'] produces ['a'] (Finding A)
- Fix Set spread in toolkit loop: pre-compute toolDefKeysList array once
alongside the Set, reuse for .some() without per-plugin allocation (Finding B)
- Add 2 filteredTools tests: blacklist-only path and combined
whitelist+blacklist composition (Finding C)
- Add 3 checkCapability tests: capability present, capability absent,
fallback to defaultAgentCapabilities for non-agents endpoints (Finding D)
* 🔑 fix: Include config-override MCP servers in filterAuthorizedTools
Config-override MCP servers (defined via admin config overrides for
roles/groups) were rejected by filterAuthorizedTools because it called
getAllServerConfigs(userId) without the configServers parameter. Only
YAML and DB-backed user servers were included in the access check.
- Add configServers parameter to filterAuthorizedTools
- Resolve config servers via resolveConfigServers(req) at all 4 callsites
(create, update, duplicate, revert) using parallel Promise.all
- Pass configServers through to getAllServerConfigs(userId, configServers)
so the registry merges config-source servers into the access check
- Update filterAuthorizedTools.spec.js mock for resolveConfigServers
* fix: Skip model cache for userIdQuery endpoints, fix endpoints test types
For user-provided key endpoints (userProvide: true), skip the full model
list re-fetch during message validation — the user already selected from
a list we served them, and re-fetching with skipCache:true on every
message send is both slow and fragile (5s provider timeout = rejected model).
Instead, validate the model string format only:
- Must be a string, max 256 chars
- Must match [a-zA-Z0-9][a-zA-Z0-9_.:\-/@+ ]* (covers all known provider
model ID formats while rejecting injection attempts)
System-configured endpoints still get full model list validation as before.
* 🧪 test: Add regression tests for filterAuthorizedTools configServers and validateModel
filterAuthorizedTools:
- Add test verifying configServers is passed to getAllServerConfigs and
config-override server tools are allowed through
- Guard resolveConfigServers in createAgentHandler to only run when
MCP tools are present (skip for tool-free agent creates)
validateModel (12 new tests):
- Format validation: missing model, non-string, length overflow, leading
special char, script injection, standard model ID acceptance
- userProvide early-return: next() called immediately, getModelsConfig
not invoked (regression guard for the exact bug this fixes)
- System endpoint list validation: reject unknown model, accept known
model, handle null/missing models config
Also fix unnecessary backslash escape in MODEL_PATTERN regex.
* 🧹 fix: Remove space from MODEL_PATTERN, trim input, clean up nits
- Remove space character from MODEL_PATTERN regex — no real model ID
uses spaces; prevents spurious violation logs from whitespace artifacts
- Add model.trim() before validation to handle accidental whitespace
- Remove redundant filterUniquePlugins call on already-deduplicated output
- Add comment documenting intentional whitelist+blacklist composition
- Add getUserKeyValues.mockReset() in loadConfigModels.spec beforeEach
- Remove narrating JSDoc from getModelsConfig one-liner
- Add 2 tests: trim whitespace handling, reject spaces in model ID
* fix: Match startup tool loader semantics — includedTools takes precedence over filteredTools
The startup tool loader (loadAndFormatTools) explicitly ignores
filteredTools when includedTools is set, with a warning log. The
PluginController was applying both independently, creating inconsistent
behavior where the same config produced different results at startup
vs plugin listing time.
Restored mutually exclusive semantics: when includedTools is non-empty,
filteredTools is not evaluated.
* 🧹 chore: Simplify validateModel flow, note auth requirement on endpoints route
- Separate missing-model from invalid-model checks cleanly: type+presence
guard first, then trim+format guard (reviewer NIT)
- Add route comment noting auth is required for role/tenant scoping
* fix: Write trimmed model back to req.body.model for downstream consumers
2026-03-30 16:49:48 -04:00
|
|
|
describe('user-provided API key model fetching', () => {
|
|
|
|
|
it('fetches models using user-provided API key when key is stored', async () => {
|
|
|
|
|
const { getUserKeyValues } = require('~/models');
|
|
|
|
|
getUserKeyValues.mockResolvedValueOnce({
|
|
|
|
|
apiKey: 'sk-user-key',
|
|
|
|
|
baseURL: 'https://api.x.com/v1',
|
|
|
|
|
});
|
|
|
|
|
getAppConfig.mockResolvedValue({
|
|
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
|
|
|
|
{
|
|
|
|
|
name: 'UserEndpoint',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'user_provided',
|
|
|
|
|
models: { fetch: true, default: ['fallback-model'] },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
fetchModels.mockResolvedValue(['fetched-model-a', 'fetched-model-b']);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
|
|
|
|
|
expect(getUserKeyValues).toHaveBeenCalledWith({ userId: 'testUserId', name: 'UserEndpoint' });
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
apiKey: 'sk-user-key',
|
|
|
|
|
baseURL: 'https://api.x.com/v1',
|
|
|
|
|
skipCache: true,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(result.UserEndpoint).toEqual(['fetched-model-a', 'fetched-model-b']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('falls back to defaults when getUserKeyValues returns no apiKey', async () => {
|
|
|
|
|
const { getUserKeyValues } = require('~/models');
|
|
|
|
|
getUserKeyValues.mockResolvedValueOnce({ baseURL: 'https://api.x.com/v1' });
|
|
|
|
|
getAppConfig.mockResolvedValue({
|
|
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
|
|
|
|
{
|
|
|
|
|
name: 'NoKeyEndpoint',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'https://api.x.com/v1',
|
|
|
|
|
models: { fetch: true, default: ['default-model'] },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
|
|
|
|
|
expect(fetchModels).not.toHaveBeenCalled();
|
|
|
|
|
expect(result.NoKeyEndpoint).toEqual(['default-model']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('falls back to defaults and logs warn when getUserKeyValues throws infra error', async () => {
|
|
|
|
|
const { getUserKeyValues } = require('~/models');
|
|
|
|
|
const { logger } = require('@librechat/data-schemas');
|
|
|
|
|
getUserKeyValues.mockRejectedValueOnce(new Error('DB connection timeout'));
|
|
|
|
|
getAppConfig.mockResolvedValue({
|
|
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
|
|
|
|
{
|
|
|
|
|
name: 'ErrorEndpoint',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'https://api.example.com/v1',
|
|
|
|
|
models: { fetch: true, default: ['fallback'] },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
|
|
|
|
|
expect(fetchModels).not.toHaveBeenCalled();
|
|
|
|
|
expect(result.ErrorEndpoint).toEqual(['fallback']);
|
|
|
|
|
expect(logger.warn).toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining(
|
|
|
|
|
'Failed to retrieve user key for "ErrorEndpoint": DB connection timeout',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(logger.debug).not.toHaveBeenCalledWith(expect.stringContaining('No user key stored'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('logs debug (not warn) for NO_USER_KEY errors', async () => {
|
|
|
|
|
const { getUserKeyValues } = require('~/models');
|
|
|
|
|
const { logger } = require('@librechat/data-schemas');
|
|
|
|
|
getUserKeyValues.mockRejectedValueOnce(new Error(JSON.stringify({ type: 'no_user_key' })));
|
|
|
|
|
getAppConfig.mockResolvedValue({
|
|
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
|
|
|
|
{
|
|
|
|
|
name: 'MissingKeyEndpoint',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'https://api.example.com/v1',
|
|
|
|
|
models: { fetch: true, default: ['default-model'] },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
|
|
|
|
|
expect(result.MissingKeyEndpoint).toEqual(['default-model']);
|
|
|
|
|
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('No user key stored'));
|
|
|
|
|
expect(logger.warn).not.toHaveBeenCalledWith(
|
|
|
|
|
expect.stringContaining('Failed to retrieve user key'),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('skips user key lookup when req.user.id is undefined', async () => {
|
|
|
|
|
const { getUserKeyValues } = require('~/models');
|
|
|
|
|
getAppConfig.mockResolvedValue({
|
|
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
|
|
|
|
{
|
|
|
|
|
name: 'NoUserEndpoint',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'https://api.x.com/v1',
|
|
|
|
|
models: { fetch: true, default: ['anon-model'] },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels({ user: {} });
|
|
|
|
|
|
|
|
|
|
expect(getUserKeyValues).not.toHaveBeenCalled();
|
|
|
|
|
expect(result.NoUserEndpoint).toEqual(['anon-model']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses stored baseURL only when baseURL is user_provided', async () => {
|
|
|
|
|
const { getUserKeyValues } = require('~/models');
|
|
|
|
|
getUserKeyValues.mockResolvedValueOnce({ apiKey: 'sk-key' });
|
|
|
|
|
getAppConfig.mockResolvedValue({
|
|
|
|
|
endpoints: {
|
|
|
|
|
custom: [
|
|
|
|
|
{
|
|
|
|
|
name: 'KeyOnly',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'https://fixed-base.com/v1',
|
|
|
|
|
models: { fetch: true, default: ['default'] },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
fetchModels.mockResolvedValue(['model-from-fixed-base']);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
|
|
|
|
|
expect(fetchModels).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
apiKey: 'sk-key',
|
|
|
|
|
baseURL: 'https://fixed-base.com/v1',
|
|
|
|
|
skipCache: true,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(result.KeyOnly).toEqual(['model-from-fixed-base']);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2024-11-04 12:59:04 -05:00
|
|
|
it('normalizes Ollama endpoint name to lowercase', async () => {
|
|
|
|
|
const testCases = [
|
|
|
|
|
{
|
|
|
|
|
name: 'Ollama',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'http://localhost:11434/v1/',
|
|
|
|
|
models: {
|
|
|
|
|
default: ['mistral', 'llama2'],
|
|
|
|
|
fetch: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'OLLAMA',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'http://localhost:11434/v1/',
|
|
|
|
|
models: {
|
|
|
|
|
default: ['mixtral', 'codellama'],
|
|
|
|
|
fetch: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'OLLaMA',
|
|
|
|
|
apiKey: 'user_provided',
|
|
|
|
|
baseURL: 'http://localhost:11434/v1/',
|
|
|
|
|
models: {
|
|
|
|
|
default: ['phi', 'neural-chat'],
|
|
|
|
|
fetch: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2025-08-26 12:10:18 -04:00
|
|
|
getAppConfig.mockResolvedValue({
|
2024-11-04 12:59:04 -05:00
|
|
|
endpoints: {
|
|
|
|
|
custom: testCases,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await loadConfigModels(mockRequest);
|
|
|
|
|
|
|
|
|
|
// All variations of "Ollama" should be normalized to lowercase "ollama"
|
|
|
|
|
// and the last config in the array should override previous ones
|
|
|
|
|
expect(result.Ollama).toBeUndefined();
|
|
|
|
|
expect(result.OLLAMA).toBeUndefined();
|
|
|
|
|
expect(result.OLLaMA).toBeUndefined();
|
|
|
|
|
expect(result.ollama).toEqual(['phi', 'neural-chat']);
|
|
|
|
|
|
|
|
|
|
// Verify fetchModels was not called since these are user_provided
|
|
|
|
|
expect(fetchModels).not.toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
name: 'Ollama',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(fetchModels).not.toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
name: 'OLLAMA',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(fetchModels).not.toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
name: 'OLLaMA',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
2024-03-05 15:42:19 -05:00
|
|
|
});
|