diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index d7c51cfecf..de58d54165 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -321,7 +321,7 @@ const useNewConvo = (index = 0) => { }; let preset = _preset; - const result = getDefaultModelSpec(startupConfig); + const result = getDefaultModelSpec(startupConfig, endpointsConfig); const defaultModelSpec = result?.default ?? result?.last ?? result?.softDefault; const shouldApplyModelSpec = result?.softDefault != null @@ -383,6 +383,7 @@ const useNewConvo = (index = 0) => { resetBadges, startupConfig, saveBadgesState, + endpointsConfig, pauseGlobalAudio, switchToConversation, applyModelSpecEffects, diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index eebbc88106..5f55df51dd 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -166,7 +166,7 @@ export default function ChatRoute() { } const getNewConvoPreset = () => { - const result = getDefaultModelSpec(startupConfig); + const result = getDefaultModelSpec(startupConfig, endpointsQuery.data); const spec = result?.default ?? result?.last ?? result?.softDefault; const specPreset = spec ? getModelSpecPreset(spec) : undefined; @@ -212,7 +212,7 @@ export default function ChatRoute() { initialConvoQuery.isError && isNotFoundError(initialConvoQuery.error) ) { - const result = getDefaultModelSpec(startupConfig); + const result = getDefaultModelSpec(startupConfig, endpointsQuery.data); const spec = result?.default ?? result?.last ?? result?.softDefault; showToast({ message: localize('com_ui_conversation_not_found'), diff --git a/client/src/utils/__tests__/getDefaultModelSpec.test.ts b/client/src/utils/__tests__/getDefaultModelSpec.test.ts index 1127399d2b..e2032e06de 100644 --- a/client/src/utils/__tests__/getDefaultModelSpec.test.ts +++ b/client/src/utils/__tests__/getDefaultModelSpec.test.ts @@ -1,5 +1,5 @@ import { EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider'; -import type { TModelSpec, TStartupConfig } from 'librechat-data-provider'; +import type { TModelSpec, TStartupConfig, TEndpointsConfig } from 'librechat-data-provider'; import { getDefaultModelSpec } from '../endpoints'; const createModelSpec = (name: string, overrides: Partial = {}): TModelSpec => @@ -13,17 +13,80 @@ const createModelSpec = (name: string, overrides: Partial = {}): TMo ...overrides, }) as TModelSpec; -const createStartupConfig = (list: TModelSpec[]): TStartupConfig => +const createStartupConfig = ( + list: TModelSpec[], + { + prioritize = true, + modelSelect = true, + addedEndpoints, + }: { prioritize?: boolean; modelSelect?: boolean; addedEndpoints?: string[] } = {}, +): TStartupConfig => ({ interface: { - modelSelect: true, + modelSelect, }, modelSpecs: { - prioritize: true, + prioritize, list, + ...(addedEndpoints ? { addedEndpoints } : {}), }, }) as TStartupConfig; +const fullEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.openAI]: { order: 0 }, + [EModelEndpoint.agents]: { order: 1 }, +}; + +const agentsOnlyEndpointsConfig: TEndpointsConfig = { + [EModelEndpoint.agents]: { order: 0 }, +}; + +const writeLastModel = (endpoint: string, model: string) => { + const stored = JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_MODEL) ?? '{}') as Record< + string, + string + >; + stored[endpoint] = model; + localStorage.setItem(LocalStorageKeys.LAST_MODEL, JSON.stringify(stored)); +}; + +/** Mirrors what the conversation effect persists after a spec preset is applied */ +const persistAppliedSpec = (spec: TModelSpec) => { + localStorage.setItem(LocalStorageKeys.LAST_SPEC, spec.name); + writeLastModel(spec.preset.endpoint as string, spec.preset.model as string); + localStorage.setItem( + `${LocalStorageKeys.LAST_CONVO_SETUP}_0`, + JSON.stringify({ + endpoint: spec.preset.endpoint, + model: spec.preset.model, + spec: spec.name, + }), + ); +}; + +/** Mirrors what the conversation effect persists after the user selects an agent */ +const persistAgentSelection = (agentId: string) => { + localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, agentId); + localStorage.setItem( + `${LocalStorageKeys.LAST_CONVO_SETUP}_0`, + JSON.stringify({ + endpoint: EModelEndpoint.agents, + agent_id: agentId, + model: null, + spec: null, + }), + ); +}; + +/** Mirrors what the conversation effect persists after an ephemeral endpoint → model pick */ +const persistEphemeralSelection = (endpoint: string, model: string) => { + writeLastModel(endpoint, model); + localStorage.setItem( + `${LocalStorageKeys.LAST_CONVO_SETUP}_0`, + JSON.stringify({ endpoint, model, spec: null }), + ); +}; + describe('getDefaultModelSpec', () => { beforeEach(() => { localStorage.clear(); @@ -55,7 +118,7 @@ describe('getDefaultModelSpec', () => { JSON.stringify({ [EModelEndpoint.openAI]: 'gpt-4o' }), ); - const result = getDefaultModelSpec(createStartupConfig([softSpec])); + const result = getDefaultModelSpec(createStartupConfig([softSpec]), fullEndpointsConfig); expect(result).toBeUndefined(); }); @@ -64,7 +127,7 @@ describe('getDefaultModelSpec', () => { const softSpec = createModelSpec('soft-spec', { softDefault: true }); localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, 'agent_123'); - const result = getDefaultModelSpec(createStartupConfig([softSpec])); + const result = getDefaultModelSpec(createStartupConfig([softSpec]), fullEndpointsConfig); expect(result).toBeUndefined(); }); @@ -87,4 +150,243 @@ describe('getDefaultModelSpec', () => { expect(result).toEqual({ default: firstSpec }); }); + + describe('soft default auto-application residue', () => { + const softSpec = createModelSpec('soft-spec', { softDefault: true }); + const otherSpec = createModelSpec('other-spec'); + + it('stays soft after its own application is persisted (prioritized config)', () => { + persistAppliedSpec(softSpec); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec]), + fullEndpointsConfig, + ); + + expect(result).toEqual({ softDefault: softSpec }); + }); + + it('stays soft after its own application is persisted (modelSelect config)', () => { + persistAppliedSpec(softSpec); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { prioritize: false }), + fullEndpointsConfig, + ); + + expect(result).toEqual({ softDefault: softSpec }); + }); + + it('yields to an agent selected after the soft default was applied', () => { + persistAppliedSpec(softSpec); + persistAgentSelection('agent_abc'); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { prioritize: false }), + fullEndpointsConfig, + ); + + expect(result).toBeUndefined(); + }); + + it('yields to an agent selection even with the prioritized config', () => { + persistAppliedSpec(softSpec); + persistAgentSelection('agent_abc'); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec]), + fullEndpointsConfig, + ); + + expect(result).toBeUndefined(); + }); + + it('yields to an ephemeral endpoint → model pick after the soft default was applied', () => { + persistAppliedSpec(softSpec); + persistEphemeralSelection(EModelEndpoint.openAI, 'gpt-4o'); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { prioritize: false }), + fullEndpointsConfig, + ); + + expect(result).toBeUndefined(); + }); + + it('yields to a different spec selected after the soft default was applied', () => { + persistAppliedSpec(softSpec); + persistAppliedSpec(otherSpec); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { prioritize: false }), + fullEndpointsConfig, + ); + + expect(result).toEqual({ last: otherSpec }); + }); + + it('yields to a stored agent even when it matches the soft default agent spec', () => { + const softAgentSpec = createModelSpec('soft-agent-spec', { + softDefault: true, + preset: { endpoint: EModelEndpoint.agents, agent_id: 'agent_soft' }, + } as Partial); + localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, 'agent_soft'); + + const result = getDefaultModelSpec( + createStartupConfig([softAgentSpec], { prioritize: false }), + fullEndpointsConfig, + ); + + expect(result).toBeUndefined(); + }); + + it('yields to a stored model selection matching the soft default preset', () => { + localStorage.setItem( + LocalStorageKeys.LAST_MODEL, + JSON.stringify({ [softSpec.preset.endpoint as string]: softSpec.preset.model }), + ); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { prioritize: false }), + fullEndpointsConfig, + ); + + expect(result).toBeUndefined(); + }); + + it('yields when a different agent is stored than the soft default agent spec', () => { + const softAgentSpec = createModelSpec('soft-agent-spec', { + softDefault: true, + preset: { endpoint: EModelEndpoint.agents, agent_id: 'agent_soft' }, + } as Partial); + localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}0`, 'agent_other'); + + const result = getDefaultModelSpec( + createStartupConfig([softAgentSpec], { prioritize: false }), + fullEndpointsConfig, + ); + + expect(result).toBeUndefined(); + }); + }); + + describe('explicit soft default selection', () => { + const softSpec = createModelSpec('soft-spec', { softDefault: true }); + const otherSpec = createModelSpec('other-spec'); + + it('keeps the soft default selected over older model history (prioritized config)', () => { + persistEphemeralSelection(EModelEndpoint.openAI, 'gpt-4o'); + persistAppliedSpec(softSpec); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec]), + fullEndpointsConfig, + ); + + expect(result).toEqual({ softDefault: softSpec }); + }); + + it('keeps the soft default selected over older model history (modelSelect config)', () => { + persistEphemeralSelection(EModelEndpoint.openAI, 'gpt-4o'); + persistAppliedSpec(softSpec); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { prioritize: false }), + fullEndpointsConfig, + ); + + expect(result).toEqual({ softDefault: softSpec }); + }); + }); + + describe('no ephemeral endpoint → model options (edge case)', () => { + const softSpec = createModelSpec('soft-spec', { softDefault: true }); + const otherSpec = createModelSpec('other-spec'); + + it('applies the soft default despite a stored agent when modelSelect is disabled', () => { + persistAgentSelection('agent_abc'); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { modelSelect: false }), + fullEndpointsConfig, + ); + + expect(result).toEqual({ softDefault: softSpec }); + }); + + it('applies the soft default despite a stored agent when addedEndpoints only includes agents', () => { + persistAgentSelection('agent_abc'); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { + prioritize: false, + addedEndpoints: [EModelEndpoint.agents], + }), + fullEndpointsConfig, + ); + + expect(result).toEqual({ softDefault: softSpec }); + }); + + it('applies the soft default despite a stored agent when agents is the only endpoint', () => { + persistAgentSelection('agent_abc'); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { prioritize: false }), + agentsOnlyEndpointsConfig, + ); + + expect(result).toEqual({ softDefault: softSpec }); + }); + + it('still defers to the last selected spec', () => { + persistAppliedSpec(otherSpec); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { prioritize: false }), + agentsOnlyEndpointsConfig, + ); + + expect(result).toEqual({ last: otherSpec }); + }); + + it('keeps the selection gate when addedEndpoints includes ephemeral endpoints', () => { + persistAgentSelection('agent_abc'); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { + prioritize: false, + addedEndpoints: [EModelEndpoint.agents, EModelEndpoint.openAI], + }), + fullEndpointsConfig, + ); + + expect(result).toBeUndefined(); + }); + + it('keeps the selection gate while the endpoints config has not loaded', () => { + persistAgentSelection('agent_abc'); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { prioritize: false }), + undefined, + ); + + expect(result).toBeUndefined(); + }); + + it('detects an agents-only allow-list before the endpoints config loads', () => { + persistAgentSelection('agent_abc'); + + const result = getDefaultModelSpec( + createStartupConfig([otherSpec, softSpec], { + prioritize: false, + addedEndpoints: [EModelEndpoint.agents], + }), + undefined, + ); + + expect(result).toEqual({ softDefault: softSpec }); + }); + }); }); diff --git a/client/src/utils/endpoints.ts b/client/src/utils/endpoints.ts index 03923c3289..e4cbfc6f46 100644 --- a/client/src/utils/endpoints.ts +++ b/client/src/utils/endpoints.ts @@ -203,8 +203,14 @@ export function hasModelSelection(selection?: Partial | nu ); } -function hasStoredModelSelection(): boolean { - if (hasSelectionValue(localStorage.getItem(LocalStorageKeys.LAST_SPEC))) { +/** + * Whether localStorage holds a model selection the user actually made. + * Only spec entries naming `softDefaultSpec` are residue of the soft default + * itself — selections that merely match its preset still count as user choices. + */ +function hasStoredModelSelection(softDefaultSpec?: t.TModelSpec): boolean { + const lastSpecName = localStorage.getItem(LocalStorageKeys.LAST_SPEC); + if (hasSelectionValue(lastSpecName) && lastSpecName !== softDefaultSpec?.name) { return true; } @@ -222,10 +228,51 @@ function hasStoredModelSelection(): boolean { const lastConversationSetup = parseStoredModelSelection( localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP + '_0'), ); + if (softDefaultSpec && lastConversationSetup?.spec === softDefaultSpec.name) { + return false; + } return hasModelSelection(lastConversationSetup); } +/** + * Whether the selector offers any ephemeral endpoint → model options. + * False when the endpoints menu is hidden (`modelSelect` disabled) or only + * agents/assistants picks remain; an empty endpoints config means not loaded. + */ +function hasEphemeralModelOptions({ + endpointsConfig, + addedEndpoints, + modelSelect, +}: { + endpointsConfig?: t.TEndpointsConfig; + addedEndpoints?: Array; + modelSelect?: boolean; +}): boolean { + if (!modelSelect) { + return false; + } + const included = new Set(addedEndpoints ?? []); + const includesEphemeral = + included.size === 0 || + [...included].some( + (endpoint) => !isAgentsEndpoint(endpoint) && !isAssistantsEndpoint(endpoint), + ); + if (!includesEphemeral) { + return false; + } + if (endpointsConfig == null || Object.keys(endpointsConfig).length === 0) { + return true; + } + return Object.entries(endpointsConfig).some( + ([endpoint, config]) => + config != null && + !isAgentsEndpoint(endpoint) && + !isAssistantsEndpoint(endpoint) && + (included.size === 0 || included.has(endpoint)), + ); +} + /** Get the conditional logic for switching conversations */ export function getConvoSwitchLogic(params: ConversationInitParams): InitiatedTemplateResult { const { conversation, newEndpoint, endpointsConfig, modularChat = false } = params; @@ -358,10 +405,15 @@ export function applyModelSpecEphemeralAgent({ /** * Gets default model spec from config and user preferences. - * Priority: hard admin default → prior user selection → soft first-time default. + * Priority: hard admin default → prior user selection → soft default. + * The soft default yields only to selections the user actually made, and acts + * as the fallback default when no ephemeral endpoint → model options exist. * Legacy first-spec prioritization remains only when no soft default is configured. */ -export function getDefaultModelSpec(startupConfig?: t.TStartupConfig): +export function getDefaultModelSpec( + startupConfig?: t.TStartupConfig, + endpointsConfig?: t.TEndpointsConfig, +): | { default?: t.TModelSpec; last?: t.TModelSpec; @@ -369,39 +421,60 @@ export function getDefaultModelSpec(startupConfig?: t.TStartupConfig): } | undefined { const { modelSpecs, interface: interfaceConfig } = startupConfig ?? {}; - const { list, prioritize } = modelSpecs ?? {}; + const { list, prioritize, addedEndpoints } = modelSpecs ?? {}; if (!list) { return; } const defaultSpec = list?.find((spec) => spec.default); const softDefaultSpec = list?.find((spec) => spec.softDefault); + const lastConversationSetup = parseStoredModelSelection( + localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP + '_0'), + ); + const resolveSoftDefault = (): { softDefault: t.TModelSpec } | undefined => { + if (!softDefaultSpec) { + return; + } + if (lastConversationSetup?.spec === softDefaultSpec.name) { + return { softDefault: softDefaultSpec }; + } + const ephemeralOptions = hasEphemeralModelOptions({ + endpointsConfig, + addedEndpoints, + modelSelect: interfaceConfig?.modelSelect, + }); + if (!ephemeralOptions) { + return { softDefault: softDefaultSpec }; + } + return hasStoredModelSelection(softDefaultSpec) ? undefined : { softDefault: softDefaultSpec }; + }; if (prioritize === true || !interfaceConfig?.modelSelect) { const lastSelectedSpecName = localStorage.getItem(LocalStorageKeys.LAST_SPEC); const lastSelectedSpec = list?.find((spec) => spec.name === lastSelectedSpecName); if (defaultSpec) { return { default: defaultSpec }; } + if (lastSelectedSpec && lastSelectedSpec.name === softDefaultSpec?.name) { + return resolveSoftDefault(); + } if (lastSelectedSpec) { return { last: lastSelectedSpec }; } if (softDefaultSpec) { - return hasStoredModelSelection() ? undefined : { softDefault: softDefaultSpec }; + return resolveSoftDefault(); } return { default: list?.[0] }; } else if (defaultSpec) { return { default: defaultSpec }; } - const lastConversationSetup = parseStoredModelSelection( - localStorage.getItem(LocalStorageKeys.LAST_CONVO_SETUP + '_0'), - ); const lastConversationSpecName = lastConversationSetup?.spec; if (!hasSelectionValue(lastConversationSpecName)) { - if (softDefaultSpec && !hasStoredModelSelection()) { - return { softDefault: softDefaultSpec }; - } - return; + return resolveSoftDefault(); } - return { last: list?.find((spec) => spec.name === lastConversationSpecName) }; + const lastSpec = list?.find((spec) => spec.name === lastConversationSpecName); + if (lastSpec && lastSpec.name === softDefaultSpec?.name) { + return resolveSoftDefault(); + } + return { last: lastSpec }; } export function getModelSpecPreset(modelSpec?: t.TModelSpec) { diff --git a/e2e/config/librechat.e2e.yaml b/e2e/config/librechat.e2e.yaml index 4ed7224f53..a330ca23d7 100644 --- a/e2e/config/librechat.e2e.yaml +++ b/e2e/config/librechat.e2e.yaml @@ -26,9 +26,28 @@ endpoints: titleConvo: false modelDisplayLabel: 'Mock Provider B' + # No model spec mirrors this endpoint's label, so it stays unambiguous in the + # selector and gives e2e tests a real ephemeral endpoint → model option. + - name: 'Mock Provider C' + apiKey: 'e2e-mock-key-c' + baseURL: 'http://127.0.0.1:8889/v1' + models: + default: + - 'mock-model-c' + fetch: false + titleConvo: false + modelDisplayLabel: 'Mock Provider C' + modelSpecs: prioritize: true - enforce: true + # Enforcement would reject sends from the non-spec paths addedEndpoints + # exposes below (buildEndpointOption requires a spec when enforce is true). + enforce: false + # Surfaces the endpoints menu (modelSelect defaults on when addedEndpoints is + # set) limited to entries that don't collide with the spec labels above. + addedEndpoints: + - 'Mock Provider C' + - 'agents' list: - name: 'e2e-mock-provider-a' label: 'Mock Provider A' @@ -58,3 +77,10 @@ modelSpecs: - 'e2e-model-spec-allowed' - 'e2e-model-spec-missing' - 'e2e-model-spec-inaccessible' + + - name: 'e2e-soft-default' + label: 'E2E Soft Default' + softDefault: true + preset: + endpoint: 'Mock Provider A' + model: 'mock-model-a' diff --git a/e2e/specs/mock/soft-default.spec.ts b/e2e/specs/mock/soft-default.spec.ts new file mode 100644 index 0000000000..521e8f54fc --- /dev/null +++ b/e2e/specs/mock/soft-default.spec.ts @@ -0,0 +1,106 @@ +import { expect, test } from '@playwright/test'; +import type { Page } from '@playwright/test'; +import { NEW_CHAT_PATH, getAccessToken, requestJson } from './helpers'; + +/** Label of the `softDefault: true` spec in e2e/config/librechat.e2e.yaml. */ +const SOFT_DEFAULT_LABEL = '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' }; + +const uniqueName = (prefix: string) => `${prefix} ${Date.now()}-${Math.floor(Math.random() * 1e4)}`; + +const modelTrigger = (page: Page) => page.getByRole('button', { name: 'Select a model' }).first(); + +/** Reset selection state so the test starts as a fresh instance (auth stays in cookies). */ +async function startFresh(page: Page) { + await page.goto(NEW_CHAT_PATH, { timeout: 10000 }); + await page.evaluate(() => localStorage.clear()); + await page.goto(NEW_CHAT_PATH, { timeout: 10000 }); +} + +type AgentResponse = { + id: string; + name?: string | null; +}; + +async function createAgent(page: Page, name: string): Promise { + const token = await getAccessToken(page); + return requestJson(page, { + path: '/api/agents', + token, + method: 'POST', + body: { + name, + provider: 'Mock Provider A', + model: 'mock-model-a', + model_parameters: {}, + }, + }); +} + +async function selectAgent(page: Page, agentName: string) { + await modelTrigger(page).click(); + await page.getByRole('option', { name: 'My Agents' }).click(); + await page.getByRole('option', { name: agentName }).click(); + await expect(modelTrigger(page)).toContainText(agentName); +} + +async function selectEphemeralModel(page: Page) { + await modelTrigger(page).click(); + await page.getByRole('option', { name: EPHEMERAL_ENDPOINT.label }).click(); + await page.getByRole('option', { name: EPHEMERAL_ENDPOINT.model, exact: true }).click(); + await expect(modelTrigger(page)).toContainText(EPHEMERAL_ENDPOINT.model); +} + +test.describe('soft default model spec', () => { + test('applies the soft default on a fresh instance and stays applied across reloads', async ({ + page, + }) => { + await startFresh(page); + + await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 }); + + // Its own auto-application must not convert it into a sticky "last" selection + // that would behave differently on the next load. + await page.reload({ timeout: 10000 }); + await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 }); + }); + + test('a previously selected agent outranks the soft default', async ({ page }) => { + test.setTimeout(120000); + await startFresh(page); + await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 }); + + const agentName = uniqueName('E2E Soft Agent'); + await createAgent(page, agentName); + await page.goto(NEW_CHAT_PATH, { timeout: 10000 }); + + await selectAgent(page, agentName); + + await page.reload({ timeout: 10000 }); + await expect(modelTrigger(page)).toContainText(agentName, { timeout: 15000 }); + + await page.getByTestId('new-chat-button').click(); + await expect(page).toHaveURL(/\/c\/new/, { timeout: 15000 }); + await expect(modelTrigger(page)).toContainText(agentName, { timeout: 15000 }); + }); + + test('a previous ephemeral endpoint and model selection outranks the soft default', async ({ + page, + }) => { + test.setTimeout(120000); + await startFresh(page); + await expect(modelTrigger(page)).toContainText(SOFT_DEFAULT_LABEL, { timeout: 15000 }); + + await selectEphemeralModel(page); + + await page.reload({ timeout: 10000 }); + await expect(modelTrigger(page)).toContainText(EPHEMERAL_ENDPOINT.model, { timeout: 15000 }); + await expect(modelTrigger(page)).not.toContainText(SOFT_DEFAULT_LABEL); + + await page.getByTestId('new-chat-button').click(); + await expect(page).toHaveURL(/\/c\/new/, { timeout: 15000 }); + await expect(modelTrigger(page)).toContainText(EPHEMERAL_ENDPOINT.model, { timeout: 15000 }); + }); +});