🏷️ 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:
Danny Avila 2026-03-17 02:12:34 -04:00 committed by GitHub
parent 9a64791e3e
commit 0c378811f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 195 additions and 19 deletions

View file

@ -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 !== '',

View file

@ -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) {

View 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);
});
});

View file

@ -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