From 7aa0649e3edcbd790f4657f8447a3abe1425ab45 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Thu, 18 Jun 2026 15:47:11 -0400 Subject: [PATCH] feat(#4336): add vendor prefix sub-grouping in model picker for OpenRouter and Nous --- static/style.css | 1 + static/ui.js | 94 +++++++++++++++++++++------- tests/test_model_picker_subgroups.py | 37 +++++++++++ 3 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 tests/test_model_picker_subgroups.py diff --git a/static/style.css b/static/style.css index 5de39077..44c15646 100644 --- a/static/style.css +++ b/static/style.css @@ -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;} diff --git a/static/ui.js b/static/ui.js index 4ba64133..8132be27 100644 --- a/static/ui.js +++ b/static/ui.js @@ -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?`${esc(m.badge.label||'Configured')}`:''; + 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)?`${esc(_plainGroup)}`:''; + row.innerHTML=`
${esc(m.name)}${badgeHtml}${providerChip}
${esc(m.id)}`; + 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?`${esc(m.badge.label||'Configured')}`:''; - // 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)?`${esc(_plainGroup)}`:''; - row.innerHTML=`
${esc(m.name)}${badgeHtml}${providerChip}
${esc(m.id)}`; - 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'); diff --git a/tests/test_model_picker_subgroups.py b/tests/test_model_picker_subgroups.py new file mode 100644 index 00000000..94a8bb41 --- /dev/null +++ b/tests/test_model_picker_subgroups.py @@ -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