fix(models): stale cross-provider model no longer shows as unavailable in picker (closes #829)

* fix(models): stale cross-provider model no longer shows as unavailable in picker

Two bugs allowed an openai/gpt-5.4-mini stale session model to appear as
'(unavailable)' under a custom provider group for users who never configured
OpenAI (#829).

Backend (api/routes.py): _resolve_compatible_session_model() had a blanket
early-return for active_provider in {custom, openrouter} that skipped all
normalization regardless of whether any catalog group could route the model's
prefix. A custom_providers-only user with a stale openai/... session model
was never corrected. Fixed: only skip normalization when the model prefix is
actually routable (matches a catalog group provider_id, or an openrouter
group is present that can route any provider/model).

Frontend (static/ui.js): renderSession() injected a bare <option> (not in
any <optgroup>) for models not found in the dropdown. renderModelDropdown()
rendered bare options without emitting a group heading, so they visually
inherited the last rendered provider heading — making the stale model appear
to belong to the custom provider group. Fixed: silently reset to the first
available model and fire a PATCH to persist the correction instead of
injecting a misleading (unavailable) option.

5 new tests in test_provider_mismatch.py cover:
- stale openai model cleared when custom_providers-only + no default_model
- stale openai model cleared when custom_providers-only + default_model set
- openrouter model preserved when openrouter group present
- custom/ namespace always preserved
- ui.js no longer injects model_unavailable option

* fix(ui): declare modelSel locally in syncTopbar reset path; fix test assertion

- Use const modelSel=$('modelSelect') instead of undeclared sel in the
  stale-model reset branch of syncTopbar() (caught in Opus review)
- Fix test assertion: or → and for model_unavailable key absence check

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
This commit is contained in:
nesquena-hermes
2026-04-21 22:20:08 -07:00
committed by GitHub
parent 880085a09e
commit 1239129ae2
4 changed files with 190 additions and 13 deletions

View File

@@ -1209,18 +1209,24 @@ function syncTopbar(){
currentModel=modelOverride;
} else {
const applied=_applyModelToDropdown(currentModel,$('modelSelect'));
// If the model isn't in the current provider list, add it as a visually marked
// "(unavailable)" entry so the session value is preserved without misleading the user.
// Selecting it will still attempt to send (same as before), but the label makes
// clear it's a stale model from a previous session.
// If the model isn't in the current provider list, silently reset to the
// first available model so stale values don't pollute the picker (#829).
if(!applied && currentModel){
const opt=document.createElement('option');
opt.value=currentModel;
opt.textContent=getModelLabel(currentModel)+t('model_unavailable');
opt.style.color='var(--muted, #888)';
opt.title=t('model_unavailable_title');
$('modelSelect').appendChild(opt);
$('modelSelect').value=currentModel;
// Stale session model not in the current provider catalog — reset to the
// first available model rather than injecting an "(unavailable)" option
// that visually appears under the wrong provider group (#829).
const modelSel=$('modelSelect');
const first=modelSel&&modelSel.querySelector('optgroup > option, option');
if(first){
modelSel.value=first.value;
S.session.model=first.value;
// Persist the correction so the session doesn't re-inject on next load.
fetch(new URL('api/session/update',location.href).href,{
method:'POST',credentials:'include',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({session_id:S.session.id||S.session.session_id,model:first.value})
}).catch(()=>{});
}
}
}
if(typeof syncModelChip==='function') syncModelChip();