* fix(settings): show live models in default model picker and apply to new chats (#872) Two related bugs: 1. Settings > Preferences > Default Model dropdown only showed static models from /api/models — live-fetched models (e.g. @nous:anthropic/claude-opus-4.7) were missing. Now calls _fetchLiveModels() on the settings picker too. 2. New chats ignored the saved default model preference — they always used the chat-header dropdown value (which reflects the previous session's model). Now newSession() uses the saved default_model and syncs the dropdown. Extracted _addLiveModelsToSelect() from _fetchLiveModels() so cached live models can be applied to any <select> element (chat-header or settings picker). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update live-model prefix tests for _addLiveModelsToSelect extraction The tests searched for og.dataset.provider, _isPortalFetch, and openrouter exclusion patterns inside _fetchLiveModels(). These were extracted into _addLiveModelsToSelect() as part of the #872 fix. Updated regex targets to check _addLiveModelsToSelect first, falling back to _fetchLiveModels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add multi-tab note on window._defaultModel Clarifies that window._defaultModel is per-page-load and not synced across browser tabs, following maintainer feedback on #889. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: CHANGELOG for v0.50.170 * chore: trigger PR refresh after rebase --------- Co-authored-by: fr33m1nd <bergeouss@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
This commit is contained in:
@@ -1431,7 +1431,7 @@ async function loadSettingsPanel(){
|
||||
setLocale(resolvedLanguage);
|
||||
if(typeof applyLocaleToDOM==='function') applyLocaleToDOM();
|
||||
}
|
||||
// Populate model dropdown from /api/models
|
||||
// Populate model dropdown from /api/models + live model fetch (#872)
|
||||
const modelSel=$('settingsModel');
|
||||
if(modelSel){
|
||||
modelSel.innerHTML='';
|
||||
@@ -1441,6 +1441,7 @@ async function loadSettingsPanel(){
|
||||
for(const g of ((models||{}).groups||[])){
|
||||
const og=document.createElement('optgroup');
|
||||
og.label=g.provider;
|
||||
if(g.provider_id) og.dataset.provider=g.provider_id;
|
||||
for(const m of g.models){
|
||||
const opt=document.createElement('option');
|
||||
opt.value=m.id;opt.textContent=m.label;
|
||||
@@ -1448,6 +1449,11 @@ async function loadSettingsPanel(){
|
||||
}
|
||||
modelSel.appendChild(og);
|
||||
}
|
||||
// Append live-fetched models for the active provider, same as the
|
||||
// chat-header dropdown does via _fetchLiveModels() (#872).
|
||||
if(models.active_provider && typeof _fetchLiveModels==='function'){
|
||||
_fetchLiveModels(models.active_provider, modelSel);
|
||||
}
|
||||
}catch(e){}
|
||||
_settingsHermesDefaultModelOnOpen=(models&&models.default_model)||'';
|
||||
modelSel.value=_settingsHermesDefaultModelOnOpen;
|
||||
|
||||
@@ -62,12 +62,23 @@ async function newSession(flash){
|
||||
const switchWs=S._profileSwitchWorkspace;
|
||||
S._profileSwitchWorkspace=null;
|
||||
const inheritWs=switchWs||(S.session?S.session.workspace:null)||(S._profileDefaultWorkspace||null);
|
||||
const data=await api('/api/session/new',{method:'POST',body:JSON.stringify({model:$('modelSelect').value,workspace:inheritWs,profile:S.activeProfile||'default'})});
|
||||
// Use the saved default model for new sessions (#872). The user's saved
|
||||
// default_model (from Settings) takes priority over the chat-header dropdown
|
||||
// value, which reflects the *previous* session's model. Fall back to the
|
||||
// dropdown value only when no default_model is configured.
|
||||
const newModel=window._defaultModel||$('modelSelect').value;
|
||||
const data=await api('/api/session/new',{method:'POST',body:JSON.stringify({model:newModel,workspace:inheritWs,profile:S.activeProfile||'default'})});
|
||||
S.session=data.session;S.messages=data.session.messages||[];
|
||||
S.lastUsage={...(data.session.last_usage||{})};
|
||||
if(flash)S.session._flash=true;
|
||||
localStorage.setItem('hermes-webui-session',S.session.session_id);
|
||||
_setSessionViewedCount(S.session.session_id, S.session.message_count || 0);
|
||||
// Sync chat-header dropdown to the session's model so the UI reflects
|
||||
// the default model the server actually used (#872).
|
||||
if(S.session.model && S.session.model!==$('modelSelect').value && typeof _applyModelToDropdown==='function'){
|
||||
_applyModelToDropdown(S.session.model,$('modelSelect'));
|
||||
if(typeof syncModelChip==='function') syncModelChip();
|
||||
}
|
||||
// Reset per-session visual state: a fresh chat is idle even if another
|
||||
// conversation is still streaming in the background.
|
||||
S.busy=false;
|
||||
|
||||
106
static/ui.js
106
static/ui.js
@@ -84,6 +84,9 @@ async function populateModelDropdown(){
|
||||
if(!data.groups||!data.groups.length) return; // keep HTML defaults
|
||||
// Store active provider globally so the send path can warn on mismatch
|
||||
window._activeProvider=data.active_provider||null;
|
||||
// Store default model so newSession() can apply it (#872).
|
||||
// Per-page-load — not synced across browser tabs.
|
||||
window._defaultModel=data.default_model||null;
|
||||
// Clear existing options
|
||||
sel.innerHTML='';
|
||||
_dynamicModelLabels={};
|
||||
@@ -118,72 +121,61 @@ async function populateModelDropdown(){
|
||||
// Cache so we don't re-fetch on every page load
|
||||
const _liveModelCache={};
|
||||
|
||||
function _addLiveModelsToSelect(provider, models, sel){
|
||||
if(!provider||!models||!models.length||!sel) return 0;
|
||||
const currentVal=sel.value;
|
||||
let providerGroup=null;
|
||||
for(const og of sel.querySelectorAll('optgroup')){
|
||||
if(og.dataset.provider&&og.dataset.provider===provider){
|
||||
providerGroup=og; break;
|
||||
}
|
||||
if(og.label&&og.label.toLowerCase().includes(provider.toLowerCase())){
|
||||
providerGroup=og; break;
|
||||
}
|
||||
}
|
||||
if(!providerGroup){
|
||||
providerGroup=document.createElement('optgroup');
|
||||
providerGroup.label=provider.charAt(0).toUpperCase()+provider.slice(1)+' (live)';
|
||||
sel.appendChild(providerGroup);
|
||||
}
|
||||
const existingIds=new Set([...sel.options].map(o=>o.value));
|
||||
let added=0;
|
||||
const _ap=(window._activeProvider||'').toLowerCase();
|
||||
const _isPortalFetch=_ap && _ap!=='openrouter' && _ap!=='custom' && provider===_ap;
|
||||
for(const m of models){
|
||||
let mid=m.id;
|
||||
if(_isPortalFetch && !mid.startsWith('@')){
|
||||
mid=`@${provider}:${mid}`;
|
||||
}
|
||||
if(existingIds.has(mid)) continue;
|
||||
const opt=document.createElement('option');
|
||||
opt.value=mid;
|
||||
opt.textContent=m.label||m.id;
|
||||
opt.title='Live model — fetched from provider';
|
||||
providerGroup.appendChild(opt);
|
||||
_dynamicModelLabels[mid]=m.label||m.id;
|
||||
added++;
|
||||
}
|
||||
if(added>0 && currentVal) _applyModelToDropdown(currentVal, sel);
|
||||
return added;
|
||||
}
|
||||
|
||||
async function _fetchLiveModels(provider, sel){
|
||||
if(!provider||!sel) return;
|
||||
// Don't fetch for providers where we know it's unsupported or unnecessary
|
||||
// All providers now supported via agent's provider_model_ids() — no exclusions needed
|
||||
if(_liveModelCache[provider]) return; // already fetched this session
|
||||
// Already fetched — apply cached models to this select element (#872)
|
||||
if(_liveModelCache[provider]){
|
||||
const added=_addLiveModelsToSelect(provider,_liveModelCache[provider],sel);
|
||||
if(added>0 && typeof syncModelChip==='function') syncModelChip();
|
||||
return;
|
||||
}
|
||||
try{
|
||||
const url=new URL('api/models/live',location.href);
|
||||
url.searchParams.set('provider',provider);
|
||||
const data=await fetch(url.href,{credentials:'include'}).then(r=>r.json());
|
||||
if(!data.models||!data.models.length) return;
|
||||
_liveModelCache[provider]=data.models;
|
||||
// Remember current selection before rebuilding options
|
||||
const currentVal=sel.value;
|
||||
// Rebuild the optgroup for this provider with live models
|
||||
// Keep other providers' optgroups intact
|
||||
let providerGroup=null;
|
||||
for(const og of sel.querySelectorAll('optgroup')){
|
||||
// Prefer exact data-provider match (set from provider_id in API response)
|
||||
// over substring label match — avoids false positives like 'zai' not matching
|
||||
// 'Z.AI / GLM' and vice versa.
|
||||
if(og.dataset.provider&&og.dataset.provider===provider){
|
||||
providerGroup=og; break;
|
||||
}
|
||||
if(og.label&&og.label.toLowerCase().includes(provider.toLowerCase())){
|
||||
providerGroup=og; break;
|
||||
}
|
||||
}
|
||||
if(!providerGroup){
|
||||
// No existing group — add a new one
|
||||
providerGroup=document.createElement('optgroup');
|
||||
providerGroup.label=provider.charAt(0).toUpperCase()+provider.slice(1)+' (live)';
|
||||
sel.appendChild(providerGroup);
|
||||
}
|
||||
// Rebuild options from live data
|
||||
const existingIds=new Set([...sel.options].map(o=>o.value));
|
||||
let added=0;
|
||||
// Apply @provider: prefix to live-fetched model IDs (mirrors the server-side
|
||||
// behaviour for static lists). Portal providers like Nous return upstream
|
||||
// vendor IDs (e.g. "minimax/minimax-m2.7", "anthropic/claude-opus-4.7") —
|
||||
// without a `@nous:` prefix, `resolve_model_provider()` sees the slash and
|
||||
// mis-routes via OpenRouter → 404. Prefixing with `@${provider}:` makes
|
||||
// the portal hint explicit so routing honours it (#854).
|
||||
//
|
||||
// Scope: only apply the prefix when this fetch is for the active provider
|
||||
// and that provider is a portal (not OpenRouter / custom, which use bare
|
||||
// or cross-namespace IDs natively). Skip IDs that already carry an
|
||||
// `@prefix:` — they've already been disambiguated upstream.
|
||||
const _ap=(window._activeProvider||'').toLowerCase();
|
||||
const _isPortalFetch=_ap && _ap!=='openrouter' && _ap!=='custom' && provider===_ap;
|
||||
for(const m of data.models){
|
||||
let mid=m.id;
|
||||
if(_isPortalFetch && !mid.startsWith('@')){
|
||||
mid=`@${provider}:${mid}`;
|
||||
}
|
||||
if(existingIds.has(mid)) continue; // already shown from static list
|
||||
const opt=document.createElement('option');
|
||||
opt.value=mid;
|
||||
opt.textContent=m.label||m.id;
|
||||
opt.title='Live model — fetched from provider';
|
||||
providerGroup.appendChild(opt);
|
||||
_dynamicModelLabels[mid]=m.label||m.id;
|
||||
added++;
|
||||
}
|
||||
const added=_addLiveModelsToSelect(provider,data.models,sel);
|
||||
if(added>0){
|
||||
// Restore selection
|
||||
if(currentVal) _applyModelToDropdown(currentVal, sel);
|
||||
if(typeof syncModelChip==='function') syncModelChip();
|
||||
console.log('[hermes] Live models loaded for',provider+':',added,'new models added');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user