mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-06 07:47:20 +02:00
🧹 chore: Clean Up Config Fields (#12537)
* chore: remove unused `interface.endpointsMenu` config field * chore: address review — restore JSDoc UI-only example, add Zod strip test * chore: remove unused `interface.sidePanel` config field * chore: restrict fileStrategy/fileStrategies schema to valid storage backends * fix: use valid FileStorage value in AppService test * chore: address review — version bump, exhaustiveness guard, JSDoc, configSchema test * chore: remove debug logger.log from MessageIcon render path * fix: rewrite MessageIcon render tests to use render counting instead of logger spying * chore: bump librechat-data-provider to 0.8.407 * chore: sync example YAML version to 1.3.7
This commit is contained in:
parent
b4d97bd888
commit
ea28dbfa89
17 changed files with 211 additions and 190 deletions
|
|
@ -29,14 +29,11 @@ export async function loadDefaultInterface({
|
|||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ function fakeConfig(overrides: Record<string, unknown>, priority: number): IConf
|
|||
}
|
||||
|
||||
const baseConfig = {
|
||||
interfaceConfig: { endpointsMenu: true, sidePanel: true },
|
||||
interfaceConfig: { modelSelect: true, parameters: true },
|
||||
registration: { enabled: true },
|
||||
endpoints: ['openAI'],
|
||||
} as unknown as AppConfig;
|
||||
|
|
@ -32,11 +32,11 @@ describe('mergeConfigOverrides', () => {
|
|||
});
|
||||
|
||||
it('deep merges interface UI fields into interfaceConfig', () => {
|
||||
const configs = [fakeConfig({ interface: { endpointsMenu: false } }, 10)];
|
||||
const configs = [fakeConfig({ interface: { modelSelect: false } }, 10)];
|
||||
const result = mergeConfigOverrides(baseConfig, configs) as unknown as Record<string, unknown>;
|
||||
const iface = result.interfaceConfig as Record<string, unknown>;
|
||||
expect(iface.endpointsMenu).toBe(false);
|
||||
expect(iface.sidePanel).toBe(true);
|
||||
expect(iface.modelSelect).toBe(false);
|
||||
expect(iface.parameters).toBe(true);
|
||||
});
|
||||
|
||||
it('sorts by priority — higher priority wins', () => {
|
||||
|
|
@ -58,16 +58,16 @@ describe('mergeConfigOverrides', () => {
|
|||
|
||||
it('does not mutate the base config', () => {
|
||||
const original = JSON.parse(JSON.stringify(baseConfig));
|
||||
const configs = [fakeConfig({ interface: { endpointsMenu: false } }, 10)];
|
||||
const configs = [fakeConfig({ interface: { modelSelect: false } }, 10)];
|
||||
mergeConfigOverrides(baseConfig, configs);
|
||||
expect(baseConfig).toEqual(original);
|
||||
});
|
||||
|
||||
it('handles null override values', () => {
|
||||
const configs = [fakeConfig({ interface: { endpointsMenu: null } }, 10)];
|
||||
const configs = [fakeConfig({ interface: { modelSelect: null } }, 10)];
|
||||
const result = mergeConfigOverrides(baseConfig, configs) as unknown as Record<string, unknown>;
|
||||
const iface = result.interfaceConfig as Record<string, unknown>;
|
||||
expect(iface.endpointsMenu).toBeNull();
|
||||
expect(iface.modelSelect).toBeNull();
|
||||
});
|
||||
|
||||
it('skips configs with no overrides object', () => {
|
||||
|
|
@ -97,20 +97,20 @@ describe('mergeConfigOverrides', () => {
|
|||
|
||||
it('merges three priority levels in order', () => {
|
||||
const configs = [
|
||||
fakeConfig({ interface: { endpointsMenu: false } }, 0),
|
||||
fakeConfig({ interface: { endpointsMenu: true, sidePanel: false } }, 10),
|
||||
fakeConfig({ interface: { sidePanel: true } }, 100),
|
||||
fakeConfig({ interface: { modelSelect: false } }, 0),
|
||||
fakeConfig({ interface: { modelSelect: true, parameters: false } }, 10),
|
||||
fakeConfig({ interface: { parameters: true } }, 100),
|
||||
];
|
||||
const result = mergeConfigOverrides(baseConfig, configs) as unknown as Record<string, unknown>;
|
||||
const iface = result.interfaceConfig as Record<string, unknown>;
|
||||
expect(iface.endpointsMenu).toBe(true);
|
||||
expect(iface.sidePanel).toBe(true);
|
||||
expect(iface.modelSelect).toBe(true);
|
||||
expect(iface.parameters).toBe(true);
|
||||
});
|
||||
|
||||
it('remaps all renamed YAML keys (exhaustiveness check)', () => {
|
||||
const base = {
|
||||
mcpConfig: null,
|
||||
interfaceConfig: { endpointsMenu: true },
|
||||
interfaceConfig: { modelSelect: true },
|
||||
turnstileConfig: {},
|
||||
} as unknown as AppConfig;
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ describe('mergeConfigOverrides', () => {
|
|||
fakeConfig(
|
||||
{
|
||||
mcpServers: { srv: { url: 'http://mcp' } },
|
||||
interface: { endpointsMenu: false },
|
||||
interface: { modelSelect: false },
|
||||
turnstile: { siteKey: 'key-123' },
|
||||
},
|
||||
10,
|
||||
|
|
@ -127,7 +127,7 @@ describe('mergeConfigOverrides', () => {
|
|||
const result = mergeConfigOverrides(base, configs) as unknown as Record<string, unknown>;
|
||||
|
||||
expect(result.mcpConfig).toEqual({ srv: { url: 'http://mcp' } });
|
||||
expect((result.interfaceConfig as Record<string, unknown>).endpointsMenu).toBe(false);
|
||||
expect((result.interfaceConfig as Record<string, unknown>).modelSelect).toBe(false);
|
||||
expect((result.turnstileConfig as Record<string, unknown>).siteKey).toBe('key-123');
|
||||
|
||||
expect(result.mcpServers).toBeUndefined();
|
||||
|
|
@ -137,14 +137,14 @@ describe('mergeConfigOverrides', () => {
|
|||
|
||||
it('strips interface permission fields from overrides', () => {
|
||||
const base = {
|
||||
interfaceConfig: { endpointsMenu: true, sidePanel: true },
|
||||
interfaceConfig: { modelSelect: true, parameters: true },
|
||||
} as unknown as AppConfig;
|
||||
|
||||
const configs = [
|
||||
fakeConfig(
|
||||
{
|
||||
interface: {
|
||||
endpointsMenu: false,
|
||||
modelSelect: false,
|
||||
prompts: false,
|
||||
agents: { use: false },
|
||||
marketplace: { use: false },
|
||||
|
|
@ -157,14 +157,14 @@ describe('mergeConfigOverrides', () => {
|
|||
const iface = result.interfaceConfig as Record<string, unknown>;
|
||||
|
||||
// UI field should be merged
|
||||
expect(iface.endpointsMenu).toBe(false);
|
||||
expect(iface.modelSelect).toBe(false);
|
||||
// Boolean permission fields should be stripped
|
||||
expect(iface.prompts).toBeUndefined();
|
||||
// Object permission fields with only permission sub-keys should be stripped
|
||||
expect(iface.agents).toBeUndefined();
|
||||
expect(iface.marketplace).toBeUndefined();
|
||||
// Untouched base field preserved
|
||||
expect(iface.sidePanel).toBe(true);
|
||||
expect(iface.parameters).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves UI sub-keys in composite permission fields like mcpServers', () => {
|
||||
|
|
@ -220,7 +220,7 @@ describe('mergeConfigOverrides', () => {
|
|||
|
||||
it('drops interface entirely when only permission fields are present', () => {
|
||||
const base = {
|
||||
interfaceConfig: { endpointsMenu: true },
|
||||
interfaceConfig: { modelSelect: true },
|
||||
} as unknown as AppConfig;
|
||||
|
||||
const configs = [fakeConfig({ interface: { prompts: false, agents: false } }, 10)];
|
||||
|
|
@ -228,7 +228,7 @@ describe('mergeConfigOverrides', () => {
|
|||
const iface = result.interfaceConfig as Record<string, unknown>;
|
||||
|
||||
// Base should be unchanged
|
||||
expect(iface.endpointsMenu).toBe(true);
|
||||
expect(iface.modelSelect).toBe(true);
|
||||
expect(iface.prompts).toBeUndefined();
|
||||
expect(iface.agents).toBeUndefined();
|
||||
});
|
||||
|
|
@ -281,7 +281,7 @@ describe('INTERFACE_PERMISSION_FIELDS', () => {
|
|||
});
|
||||
|
||||
it('does not contain UI-only fields', () => {
|
||||
const uiFields = ['endpointsMenu', 'modelSelect', 'parameters', 'presets', 'sidePanel'];
|
||||
const uiFields = ['modelSelect', 'parameters', 'presets'];
|
||||
for (const field of uiFields) {
|
||||
expect(INTERFACE_PERMISSION_FIELDS.has(field)).toBe(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ describe('upsertConfig', () => {
|
|||
PrincipalType.ROLE,
|
||||
'admin',
|
||||
PrincipalModel.ROLE,
|
||||
{ interface: { endpointsMenu: false } },
|
||||
{ interface: { modelSelect: false } },
|
||||
10,
|
||||
);
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ describe('upsertConfig', () => {
|
|||
PrincipalType.ROLE,
|
||||
'admin',
|
||||
PrincipalModel.ROLE,
|
||||
{ interface: { endpointsMenu: false } },
|
||||
{ interface: { modelSelect: false } },
|
||||
10,
|
||||
);
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ describe('upsertConfig', () => {
|
|||
PrincipalType.ROLE,
|
||||
'admin',
|
||||
PrincipalModel.ROLE,
|
||||
{ interface: { endpointsMenu: true } },
|
||||
{ interface: { modelSelect: true } },
|
||||
10,
|
||||
);
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ describe('upsertConfig', () => {
|
|||
PrincipalType.ROLE,
|
||||
'admin',
|
||||
PrincipalModel.ROLE,
|
||||
{ interface: { endpointsMenu: true } },
|
||||
{ interface: { modelSelect: true } },
|
||||
10,
|
||||
);
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ describe('upsertConfig', () => {
|
|||
PrincipalType.ROLE,
|
||||
'admin',
|
||||
PrincipalModel.ROLE,
|
||||
{ interface: { endpointsMenu: false } },
|
||||
{ interface: { modelSelect: false } },
|
||||
10,
|
||||
);
|
||||
|
||||
|
|
@ -240,7 +240,7 @@ describe('patchConfigFields', () => {
|
|||
PrincipalType.ROLE,
|
||||
'admin',
|
||||
PrincipalModel.ROLE,
|
||||
{ interface: { endpointsMenu: true, sidePanel: true } },
|
||||
{ interface: { modelSelect: true, parameters: true } },
|
||||
10,
|
||||
);
|
||||
|
||||
|
|
@ -248,14 +248,14 @@ describe('patchConfigFields', () => {
|
|||
PrincipalType.ROLE,
|
||||
'admin',
|
||||
PrincipalModel.ROLE,
|
||||
{ 'interface.endpointsMenu': false },
|
||||
{ 'interface.modelSelect': false },
|
||||
10,
|
||||
);
|
||||
|
||||
const overrides = result!.overrides as Record<string, unknown>;
|
||||
const iface = overrides.interface as Record<string, unknown>;
|
||||
expect(iface.endpointsMenu).toBe(false);
|
||||
expect(iface.sidePanel).toBe(true);
|
||||
expect(iface.modelSelect).toBe(false);
|
||||
expect(iface.parameters).toBe(true);
|
||||
});
|
||||
|
||||
it('creates a config if none exists (upsert)', async () => {
|
||||
|
|
@ -263,7 +263,7 @@ describe('patchConfigFields', () => {
|
|||
PrincipalType.ROLE,
|
||||
'newrole',
|
||||
PrincipalModel.ROLE,
|
||||
{ 'interface.endpointsMenu': false },
|
||||
{ 'interface.modelSelect': false },
|
||||
10,
|
||||
);
|
||||
|
||||
|
|
@ -278,19 +278,19 @@ describe('unsetConfigField', () => {
|
|||
PrincipalType.ROLE,
|
||||
'admin',
|
||||
PrincipalModel.ROLE,
|
||||
{ interface: { endpointsMenu: false, sidePanel: false } },
|
||||
{ interface: { modelSelect: false, parameters: false } },
|
||||
10,
|
||||
);
|
||||
|
||||
const result = await methods.unsetConfigField(
|
||||
PrincipalType.ROLE,
|
||||
'admin',
|
||||
'interface.endpointsMenu',
|
||||
'interface.modelSelect',
|
||||
);
|
||||
const overrides = result!.overrides as Record<string, unknown>;
|
||||
const iface = overrides.interface as Record<string, unknown>;
|
||||
expect(iface.endpointsMenu).toBeUndefined();
|
||||
expect(iface.sidePanel).toBe(false);
|
||||
expect(iface.modelSelect).toBeUndefined();
|
||||
expect(iface.parameters).toBe(false);
|
||||
});
|
||||
|
||||
it('returns null for non-existent config', async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type {
|
||||
TEndpoint,
|
||||
FileSources,
|
||||
FileStorage,
|
||||
TFileConfig,
|
||||
TAzureConfig,
|
||||
TCustomConfig,
|
||||
|
|
@ -62,7 +62,7 @@ export interface AppConfig {
|
|||
/** Web search configuration */
|
||||
webSearch?: TCustomConfig['webSearch'];
|
||||
/** File storage strategy ('local', 's3', 'firebase', 'azure_blob') */
|
||||
fileStrategy: FileSources.local | FileSources.s3 | FileSources.firebase | FileSources.azure_blob;
|
||||
fileStrategy: FileStorage;
|
||||
/** File strategies configuration */
|
||||
fileStrategies?: TCustomConfig['fileStrategies'];
|
||||
/** Registration configurations */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue