mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 17:44:11 +00:00
🪁 fix: Honor Endpoint Drop Params in Parameter Panels (#15610)
* Remove parameters from UI when parameter is dropped through dropParams * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Addressed copilot and codex review suggestions * 1. Normalize drop parameter names in client/src/components/SidePanel/Parameters/Panel.tsx Admin-configured dropParams for Azure/OpenAI-compatible custom endpoints use the effective backend field names (maxTokens, topP, frequencyPenalty, presencePenalty), but the panel's filter compared them against the UI's snake_case keys (max_tokens, top_p, etc.), so the controls stayed visible and silently discarded whatever the user set. Added a dropParamsBackendToUIKey map (in packages/data-provider/src/parameterSettings.ts) and normalize each dropParams entry through it before filtering. 2. Apply the same drop-parameter filtering in client/src/components/SidePanel/Agents/ModelPanel.tsx The Agent builder's model panel built its parameter list independently and never consulted endpointsDropParamsMap at all, so it kept offering controls for Azure/custom providers whose values the backend drops. Reused the same resolution logic (including the new backend→UI key normalization) there, sourced from useGetStartupConfig. 3. Remove any escapes from Azure fixtures in packages/api/src/app/config.test.ts Three tests built azure groupMap/modelGroupMap fixtures via ... as any as AppConfig['endpoints'], bypassing type checking for that shape. Replaced them with two typed helpers, createAzureGroupMap and createAzureConfig, built from the existing TAzureConfig/TAzureGroupMap/TAzureModelGroupMap types, so the fixtures are now fully type-checked with no any. * HAL-1081 Deploy librechat open 1. Gate the backend-name alias to OpenAI-compatible parameter sets (packages/data-provider/src/parameterSettings.ts) dropParamsBackendToUIKey had been applied unconditionally, rewriting a dropped topP to top_p even for a custom endpoint whose defaultParamsEndpoint is anthropic/google (or a native bedrock-* endpoint) — where topP is the UI key, so the rewrite broke hiding the control. Replaced the plain map with resolveDropParamsUIKeys(dropParams, endpointKey), which only aliases backend names for OpenAI-compatible endpoint keys (openAI, azureOpenAI, custom, openRouter) and passes native-provider keys through unchanged otherwise. Updated both Panel.tsx and ModelPanel.tsx to call it with overriddenEndpointKey, and added a resolveDropParamsUIKeys test suite in parameterSettings.spec.ts. 2. Prune stale model_parameters when a control becomes hidden (client/src/components/SidePanel/Agents/ModelPanel.tsx) When a parameter an agent already had set gets added to its endpoint's dropParams, the control disappeared but the value stayed in model_parameters and was saved unchanged by composeAgentUpdatePayload — invisible to the user and silently reactivated if the endpoint later stopped dropping it. Added a useEffect, mirroring the conversation panel's existing pruning effect, that strips any model_parameters key no longer present in the currently-visible parameters list. Also fixed a pre-existing test regression (missing useGetStartupConfig mock) and added two tests covering pruning-on-drop and value retention when the control stays visible. * fix: Preserve parameters for unknown agent providers * fix: Sanitize agent parameters on submission * fix: Preserve agent model overrides during pruning --------- Co-authored-by: Marc Amick <MarcAmick@jhu.edu> Co-authored-by: MarcAmick <5194465+MarcAmick@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
3200f0133a
commit
c302ae6f35
13 changed files with 683 additions and 50 deletions
|
|
@ -292,6 +292,20 @@ describe('GET /api/config', () => {
|
|||
expect(response.statusCode).toBe(500);
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
|
||||
it('should not expose endpointsDropParamsMap to unauthenticated callers', async () => {
|
||||
mockGetAppConfig.mockResolvedValue({
|
||||
...baseAppConfig,
|
||||
endpoints: {
|
||||
custom: [{ name: 'custom-provider', dropParams: ['temperature'] }],
|
||||
},
|
||||
});
|
||||
const app = createApp(null);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body).not.toHaveProperty('endpointsDropParamsMap');
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticated (req.user exists)', () => {
|
||||
|
|
@ -689,6 +703,95 @@ describe('GET /api/config', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('endpointsDropParamsMap', () => {
|
||||
it('maps dropParams for array-configured custom endpoints', async () => {
|
||||
mockGetAppConfig.mockResolvedValue({
|
||||
...baseAppConfig,
|
||||
endpoints: {
|
||||
custom: [
|
||||
{ name: 'custom-provider', dropParams: ['temperature', 'top_p'] },
|
||||
{ name: 'no-drop-provider' },
|
||||
],
|
||||
},
|
||||
});
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.endpointsDropParamsMap).toEqual({
|
||||
'custom-provider': ['temperature', 'top_p'],
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes an ollama custom endpoint name to lowercase', async () => {
|
||||
mockGetAppConfig.mockResolvedValue({
|
||||
...baseAppConfig,
|
||||
endpoints: {
|
||||
custom: [{ name: 'Ollama', dropParams: ['stop'] }],
|
||||
},
|
||||
});
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.endpointsDropParamsMap).toEqual({ ollama: ['stop'] });
|
||||
});
|
||||
|
||||
it('keeps azureOpenAI dropParams model-specific instead of merging across groups', async () => {
|
||||
mockGetAppConfig.mockResolvedValue({
|
||||
...baseAppConfig,
|
||||
endpoints: {
|
||||
azureOpenAI: {
|
||||
groupMap: {
|
||||
groupA: { dropParams: ['temperature'] },
|
||||
groupB: { dropParams: ['temperature', 'top_p'] },
|
||||
},
|
||||
modelGroupMap: {
|
||||
'model-a': { group: 'groupA' },
|
||||
'model-b': { group: 'groupB' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.endpointsDropParamsMap.azureOpenAI).toEqual({
|
||||
'model-a': ['temperature'],
|
||||
'model-b': ['temperature', 'top_p'],
|
||||
});
|
||||
});
|
||||
|
||||
it('excludes endpoints without dropParams and non-param endpoints like agents', async () => {
|
||||
mockGetAppConfig.mockResolvedValue({
|
||||
...baseAppConfig,
|
||||
endpoints: {
|
||||
custom: [{ name: 'no-drop-provider' }],
|
||||
azureOpenAI: {
|
||||
groupMap: { groupA: {} },
|
||||
modelGroupMap: { 'model-a': { group: 'groupA' } },
|
||||
},
|
||||
agents: [{ name: 'agents-provider', dropParams: ['temperature'] }],
|
||||
},
|
||||
});
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.endpointsDropParamsMap).toEqual({});
|
||||
});
|
||||
|
||||
it('returns an empty map when appConfig has no endpoints', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.endpointsDropParamsMap).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildInfo payload', () => {
|
||||
const populatedBuildInfo = {
|
||||
commit: 'abcdef1234567890abcdef1234567890abcdef12',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const {
|
|||
sanitizeModelSpecs,
|
||||
excludeHiddenModelSpecs,
|
||||
isFileSnapshotEnabled,
|
||||
getEndpointsDropParamsMap,
|
||||
} = require('@librechat/api');
|
||||
const { EModelEndpoint, defaultSocialLogins } = require('librechat-data-provider');
|
||||
const { logger, getTenantId, SystemCapabilities } = require('@librechat/data-schemas');
|
||||
|
|
@ -249,6 +250,8 @@ router.get('/', async function (req, res) {
|
|||
|
||||
const appConfig = await getAppConfig(getAppConfigOptionsFromUser(req.user));
|
||||
|
||||
const endpointsDropParamsMap = getEndpointsDropParamsMap(appConfig?.endpoints);
|
||||
|
||||
const balanceConfig = getBalanceConfig(appConfig);
|
||||
const cloudFront = buildCloudFrontStartupConfig();
|
||||
const langfuseFanoutEnabled = isLangfuseFanoutEnabled();
|
||||
|
|
@ -308,6 +311,7 @@ router.get('/', async function (req, res) {
|
|||
...(cloudFront ? { cloudFront } : {}),
|
||||
...(rum ? { rum } : {}),
|
||||
fileUploadSseEnabled: isEnabled(process.env.FILE_UPLOAD_SSE_ENABLED),
|
||||
endpointsDropParamsMap: endpointsDropParamsMap,
|
||||
};
|
||||
|
||||
const webSearch = buildWebSearchConfig(appConfig);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
import type { Agent, AgentUpdateParams } from 'librechat-data-provider';
|
||||
import type { FieldNamesMarkedBoolean } from 'react-hook-form';
|
||||
import type { TranslationKeys } from '~/hooks/useLocalize';
|
||||
import type { AgentParameterConfig } from './parameters';
|
||||
import type { AgentForm, StringOption } from '~/common';
|
||||
import {
|
||||
useCreateAgentMutation,
|
||||
|
|
@ -32,6 +33,7 @@ import {
|
|||
getAvailableAgentSelection,
|
||||
getDefaultAgentFormValues,
|
||||
} from '~/utils';
|
||||
import { pruneAgentModelParameters, resolveAgentParameterSettings } from './parameters';
|
||||
import { useResourcePermissions } from '~/hooks/useResourcePermissions';
|
||||
import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
|
||||
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||
|
|
@ -68,14 +70,18 @@ function getUpdateToastMessage(
|
|||
* @param {string | null} [agent_id] - Agent identifier, if the agent already exists.
|
||||
* @returns {{ payload: Partial<AgentForm>; provider: string; model: string }} Payload metadata.
|
||||
*/
|
||||
export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | null) {
|
||||
export function composeAgentUpdatePayload(
|
||||
data: AgentForm,
|
||||
agent_id?: string | null,
|
||||
parameterConfig?: AgentParameterConfig,
|
||||
) {
|
||||
const {
|
||||
name,
|
||||
artifacts,
|
||||
description,
|
||||
instructions,
|
||||
model: _model,
|
||||
model_parameters,
|
||||
model_parameters: currentModelParameters,
|
||||
provider: _provider,
|
||||
agent_ids,
|
||||
edges,
|
||||
|
|
@ -110,6 +116,12 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n
|
|||
const model = _model ?? '';
|
||||
const provider =
|
||||
(typeof _provider === 'string' ? _provider : (_provider as StringOption).value) ?? '';
|
||||
const modelParameterSettings = parameterConfig
|
||||
? resolveAgentParameterSettings({ ...parameterConfig, model, provider })
|
||||
: undefined;
|
||||
const model_parameters = modelParameterSettings
|
||||
? pruneAgentModelParameters(currentModelParameters, modelParameterSettings)
|
||||
: currentModelParameters;
|
||||
|
||||
return {
|
||||
payload: {
|
||||
|
|
@ -297,6 +309,7 @@ export default function AgentPanel() {
|
|||
const {
|
||||
activePanel,
|
||||
agentsConfig,
|
||||
startupConfig,
|
||||
setActivePanel,
|
||||
endpointsConfig,
|
||||
setCurrentAgentId,
|
||||
|
|
@ -570,7 +583,14 @@ export default function AgentPanel() {
|
|||
async (data: AgentForm) => {
|
||||
const tools = Array.from(new Set([...(data.tools ?? []), ...resolveCapabilityTools(data)]));
|
||||
|
||||
const { payload: basePayload, provider, model } = composeAgentUpdatePayload(data, agent_id);
|
||||
const {
|
||||
payload: basePayload,
|
||||
provider,
|
||||
model,
|
||||
} = composeAgentUpdatePayload(data, agent_id, {
|
||||
endpointsConfig,
|
||||
startupConfig,
|
||||
});
|
||||
|
||||
if (agent_id) {
|
||||
if (data.avatar_action === 'upload' && isAvatarUploadOnlyDirty(dirtyFields)) {
|
||||
|
|
@ -626,12 +646,14 @@ export default function AgentPanel() {
|
|||
agent_id,
|
||||
create,
|
||||
dirtyFields,
|
||||
endpointsConfig,
|
||||
handleAvatarUpload,
|
||||
models,
|
||||
modelsError,
|
||||
modelsReady,
|
||||
update,
|
||||
showToast,
|
||||
startupConfig,
|
||||
localize,
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@
|
|||
* @jest-environment jsdom
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Providers } from 'librechat-data-provider';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { Providers, EModelEndpoint, agentParamSettings } from 'librechat-data-provider';
|
||||
import type { TStartupConfig } from 'librechat-data-provider';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
import ModelPanel from './ModelPanel';
|
||||
|
||||
const mockStartupConfig = jest.fn<Partial<TStartupConfig>, []>(() => ({}));
|
||||
|
||||
jest.mock('@librechat/client', () => ({
|
||||
Alert: ({ children }: { children: React.ReactNode }) => <div role="alert">{children}</div>,
|
||||
Button: ({ children, onClick, type }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
|
|
@ -59,6 +63,7 @@ jest.mock('~/components/SidePanel/Parameters/components', () => ({
|
|||
|
||||
jest.mock('~/data-provider', () => ({
|
||||
useGetEndpointsQuery: () => ({ data: {} }),
|
||||
useGetStartupConfig: () => ({ data: mockStartupConfig() }),
|
||||
}));
|
||||
|
||||
jest.mock('~/Providers', () => ({
|
||||
|
|
@ -75,14 +80,18 @@ jest.mock('~/utils', () => ({
|
|||
|
||||
function TestForm({
|
||||
defaultModel = '',
|
||||
defaultModelParameters = {},
|
||||
defaultProvider = '',
|
||||
formRef,
|
||||
models,
|
||||
modelsError = false,
|
||||
modelsReady,
|
||||
providers = [{ label: 'Custom', value: 'custom' }],
|
||||
}: {
|
||||
defaultModel?: string;
|
||||
defaultModelParameters?: Partial<AgentForm['model_parameters']>;
|
||||
defaultProvider?: string;
|
||||
formRef?: React.MutableRefObject<UseFormReturn<AgentForm> | null>;
|
||||
models: Record<string, string[]>;
|
||||
modelsError?: boolean;
|
||||
modelsReady: boolean;
|
||||
|
|
@ -92,10 +101,14 @@ function TestForm({
|
|||
defaultValues: {
|
||||
provider: defaultProvider,
|
||||
model: defaultModel,
|
||||
model_parameters: {},
|
||||
model_parameters: defaultModelParameters as AgentForm['model_parameters'],
|
||||
},
|
||||
});
|
||||
|
||||
if (formRef) {
|
||||
formRef.current = methods;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<ModelPanel
|
||||
|
|
@ -110,7 +123,10 @@ function TestForm({
|
|||
}
|
||||
|
||||
describe('ModelPanel', () => {
|
||||
beforeEach(() => localStorage.clear());
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
mockStartupConfig.mockReturnValue({});
|
||||
});
|
||||
|
||||
it('disables model selection until the model catalogue is ready', () => {
|
||||
const { getByTestId } = render(
|
||||
|
|
@ -258,4 +274,93 @@ describe('ModelPanel', () => {
|
|||
expect(container.querySelector('#provider')).not.toBeNull();
|
||||
expect(container.querySelector('#model')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('prunes a saved model_parameters value once its endpoint drops the matching param', async () => {
|
||||
mockStartupConfig.mockReturnValue({
|
||||
endpointsDropParamsMap: { [EModelEndpoint.openAI]: ['topP'] },
|
||||
});
|
||||
|
||||
const formRef: React.MutableRefObject<UseFormReturn<AgentForm> | null> = { current: null };
|
||||
render(
|
||||
<TestForm
|
||||
defaultProvider={EModelEndpoint.openAI}
|
||||
defaultModel="gpt-4o"
|
||||
defaultModelParameters={{ model: 'deployment-override', temperature: 0.5, top_p: 0.9 }}
|
||||
formRef={formRef}
|
||||
models={{ [EModelEndpoint.openAI]: ['gpt-4o'] }}
|
||||
modelsReady={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(formRef.current?.getValues('model_parameters')).toEqual({
|
||||
model: 'deployment-override',
|
||||
temperature: 0.5,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps a saved model_parameters value while its control is still visible', () => {
|
||||
const formRef: React.MutableRefObject<UseFormReturn<AgentForm> | null> = { current: null };
|
||||
render(
|
||||
<TestForm
|
||||
defaultProvider={EModelEndpoint.openAI}
|
||||
defaultModel="gpt-4o"
|
||||
defaultModelParameters={{ temperature: 0.5, top_p: 0.9 }}
|
||||
formRef={formRef}
|
||||
models={{ [EModelEndpoint.openAI]: ['gpt-4o'] }}
|
||||
modelsReady={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(formRef.current?.getValues('model_parameters')).toEqual({
|
||||
temperature: 0.5,
|
||||
top_p: 0.9,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps saved model_parameters when the provider has no known parameter settings', () => {
|
||||
const formRef: React.MutableRefObject<UseFormReturn<AgentForm> | null> = { current: null };
|
||||
render(
|
||||
<TestForm
|
||||
defaultProvider="removed-provider"
|
||||
defaultModel="removed-model"
|
||||
defaultModelParameters={{ temperature: 0.5, top_p: 0.9 }}
|
||||
formRef={formRef}
|
||||
models={{ 'removed-provider': ['removed-model'] }}
|
||||
modelsReady={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(formRef.current?.getValues('model_parameters')).toEqual({
|
||||
temperature: 0.5,
|
||||
top_p: 0.9,
|
||||
});
|
||||
});
|
||||
|
||||
it('prunes saved model_parameters when every known setting is dropped', async () => {
|
||||
mockStartupConfig.mockReturnValue({
|
||||
endpointsDropParamsMap: {
|
||||
[EModelEndpoint.openAI]: (agentParamSettings[EModelEndpoint.openAI] ?? []).map(
|
||||
({ key }) => key,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const formRef: React.MutableRefObject<UseFormReturn<AgentForm> | null> = { current: null };
|
||||
render(
|
||||
<TestForm
|
||||
defaultProvider={EModelEndpoint.openAI}
|
||||
defaultModel="gpt-4o"
|
||||
defaultModelParameters={{ temperature: 0.5, top_p: 0.9 }}
|
||||
formRef={formRef}
|
||||
models={{ [EModelEndpoint.openAI]: ['gpt-4o'] }}
|
||||
modelsReady={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(formRef.current?.getValues('model_parameters')).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,22 +1,13 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import { ChevronLeft, RotateCcw } from 'lucide-react';
|
||||
import { Alert, Button, ControlCombobox } from '@librechat/client';
|
||||
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
||||
import {
|
||||
alternateName,
|
||||
getSettingsKeys,
|
||||
getEndpointField,
|
||||
LocalStorageKeys,
|
||||
resolveModelCatalogKey,
|
||||
SettingDefinition,
|
||||
agentParamSettings,
|
||||
applyModelAwareDefaults,
|
||||
} from 'librechat-data-provider';
|
||||
import { alternateName, LocalStorageKeys, resolveModelCatalogKey } from 'librechat-data-provider';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
||||
import { pruneAgentModelParameters, resolveAgentParameterSettings } from './parameters';
|
||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
|
||||
import { useLiveAnnouncer } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Panel } from '~/common';
|
||||
|
|
@ -45,7 +36,7 @@ export default function ModelPanel({
|
|||
const localize = useLocalize();
|
||||
const { announcePolite } = useLiveAnnouncer();
|
||||
|
||||
const { control, setValue } = useFormContext<AgentForm>();
|
||||
const { control, setValue, getValues } = useFormContext<AgentForm>();
|
||||
|
||||
const model = useWatch({ control, name: 'model' });
|
||||
const providerOption = useWatch({ control, name: 'provider' });
|
||||
|
|
@ -66,33 +57,39 @@ export default function ModelPanel({
|
|||
const selectionDisabled = !modelsReady || modelsError;
|
||||
|
||||
const { data: endpointsConfig = {} } = useGetEndpointsQuery();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
||||
const bedrockRegions = useMemo(() => {
|
||||
return endpointsConfig?.[provider]?.availableRegions ?? [];
|
||||
}, [endpointsConfig, provider]);
|
||||
|
||||
const endpointType = useMemo(
|
||||
() => getEndpointField(endpointsConfig, provider, 'type'),
|
||||
[provider, endpointsConfig],
|
||||
const parameterSettings = useMemo(
|
||||
() =>
|
||||
resolveAgentParameterSettings({
|
||||
endpointsConfig,
|
||||
model: model ?? '',
|
||||
provider,
|
||||
startupConfig,
|
||||
}),
|
||||
[endpointsConfig, model, provider, startupConfig],
|
||||
);
|
||||
const { parameters } = parameterSettings;
|
||||
|
||||
const parameters = useMemo((): SettingDefinition[] => {
|
||||
const customParams = endpointsConfig[provider]?.customParams ?? {};
|
||||
const [combinedKey, endpointKey] = getSettingsKeys(endpointType ?? provider, model ?? '');
|
||||
const overriddenEndpointKey = customParams.defaultParamsEndpoint ?? endpointKey;
|
||||
const defaultParams =
|
||||
agentParamSettings[combinedKey] ?? agentParamSettings[overriddenEndpointKey] ?? [];
|
||||
const overriddenParams = endpointsConfig[provider]?.customParams?.paramDefinitions ?? [];
|
||||
const overriddenParamsMap = keyBy(overriddenParams, 'key');
|
||||
const modelAwareParams = applyModelAwareDefaults(
|
||||
defaultParams.filter((param) => param != null),
|
||||
overriddenEndpointKey,
|
||||
model ?? '',
|
||||
);
|
||||
return modelAwareParams.map(
|
||||
(param) => (overriddenParamsMap[param.key] as SettingDefinition) ?? param,
|
||||
);
|
||||
}, [endpointType, endpointsConfig, model, provider]);
|
||||
/**
|
||||
* Prunes `model_parameters` entries that no longer have a visible control (e.g. a
|
||||
* parameter newly added to the endpoint's `dropParams`), mirroring the conversation
|
||||
* panel's pruning effect. Otherwise the stale value stays saved and silently reactivates
|
||||
* if the endpoint config later stops dropping it, with no control to inspect or clear it.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const currentParameters = getValues('model_parameters') ?? ({} as t.AgentModelParameters);
|
||||
const prunedParameters = pruneAgentModelParameters(currentParameters, parameterSettings);
|
||||
if (prunedParameters === currentParameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue('model_parameters', prunedParameters);
|
||||
}, [parameterSettings, getValues, setValue]);
|
||||
|
||||
const setOption = (optionKey: keyof t.AgentModelParameters) => (value: t.AgentParameterValue) => {
|
||||
setValue(`model_parameters.${optionKey}`, value);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { Constants, type Agent } from 'librechat-data-provider';
|
||||
import { Constants, EModelEndpoint, type Agent } from 'librechat-data-provider';
|
||||
import type { AgentModelParameters } from 'librechat-data-provider';
|
||||
import type { FieldNamesMarkedBoolean } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
|
|
@ -171,6 +171,35 @@ describe('composeAgentUpdatePayload', () => {
|
|||
expect(payload.skills_enabled).toBe(false);
|
||||
expect(payload.skill_authoring_enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('prunes dropped model parameters during submission', () => {
|
||||
const form = createForm();
|
||||
form.provider = EModelEndpoint.openAI;
|
||||
form.model_parameters.model = 'deployment-override';
|
||||
|
||||
const { payload } = composeAgentUpdatePayload(form, 'agent_123', {
|
||||
endpointsConfig: {},
|
||||
startupConfig: {
|
||||
endpointsDropParamsMap: { [EModelEndpoint.openAI]: ['topP'] },
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.model_parameters?.temperature).toBe(1);
|
||||
expect(payload.model_parameters?.top_p).toBeUndefined();
|
||||
expect(payload.model_parameters?.model).toBe('deployment-override');
|
||||
});
|
||||
|
||||
it('preserves model parameters during submission when the provider schema is unknown', () => {
|
||||
const form = createForm();
|
||||
form.provider = 'removed-provider';
|
||||
|
||||
const { payload } = composeAgentUpdatePayload(form, 'agent_123', {
|
||||
endpointsConfig: {},
|
||||
startupConfig: {},
|
||||
});
|
||||
|
||||
expect(payload.model_parameters).toEqual(form.model_parameters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistAvatarChanges', () => {
|
||||
|
|
|
|||
98
client/src/components/SidePanel/Agents/parameters.ts
Normal file
98
client/src/components/SidePanel/Agents/parameters.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import keyBy from 'lodash/keyBy';
|
||||
import {
|
||||
agentParamSettings,
|
||||
applyModelAwareDefaults,
|
||||
getEndpointField,
|
||||
getSettingsKeys,
|
||||
normalizeEndpointName,
|
||||
resolveDropParamsUIKeys,
|
||||
} from 'librechat-data-provider';
|
||||
import type {
|
||||
AgentModelParameters,
|
||||
SettingDefinition,
|
||||
TEndpointsConfig,
|
||||
TStartupConfig,
|
||||
} from 'librechat-data-provider';
|
||||
|
||||
export type AgentParameterConfig = {
|
||||
endpointsConfig?: TEndpointsConfig | null;
|
||||
startupConfig?: Pick<TStartupConfig, 'endpointsDropParamsMap'> | null;
|
||||
};
|
||||
|
||||
export type ResolvedAgentParameterSettings = {
|
||||
parameters: SettingDefinition[];
|
||||
schemaResolved: boolean;
|
||||
};
|
||||
|
||||
const NON_CONTROL_PARAMETER_KEYS = new Set<keyof AgentModelParameters>(['model']);
|
||||
|
||||
export function resolveAgentParameterSettings({
|
||||
endpointsConfig,
|
||||
model,
|
||||
provider,
|
||||
startupConfig,
|
||||
}: AgentParameterConfig & {
|
||||
model: string;
|
||||
provider: string;
|
||||
}): ResolvedAgentParameterSettings {
|
||||
const resolvedEndpointsConfig = endpointsConfig ?? {};
|
||||
const endpointType = getEndpointField(resolvedEndpointsConfig, provider, 'type');
|
||||
const customParams = resolvedEndpointsConfig[provider]?.customParams;
|
||||
const [combinedKey, endpointKey] = getSettingsKeys(endpointType ?? provider, model);
|
||||
const overriddenEndpointKey = customParams?.defaultParamsEndpoint ?? endpointKey;
|
||||
const dropParamsMap = startupConfig?.endpointsDropParamsMap;
|
||||
const dropParamsEntry =
|
||||
dropParamsMap?.[provider] ?? dropParamsMap?.[normalizeEndpointName(provider)];
|
||||
const resolvedDropParams = Array.isArray(dropParamsEntry)
|
||||
? dropParamsEntry
|
||||
: dropParamsEntry?.[model];
|
||||
const dropParamsSet = resolveDropParamsUIKeys(
|
||||
Array.isArray(resolvedDropParams) ? resolvedDropParams : undefined,
|
||||
overriddenEndpointKey,
|
||||
);
|
||||
const defaultParams =
|
||||
agentParamSettings[combinedKey] ?? agentParamSettings[overriddenEndpointKey];
|
||||
const overriddenParams = customParams?.paramDefinitions;
|
||||
const overriddenParamsMap = keyBy(overriddenParams ?? [], 'key');
|
||||
const modelAwareParams = applyModelAwareDefaults(
|
||||
(defaultParams ?? []).filter((param) => param != null && !dropParamsSet.has(param.key)),
|
||||
overriddenEndpointKey,
|
||||
model,
|
||||
);
|
||||
|
||||
return {
|
||||
schemaResolved: defaultParams != null || overriddenParams != null,
|
||||
parameters: modelAwareParams.map(
|
||||
(param) => (overriddenParamsMap[param.key] as SettingDefinition) ?? param,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function pruneAgentModelParameters(
|
||||
currentParameters: AgentModelParameters | undefined,
|
||||
settings: ResolvedAgentParameterSettings,
|
||||
): AgentModelParameters {
|
||||
const normalizedParameters = currentParameters ?? ({} as AgentModelParameters);
|
||||
if (!settings.schemaResolved) {
|
||||
return normalizedParameters;
|
||||
}
|
||||
|
||||
const parameterKeys = new Set(settings.parameters.map((setting) => setting.key));
|
||||
const currentKeys = Object.keys(normalizedParameters) as Array<keyof AgentModelParameters>;
|
||||
let prunedParameters: Partial<AgentModelParameters> | undefined;
|
||||
|
||||
for (const key of currentKeys) {
|
||||
if (
|
||||
NON_CONTROL_PARAMETER_KEYS.has(key) ||
|
||||
parameterKeys.has(key) ||
|
||||
normalizedParameters[key] == null
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
prunedParameters ??= { ...normalizedParameters };
|
||||
delete prunedParameters[key];
|
||||
}
|
||||
|
||||
return (prunedParameters ?? normalizedParameters) as AgentModelParameters;
|
||||
}
|
||||
|
|
@ -10,17 +10,20 @@ import {
|
|||
SettingDefinition,
|
||||
tConvoUpdateSchema,
|
||||
applyModelAwareDefaults,
|
||||
normalizeEndpointName,
|
||||
resolveDropParamsUIKeys,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
|
||||
import { useChatContext, useLiveAnnouncer } from '~/Providers';
|
||||
import { SaveAsPresetDialog } from '~/components/Endpoints';
|
||||
import { useSetIndexOptions, useLocalize } from '~/hooks';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { componentMapping } from './components';
|
||||
import { logger, cn } from '~/utils';
|
||||
|
||||
export default function Parameters() {
|
||||
const localize = useLocalize();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { conversation, setConversation } = useChatContext();
|
||||
const { announcePolite } = useLiveAnnouncer();
|
||||
const { setOption } = useSetIndexOptions();
|
||||
|
|
@ -47,18 +50,28 @@ export default function Parameters() {
|
|||
const customParams = endpointsConfig[provider]?.customParams ?? {};
|
||||
const [combinedKey, endpointKey] = getSettingsKeys(endpointType ?? provider, model);
|
||||
const overriddenEndpointKey = customParams.defaultParamsEndpoint ?? endpointKey;
|
||||
const dropParamsMap = startupConfig?.endpointsDropParamsMap;
|
||||
const dropParamsEntry =
|
||||
dropParamsMap?.[provider] ?? dropParamsMap?.[normalizeEndpointName(provider)];
|
||||
const resolvedDropParams = Array.isArray(dropParamsEntry)
|
||||
? dropParamsEntry
|
||||
: dropParamsEntry?.[model];
|
||||
const dropParamsSet = resolveDropParamsUIKeys(
|
||||
Array.isArray(resolvedDropParams) ? resolvedDropParams : undefined,
|
||||
overriddenEndpointKey,
|
||||
);
|
||||
const defaultParams = paramSettings[combinedKey] ?? paramSettings[overriddenEndpointKey] ?? [];
|
||||
const overriddenParams = endpointsConfig[provider]?.customParams?.paramDefinitions ?? [];
|
||||
const overriddenParamsMap = keyBy(overriddenParams, 'key');
|
||||
const modelAwareParams = applyModelAwareDefaults(
|
||||
defaultParams.filter((param) => param != null),
|
||||
defaultParams.filter((param) => param != null && !dropParamsSet.has(param.key)),
|
||||
overriddenEndpointKey,
|
||||
model,
|
||||
);
|
||||
return modelAwareParams.map(
|
||||
(param) => (overriddenParamsMap[param.key] as SettingDefinition) ?? param,
|
||||
);
|
||||
}, [endpointType, endpointsConfig, model, provider]);
|
||||
}, [endpointType, endpointsConfig, model, provider, startupConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!parameters) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
import { logger, encryptV3 } from '@librechat/data-schemas';
|
||||
import { FileSources, EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TEndpoint } from 'librechat-data-provider';
|
||||
import type {
|
||||
TCustomConfig,
|
||||
TEndpoint,
|
||||
TAzureConfig,
|
||||
TAzureGroupMap,
|
||||
TAzureModelGroupMap,
|
||||
} from 'librechat-data-provider';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import { getTransactionsConfig, getBalanceConfig, getCustomEndpointConfig } from './config';
|
||||
import {
|
||||
getBalanceConfig,
|
||||
getCustomEndpointConfig,
|
||||
getTransactionsConfig,
|
||||
getEndpointsDropParamsMap,
|
||||
} from './config';
|
||||
|
||||
// Helper function to create a minimal AppConfig for testing
|
||||
const createTestAppConfig = (overrides: Partial<AppConfig> = {}): AppConfig => {
|
||||
|
|
@ -32,6 +43,31 @@ const createTestAppConfig = (overrides: Partial<AppConfig> = {}): AppConfig => {
|
|||
};
|
||||
};
|
||||
|
||||
/** Builds azureOpenAI `groupMap` entries with the required `apiKey`/`models` fields. */
|
||||
const createAzureGroupMap = (groups: Record<string, string[] | undefined>): TAzureGroupMap =>
|
||||
Object.fromEntries(
|
||||
Object.entries(groups).map(([groupName, dropParams]) => [
|
||||
groupName,
|
||||
{
|
||||
apiKey: 'test-key',
|
||||
models: {},
|
||||
...(dropParams ? { dropParams } : {}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
/** Builds a minimal, valid azureOpenAI endpoint config for testing `getEndpointsDropParamsMap`. */
|
||||
const createAzureConfig = (
|
||||
groupMap: TAzureGroupMap,
|
||||
modelGroupMap: TAzureModelGroupMap,
|
||||
): TAzureConfig => ({
|
||||
isValid: true,
|
||||
errors: [],
|
||||
modelNames: Object.keys(modelGroupMap),
|
||||
groupMap,
|
||||
modelGroupMap,
|
||||
});
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => {
|
||||
process.env.CREDS_KEY =
|
||||
process.env.CREDS_KEY ?? '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
|
|
@ -381,3 +417,95 @@ describe('getCustomEndpointConfig', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEndpointsDropParamsMap', () => {
|
||||
it('returns an empty map when endpoints is undefined', () => {
|
||||
expect(getEndpointsDropParamsMap(undefined)).toEqual({});
|
||||
});
|
||||
|
||||
it('returns an empty map when no configured endpoint has dropParams', () => {
|
||||
const result = getEndpointsDropParamsMap({
|
||||
[EModelEndpoint.custom]: [{ name: 'no-drop-provider', apiKey: 'k' } as TEndpoint],
|
||||
});
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('maps dropParams for array-configured custom endpoints', () => {
|
||||
const result = getEndpointsDropParamsMap({
|
||||
[EModelEndpoint.custom]: [
|
||||
{ name: 'custom-provider', dropParams: ['temperature', 'top_p'] } as TEndpoint,
|
||||
{ name: 'no-drop-provider' } as TEndpoint,
|
||||
],
|
||||
});
|
||||
expect(result).toEqual({
|
||||
'custom-provider': ['temperature', 'top_p'],
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes an ollama custom endpoint name to lowercase', () => {
|
||||
const result = getEndpointsDropParamsMap({
|
||||
[EModelEndpoint.custom]: [{ name: 'Ollama', dropParams: ['stop'] } as TEndpoint],
|
||||
});
|
||||
expect(result).toEqual({ ollama: ['stop'] });
|
||||
});
|
||||
|
||||
it('keeps azureOpenAI dropParams model-specific instead of merging across groups', () => {
|
||||
const endpoints: AppConfig['endpoints'] = {
|
||||
[EModelEndpoint.azureOpenAI]: createAzureConfig(
|
||||
createAzureGroupMap({
|
||||
groupA: ['temperature'],
|
||||
groupB: ['temperature', 'top_p'],
|
||||
}),
|
||||
{
|
||||
'model-a': { group: 'groupA' },
|
||||
'model-b': { group: 'groupB' },
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
const result = getEndpointsDropParamsMap(endpoints);
|
||||
|
||||
expect(result[EModelEndpoint.azureOpenAI]).toEqual({
|
||||
'model-a': ['temperature'],
|
||||
'model-b': ['temperature', 'top_p'],
|
||||
});
|
||||
});
|
||||
|
||||
it('omits an azureOpenAI model from the map when its group has no dropParams', () => {
|
||||
const endpoints: AppConfig['endpoints'] = {
|
||||
[EModelEndpoint.azureOpenAI]: createAzureConfig(
|
||||
createAzureGroupMap({
|
||||
groupA: ['temperature'],
|
||||
groupB: undefined,
|
||||
}),
|
||||
{
|
||||
'model-a': { group: 'groupA' },
|
||||
'model-b': { group: 'groupB' },
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
expect(getEndpointsDropParamsMap(endpoints)).toEqual({
|
||||
[EModelEndpoint.azureOpenAI]: { 'model-a': ['temperature'] },
|
||||
});
|
||||
});
|
||||
|
||||
it('excludes azureOpenAI when no group has dropParams', () => {
|
||||
const endpoints: AppConfig['endpoints'] = {
|
||||
[EModelEndpoint.azureOpenAI]: createAzureConfig(createAzureGroupMap({ groupA: undefined }), {
|
||||
'model-a': { group: 'groupA' },
|
||||
}),
|
||||
};
|
||||
|
||||
expect(getEndpointsDropParamsMap(endpoints)).toEqual({});
|
||||
});
|
||||
|
||||
it('ignores endpoint shapes without dropParams support, like agents', () => {
|
||||
const endpoints = {
|
||||
[EModelEndpoint.custom]: [{ name: 'no-drop-provider' } as TEndpoint],
|
||||
[EModelEndpoint.agents]: { titleConvo: true },
|
||||
} as AppConfig['endpoints'];
|
||||
|
||||
expect(getEndpointsDropParamsMap(endpoints)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ import {
|
|||
removeNullishValues,
|
||||
normalizeEndpointName,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TEndpoint, TTransactionsConfig } from 'librechat-data-provider';
|
||||
import type {
|
||||
TEndpoint,
|
||||
TCustomConfig,
|
||||
EndpointsDropParamsMap,
|
||||
TTransactionsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import { resolveCustomEndpointSecrets } from '~/admin/secrets';
|
||||
import { isEnabled } from '~/utils';
|
||||
|
|
@ -69,3 +74,43 @@ export const getCustomEndpointConfig = ({
|
|||
);
|
||||
return endpointConfig && resolveCustomEndpointSecrets(endpointConfig);
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a map of normalized endpoint name -> dropParams for the endpoint shapes that
|
||||
* support per-endpoint dropParams: array-configured `custom` endpoints map directly to
|
||||
* their dropParams, while `azureOpenAI` maps to a per-model lookup (model name ->
|
||||
* dropParams) built from `modelGroupMap`/`groupMap`, mirroring the per-request group
|
||||
* resolution in `initializeOpenAI` so a parameter dropped for one group doesn't hide it
|
||||
* for models in another group.
|
||||
*/
|
||||
export function getEndpointsDropParamsMap(
|
||||
endpoints: AppConfig['endpoints'],
|
||||
): EndpointsDropParamsMap {
|
||||
const result: EndpointsDropParamsMap = {};
|
||||
if (!endpoints) {
|
||||
return result;
|
||||
}
|
||||
|
||||
endpoints[EModelEndpoint.custom]?.forEach((endpoint) => {
|
||||
if (endpoint?.dropParams && endpoint.dropParams.length > 0) {
|
||||
result[normalizeEndpointName(endpoint.name ?? '')] = endpoint.dropParams;
|
||||
}
|
||||
});
|
||||
|
||||
const azureConfig = endpoints[EModelEndpoint.azureOpenAI];
|
||||
if (azureConfig?.groupMap && azureConfig?.modelGroupMap) {
|
||||
const { groupMap, modelGroupMap } = azureConfig;
|
||||
const modelDropParams: Record<string, string[]> = {};
|
||||
Object.entries(modelGroupMap).forEach(([modelName, modelConfig]) => {
|
||||
const dropParams = modelConfig && groupMap[modelConfig.group]?.dropParams;
|
||||
if (dropParams && dropParams.length > 0) {
|
||||
modelDropParams[modelName] = dropParams;
|
||||
}
|
||||
});
|
||||
if (Object.keys(modelDropParams).length > 0) {
|
||||
result[normalizeEndpointName(EModelEndpoint.azureOpenAI)] = modelDropParams;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2004,6 +2004,13 @@ export type TRumConfig = {
|
|||
|
||||
export type StartupConfigContext = 'share';
|
||||
|
||||
/**
|
||||
* Per-endpoint dropParams for the startup config. Most endpoints (e.g. `custom`) map a
|
||||
* provider name directly to its dropParams. `azureOpenAI` instead maps a provider name to
|
||||
* a per-model dropParams lookup, since each Azure group can drop different parameters.
|
||||
*/
|
||||
export type EndpointsDropParamsMap = Record<string, string[] | Record<string, string[]>>;
|
||||
|
||||
export type TStartupConfig = {
|
||||
appTitle: string;
|
||||
socialLogins?: string[];
|
||||
|
|
@ -2101,6 +2108,7 @@ export type TStartupConfig = {
|
|||
buildDate?: string | null;
|
||||
};
|
||||
fileUploadSseEnabled?: boolean;
|
||||
endpointsDropParamsMap?: EndpointsDropParamsMap;
|
||||
};
|
||||
|
||||
export type TSharedLinkStartupInterface = Pick<
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import type { SettingDefinition } from './generate';
|
||||
import { applyModelAwareDefaults, paramSettings } from './parameterSettings';
|
||||
import { EModelEndpoint } from './types';
|
||||
import {
|
||||
applyModelAwareDefaults,
|
||||
resolveDropParamsUIKeys,
|
||||
paramSettings,
|
||||
} from './parameterSettings';
|
||||
import { EModelEndpoint, Providers } from './types';
|
||||
|
||||
const googleParams = paramSettings[EModelEndpoint.google] as SettingDefinition[];
|
||||
const anthropicParams = paramSettings[EModelEndpoint.anthropic] as SettingDefinition[];
|
||||
|
|
@ -131,3 +135,38 @@ describe('maxContextTokens bounds', () => {
|
|||
expect(bounded).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDropParamsUIKeys', () => {
|
||||
it('aliases backend param names to their UI keys for OpenAI-compatible endpoints', () => {
|
||||
expect(
|
||||
resolveDropParamsUIKeys(
|
||||
['maxTokens', 'topP', 'frequencyPenalty', 'presencePenalty'],
|
||||
EModelEndpoint.openAI,
|
||||
),
|
||||
).toEqual(new Set(['max_tokens', 'top_p', 'frequency_penalty', 'presence_penalty']));
|
||||
});
|
||||
|
||||
it('aliases backend param names for azureOpenAI, custom, and openRouter endpoints', () => {
|
||||
expect(resolveDropParamsUIKeys(['maxTokens'], EModelEndpoint.azureOpenAI)).toEqual(
|
||||
new Set(['max_tokens']),
|
||||
);
|
||||
expect(resolveDropParamsUIKeys(['topP'], EModelEndpoint.custom)).toEqual(new Set(['top_p']));
|
||||
expect(resolveDropParamsUIKeys(['topP'], Providers.OPENROUTER)).toEqual(new Set(['top_p']));
|
||||
});
|
||||
|
||||
it('preserves native keys for a custom endpoint overridden to anthropic/google, since their UI key already matches the backend name', () => {
|
||||
expect(resolveDropParamsUIKeys(['topP'], EModelEndpoint.anthropic)).toEqual(new Set(['topP']));
|
||||
expect(resolveDropParamsUIKeys(['topP'], EModelEndpoint.google)).toEqual(new Set(['topP']));
|
||||
});
|
||||
|
||||
it('preserves native keys for bedrock endpoints', () => {
|
||||
expect(
|
||||
resolveDropParamsUIKeys(['maxTokens', 'topP'], `${EModelEndpoint.bedrock}-anthropic`),
|
||||
).toEqual(new Set(['maxTokens', 'topP']));
|
||||
});
|
||||
|
||||
it('returns an empty set when dropParams is undefined or empty', () => {
|
||||
expect(resolveDropParamsUIKeys(undefined, EModelEndpoint.openAI)).toEqual(new Set());
|
||||
expect(resolveDropParamsUIKeys([], EModelEndpoint.openAI)).toEqual(new Set());
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1190,6 +1190,48 @@ export const paramSettings: Record<string, SettingsConfiguration | undefined> =
|
|||
[EModelEndpoint.google]: googleConfig,
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps effective backend param names for OpenAI-compatible/Azure endpoints (as deleted from
|
||||
* `llmConfig` via `dropParams`, e.g. `maxTokens`) to their corresponding UI/conversation keys
|
||||
* (e.g. `max_tokens`). Native providers (anthropic, google, bedrock, ...) already render these
|
||||
* same camelCase names as their UI key (e.g. `topP`), so this alias must only be applied to
|
||||
* OpenAI-compatible parameter sets — see `resolveDropParamsUIKeys`.
|
||||
*/
|
||||
const dropParamsBackendToUIKey: Record<string, string> = {
|
||||
maxTokens: 'max_tokens',
|
||||
topP: 'top_p',
|
||||
frequencyPenalty: 'frequency_penalty',
|
||||
presencePenalty: 'presence_penalty',
|
||||
};
|
||||
|
||||
/** Endpoint keys whose parameter settings render the OpenAI-compatible (snake_case) UI keys. */
|
||||
const openAILikeParamEndpointKeys: Set<string> = new Set([
|
||||
EModelEndpoint.openAI,
|
||||
EModelEndpoint.azureOpenAI,
|
||||
EModelEndpoint.custom,
|
||||
Providers.OPENROUTER,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Normalizes an admin-configured `dropParams` list into the UI/conversation keys used to hide
|
||||
* the matching controls in the settings panels. `endpointKey` should be the same key used to
|
||||
* resolve the panel's parameter settings (e.g. `overriddenEndpointKey`); the backend-name alias
|
||||
* is only applied for OpenAI-compatible endpoints, since native providers (anthropic, google,
|
||||
* bedrock, ...) already use these backend names as their UI key.
|
||||
*/
|
||||
export function resolveDropParamsUIKeys(
|
||||
dropParams: string[] | undefined,
|
||||
endpointKey: string,
|
||||
): Set<string> {
|
||||
if (!dropParams || dropParams.length === 0) {
|
||||
return new Set();
|
||||
}
|
||||
if (!openAILikeParamEndpointKeys.has(endpointKey)) {
|
||||
return new Set(dropParams);
|
||||
}
|
||||
return new Set(dropParams.map((param) => dropParamsBackendToUIKey[param] ?? param));
|
||||
}
|
||||
|
||||
const openAIColumns = {
|
||||
col1: openAICol1,
|
||||
col2: openAICol2,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue