From 96265f6ae481de1eac781643e56119847b9d1ca5 Mon Sep 17 00:00:00 2001 From: jja881 Date: Sat, 20 Jun 2026 23:18:13 +0800 Subject: [PATCH] fix: preserve cross-provider model selection across conversation turns Two-part fix for the model selector jumping back to the profile default after the first message when a cross-provider model (e.g. a custom relay like custom:glm-free-relay) is selected. Backend (api/routes.py): - _catalog_has_provider() now matches compound provider IDs (e.g. custom:zenmux-relay) whose normalized form (custom) is a raw catalog provider_id. Previously the three existing checks missed normalized-vs-raw comparison for compound hints, causing the catalog lookup to fail and the fallback branch to silently revert the model to the profile default on every non-explicit turn. Frontend (static/messages.js): - The explicit_model_pick marker was consumed after the first send(), so subsequent sends lost the signal and the server repaired the model back to the profile default (#3737 regression). - Now, when the session has a non-default model from a different provider than the profile's active provider, every send() sends explicit_model_pick=true so the server honors the user's choice across the entire conversation. - The pending onchange marker is still consumed after the first send (unchanged behavior); only the explicit_pick inference is broadened. --- api/routes.py | 12 ++++++++++++ static/messages.js | 18 ++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/api/routes.py b/api/routes.py index e8988bf7..ec50dc7c 100644 --- a/api/routes.py +++ b/api/routes.py @@ -3196,6 +3196,18 @@ def _catalog_has_provider( provider_raw in raw_provider_ids or (provider_normalized and provider_normalized in raw_provider_ids) or (provider_normalized and provider_normalized in normalized_provider_ids) + # Compound provider IDs like "custom:zenmux-relay" normalize to + # "custom" which is a raw catalog provider_id. The three checks + # above compare raw-vs-raw and normalized-vs-normalized but miss + # normalized-vs-raw for compound hints. Without this, a + # @custom:relay-name:model selection is silently reverted to the + # profile default on every non-explicit turn because the catalog + # lookup fails and the fallback branch at line ~3932 fires. + or ( + provider_normalized + and ":" in provider_raw + and provider_normalized in raw_provider_ids + ) ) diff --git a/static/messages.js b/static/messages.js index 2369344e..4ae2b75e 100644 --- a/static/messages.js +++ b/static/messages.js @@ -1267,15 +1267,29 @@ async function send(){ const _pendingPick=(typeof _readPendingSessionModel==='function') ? _readPendingSessionModel(activeSid) : null; - const _explicitPick=_pendingPick + const _pendingPickMatch=_pendingPick && _pendingPick.model===_modelState.model && String(_pendingPick.model_provider||'')===String(_modelState.model_provider||''); + // ── Persisted cross-provider pick (#3737 follow-up) ── + // The onchange marker is consumed after the first send, so subsequent sends + // lose explicit_model_pick and the server "repairs" the model back to the + // profile default. When the session has a non-default model from a different + // provider than the profile's active provider, treat every send as explicit + // so the server honors the user's choice across the entire conversation. + const _defaultModel=(typeof window!=='undefined' && window._defaultModel)||''; + const _activeProvider=(typeof window!=='undefined' && window._activeProvider)||null; + const _isCrossProviderPick = _modelState.model + && _modelState.model_provider + && _defaultModel + && _modelState.model !== _defaultModel + && String(_modelState.model_provider||'') !== String(_activeProvider||''); + const _explicitPick = _pendingPickMatch || _isCrossProviderPick; // Consume the pending explicit-pick marker for THIS send only. The marker is // recorded on modelSelect.onchange and intentionally kept (not cleared on // session-update) so it survives the normal pick→update→send flow; clear it here // once read so a later send of an unchanged dropdown isn't treated as an explicit // pick. (#3739/#3737, Codex catch) - if(_explicitPick && typeof _clearPendingSessionModel==='function') _clearPendingSessionModel(activeSid); + if(_pendingPickMatch && typeof _clearPendingSessionModel==='function') _clearPendingSessionModel(activeSid); explicitPickForPostStart=_explicitPick; const startData=await api('/api/chat/start',{method:'POST',body:JSON.stringify({ session_id:activeSid,message:msgText,