🧬 fix: Resolve URL-Named Model Spec to Its Full Preset on Cold Load (#14610)

* 🧬 fix: Resolve URL-Named Model Spec to Its Full Preset on Cold Load

* 🧬 fix: Preserve Hidden Spec Names for Server-Side Resolution
This commit is contained in:
Danny Avila 2026-08-03 13:02:31 -04:00 • committed by GitHub
parent 6bbbee7a78
commit f738810c11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 5 deletions

View file

@ -166,10 +166,6 @@ export default function ChatRoute() {
}
const getNewConvoPreset = () => {
const result = getDefaultModelSpec(startupConfig, endpointsQuery.data);
const spec = result?.default ?? result?.last ?? result?.softDefault;
const specPreset = spec ? getModelSpecPreset(spec) : undefined;
const queryParams: Record<string, string> = {};
searchParams.forEach((value, key) => {
if (key !== 'prompt' && key !== 'q' && key !== 'submit' && key !== 'projectId') {
@ -178,6 +174,18 @@ export default function ChatRoute() {
});
const querySettings = processValidSettings(queryParams);
/** A spec named in the URL is an explicit selection: it must resolve to its own
* full preset, or stale last-selection state (endpoint/agent) fills the gaps.
* Names absent from the client config (e.g. `showInMenu: false`) stay in the
* query settings untouched, since they remain resolvable server-side by name. */
const urlSpec = querySettings.spec
? startupConfig?.modelSpecs?.list?.find((spec) => spec.name === querySettings.spec)
: undefined;
const result = urlSpec ? undefined : getDefaultModelSpec(startupConfig, endpointsQuery.data);
const spec = urlSpec ?? result?.default ?? result?.last ?? result?.softDefault;
const specPreset = spec ? getModelSpecPreset(spec) : undefined;
if (Object.keys(querySettings).length > 0) {
return mergeQuerySettingsWithSpec(specPreset, querySettings);
}

View file

@ -1,6 +1,7 @@
import { expect, test } from '@playwright/test';
import type { TStartupConfig } from 'librechat-data-provider';
import type { Page } from '@playwright/test';
import { getPrimaryE2EUser } from '../../setup/users.mock';
import {
NEW_CHAT_PATH,
getAccessToken,
@ -13,6 +14,9 @@ import {
/** Label of the `softDefault: true` spec in e2e/config/librechat.e2e.yaml. */
const SOFT_DEFAULT_LABEL = 'E2E Soft Default';
/** Name (URL identity) of the `softDefault: true` spec in e2e/config/librechat.e2e.yaml. */
const SOFT_DEFAULT_NAME = 'e2e-soft-default';
/** Ephemeral endpoint from e2e/config/librechat.e2e.yaml with no mirroring spec. */
const EPHEMERAL_ENDPOINT = { label: 'Mock Provider C', model: 'mock-model-c' };
@ -32,7 +36,7 @@ type AgentResponse = {
name?: string | null;
};
async function createAgent(page: Page, name: string): Promise<AgentResponse> {
async function createAgent(page: Page, name: string, description?: string): Promise<AgentResponse> {
const token = await getAccessToken(page);
return requestJson<AgentResponse>(page, {
path: '/api/agents',
@ -40,6 +44,7 @@ async function createAgent(page: Page, name: string): Promise<AgentResponse> {
method: 'POST',
body: {
name,
...(description ? { description } : {}),
provider: 'Mock Provider A',
model: 'mock-model-a',
model_parameters: {},
@ -226,4 +231,45 @@ test.describe('soft default model spec', () => {
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
await expect(modelTrigger(page)).not.toHaveText('Select a model');
});
// Regression: two tabs share localStorage — one on the soft default spec, one on a
// configured agent. Refreshing the agent tab stamps the agent as the last setup; a
// cold load of `/c/new?spec=<name>` then carried only the spec NAME while the
// endpoint and agent_id resurfaced from storage, rendering a chimera (spec chip over
// an agent landing/composer). A spec named in the URL must resolve to its full preset.
test('a spec named in the URL wins over a previously selected agent on a cold load', async ({
page,
}) => {
test.setTimeout(120000);
await startFresh(page);
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
const agentName = uniqueName('E2E URL Spec Agent');
const agentDescription = 'Powered by E2E Mock';
await createAgent(page, agentName, agentDescription);
await page.goto(NEW_CHAT_PATH, { timeout: 10000 });
await selectAgent(page, agentName);
// Mirrors refreshing the agent tab: the restored selection is re-stamped as the
// last conversation setup.
await page.reload({ timeout: 10000 });
await expect(modelTrigger(page)).toContainText(agentName, { timeout: 15000 });
await page.goto(`${NEW_CHAT_PATH}?spec=${SOFT_DEFAULT_NAME}`, { timeout: 10000 });
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
const main = page.getByRole('main');
const composer = page.getByRole('textbox', { name: 'Message input' });
const user = getPrimaryE2EUser();
await expect(composer).toHaveAttribute('placeholder', /Mock Provider A/, { timeout: 15000 });
await expect(main).toContainText(user.name, { timeout: 15000 });
await expect(main).not.toContainText(agentName);
await expect(main).not.toContainText(agentDescription);
// The mixed state used to be written back to localStorage; a follow-up cold load
// of a plain New Chat must stay on the spec, not resurrect the agent.
await page.goto(NEW_CHAT_PATH, { timeout: 10000 });
await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 });
await expect(main).not.toContainText(agentName);
});
});