diff --git a/static/ui.js b/static/ui.js index cab1ec06..46f9b73c 100644 --- a/static/ui.js +++ b/static/ui.js @@ -869,6 +869,24 @@ function _applyModelToDropdown(modelId, sel, preferredProviderId){ } return null; } +function _ensureModelOptionInDropdown(modelId, sel, preferredProviderId){ + if(!modelId||!sel) return null; + const applied=_applyModelToDropdown(modelId,sel,preferredProviderId); + if(applied) return applied; + const opt=document.createElement('option'); + opt.value=modelId; + opt.textContent=typeof getModelLabel==='function'?getModelLabel(modelId):modelId; + opt.dataset.custom='1'; + const provider=preferredProviderId||_providerFromModelValue(modelId)||''; + if(provider) opt.dataset.provider=provider; + sel.appendChild(opt); + sel.value=modelId; + if(sel.id==='modelSelect'){ + if(typeof syncModelChip==='function') syncModelChip(); + _refreshOpenModelDropdown(); + } + return modelId; +} function _modelStateFromAppliedDropdown(sel, modelValue){ const state=(typeof _modelStateForSelect==='function') ? _modelStateForSelect(sel,modelValue) @@ -4813,28 +4831,36 @@ function syncTopbar(){ } } else { const applied=_applyModelToDropdown(currentModel,modelSel,S.session.model_provider||null); - // If the model isn't in the current provider list, reset to the configured - // default rather than silently retaining the previous chat's selection (#1771). + // If the session model is missing from the current provider list, inject + // a session-scoped option instead of displaying the previous/static + // selection. Only fall back if that repair path is unavailable. if(!applied){ - const deferModelCorrection=Boolean(S.session._modelResolutionDeferred); - const missingModelIsRoutable=_providerDefersMissingModelFallback(S.session.model_provider||window._activeProvider||null); - // Also defer if a live model fetch is still in flight — the model may be - // in the list once the fetch completes. Persisting now would corrupt the - // session with the wrong model before live models arrive (#1169). - const liveStillPending=window._activeProvider&&_liveModelFetchPending.has(window._activeProvider); - if(liveStillPending||missingModelIsRoutable){ - // Live fetch in flight — don't touch sel.value or S.session.model yet. - // _addLiveModelsToSelect() will re-apply S.session.model once done (#1169). - // Named custom providers/OpenRouter can also route vendor-prefixed IDs - // outside the static catalog, so preserve the user's explicit choice. + const sessionOption=(typeof _ensureModelOptionInDropdown==='function') + ? _ensureModelOptionInDropdown(currentModel,modelSel,S.session.model_provider||null) + : null; + if(sessionOption){ + currentModel=sessionOption; } else { - const fallback=_applySessionModelFallback(modelSel); - if(fallback&&!deferModelCorrection){ - S.session.model=fallback.model; - S.session.model_provider=fallback.model_provider||null; - currentModel=fallback.model; - // Persist the correction so the session doesn't re-inject on next load. - _persistSessionModelCorrection(fallback.model,S.session.model_provider||null); + const deferModelCorrection=Boolean(S.session._modelResolutionDeferred); + const missingModelIsRoutable=_providerDefersMissingModelFallback(S.session.model_provider||window._activeProvider||null); + // Also defer if a live model fetch is still in flight — the model may be + // in the list once the fetch completes. Persisting now would corrupt the + // session with the wrong model before live models arrive (#1169). + const liveStillPending=window._activeProvider&&_liveModelFetchPending.has(window._activeProvider); + if(liveStillPending||missingModelIsRoutable){ + // Live fetch in flight — don't touch sel.value or S.session.model yet. + // _addLiveModelsToSelect() will re-apply S.session.model once done (#1169). + // Named custom providers/OpenRouter can also route vendor-prefixed IDs + // outside the static catalog, so preserve the user's explicit choice. + } else { + const fallback=_applySessionModelFallback(modelSel); + if(fallback&&!deferModelCorrection){ + S.session.model=fallback.model; + S.session.model_provider=fallback.model_provider||null; + currentModel=fallback.model; + // Persist the correction so the session doesn't re-inject on next load. + _persistSessionModelCorrection(fallback.model,S.session.model_provider||null); + } } } } diff --git a/tests/test_new_chat_default_model_frontend.py b/tests/test_new_chat_default_model_frontend.py index 43e1bd32..267c3105 100644 --- a/tests/test_new_chat_default_model_frontend.py +++ b/tests/test_new_chat_default_model_frontend.py @@ -68,6 +68,27 @@ def test_hard_refresh_hydrates_saved_session_model_before_revealing_model_chip() ) +def test_hard_refresh_injects_missing_active_session_model_option(): + boot_js = Path("static/boot.js").read_text(encoding="utf-8") + marker = "if(!applied&&sessionModelState&&typeof _ensureModelOptionInDropdown==='function')" + assert marker in boot_js + branch = boot_js[boot_js.index(marker) : boot_js.index("else if(!applied&&!sessionModelState", boot_js.index(marker))] + assert "_ensureModelOptionInDropdown(sessionModelState.model,$('modelSelect'),sessionModelState.model_provider||null)" in branch + + +def test_sync_topbar_preserves_missing_session_model_as_dropdown_option(): + ui_js = Path("static/ui.js").read_text(encoding="utf-8") + assert "function _ensureModelOptionInDropdown" in ui_js + sync_topbar = _extract_function(ui_js, "function syncTopbar") + branch_start = sync_topbar.index("const applied=_applyModelToDropdown(currentModel,modelSel,S.session.model_provider||null);") + session_model_branch = sync_topbar[branch_start:] + assert "_ensureModelOptionInDropdown(currentModel,modelSel,S.session.model_provider||null)" in session_model_branch + assert "const fallback=_applySessionModelFallback(modelSel);" in session_model_branch + assert session_model_branch.index("_ensureModelOptionInDropdown(currentModel,modelSel,S.session.model_provider||null)") < session_model_branch.index("const fallback=_applySessionModelFallback(modelSel);"), ( + "active session models missing from the current catalog must be injected before fallback can select the static/default model" + ) + + def test_new_chat_does_not_send_stale_dropdown_model_when_session_has_default_model(): assert "model:S.session.model||$('modelSelect').value" in MESSAGES_JS assert "model_provider:S.session.model_provider||null" in MESSAGES_JS