feat(#4336): add vendor prefix sub-grouping in model picker for OpenRouter and Nous

This commit is contained in:
Rod Boev committed 2026-06-18 15:47:11 -04:00
1 parent 6f233d3a20
commit 7aa0649e3e
3 files changed
+109 -23

No files matched your search

+1
View File
@@ -2519,6 +2519,7 @@
.model-dropdown.open{display:block;}
.model-scope-note{position:sticky;top:0;z-index:2;padding:9px 14px;border-bottom:1px solid var(--border);color:var(--text);font-size:11px;font-weight:650;line-height:1.4;background:color-mix(in srgb,var(--surface) 82%,var(--accent-bg));box-shadow:0 1px 0 rgba(0,0,0,.12);}
.model-group{padding:8px 14px 4px;font-size:10px;font-weight:700;letter-spacing:.04em;color:var(--muted);text-transform:uppercase;border-top:1px solid var(--border2);margin-top:2px;}.model-group.collapsible{cursor:pointer;user-select:none}.model-group.collapsible:before{content:'\25b6';display:inline-block;font-size:8px;margin-right:5px;transition:transform .15s ease}.model-group.collapsible.open:before{transform:rotate(90deg)}.model-group-body:empty{display:none}
.model-group.sub{padding:4px 14px 2px 22px;font-size:9px;border-top:none;margin-top:0;letter-spacing:.03em;color:var(--muted);background:transparent;}.model-group.sub.collapsible:before{content:'\25b6';display:inline-block;font-size:7px;margin-right:4px;transition:transform .15s ease}.model-group.sub.collapsible.open:before{transform:rotate(90deg)}.model-group-body.sub{padding-left:8px;border-left:1px solid var(--border);}
.model-opt{padding:10px 14px;cursor:pointer;transition:background .12s;display:flex;flex-direction:column;gap:3px;align-items:flex-start;}
.model-opt:hover{background:rgba(255,255,255,.07);}
.model-opt.is-highlighted{background:rgba(255,255,255,.1);outline:1px solid var(--border2);outline-offset:-1px;}
+71 -23
View File
@@ -2296,6 +2296,13 @@ function renderModelDropdown(){
}
return _groupMeta.get(groupKey);
};
const _vendorPrefix=(rawId)=>{
const stripped=String(rawId||'').replace(/^@([^:]+:)+/,'');
const slash=stripped.indexOf('/');
return slash>0?stripped.slice(0,slash):'';
};
const SUB_GROUP_PROVIDERS=new Set(['openrouter','nous']);
const SUB_GROUP_MIN_MODELS=8;
for(const child of Array.from(sel.children)){
if(child.tagName==='OPTGROUP'){
const providerId=child.dataset&&child.dataset.provider?child.dataset.provider:'';
@@ -2515,6 +2522,17 @@ function renderModelDropdown(){
const _hit=_modelData.find(m=>m&&!m.endpointErrorOnly&&String(m.value||'')===_selVal);
return _hit?_hit.groupKey:null;
})();
const _makeModelRow=(m,sel,configuredIds)=>{
const row=document.createElement('div');
row.className='model-opt'+(m.value===sel.value?' active':'');
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(m.badge.label||'Configured')}</span>`:'';
const _plainGroup=m.group?String(m.group).replace(/\s*\(\d+\s+of\s+\d+\)\s*$/,''):'';
const _underOwnHeading=shouldRenderHeading&&!!(m.groupKey&&_groupWrappers[m.groupKey]);
const providerChip=(_plainGroup&&!_underOwnHeading)?`<span class="model-opt-provider">${esc(_plainGroup)}</span>`:'';
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(m.name)}</span>${badgeHtml}${providerChip}</div><span class="model-opt-id">${esc(m.id)}</span>`;
row.onclick=()=>selectModelFromDropdown(m.value,m.providerId||(m.badge&&m.badge.provider)||null);
return row;
};
const _filterModels=(term)=>{
term=term.trim().toLowerCase();
const hasSearch=!!term;
@@ -2669,29 +2687,52 @@ function renderModelDropdown(){
heading.classList.toggle('open',closed);
});
}
for(const m of groupRows){
const row=document.createElement('div');
row.className='model-opt'+(m.value===sel.value?' active':'');
const badgeHtml=m.badge?`<span class="model-opt-badge model-opt-badge--${esc(m.badge.role||'configured')}">${esc(m.badge.label||'Configured')}</span>`:'';
// The group heading carries the overflow count ("Provider (15 of 30)"); the
// per-row provider chip must show the PLAIN provider label only — otherwise
// the count is stamped redundantly on every row and reads as nonsense after
// "Show all" expands the group (e.g. row 30 still showing "(15 of 30)"). (#3691)
const _plainGroup=m.group?String(m.group).replace(/\s*\(\d+\s+of\s+\d+\)\s*$/,''):'';
// Only show the per-row provider chip when the row is NOT already under its
// own provider heading — i.e. it's a flat/hoisted row appended straight to
// the dropdown (no group wrapper). Repeating the provider name on every row
// beneath its own "PROVIDER" heading is pure visual noise. The chip still
// orients hoisted/configured rows and search results that render flat.
const _underOwnHeading=shouldRenderHeading&&!!(m.groupKey&&_groupWrappers[m.groupKey]);
const providerChip=(_plainGroup&&!_underOwnHeading)?`<span class="model-opt-provider">${esc(_plainGroup)}</span>`:'';
row.innerHTML=`<div class="model-opt-top"><span class="model-opt-name">${esc(m.name)}</span>${badgeHtml}${providerChip}</div><span class="model-opt-id">${esc(m.id)}</span>`;
row.onclick=()=>selectModelFromDropdown(m.value,m.providerId||(m.badge&&m.badge.provider)||null);
if(m.groupKey&&_groupWrappers[m.groupKey]){
_groupWrappers[m.groupKey].appendChild(row);
}else{
dd.appendChild(row);
const useSubGroups=(
SUB_GROUP_PROVIDERS.has(meta.providerId) &&
groupRows.length>=SUB_GROUP_MIN_MODELS
);
if(useSubGroups){
const byPrefix=new Map();
for(const m of groupRows){
const pfx=_vendorPrefix(m.value)||'other';
if(!byPrefix.has(pfx)) byPrefix.set(pfx,[]);
byPrefix.get(pfx).push(m);
}
const sorted=[...byPrefix.entries()].sort((a,b)=>{
if(a[0]==='other') return 1;
if(b[0]==='other') return -1;
return b[1].length-a[1].length;
});
for(const [pfx,pfxRows] of sorted){
if(pfxRows.length>=2){
const subKey=`${groupKey}::${pfx}`;
if(!(subKey in _groupOpenState)) _groupOpenState[subKey]=true;
if(hasSearch) _groupOpenState[subKey]=true;
const subHeading=document.createElement('div');
subHeading.className='model-group sub collapsible';
subHeading.dataset.group=subKey;
if(_groupOpenState[subKey]) subHeading.classList.add('open');
subHeading.textContent=pfx;
const subWrapper=document.createElement('div');
subWrapper.className='model-group-body sub';
subWrapper.dataset.group=subKey;
if(!_groupOpenState[subKey]) subWrapper.style.display='none';
subHeading.addEventListener('click',(e)=>{
e.stopPropagation();
const closed=subWrapper.style.display==='none';
subWrapper.style.display=closed?'':'none';
_groupOpenState[subKey]=closed;
subHeading.classList.toggle('open',closed);
});
wrapper.appendChild(subHeading);
wrapper.appendChild(subWrapper);
for(const m of pfxRows) subWrapper.appendChild(_makeModelRow(m,sel,configuredIds));
} else {
for(const m of pfxRows) wrapper.appendChild(_makeModelRow(m,sel,configuredIds));
}
}
} else {
for(const m of groupRows) wrapper.appendChild(_makeModelRow(m,sel,configuredIds));
}
if(!term&&hiddenCount){
const showAll=document.createElement('div');
@@ -2733,7 +2774,14 @@ function renderModelDropdown(){
};
_si.addEventListener('input',()=>_filterModels(_si.value));
// Keyboard navigation through filtered model rows (#2791).
const _visibleModelRows=()=>Array.from(dd.querySelectorAll('.model-opt,.model-opt-more')).filter(el=>!el.closest('.model-group-body')||el.closest('.model-group-body').style.display!=='none');
const _visibleModelRows=()=>Array.from(dd.querySelectorAll('.model-opt,.model-opt-more')).filter(el=>{
let node=el.parentElement;
while(node&&node!==dd){
if(node.classList.contains('model-group-body')&&node.style.display==='none') return false;
node=node.parentElement;
}
return true;
});
const _activeRowIndex=(rows)=>rows.findIndex(r=>r.classList.contains('is-highlighted'));
const _highlightRow=(rows,idx)=>{
for(const r of rows) r.classList.remove('is-highlighted');
+37
View File
@@ -0,0 +1,37 @@
from pathlib import Path
import re
ROOT=Path(__file__).resolve().parents[1]
UI=(ROOT/"static"/"ui.js").read_text(encoding="utf-8")
CSS=(ROOT/"static"/"style.css").read_text(encoding="utf-8")
def test_vendor_subgroup_allowlist():
assert "SUB_GROUP_PROVIDERS" in UI
assert "openrouter" in UI
assert "nous" in UI
def test_vendor_prefix_strips_routing_prefix():
# The strip pattern must match the existing normalizer at line 1749
assert "replace(/^@([^:]+:)+/,'')" in UI
assert re.search(r"indexOf\('/'\)|split\('/'\)", UI)
def test_subgroup_keys_use_double_colon_separator():
assert "::" in UI
assert re.search(r"_groupOpenState\[subKey\]", UI)
def test_single_model_vendor_buckets_stay_flat():
# pfxRows.length>=2 gate means single-model prefixes render flat
assert re.search(r"\.length\s*>=\s*2", UI)
def test_visible_rows_walk_all_group_body_ancestors():
visible_match=re.search(r"const _visibleModelRows=\(\)=>[\s\S]+?\}\);", UI)
assert visible_match, "_visibleModelRows definition not found"
visible=visible_match.group(0)
assert "while(" in visible
assert "parentElement" in visible
assert "model-group-body" in visible
assert "closest('.model-group-body')" not in visible
def test_nested_group_css_exists():
assert ".model-group.sub" in CSS
assert ".model-group-body.sub" in CSS