🛡️ fix: Preset and Validation Logic for URL Query Params (#7407)

* chore(store/families): linting

* refactor: Update `createChatSearchParams` to use `tQueryParamsSchema` for allowed parameters and add `modelLabel` to schema

* refactor: Enhance `useQueryParams` to streamline parameter processing and improve submission handling

* chore: linting

* fix: Add `disableParams` option to conversation handling and related schemas to prevent search params from updating due to use of default preset

* fix: Update `createChatSearchParams` to correctly ignore `agent_id` when it matches `EPHEMERAL_AGENT_ID`

* chore: revert modelLabel addition to query params, as no longer necessary due to `disableParams`

* fix: Refine logic for `disableParams` to ensure correct handling of active preset comparison

* fix: Add `disableParams` option to `NewConversationParams` and update related hooks for preset handling

* fix: Refactor validation logic in `validateSettingDefinitions` to improve handling of `includeInput` and update conversation schema

* fix: Bump version of `librechat-data-provider` to 0.7.83
This commit is contained in:
Danny Avila 2025-05-15 17:46:48 -04:00 committed by GitHub
parent 7a91f6ca62
commit 2f4a03b581
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 126 additions and 112 deletions

View file

@ -535,6 +535,7 @@ export type NewConversationParams = {
buildDefault?: boolean; buildDefault?: boolean;
keepLatestMessage?: boolean; keepLatestMessage?: boolean;
keepAddedConvos?: boolean; keepAddedConvos?: boolean;
disableParams?: boolean;
}; };
export type ConvoGenerator = (params: NewConversationParams) => void | t.TConversation; export type ConvoGenerator = (params: NewConversationParams) => void | t.TConversation;

View file

@ -79,19 +79,19 @@ export default function HeaderOptions({
{!noSettings[endpoint] && {!noSettings[endpoint] &&
interfaceConfig?.parameters === true && interfaceConfig?.parameters === true &&
paramEndpoint === false && ( paramEndpoint === false && (
<TooltipAnchor <TooltipAnchor
id="parameters-button" id="parameters-button"
aria-label={localize('com_ui_model_parameters')} aria-label={localize('com_ui_model_parameters')}
description={localize('com_ui_model_parameters')} description={localize('com_ui_model_parameters')}
tabIndex={0} tabIndex={0}
role="button" role="button"
onClick={triggerAdvancedMode} onClick={triggerAdvancedMode}
data-testid="parameters-button" data-testid="parameters-button"
className="inline-flex size-10 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary" className="inline-flex size-10 items-center justify-center rounded-lg border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
> >
<Settings2 size={16} aria-label="Settings/Parameters Icon" /> <Settings2 size={16} aria-label="Settings/Parameters Icon" />
</TooltipAnchor> </TooltipAnchor>
)} )}
</div> </div>
{interfaceConfig?.parameters === true && paramEndpoint === false && ( {interfaceConfig?.parameters === true && paramEndpoint === false && (
<OptionsPopover <OptionsPopover

View file

@ -58,10 +58,11 @@ export default function usePresets() {
} }
setDefaultPreset(defaultPreset); setDefaultPreset(defaultPreset);
if (!conversation?.conversationId || conversation.conversationId === 'new') { if (!conversation?.conversationId || conversation.conversationId === 'new') {
newConversation({ preset: defaultPreset, modelsData }); newConversation({ preset: defaultPreset, modelsData, disableParams: true });
} }
hasLoaded.current = true; hasLoaded.current = true;
// dependencies are stable and only needed once // dependencies are stable and only needed once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [presetsQuery.data, user, modelsData]); }, [presetsQuery.data, user, modelsData]);
const setPresets = useCallback( const setPresets = useCallback(
@ -102,7 +103,7 @@ export default function usePresets() {
if (data.defaultPreset && data.presetId !== _defaultPreset?.presetId) { if (data.defaultPreset && data.presetId !== _defaultPreset?.presetId) {
message = `${toastTitle} ${localize('com_endpoint_preset_default')}`; message = `${toastTitle} ${localize('com_endpoint_preset_default')}`;
setDefaultPreset(data); setDefaultPreset(data);
newConversation({ preset: data }); newConversation({ preset: data, disableParams: true });
} else if (preset.defaultPreset === false) { } else if (preset.defaultPreset === false) {
setDefaultPreset(null); setDefaultPreset(null);
message = `${toastTitle} ${localize('com_endpoint_preset_default_removed')}`; message = `${toastTitle} ${localize('com_endpoint_preset_default_removed')}`;
@ -185,6 +186,7 @@ export default function usePresets() {
newPreset.iconURL = newPreset.iconURL ?? null; newPreset.iconURL = newPreset.iconURL ?? null;
newPreset.modelLabel = newPreset.modelLabel ?? null; newPreset.modelLabel = newPreset.modelLabel ?? null;
const isModular = isCurrentModular && isNewModular && shouldSwitch; const isModular = isCurrentModular && isNewModular && shouldSwitch;
const disableParams = newPreset.defaultPreset === true;
if (isExistingConversation && isModular) { if (isExistingConversation && isModular) {
const currentConvo = getDefaultConversation({ const currentConvo = getDefaultConversation({
/* target endpointType is necessary to avoid endpoint mixing */ /* target endpointType is necessary to avoid endpoint mixing */
@ -205,11 +207,12 @@ export default function usePresets() {
preset: currentConvo, preset: currentConvo,
keepLatestMessage: true, keepLatestMessage: true,
keepAddedConvos: true, keepAddedConvos: true,
disableParams,
}); });
return; return;
} }
newConversation({ preset: newPreset, keepAddedConvos: isModular }); newConversation({ preset: newPreset, keepAddedConvos: isModular, disableParams });
}; };
const onChangePreset = (preset: TPreset) => { const onChangePreset = (preset: TPreset) => {

View file

@ -258,35 +258,6 @@ export default function useQueryParams({
})(); })();
}, [methods, submitMessage, conversation]); }, [methods, submitMessage, conversation]);
useEffect(() => {
// Only proceed if we've already processed URL parameters but haven't yet handled submission
if (
!processedRef.current ||
submissionHandledRef.current ||
settingsAppliedRef.current ||
!validSettingsRef.current ||
!conversation
) {
return;
}
const allSettingsApplied = areSettingsApplied();
if (allSettingsApplied) {
settingsAppliedRef.current = true;
if (pendingSubmitRef.current) {
if (settingsTimeoutRef.current) {
clearTimeout(settingsTimeoutRef.current);
settingsTimeoutRef.current = null;
}
console.log('Settings fully applied, processing submission');
processSubmission();
}
}
}, [conversation, processSubmission, areSettingsApplied]);
useEffect(() => { useEffect(() => {
const processQueryParams = () => { const processQueryParams = () => {
const queryParams: Record<string, string> = {}; const queryParams: Record<string, string> = {};
@ -332,14 +303,15 @@ export default function useQueryParams({
/** Mark processing as complete and clean up as needed */ /** Mark processing as complete and clean up as needed */
const success = () => { const success = () => {
const currentParams = new URLSearchParams(searchParams.toString()); const paramString = searchParams.toString();
const currentParams = new URLSearchParams(paramString);
currentParams.delete('prompt'); currentParams.delete('prompt');
currentParams.delete('q'); currentParams.delete('q');
currentParams.delete('submit'); currentParams.delete('submit');
setSearchParams(currentParams, { replace: true }); setSearchParams(currentParams, { replace: true });
processedRef.current = true; processedRef.current = true;
console.log('Parameters processed successfully'); console.log('Parameters processed successfully', paramString);
clearInterval(intervalId); clearInterval(intervalId);
// Only clean URL if there's no pending submission // Only clean URL if there's no pending submission
@ -417,4 +389,33 @@ export default function useQueryParams({
queryClient, queryClient,
processSubmission, processSubmission,
]); ]);
useEffect(() => {
// Only proceed if we've already processed URL parameters but haven't yet handled submission
if (
!processedRef.current ||
submissionHandledRef.current ||
settingsAppliedRef.current ||
!validSettingsRef.current ||
!conversation
) {
return;
}
const allSettingsApplied = areSettingsApplied();
if (allSettingsApplied) {
settingsAppliedRef.current = true;
if (pendingSubmitRef.current) {
if (settingsTimeoutRef.current) {
clearTimeout(settingsTimeoutRef.current);
settingsTimeoutRef.current = null;
}
console.log('Settings fully applied, processing submission');
processSubmission();
}
}
}, [conversation, processSubmission, areSettingsApplied]);
} }

View file

@ -225,6 +225,7 @@ export default function useSelectMention({
newPreset.iconURL = newPreset.iconURL ?? null; newPreset.iconURL = newPreset.iconURL ?? null;
newPreset.modelLabel = newPreset.modelLabel ?? null; newPreset.modelLabel = newPreset.modelLabel ?? null;
const isModular = isCurrentModular && isNewModular && shouldSwitch; const isModular = isCurrentModular && isNewModular && shouldSwitch;
const disableParams = newPreset.defaultPreset === true;
if (isExistingConversation && isModular) { if (isExistingConversation && isModular) {
template.endpointType = newEndpointType as EModelEndpoint | undefined; template.endpointType = newEndpointType as EModelEndpoint | undefined;
template.spec = null; template.spec = null;
@ -244,12 +245,17 @@ export default function useSelectMention({
preset: newPreset, preset: newPreset,
keepLatestMessage: true, keepLatestMessage: true,
keepAddedConvos: true, keepAddedConvos: true,
disableParams,
}); });
return; return;
} }
logger.info('conversation', 'Switching conversation to new preset', template); logger.info('conversation', 'Switching conversation to new preset', template);
newConversation({ preset: newPreset, keepAddedConvos: isModular }); newConversation({
preset: newPreset,
keepAddedConvos: isModular,
disableParams,
});
}, },
[ [
modularChat, modularChat,

View file

@ -71,6 +71,7 @@ const useNewConvo = (index = 0) => {
keepLatestMessage?: boolean, keepLatestMessage?: boolean,
keepAddedConvos?: boolean, keepAddedConvos?: boolean,
disableFocus?: boolean, disableFocus?: boolean,
_disableParams?: boolean,
) => { ) => {
const modelsConfig = modelsData ?? modelsQuery.data; const modelsConfig = modelsData ?? modelsQuery.data;
const { endpoint = null } = conversation; const { endpoint = null } = conversation;
@ -87,6 +88,12 @@ const useNewConvo = (index = 0) => {
? defaultPreset ? defaultPreset
: preset; : preset;
const disableParams =
_disableParams ??
(activePreset?.presetId != null &&
activePreset.presetId &&
activePreset.presetId === defaultPreset?.presetId);
if (buildDefaultConversation) { if (buildDefaultConversation) {
let defaultEndpoint = getDefaultEndpoint({ let defaultEndpoint = getDefaultEndpoint({
convoSetup: activePreset ?? conversation, convoSetup: activePreset ?? conversation,
@ -148,6 +155,10 @@ const useNewConvo = (index = 0) => {
}); });
} }
if (disableParams === true) {
conversation.disableParams = true;
}
if (!(keepAddedConvos ?? false)) { if (!(keepAddedConvos ?? false)) {
clearAllConversations(true); clearAllConversations(true);
} }
@ -160,7 +171,7 @@ const useNewConvo = (index = 0) => {
); );
setConversation({ setConversation({
...conversation, ...conversation,
conversationId: 'new', conversationId: Constants.NEW_CONVO as string,
}); });
} else { } else {
logger.log('conversation', 'Setting conversation from `useNewConvo`', conversation); logger.log('conversation', 'Setting conversation from `useNewConvo`', conversation);
@ -205,6 +216,7 @@ const useNewConvo = (index = 0) => {
buildDefault = true, buildDefault = true,
keepLatestMessage = false, keepLatestMessage = false,
keepAddedConvos = false, keepAddedConvos = false,
disableParams,
}: { }: {
template?: Partial<TConversation>; template?: Partial<TConversation>;
preset?: Partial<TPreset>; preset?: Partial<TPreset>;
@ -213,6 +225,7 @@ const useNewConvo = (index = 0) => {
disableFocus?: boolean; disableFocus?: boolean;
keepLatestMessage?: boolean; keepLatestMessage?: boolean;
keepAddedConvos?: boolean; keepAddedConvos?: boolean;
disableParams?: boolean;
} = {}) { } = {}) {
pauseGlobalAudio(); pauseGlobalAudio();
if (!saveBadgesState) { if (!saveBadgesState) {
@ -282,17 +295,19 @@ const useNewConvo = (index = 0) => {
keepLatestMessage, keepLatestMessage,
keepAddedConvos, keepAddedConvos,
disableFocus, disableFocus,
disableParams,
); );
}, },
[ [
pauseGlobalAudio,
startupConfig,
saveDrafts,
switchToConversation,
files, files,
setFiles, setFiles,
saveDrafts,
mutateAsync, mutateAsync,
resetBadges, resetBadges,
startupConfig,
saveBadgesState,
pauseGlobalAudio,
switchToConversation,
], ],
); );

View file

@ -106,10 +106,13 @@ const conversationByIndex = atomFamily<TConversation | null, string | number>({
JSON.stringify(newValue), JSON.stringify(newValue),
); );
const disableParams = newValue.disableParams === true;
const shouldUpdateParams = const shouldUpdateParams =
index === 0 &&
!disableParams &&
newValue.createdAt === '' && newValue.createdAt === '' &&
JSON.stringify(newValue) !== JSON.stringify(oldValue) && JSON.stringify(newValue) !== JSON.stringify(oldValue) &&
(oldValue as TConversation)?.conversationId === 'new'; (oldValue as TConversation)?.conversationId === Constants.NEW_CONVO;
if (shouldUpdateParams) { if (shouldUpdateParams) {
const newParams = createChatSearchParams(newValue); const newParams = createChatSearchParams(newValue);
@ -299,10 +302,10 @@ const conversationByKeySelector = selectorFamily({
key: 'conversationByKeySelector', key: 'conversationByKeySelector',
get: get:
(index: string | number) => (index: string | number) =>
({ get }) => { ({ get }) => {
const conversation = get(conversationByIndex(index)); const conversation = get(conversationByIndex(index));
return conversation; return conversation;
}, },
}); });
function useClearSubmissionState() { function useClearSubmissionState() {
@ -361,24 +364,24 @@ const updateConversationSelector = selectorFamily({
get: () => () => null as Partial<TConversation> | null, get: () => () => null as Partial<TConversation> | null,
set: set:
(conversationId: string) => (conversationId: string) =>
({ set, get }, newPartialConversation) => { ({ set, get }, newPartialConversation) => {
if (newPartialConversation instanceof DefaultValue) { if (newPartialConversation instanceof DefaultValue) {
return; return;
} }
const keys = get(conversationKeysAtom); const keys = get(conversationKeysAtom);
keys.forEach((key) => { keys.forEach((key) => {
set(conversationByIndex(key), (prevConversation) => { set(conversationByIndex(key), (prevConversation) => {
if (prevConversation && prevConversation.conversationId === conversationId) { if (prevConversation && prevConversation.conversationId === conversationId) {
return { return {
...prevConversation, ...prevConversation,
...newPartialConversation, ...newPartialConversation,
}; };
} }
return prevConversation; return prevConversation;
});
}); });
}, });
},
}); });
export default { export default {

View file

@ -1,6 +1,12 @@
import { isAgentsEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider'; import {
Constants,
isAgentsEndpoint,
tQueryParamsSchema,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import type { TConversation, TPreset } from 'librechat-data-provider'; import type { TConversation, TPreset } from 'librechat-data-provider';
const allowedParams = Object.keys(tQueryParamsSchema.shape);
export default function createChatSearchParams( export default function createChatSearchParams(
input: TConversation | TPreset | Record<string, string> | null, input: TConversation | TPreset | Record<string, string> | null,
): URLSearchParams { ): URLSearchParams {
@ -10,25 +16,6 @@ export default function createChatSearchParams(
const params = new URLSearchParams(); const params = new URLSearchParams();
const allowedParams = [
'endpoint',
'model',
'temperature',
'presence_penalty',
'frequency_penalty',
'stop',
'top_p',
'max_tokens',
'topP',
'topK',
'maxOutputTokens',
'promptCache',
'region',
'maxTokens',
'agent_id',
'assistant_id',
];
if (input && typeof input === 'object' && !('endpoint' in input) && !('model' in input)) { if (input && typeof input === 'object' && !('endpoint' in input) && !('model' in input)) {
Object.entries(input as Record<string, string>).forEach(([key, value]) => { Object.entries(input as Record<string, string>).forEach(([key, value]) => {
if (value != null && allowedParams.includes(key)) { if (value != null && allowedParams.includes(key)) {
@ -64,20 +51,15 @@ export default function createChatSearchParams(
params.set('model', conversation.model); params.set('model', conversation.model);
} }
const paramMap = { const paramMap: Record<string, any> = {};
temperature: conversation.temperature, allowedParams.forEach((key) => {
presence_penalty: conversation.presence_penalty, if (key === 'agent_id' && conversation.agent_id === Constants.EPHEMERAL_AGENT_ID) {
frequency_penalty: conversation.frequency_penalty, return;
stop: conversation.stop, }
top_p: conversation.top_p, if (key !== 'endpoint' && key !== 'model') {
max_tokens: conversation.max_tokens, paramMap[key] = (conversation as any)[key];
topP: conversation.topP, }
topK: conversation.topK, });
maxOutputTokens: conversation.maxOutputTokens,
promptCache: conversation.promptCache,
region: conversation.region,
maxTokens: conversation.maxTokens,
};
return Object.entries(paramMap).reduce((params, [key, value]) => { return Object.entries(paramMap).reduce((params, [key, value]) => {
if (value != null) { if (value != null) {

2
package-lock.json generated
View file

@ -45306,7 +45306,7 @@
}, },
"packages/data-provider": { "packages/data-provider": {
"name": "librechat-data-provider", "name": "librechat-data-provider",
"version": "0.7.82", "version": "0.7.83",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.8.2", "axios": "^1.8.2",

View file

@ -1,6 +1,6 @@
{ {
"name": "librechat-data-provider", "name": "librechat-data-provider",
"version": "0.7.82", "version": "0.7.83",
"description": "data services for librechat apps", "description": "data services for librechat apps",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.es.js", "module": "dist/index.es.js",

View file

@ -52,6 +52,7 @@ export const excludedKeys = new Set([
'model', 'model',
'files', 'files',
'spec', 'spec',
'disableParams',
]); ]);
export enum SettingsViews { export enum SettingsViews {

View file

@ -358,7 +358,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
// continue; // continue;
} }
setting.includeInput = setting.includeInput =
setting.type === SettingTypes.Number ? setting.includeInput ?? true : false; // Default to true if type is number setting.type === SettingTypes.Number ? (setting.includeInput ?? true) : false; // Default to true if type is number
} }
if (setting.component === ComponentTypes.Slider && setting.type === SettingTypes.Number) { if (setting.component === ComponentTypes.Slider && setting.type === SettingTypes.Number) {
@ -445,7 +445,8 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
// Validate optionType and conversation schema // Validate optionType and conversation schema
if (setting.optionType !== OptionTypes.Custom) { if (setting.optionType !== OptionTypes.Custom) {
const conversationSchema = tConversationSchema.shape[setting.key as keyof TConversation]; const conversationSchema =
tConversationSchema.shape[setting.key as keyof Omit<TConversation, 'disableParams'>];
if (!conversationSchema) { if (!conversationSchema) {
errors.push({ errors.push({
code: ZodIssueCode.custom, code: ZodIssueCode.custom,

View file

@ -745,6 +745,7 @@ export type TSetOption = (
export type TConversation = z.infer<typeof tConversationSchema> & { export type TConversation = z.infer<typeof tConversationSchema> & {
presetOverride?: Partial<TPreset>; presetOverride?: Partial<TPreset>;
disableParams?: boolean;
}; };
export const tSharedLinkSchema = z.object({ export const tSharedLinkSchema = z.object({