mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-25 17:16:33 +01:00
🏷️ fix: Clear ModelSpec Display Fields When Navigating via Agent Share Link (#12274)
- Extract `specDisplayFieldReset` constant and `mergeQuerySettingsWithSpec` utility to `client/src/utils/endpoints.ts` as a single source of truth for spec display fields that must be cleared on non-spec transitions. - Clear `spec`, `iconURL`, `modelLabel`, and `greeting` from the merged preset in `ChatRoute.getNewConvoPreset()` when URL query parameters override the conversation without explicitly setting a spec. - Also clear `greeting` in the parallel cleanup in `useQueryParams.newQueryConvo` using the shared `specDisplayFieldReset` constant. - Guard the field reset on `specPreset != null` so null values aren't injected when no spec is configured. - Add comprehensive test coverage for the merge-and-clear logic.
This commit is contained in:
parent
9a64791e3e
commit
0c378811f1
4 changed files with 195 additions and 19 deletions
|
|
@ -12,6 +12,7 @@ import type {
|
|||
import {
|
||||
clearModelForNonEphemeralAgent,
|
||||
removeUnavailableTools,
|
||||
specDisplayFieldReset,
|
||||
processValidSettings,
|
||||
getModelSpecIconURL,
|
||||
getConvoSwitchLogic,
|
||||
|
|
@ -128,13 +129,10 @@ export default function useQueryParams({
|
|||
endpointsConfig,
|
||||
});
|
||||
|
||||
let resetParams = {};
|
||||
const resetFields = newPreset.spec == null ? specDisplayFieldReset : {};
|
||||
if (newPreset.spec == null) {
|
||||
template.spec = null;
|
||||
template.iconURL = null;
|
||||
template.modelLabel = null;
|
||||
resetParams = { spec: null, iconURL: null, modelLabel: null };
|
||||
newPreset = { ...newPreset, ...resetParams };
|
||||
Object.assign(template, specDisplayFieldReset);
|
||||
newPreset = { ...newPreset, ...specDisplayFieldReset };
|
||||
}
|
||||
|
||||
// Sync agent_id from newPreset to template, then clear model if non-ephemeral agent
|
||||
|
|
@ -152,7 +150,7 @@ export default function useQueryParams({
|
|||
conversation: {
|
||||
...(conversation ?? {}),
|
||||
endpointType: template.endpointType,
|
||||
...resetParams,
|
||||
...resetFields,
|
||||
},
|
||||
preset: template,
|
||||
cleanOutput: newPreset.spec != null && newPreset.spec !== '',
|
||||
|
|
|
|||
|
|
@ -6,20 +6,21 @@ import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
|||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
import {
|
||||
useNewConvo,
|
||||
useAppStartup,
|
||||
mergeQuerySettingsWithSpec,
|
||||
processValidSettings,
|
||||
getDefaultModelSpec,
|
||||
getModelSpecPreset,
|
||||
isNotFoundError,
|
||||
logger,
|
||||
} from '~/utils';
|
||||
import {
|
||||
useAssistantListMap,
|
||||
useIdChangeEffect,
|
||||
useAppStartup,
|
||||
useNewConvo,
|
||||
useLocalize,
|
||||
} from '~/hooks';
|
||||
import { useGetConvoIdQuery, useGetStartupConfig, useGetEndpointsQuery } from '~/data-provider';
|
||||
import {
|
||||
getDefaultModelSpec,
|
||||
getModelSpecPreset,
|
||||
processValidSettings,
|
||||
logger,
|
||||
isNotFoundError,
|
||||
} from '~/utils';
|
||||
import { ToolCallsMapProvider } from '~/Providers';
|
||||
import ChatView from '~/components/Chat/ChatView';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
|
|
@ -102,9 +103,10 @@ export default function ChatRoute() {
|
|||
});
|
||||
const querySettings = processValidSettings(queryParams);
|
||||
|
||||
return Object.keys(querySettings).length > 0
|
||||
? { ...specPreset, ...querySettings }
|
||||
: specPreset;
|
||||
if (Object.keys(querySettings).length > 0) {
|
||||
return mergeQuerySettingsWithSpec(specPreset, querySettings);
|
||||
}
|
||||
return specPreset;
|
||||
};
|
||||
|
||||
if (isNewConvo && endpointsQuery.data && modelsQuery.data) {
|
||||
|
|
|
|||
152
client/src/utils/__tests__/mergeQuerySettingsWithSpec.test.ts
Normal file
152
client/src/utils/__tests__/mergeQuerySettingsWithSpec.test.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { TPreset } from 'librechat-data-provider';
|
||||
import { mergeQuerySettingsWithSpec, specDisplayFieldReset } from '../endpoints';
|
||||
|
||||
describe('mergeQuerySettingsWithSpec', () => {
|
||||
const specPreset: TPreset = {
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model: 'gpt-4',
|
||||
spec: 'my-spec',
|
||||
iconURL: 'https://example.com/icon.png',
|
||||
modelLabel: 'My Custom GPT',
|
||||
greeting: 'Hello from the spec!',
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
describe('when specPreset is active and query has no spec', () => {
|
||||
it('clears all spec display fields for agent share links', () => {
|
||||
const querySettings: TPreset = {
|
||||
agent_id: 'agent_123',
|
||||
endpoint: EModelEndpoint.agents,
|
||||
};
|
||||
|
||||
const result = mergeQuerySettingsWithSpec(specPreset, querySettings);
|
||||
|
||||
expect(result.agent_id).toBe('agent_123');
|
||||
expect(result.endpoint).toBe(EModelEndpoint.agents);
|
||||
expect(result.spec).toBeNull();
|
||||
expect(result.iconURL).toBeNull();
|
||||
expect(result.modelLabel).toBeNull();
|
||||
expect(result.greeting).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves non-display settings from the spec base', () => {
|
||||
const querySettings: TPreset = {
|
||||
agent_id: 'agent_123',
|
||||
endpoint: EModelEndpoint.agents,
|
||||
};
|
||||
|
||||
const result = mergeQuerySettingsWithSpec(specPreset, querySettings);
|
||||
|
||||
expect(result.temperature).toBe(0.7);
|
||||
});
|
||||
|
||||
it('clears spec display fields for assistant share links', () => {
|
||||
const querySettings: TPreset = {
|
||||
assistant_id: 'asst_abc',
|
||||
endpoint: EModelEndpoint.assistants,
|
||||
};
|
||||
|
||||
const result = mergeQuerySettingsWithSpec(specPreset, querySettings);
|
||||
|
||||
expect(result.assistant_id).toBe('asst_abc');
|
||||
expect(result.endpoint).toBe(EModelEndpoint.assistants);
|
||||
expect(result.spec).toBeNull();
|
||||
expect(result.iconURL).toBeNull();
|
||||
expect(result.modelLabel).toBeNull();
|
||||
expect(result.greeting).toBeUndefined();
|
||||
});
|
||||
|
||||
it('clears spec display fields for model override links', () => {
|
||||
const querySettings: TPreset = {
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
};
|
||||
|
||||
const result = mergeQuerySettingsWithSpec(specPreset, querySettings);
|
||||
|
||||
expect(result.model).toBe('claude-sonnet-4-20250514');
|
||||
expect(result.endpoint).toBe(EModelEndpoint.anthropic);
|
||||
expect(result.spec).toBeNull();
|
||||
expect(result.iconURL).toBeNull();
|
||||
expect(result.modelLabel).toBeNull();
|
||||
expect(result.greeting).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when query explicitly sets a spec', () => {
|
||||
it('preserves spec display fields from the base', () => {
|
||||
const querySettings = { spec: 'other-spec' } as TPreset;
|
||||
|
||||
const result = mergeQuerySettingsWithSpec(specPreset, querySettings);
|
||||
|
||||
expect(result.spec).toBe('other-spec');
|
||||
expect(result.iconURL).toBe('https://example.com/icon.png');
|
||||
expect(result.modelLabel).toBe('My Custom GPT');
|
||||
expect(result.greeting).toBe('Hello from the spec!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when specPreset is undefined (no spec configured)', () => {
|
||||
it('returns querySettings without injecting null display fields', () => {
|
||||
const querySettings: TPreset = {
|
||||
agent_id: 'agent_123',
|
||||
endpoint: EModelEndpoint.agents,
|
||||
};
|
||||
|
||||
const result = mergeQuerySettingsWithSpec(undefined, querySettings);
|
||||
|
||||
expect(result.agent_id).toBe('agent_123');
|
||||
expect(result.endpoint).toBe(EModelEndpoint.agents);
|
||||
expect(result).not.toHaveProperty('spec');
|
||||
expect(result).not.toHaveProperty('iconURL');
|
||||
expect(result).not.toHaveProperty('modelLabel');
|
||||
expect(result).not.toHaveProperty('greeting');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when querySettings is empty', () => {
|
||||
it('still clears spec display fields (no query params is not an explicit spec)', () => {
|
||||
const result = mergeQuerySettingsWithSpec(specPreset, {} as TPreset);
|
||||
|
||||
expect(result.spec).toBeNull();
|
||||
expect(result.iconURL).toBeNull();
|
||||
expect(result.modelLabel).toBeNull();
|
||||
expect(result.greeting).toBeUndefined();
|
||||
expect(result.endpoint).toBe(EModelEndpoint.openAI);
|
||||
expect(result.model).toBe('gpt-4');
|
||||
expect(result.temperature).toBe(0.7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query settings override spec values', () => {
|
||||
it('overrides endpoint and model from spec', () => {
|
||||
const querySettings: TPreset = {
|
||||
endpoint: EModelEndpoint.anthropic,
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
};
|
||||
|
||||
const result = mergeQuerySettingsWithSpec(specPreset, querySettings);
|
||||
|
||||
expect(result.endpoint).toBe(EModelEndpoint.anthropic);
|
||||
expect(result.model).toBe('claude-sonnet-4-20250514');
|
||||
expect(result.temperature).toBe(0.7);
|
||||
expect(result.spec).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('specDisplayFieldReset', () => {
|
||||
it('contains all spec display fields that need clearing', () => {
|
||||
expect(specDisplayFieldReset).toEqual({
|
||||
spec: null,
|
||||
iconURL: null,
|
||||
modelLabel: null,
|
||||
greeting: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('has exactly 4 fields', () => {
|
||||
expect(Object.keys(specDisplayFieldReset)).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
|
@ -311,6 +311,30 @@ export function getModelSpecPreset(modelSpec?: t.TModelSpec) {
|
|||
};
|
||||
}
|
||||
|
||||
/** Fields set by a model spec that should be cleared when switching to a non-spec conversation. */
|
||||
export const specDisplayFieldReset = {
|
||||
spec: null as string | null,
|
||||
iconURL: null as string | null,
|
||||
modelLabel: null as string | null,
|
||||
greeting: undefined as string | undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges a spec preset base with URL query settings, clearing spec display fields
|
||||
* when the query doesn't explicitly set a spec. Prevents spec contamination on
|
||||
* agent/assistant share links.
|
||||
*/
|
||||
export function mergeQuerySettingsWithSpec(
|
||||
specPreset: t.TPreset | undefined,
|
||||
querySettings: t.TPreset,
|
||||
): t.TPreset {
|
||||
return {
|
||||
...specPreset,
|
||||
...querySettings,
|
||||
...(specPreset != null && querySettings.spec == null ? specDisplayFieldReset : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Gets the default spec iconURL by order or definition.
|
||||
*
|
||||
* First, the admin defined default, then last selected spec, followed by first spec
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue