mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
🎚️ feat: Custom Parameters (#7342)
* # * - refactor: simplified getCustomConfig func * # * - feature: persist values for parameters with optionType of custom * # * - refactor: moved `Parameters/settings.ts` into `data-provider` so that both frontend and backend code can use it. * - feature: loadCustomConfig can now parse and validate customParams property for `endpoints.custom` in `librechat.yaml` * # fixed linter * # removed .strict() in config.ts * change: added packages/data-provider/src to SOURCE_DIRS for i18n check * # removed unnecessary lodash imports * # addressed PR comments # fixed lint for updated files * # better import for lodash (w/o relying on tree-shaking)
This commit is contained in:
parent
c79ee32006
commit
7ce782fec6
23 changed files with 340 additions and 132 deletions
2
.github/workflows/i18n-unused-keys.yml
vendored
2
.github/workflows/i18n-unused-keys.yml
vendored
|
@ -22,7 +22,7 @@ jobs:
|
|||
|
||||
# Define paths
|
||||
I18N_FILE="client/src/locales/en/translation.json"
|
||||
SOURCE_DIRS=("client/src" "api")
|
||||
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src")
|
||||
|
||||
# Check if translation file exists
|
||||
if [[ ! -f "$I18N_FILE" ]]; then
|
||||
|
|
|
@ -10,17 +10,7 @@ const getLogStores = require('~/cache/getLogStores');
|
|||
* */
|
||||
async function getCustomConfig() {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
let customConfig = await cache.get(CacheKeys.CUSTOM_CONFIG);
|
||||
|
||||
if (!customConfig) {
|
||||
customConfig = await loadCustomConfig();
|
||||
}
|
||||
|
||||
if (!customConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return customConfig;
|
||||
return (await cache.get(CacheKeys.CUSTOM_CONFIG)) || (await loadCustomConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,7 +29,14 @@ async function loadConfigEndpoints(req) {
|
|||
|
||||
for (let i = 0; i < customEndpoints.length; i++) {
|
||||
const endpoint = customEndpoints[i];
|
||||
const { baseURL, apiKey, name: configName, iconURL, modelDisplayLabel } = endpoint;
|
||||
const {
|
||||
baseURL,
|
||||
apiKey,
|
||||
name: configName,
|
||||
iconURL,
|
||||
modelDisplayLabel,
|
||||
customParams,
|
||||
} = endpoint;
|
||||
const name = normalizeEndpointName(configName);
|
||||
|
||||
const resolvedApiKey = extractEnvVariable(apiKey);
|
||||
|
@ -41,6 +48,7 @@ async function loadConfigEndpoints(req) {
|
|||
userProvideURL: isUserProvided(resolvedBaseURL),
|
||||
modelDisplayLabel,
|
||||
iconURL,
|
||||
customParams,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
const path = require('path');
|
||||
const { CacheKeys, configSchema, EImageOutputType } = require('librechat-data-provider');
|
||||
const {
|
||||
CacheKeys,
|
||||
configSchema,
|
||||
EImageOutputType,
|
||||
validateSettingDefinitions,
|
||||
agentParamSettings,
|
||||
paramSettings,
|
||||
} = require('librechat-data-provider');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const loadYaml = require('~/utils/loadYaml');
|
||||
const { logger } = require('~/config');
|
||||
const axios = require('axios');
|
||||
const yaml = require('js-yaml');
|
||||
const keyBy = require('lodash/keyBy');
|
||||
|
||||
const projectRoot = path.resolve(__dirname, '..', '..', '..', '..');
|
||||
const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml');
|
||||
|
@ -105,6 +113,10 @@ https://www.librechat.ai/docs/configuration/stt_tts`);
|
|||
logger.debug('Custom config:', customConfig);
|
||||
}
|
||||
|
||||
(customConfig.endpoints?.custom ?? [])
|
||||
.filter((endpoint) => endpoint.customParams)
|
||||
.forEach((endpoint) => parseCustomParams(endpoint.name, endpoint.customParams));
|
||||
|
||||
if (customConfig.cache) {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
await cache.set(CacheKeys.CUSTOM_CONFIG, customConfig);
|
||||
|
@ -117,4 +129,52 @@ https://www.librechat.ai/docs/configuration/stt_tts`);
|
|||
return customConfig;
|
||||
}
|
||||
|
||||
// Validate and fill out missing values for custom parameters
|
||||
function parseCustomParams(endpointName, customParams) {
|
||||
const paramEndpoint = customParams.defaultParamsEndpoint;
|
||||
customParams.paramDefinitions = customParams.paramDefinitions || [];
|
||||
|
||||
// Checks if `defaultParamsEndpoint` is a key in `paramSettings`.
|
||||
const validEndpoints = new Set([
|
||||
...Object.keys(paramSettings),
|
||||
...Object.keys(agentParamSettings),
|
||||
]);
|
||||
if (!validEndpoints.has(paramEndpoint)) {
|
||||
throw new Error(
|
||||
`defaultParamsEndpoint of "${endpointName}" endpoint is invalid. ` +
|
||||
`Valid options are ${Array.from(validEndpoints).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// creates default param maps
|
||||
const regularParams = paramSettings[paramEndpoint] ?? [];
|
||||
const agentParams = agentParamSettings[paramEndpoint] ?? [];
|
||||
const defaultParams = regularParams.concat(agentParams);
|
||||
const defaultParamsMap = keyBy(defaultParams, 'key');
|
||||
|
||||
// TODO: Remove this check once we support new parameters not part of default parameters.
|
||||
// Checks if every key in `paramDefinitions` is valid.
|
||||
const validKeys = new Set(Object.keys(defaultParamsMap));
|
||||
const paramKeys = customParams.paramDefinitions.map((param) => param.key);
|
||||
if (paramKeys.some((key) => !validKeys.has(key))) {
|
||||
throw new Error(
|
||||
`paramDefinitions of "${endpointName}" endpoint contains invalid key(s). ` +
|
||||
`Valid parameter keys are ${Array.from(validKeys).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Fill out missing values for custom param definitions
|
||||
customParams.paramDefinitions = customParams.paramDefinitions.map((param) => {
|
||||
return { ...defaultParamsMap[param.key], ...param, optionType: 'custom' };
|
||||
});
|
||||
|
||||
try {
|
||||
validateSettingDefinitions(customParams.paramDefinitions);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Custom parameter definitions for "${endpointName}" endpoint is malformed: ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = loadCustomConfig;
|
||||
|
|
|
@ -1,6 +1,34 @@
|
|||
jest.mock('axios');
|
||||
jest.mock('~/cache/getLogStores');
|
||||
jest.mock('~/utils/loadYaml');
|
||||
jest.mock('librechat-data-provider', () => {
|
||||
const actual = jest.requireActual('librechat-data-provider');
|
||||
return {
|
||||
...actual,
|
||||
paramSettings: { foo: {}, bar: {}, custom: {} },
|
||||
agentParamSettings: {
|
||||
custom: [],
|
||||
google: [
|
||||
{
|
||||
key: 'pressure',
|
||||
type: 'string',
|
||||
component: 'input',
|
||||
},
|
||||
{
|
||||
key: 'temperature',
|
||||
type: 'number',
|
||||
component: 'slider',
|
||||
default: 0.5,
|
||||
range: {
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const axios = require('axios');
|
||||
const loadCustomConfig = require('./loadCustomConfig');
|
||||
|
@ -150,4 +178,126 @@ describe('loadCustomConfig', () => {
|
|||
expect(logger.info).toHaveBeenCalledWith(JSON.stringify(mockConfig, null, 2));
|
||||
expect(logger.debug).toHaveBeenCalledWith('Custom config:', mockConfig);
|
||||
});
|
||||
|
||||
describe('parseCustomParams', () => {
|
||||
const mockConfig = {
|
||||
version: '1.0',
|
||||
cache: false,
|
||||
endpoints: {
|
||||
custom: [
|
||||
{
|
||||
name: 'Google',
|
||||
apiKey: 'user_provided',
|
||||
customParams: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
async function loadCustomParams(customParams) {
|
||||
mockConfig.endpoints.custom[0].customParams = customParams;
|
||||
loadYaml.mockReturnValue(mockConfig);
|
||||
return await loadCustomConfig();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
process.env.CONFIG_PATH = 'validConfig.yaml';
|
||||
});
|
||||
|
||||
it('returns no error when customParams is undefined', async () => {
|
||||
const result = await loadCustomParams(undefined);
|
||||
expect(result).toEqual(mockConfig);
|
||||
});
|
||||
|
||||
it('returns no error when customParams is valid', async () => {
|
||||
const result = await loadCustomParams({
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [
|
||||
{
|
||||
key: 'temperature',
|
||||
default: 0.5,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result).toEqual(mockConfig);
|
||||
});
|
||||
|
||||
it('throws an error when paramDefinitions contain unsupported keys', async () => {
|
||||
const malformedCustomParams = {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.5 },
|
||||
{ key: 'unsupportedKey', range: 0.5 },
|
||||
],
|
||||
};
|
||||
await expect(loadCustomParams(malformedCustomParams)).rejects.toThrow(
|
||||
'paramDefinitions of "Google" endpoint contains invalid key(s). Valid parameter keys are pressure, temperature',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when paramDefinitions is malformed', async () => {
|
||||
const malformedCustomParams = {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [
|
||||
{
|
||||
key: 'temperature',
|
||||
type: 'noomba',
|
||||
component: 'inpoot',
|
||||
optionType: 'custom',
|
||||
},
|
||||
],
|
||||
};
|
||||
await expect(loadCustomParams(malformedCustomParams)).rejects.toThrow(
|
||||
/Custom parameter definitions for "Google" endpoint is malformed:/,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when defaultParamsEndpoint is not provided', async () => {
|
||||
const malformedCustomParams = { defaultParamsEndpoint: undefined };
|
||||
await expect(loadCustomParams(malformedCustomParams)).rejects.toThrow(
|
||||
'defaultParamsEndpoint of "Google" endpoint is invalid. Valid options are foo, bar, custom, google',
|
||||
);
|
||||
});
|
||||
|
||||
it('fills the paramDefinitions with missing values', async () => {
|
||||
const customParams = {
|
||||
defaultParamsEndpoint: 'google',
|
||||
paramDefinitions: [
|
||||
{ key: 'temperature', default: 0.7, range: { min: 0.1, max: 0.9, step: 0.1 } },
|
||||
{ key: 'pressure', component: 'textarea' },
|
||||
],
|
||||
};
|
||||
|
||||
const parsedConfig = await loadCustomParams(customParams);
|
||||
const paramDefinitions = parsedConfig.endpoints.custom[0].customParams.paramDefinitions;
|
||||
expect(paramDefinitions).toEqual([
|
||||
{
|
||||
columnSpan: 1,
|
||||
component: 'slider',
|
||||
default: 0.7, // overridden
|
||||
includeInput: true,
|
||||
key: 'temperature',
|
||||
label: 'temperature',
|
||||
optionType: 'custom',
|
||||
range: {
|
||||
// overridden
|
||||
max: 0.9,
|
||||
min: 0.1,
|
||||
step: 0.1,
|
||||
},
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
columnSpan: 1,
|
||||
component: 'textarea', // overridden
|
||||
key: 'pressure',
|
||||
label: 'pressure',
|
||||
optionType: 'custom',
|
||||
placeholder: '',
|
||||
type: 'string',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -105,6 +105,7 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
|
|||
headers: resolvedHeaders,
|
||||
addParams: endpointConfig.addParams,
|
||||
dropParams: endpointConfig.dropParams,
|
||||
customParams: endpointConfig.customParams,
|
||||
titleConvo: endpointConfig.titleConvo,
|
||||
titleModel: endpointConfig.titleModel,
|
||||
forcePrompt: endpointConfig.forcePrompt,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { getSettingsKeys } from 'librechat-data-provider';
|
|||
import type { SettingDefinition } from 'librechat-data-provider';
|
||||
import type { TModelSelectProps } from '~/common';
|
||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||
import { presetSettings } from '~/components/SidePanel/Parameters/settings';
|
||||
import { presetSettings } from 'librechat-data-provider';
|
||||
|
||||
export default function AnthropicSettings({
|
||||
conversation,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { getSettingsKeys } from 'librechat-data-provider';
|
|||
import type { SettingDefinition } from 'librechat-data-provider';
|
||||
import type { TModelSelectProps } from '~/common';
|
||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||
import { presetSettings } from '~/components/SidePanel/Parameters/settings';
|
||||
import { presetSettings } from 'librechat-data-provider';
|
||||
|
||||
export default function BedrockSettings({
|
||||
conversation,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useMemo } from 'react';
|
||||
import { getSettingsKeys } from 'librechat-data-provider';
|
||||
import type { SettingDefinition, DynamicSettingProps } from 'librechat-data-provider';
|
||||
import type { SettingDefinition } from 'librechat-data-provider';
|
||||
import type { TModelSelectProps } from '~/common';
|
||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||
import { presetSettings } from '~/components/SidePanel/Parameters/settings';
|
||||
import { presetSettings } from 'librechat-data-provider';
|
||||
|
||||
export default function OpenAISettings({
|
||||
conversation,
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import React, { useMemo, useEffect } from 'react';
|
||||
import { ChevronLeft, RotateCcw } from 'lucide-react';
|
||||
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
||||
import { getSettingsKeys, alternateName } from 'librechat-data-provider';
|
||||
import {
|
||||
getSettingsKeys,
|
||||
alternateName,
|
||||
agentParamSettings,
|
||||
SettingDefinition,
|
||||
} from 'librechat-data-provider';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||
import { agentSettings } from '~/components/SidePanel/Parameters/settings';
|
||||
import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { getEndpointField, cn } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Panel } from '~/common';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
|
||||
export default function ModelPanel({
|
||||
setActivePanel,
|
||||
|
@ -52,7 +57,7 @@ export default function ModelPanel({
|
|||
}
|
||||
}, [provider, models, modelsData, setValue, model]);
|
||||
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const { data: endpointsConfig = {} } = useGetEndpointsQuery();
|
||||
|
||||
const bedrockRegions = useMemo(() => {
|
||||
return endpointsConfig?.[provider]?.availableRegions ?? [];
|
||||
|
@ -63,10 +68,18 @@ export default function ModelPanel({
|
|||
[provider, endpointsConfig],
|
||||
);
|
||||
|
||||
const parameters = useMemo(() => {
|
||||
const parameters = useMemo((): SettingDefinition[] => {
|
||||
const customParams = endpointsConfig[provider]?.customParams ?? {};
|
||||
const [combinedKey, endpointKey] = getSettingsKeys(endpointType ?? provider, model ?? '');
|
||||
return agentSettings[combinedKey] ?? agentSettings[endpointKey];
|
||||
}, [endpointType, model, provider]);
|
||||
const overriddenEndpointKey = customParams.defaultParamsEndpoint ?? endpointKey;
|
||||
const defaultParams =
|
||||
agentParamSettings[combinedKey] ?? agentParamSettings[overriddenEndpointKey] ?? [];
|
||||
const overriddenParams = endpointsConfig[provider]?.customParams?.paramDefinitions ?? [];
|
||||
const overriddenParamsMap = keyBy(overriddenParams, 'key');
|
||||
return defaultParams.map(
|
||||
(param) => (overriddenParamsMap[param.key] as SettingDefinition) ?? param,
|
||||
);
|
||||
}, [endpointType, endpointsConfig, model, provider]);
|
||||
|
||||
const setOption = (optionKey: keyof t.AgentModelParameters) => (value: t.AgentParameterValue) => {
|
||||
setValue(`model_parameters.${optionKey}`, value);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { OptionTypes } from 'librechat-data-provider';
|
||||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui';
|
||||
import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
|
||||
import { TranslationKeys, useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
@ -23,23 +23,20 @@ function DynamicCheckbox({
|
|||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { preset } = useChatContext();
|
||||
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined));
|
||||
|
||||
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput<boolean>({
|
||||
optionKey: settingKey,
|
||||
initialValue: optionType !== OptionTypes.Custom ? conversation?.[settingKey] : defaultValue,
|
||||
setter: () => ({}),
|
||||
setOption,
|
||||
});
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
return inputValue;
|
||||
}
|
||||
|
||||
return conversation?.[settingKey] ?? defaultValue;
|
||||
}, [conversation, defaultValue, optionType, settingKey, inputValue]);
|
||||
}, [conversation, defaultValue, settingKey]);
|
||||
|
||||
const handleCheckedChange = (checked: boolean) => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
setInputValue(checked);
|
||||
return;
|
||||
}
|
||||
setOption(settingKey)(checked);
|
||||
};
|
||||
|
||||
|
@ -49,8 +46,7 @@ function DynamicCheckbox({
|
|||
defaultValue,
|
||||
conversation,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
preventDelayedUpdate: true,
|
||||
setInputValue: setLocalValue,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useMemo, useState, useCallback } from 'react';
|
||||
import { OptionTypes } from 'librechat-data-provider';
|
||||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, HoverCard, HoverCardTrigger } from '~/components/ui';
|
||||
import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||
|
@ -16,7 +15,6 @@ function DynamicCombobox({
|
|||
description = '',
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
options: _options,
|
||||
items: _items,
|
||||
showLabel = true,
|
||||
|
@ -36,11 +34,8 @@ function DynamicCombobox({
|
|||
const [inputValue, setInputValue] = useState<string | null>(null);
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
return inputValue;
|
||||
}
|
||||
return conversation?.[settingKey] ?? defaultValue;
|
||||
}, [conversation, defaultValue, optionType, settingKey, inputValue]);
|
||||
}, [conversation, defaultValue, settingKey]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (_items != null) {
|
||||
|
@ -54,13 +49,10 @@ function DynamicCombobox({
|
|||
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
setInputValue(value);
|
||||
} else {
|
||||
setOption(settingKey)(value);
|
||||
}
|
||||
},
|
||||
[optionType, setOption, settingKey],
|
||||
[setOption, settingKey],
|
||||
);
|
||||
|
||||
useParameterEffects({
|
||||
|
|
|
@ -12,7 +12,6 @@ function DynamicInput({
|
|||
settingKey,
|
||||
defaultValue,
|
||||
description = '',
|
||||
type = 'string',
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
|
@ -28,7 +27,7 @@ function DynamicInput({
|
|||
const { preset } = useChatContext();
|
||||
|
||||
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput<string | number>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
optionKey: settingKey,
|
||||
initialValue: optionType !== OptionTypes.Custom ? conversation?.[settingKey] : defaultValue,
|
||||
setter: () => ({}),
|
||||
setOption,
|
||||
|
@ -44,17 +43,7 @@ function DynamicInput({
|
|||
});
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
if (type !== 'number') {
|
||||
setInputValue(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === '') {
|
||||
setInputValue(e);
|
||||
} else if (!isNaN(Number(value))) {
|
||||
setInputValue(e, true);
|
||||
}
|
||||
setInputValue(e, !isNaN(Number(e.target.value)));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -33,7 +33,7 @@ function DynamicSlider({
|
|||
);
|
||||
|
||||
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput<string | number>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
optionKey: settingKey,
|
||||
initialValue: optionType !== OptionTypes.Custom ? conversation?.[settingKey] : defaultValue,
|
||||
setter: () => ({}),
|
||||
setOption,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useState, useMemo } from 'react';
|
||||
import { OptionTypes } from 'librechat-data-provider';
|
||||
import { useState } from 'react';
|
||||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, Switch, HoverCard, HoverCardTrigger } from '~/components/ui';
|
||||
import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
|
||||
|
@ -14,7 +13,6 @@ function DynamicSwitch({
|
|||
description = '',
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
readonly = false,
|
||||
showDefault = false,
|
||||
labelCode = false,
|
||||
|
@ -34,21 +32,10 @@ function DynamicSwitch({
|
|||
preventDelayedUpdate: true,
|
||||
});
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
return inputValue;
|
||||
}
|
||||
|
||||
return conversation?.[settingKey] ?? defaultValue;
|
||||
}, [conversation, defaultValue, optionType, settingKey, inputValue]);
|
||||
const selectedValue = conversation?.[settingKey] ?? defaultValue;
|
||||
|
||||
const handleCheckedChange = (checked: boolean) => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
setInputValue(checked);
|
||||
return;
|
||||
}
|
||||
setOption(settingKey)(checked);
|
||||
};
|
||||
|
||||
|
@ -65,7 +52,7 @@ function DynamicSwitch({
|
|||
htmlFor={`${settingKey}-dynamic-switch`}
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}{' '}
|
||||
{labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey}{' '}
|
||||
{showDefault && (
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default')}:{' '}
|
||||
|
@ -84,7 +71,11 @@ function DynamicSwitch({
|
|||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
|
||||
description={
|
||||
descriptionCode
|
||||
? (localize(description as TranslationKeys) ?? description)
|
||||
: description
|
||||
}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { OptionTypes } from 'librechat-data-provider';
|
||||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
|
||||
import { cn, defaultTextProps } from '~/utils';
|
||||
import { cn } from '~/utils';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
|
@ -15,7 +14,6 @@ function DynamicTags({
|
|||
description = '',
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
placeholder = '',
|
||||
readonly = false,
|
||||
showDefault = false,
|
||||
|
@ -38,14 +36,10 @@ function DynamicTags({
|
|||
|
||||
const updateState = useCallback(
|
||||
(update: string[]) => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
setTags(update);
|
||||
return;
|
||||
}
|
||||
setOption(settingKey)(update);
|
||||
},
|
||||
[optionType, setOption, settingKey],
|
||||
[setOption, settingKey],
|
||||
);
|
||||
|
||||
const onTagClick = useCallback(() => {
|
||||
|
@ -54,18 +48,10 @@ function DynamicTags({
|
|||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const currentTags: string[] | undefined = useMemo(() => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
return tags;
|
||||
}
|
||||
|
||||
if (!conversation?.[settingKey]) {
|
||||
return defaultValue ?? [];
|
||||
}
|
||||
|
||||
return conversation[settingKey];
|
||||
}, [conversation, defaultValue, optionType, settingKey, tags]);
|
||||
const currentValue = conversation?.[settingKey];
|
||||
const currentTags = useMemo(() => {
|
||||
return currentValue ?? defaultValue ?? [];
|
||||
}, [currentValue, defaultValue]);
|
||||
|
||||
const onTagRemove = useCallback(
|
||||
(indexToRemove: number) => {
|
||||
|
@ -75,7 +61,7 @@ function DynamicTags({
|
|||
|
||||
if (minTags != null && currentTags.length <= minTags) {
|
||||
showToast({
|
||||
message: localize('com_ui_min_tags',{ 0: minTags + '' }),
|
||||
message: localize('com_ui_min_tags', { 0: minTags + '' }),
|
||||
status: 'warning',
|
||||
});
|
||||
return;
|
||||
|
@ -126,7 +112,7 @@ function DynamicTags({
|
|||
htmlFor={`${settingKey}-dynamic-input`}
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}{' '}
|
||||
{labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey}{' '}
|
||||
{showDefault && (
|
||||
<small className="opacity-40">
|
||||
(
|
||||
|
@ -174,7 +160,11 @@ function DynamicTags({
|
|||
}
|
||||
}}
|
||||
onChange={(e) => setTagText(e.target.value)}
|
||||
placeholder={placeholderCode ? localize(placeholder as TranslationKeys) ?? placeholder : placeholder}
|
||||
placeholder={
|
||||
placeholderCode
|
||||
? (localize(placeholder as TranslationKeys) ?? placeholder)
|
||||
: placeholder
|
||||
}
|
||||
className={cn('flex h-10 max-h-10 border-none bg-surface-secondary px-3 py-2')}
|
||||
/>
|
||||
</div>
|
||||
|
@ -182,7 +172,11 @@ function DynamicTags({
|
|||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
|
||||
description={
|
||||
descriptionCode
|
||||
? (localize(description as TranslationKeys) ?? description)
|
||||
: description
|
||||
}
|
||||
side={descriptionSide as ESide}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { OptionTypes } from 'librechat-data-provider';
|
|||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, TextareaAutosize, HoverCard, HoverCardTrigger } from '~/components/ui';
|
||||
import { useLocalize, useDebouncedInput, useParameterEffects, TranslationKeys } from '~/hooks';
|
||||
import { cn, defaultTextProps } from '~/utils';
|
||||
import { cn } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
@ -27,7 +27,7 @@ function DynamicTextarea({
|
|||
const { preset } = useChatContext();
|
||||
|
||||
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput<string | null>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
optionKey: settingKey,
|
||||
initialValue:
|
||||
optionType !== OptionTypes.Custom
|
||||
? (conversation?.[settingKey] as string)
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import { RotateCcw } from 'lucide-react';
|
||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import { excludedKeys, getSettingsKeys, tConvoUpdateSchema } from 'librechat-data-provider';
|
||||
import {
|
||||
excludedKeys,
|
||||
getSettingsKeys,
|
||||
tConvoUpdateSchema,
|
||||
paramSettings,
|
||||
SettingDefinition,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
import { SaveAsPresetDialog } from '~/components/Endpoints';
|
||||
import { useSetIndexOptions, useLocalize } from '~/hooks';
|
||||
|
@ -8,7 +14,7 @@ import { useGetEndpointsQuery } from '~/data-provider';
|
|||
import { getEndpointField, logger } from '~/utils';
|
||||
import { componentMapping } from './components';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { settings } from './settings';
|
||||
import keyBy from 'lodash/keyBy';
|
||||
|
||||
export default function Parameters() {
|
||||
const localize = useLocalize();
|
||||
|
@ -18,7 +24,9 @@ export default function Parameters() {
|
|||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [preset, setPreset] = useState<TPreset | null>(null);
|
||||
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const { data: endpointsConfig = {} } = useGetEndpointsQuery();
|
||||
const provider = conversation?.endpoint ?? '';
|
||||
const model = conversation?.model ?? '';
|
||||
|
||||
const bedrockRegions = useMemo(() => {
|
||||
return endpointsConfig?.[conversation?.endpoint ?? '']?.availableRegions ?? [];
|
||||
|
@ -29,13 +37,17 @@ export default function Parameters() {
|
|||
[conversation?.endpoint, endpointsConfig],
|
||||
);
|
||||
|
||||
const parameters = useMemo(() => {
|
||||
const [combinedKey, endpointKey] = getSettingsKeys(
|
||||
endpointType ?? conversation?.endpoint ?? '',
|
||||
conversation?.model ?? '',
|
||||
const parameters = useMemo((): SettingDefinition[] => {
|
||||
const customParams = endpointsConfig[provider]?.customParams ?? {};
|
||||
const [combinedKey, endpointKey] = getSettingsKeys(endpointType ?? provider, model);
|
||||
const overriddenEndpointKey = customParams.defaultParamsEndpoint ?? endpointKey;
|
||||
const defaultParams = paramSettings[combinedKey] ?? paramSettings[overriddenEndpointKey] ?? [];
|
||||
const overriddenParams = endpointsConfig[provider]?.customParams?.paramDefinitions ?? [];
|
||||
const overriddenParamsMap = keyBy(overriddenParams, 'key');
|
||||
return defaultParams.map(
|
||||
(param) => (overriddenParamsMap[param.key] as SettingDefinition) ?? param,
|
||||
);
|
||||
return settings[combinedKey] ?? settings[endpointKey];
|
||||
}, [conversation, endpointType]);
|
||||
}, [endpointType, endpointsConfig, model, provider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!parameters) {
|
||||
|
|
|
@ -279,6 +279,12 @@ export const endpointSchema = baseEndpointSchema.merge(
|
|||
headers: z.record(z.any()).optional(),
|
||||
addParams: z.record(z.any()).optional(),
|
||||
dropParams: z.array(z.string()).optional(),
|
||||
customParams: z
|
||||
.object({
|
||||
defaultParamsEndpoint: z.string().default('custom'),
|
||||
paramDefinitions: z.array(z.record(z.any())).optional(),
|
||||
})
|
||||
.strict(),
|
||||
customOrder: z.number().optional(),
|
||||
directEndpoint: z.boolean().optional(),
|
||||
titleMessageRole: z.string().optional(),
|
||||
|
|
|
@ -467,7 +467,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
|
|||
}
|
||||
|
||||
/* Default value checks */
|
||||
if (setting.type === SettingTypes.Number && isNaN(setting.default as number)) {
|
||||
if (setting.type === SettingTypes.Number && isNaN(setting.default as number) && setting.default != null) {
|
||||
errors.push({
|
||||
code: ZodIssueCode.custom,
|
||||
message: `Invalid default value for setting ${setting.key}. Must be a number.`,
|
||||
|
@ -475,7 +475,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
|
|||
});
|
||||
}
|
||||
|
||||
if (setting.type === SettingTypes.Boolean && typeof setting.default !== 'boolean') {
|
||||
if (setting.type === SettingTypes.Boolean && typeof setting.default !== 'boolean' && setting.default != null) {
|
||||
errors.push({
|
||||
code: ZodIssueCode.custom,
|
||||
message: `Invalid default value for setting ${setting.key}. Must be a boolean.`,
|
||||
|
@ -485,7 +485,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
|
|||
|
||||
if (
|
||||
(setting.type === SettingTypes.String || setting.type === SettingTypes.Enum) &&
|
||||
typeof setting.default !== 'string'
|
||||
typeof setting.default !== 'string' && setting.default != null
|
||||
) {
|
||||
errors.push({
|
||||
code: ZodIssueCode.custom,
|
||||
|
|
|
@ -36,3 +36,4 @@ import * as dataService from './data-service';
|
|||
export * from './utils';
|
||||
export * from './actions';
|
||||
export { default as createPayload } from './createPayload';
|
||||
export * from './parameterSettings';
|
||||
|
|
|
@ -6,8 +6,8 @@ import {
|
|||
ReasoningEffort,
|
||||
BedrockProviders,
|
||||
anthropicSettings,
|
||||
} from 'librechat-data-provider';
|
||||
import type { SettingsConfiguration, SettingDefinition } from 'librechat-data-provider';
|
||||
} from './types';
|
||||
import { SettingDefinition, SettingsConfiguration } from './generate';
|
||||
|
||||
// Base definitions
|
||||
const baseDefinitions: Record<string, SettingDefinition> = {
|
||||
|
@ -654,7 +654,7 @@ const bedrockGeneralCol2: SettingsConfiguration = [
|
|||
bedrock.region,
|
||||
];
|
||||
|
||||
export const settings: Record<string, SettingsConfiguration | undefined> = {
|
||||
export const paramSettings: Record<string, SettingsConfiguration | undefined> = {
|
||||
[EModelEndpoint.openAI]: openAI,
|
||||
[EModelEndpoint.azureOpenAI]: openAI,
|
||||
[EModelEndpoint.custom]: openAI,
|
||||
|
@ -684,7 +684,7 @@ export const presetSettings: Record<
|
|||
| {
|
||||
col1: SettingsConfiguration;
|
||||
col2: SettingsConfiguration;
|
||||
}
|
||||
}
|
||||
| undefined
|
||||
> = {
|
||||
[EModelEndpoint.openAI]: openAIColumns,
|
||||
|
@ -716,9 +716,9 @@ export const presetSettings: Record<
|
|||
},
|
||||
};
|
||||
|
||||
export const agentSettings: Record<string, SettingsConfiguration | undefined> = Object.entries(
|
||||
export const agentParamSettings: Record<string, SettingsConfiguration | undefined> = Object.entries(
|
||||
presetSettings,
|
||||
).reduce((acc, [key, value]) => {
|
||||
).reduce<Record<string, SettingsConfiguration | undefined>>((acc, [key, value]) => {
|
||||
if (value) {
|
||||
acc[key] = value.col2;
|
||||
}
|
|
@ -10,6 +10,7 @@ import type {
|
|||
TConversationTag,
|
||||
TBanner,
|
||||
} from './schemas';
|
||||
import { SettingDefinition } from './generate';
|
||||
export type TOpenAIMessage = OpenAI.Chat.ChatCompletionMessageParam;
|
||||
|
||||
export * from './schemas';
|
||||
|
@ -268,6 +269,10 @@ export type TConfig = {
|
|||
disableBuilder?: boolean;
|
||||
retrievalModels?: string[];
|
||||
capabilities?: string[];
|
||||
customParams?: {
|
||||
defaultParamsEndpoint?: string;
|
||||
paramDefinitions?: SettingDefinition[];
|
||||
};
|
||||
};
|
||||
|
||||
export type TEndpointsConfig =
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue