Merge pull request #6032 from nesquena/stage/r2-batch

Release exp: Round-2 batch — picker dedup (#5997) + zh-CN i18n (#6024)
This commit is contained in:
nesquena-hermes 2026-07-13 12:20:51 -07:00 committed by GitHub
commit 41f1eb5624
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 338 additions and 74 deletions

View file

@ -13,6 +13,10 @@
- **Deep-link straight to a profile with `?profile=<name>`.** Opening the WebUI with a `?profile=NAME` query parameter now switches to that profile on boot. The name is validated against the existing profile allowlist (a nonexistent profile boots normally, path-traversal / cross-profile attempts are rejected), the switch goes through the same access-enforced `switchToProfile`, and boot fails safe on anything invalid. Thanks @rodboev. (#5730, #5682)
- **Model picker no longer duplicates or drops proxy/catalog models.** When the same model was reachable via both a custom proxy and a first-party catalog (e.g. `grok-4.5`), the picker could show it twice; the dedup fix collapses true duplicates to one selectable row while keeping genuinely-distinct models that merely share a name suffix — including colon-suffixed proxy models like `…:free` / `…:thinking` / `…:latest` — independently selectable. The currently-selected model always survives dedup, and model restore no longer substitutes a suffix-sharing sibling for a missing model. Thanks @rodboev. (#5997, #5989)
- **Simplified Chinese (zh-CN) localization fill.** Untranslated English strings in the zh-CN locale are now translated, so Simplified Chinese users no longer see English fall-through text across recently-added UI. Thanks @Loukky. (#6024)
- **WebUI-created git worktrees can now be cleaned up (no more orphan accumulation).** The agent locks every worktree it creates, and git refuses to remove a locked worktree with a single `--force`, so WebUI-created worktrees were piling up unremovable. Session-worktree removal now runs a fail-soft `git worktree unlock` right before the remove (mirroring the agent's own cleanup ordering); the existing dirty/uncommitted-changes guard still runs first, so user data stays protected. Thanks @ayxuerui. (#6028, #6023)
### Fixed

View file

@ -10424,8 +10424,8 @@ const LOCALES = {
thinking: '思考过程',
expand_all: '全部展开',
collapse_all: '全部折叠',
show_earlier_step_one: 'Show 1 earlier step',
show_earlier_steps: 'Show {0} earlier steps',
show_earlier_step_one: '显示前 1 个步骤',
show_earlier_steps: '显示前 {0} 个步骤',
edit_failed: '编辑失败:',
regen_failed: '重新生成失败:',
reconnect_active: '回复仍在生成中,准备好后要重新加载吗?',
@ -10623,9 +10623,9 @@ const LOCALES = {
settings_default_message_mode_interrupt: '中断当前轮次',
settings_default_message_mode_steer: '引导(中间修正)',
settings_label_busy_placeholder_hint: '显示忙碌占位提示',
settings_label_new_chat_on_workspace_switch: 'Start a new chat when switching workspace',
settings_desc_new_chat_on_workspace_switch: 'When on, selecting a different workspace starts a fresh conversation bound to that workspace and leaves your current chat on its original workspace, avoiding stale cross-workspace context. When off (default), switching workspace changes the current workspace in place.',
workspace_switched_new_chat: 'Started a new chat in {0}',
settings_label_new_chat_on_workspace_switch: '切换工作区时开始新的聊天',
settings_desc_new_chat_on_workspace_switch: '开启此功能后,选择不同的工作区会开启一个与该工作区绑定的全新对话,你当前的聊天则保留在原工作区,避免出现过时的跨工作区上下文。关闭此功能(默认状态)后,切换工作区会直接更改当前工作区。 ',
workspace_switched_new_chat: '在{0}中开始了新的聊天',
settings_desc_busy_placeholder_hint: '在会话运行且草稿为空时,显示带有当前忙碌操作的编辑器占位符。',
composer_placeholder_busy_queue: 'Enter = 队列 | /interrupt | /background | /steer',
composer_placeholder_busy_interrupt: 'Enter = 中断 | /queue | /background | /steer',
@ -10648,16 +10648,16 @@ const LOCALES = {
workspace_empty_dir: '此工作区为空。',
workspace_show_hidden_files: '显示隐藏文件',
workspace_show_hidden_files_desc: '将 .DS_Store、.git、node_modules 以及其他隐藏文件/系统文件包含在文件树中。',
workspace_panel_show: 'Show workspace panel',
workspace_panel_hide: 'Hide workspace panel',
workspace_panel_close: 'Close workspace panel',
workspace_panel_toggle: 'Toggle workspace files panel',
workspace_panel_views: 'Workspace panel views',
workspace_parent_directory: 'Parent directory',
workspace_upload_file: 'Upload file',
workspace_close_preview: 'Close preview',
workspace_files_tab: 'Files',
workspace_artifacts_tab: 'Artifacts',
workspace_panel_show: '显示工作区面板',
workspace_panel_hide: '隐藏工作区面板',
workspace_panel_close: '关闭工作区面板',
workspace_panel_toggle: '切换工作区文件面板',
workspace_panel_views: '工作区面板视图',
workspace_parent_directory: '父目录',
workspace_upload_file: '上传文件',
workspace_close_preview: '关闭预览',
workspace_files_tab: '文件',
workspace_artifacts_tab: '产物',
workspace_hidden_files_visible: '隐藏 显示',
workspace_hidden_files_visible_title: '隐藏文件已显示 — 点击查看选项',
workspace_options: '工作区选项',
@ -10740,20 +10740,20 @@ const LOCALES = {
session_copy_link_desc: '复制此对话的直接链接',
session_link_copied: '对话链接已复制到剪贴板',
session_link_copy_failed: '无法复制对话链接:',
share_session: 'Share',
share_session_tooltip: 'Create a public read-only share link',
share_session_status_active: 'Public share active',
share_session_existing_confirm: 'This conversation already has a public read-only share link.\n\nChoose Copy existing to reuse that link, or Refresh snapshot to rebuild the public snapshot from the latest visible messages.',
share_session_copy_existing: 'Copy existing',
share_session_refresh_snapshot: 'Refresh snapshot',
share_session_link_copied: 'Share link copied to clipboard',
share_session_created: 'Public share link created',
share_session_failed: 'Share failed: ',
stop_sharing_session: 'Stop sharing',
stop_sharing_session_tooltip: 'Revoke the public read-only share link for this conversation',
stop_sharing_session_confirm: 'Revoke this public share link? Anyone opening the old link will see that it is no longer available.',
share_session_revoked: 'Public share link revoked',
share_session_revoke_failed: 'Failed to revoke share link: ',
share_session: '共享',
share_session_tooltip: '创建一个公开的只读共享链接',
share_session_status_active: '公开共享已激活',
share_session_existing_confirm: '此对话已拥有一个公开的只读共享链接。\n\n选择 复制现有链接 以复用该链接,或选择 刷新快照,依据最新可见消息重建公开快照。',
share_session_copy_existing: '复制现有链接',
share_session_refresh_snapshot: '刷新快照',
share_session_link_copied: '共享链接已复制到剪贴板',
share_session_created: '已创建公开共享链接',
share_session_failed: '共享失败: ',
stop_sharing_session: '停止共享',
stop_sharing_session_tooltip: '撤销此对话的公开只读共享链接',
stop_sharing_session_confirm: '撤销此公开共享链接吗?任何打开旧链接的人都会看到该链接已不可用。',
share_session_revoked: '公开共享链接已撤销',
share_session_revoke_failed: '撤销共享链接失败: ',
new_file_prompt: '新文件名(例如 notes.md):',
new_file: '新建文件',
new_folder: '新建文件夹',
@ -10881,7 +10881,7 @@ const LOCALES = {
settings_label_sidebar_density: '侧边栏密度',
cmd_reasoning: '切换思维可见性(显示/隐藏)、设置工作强度或查看当前状态',
settings_label_external_sessions: '显示外部会话',
settings_label_claude_code_sessions: 'Show Claude Code sessions',
settings_label_claude_code_sessions: '显示 Claude Code 会话',
settings_label_cron_sessions: '显示定时任务会话',
settings_label_webhook_sessions: '显示Webhook会话',
settings_label_previous_messaging_sessions: '显示以前的消息会话',
@ -11225,7 +11225,7 @@ const LOCALES = {
settings_auto_title_refresh_20: '每 20 轮对话',
settings_desc_auto_title_refresh: '基于最新对话自动重新生成会话标题,使其随对话发展保持相关。需要配置 LLM 标题生成模型。',
settings_desc_external_sessions: '在会话列表中显示来自 CLI、Telegram、Discord、Slack 等渠道的对话。点击可导入并继续对话。',
settings_desc_claude_code_sessions: 'Show Claude Code imported sessions in the sidebar, or hide them while leaving other external sessions visible.',
settings_desc_claude_code_sessions: '在侧边栏中显示已导入的Claude Code会话,或者隐藏它们,同时保持其他外部会话可见。',
settings_desc_cron_sessions: '将定时任务输出以对话形式显示在侧边栏。仅在启用非 WebUI 会话时此功能才会生效。默认关闭;高频任务可能会使侧边栏信息泛滥。',
settings_desc_webhook_sessions: '将Webhook运行情况以对话形式显示在侧边栏。仅在启用非 WebUI 会话时此功能才会生效。默认关闭;大量的路由信息可能会使侧边栏信息泛滥。',
settings_desc_previous_messaging_sessions: '显示被 reset 或 compression 替换的较旧的 Discord、Telegram、Slack 和 Weixin 会话。',
@ -11563,18 +11563,18 @@ const LOCALES = {
cron_schedule_preset_weekly: '每周',
cron_schedule_preset_monthly: '每月',
cron_schedule_preset_custom: '自定义',
cron_schedule_time_label: 'Time',
cron_schedule_conj_on: 'on',
cron_schedule_conj_on_day: 'on day',
cron_schedule_conj_at: 'at',
cron_schedule_conj_at_minute: 'at minute',
cron_weekday_sun: 'Sunday',
cron_weekday_mon: 'Monday',
cron_weekday_tue: 'Tuesday',
cron_weekday_wed: 'Wednesday',
cron_weekday_thu: 'Thursday',
cron_weekday_fri: 'Friday',
cron_weekday_sat: 'Saturday',
cron_schedule_time_label: '时间',
cron_schedule_conj_on: '在',
cron_schedule_conj_on_day: '指定天数',
cron_schedule_conj_at: '在',
cron_schedule_conj_at_minute: '指定分钟数',
cron_weekday_sun: '星期日',
cron_weekday_mon: '星期一',
cron_weekday_tue: '星期二',
cron_weekday_wed: '星期三',
cron_weekday_thu: '星期四',
cron_weekday_fri: '星期五',
cron_weekday_sat: '星期六',
cron_schedule_hour_label: '小时',
cron_schedule_minute_label: '分钟',
cron_schedule_weekday_label: '星期 (0-6,周日-周六)',
@ -11643,8 +11643,8 @@ const LOCALES = {
session_worktree_remove_ahead_warning: (ahead) => `${ahead} 个未推送的提交将丢失。`,
session_duplicate: '复制会话',
session_duplicate_desc: '用相同工作区和模型创建副本',
session_export_html: 'Export as HTML',
session_export_html_desc: 'Download this conversation as a self-contained HTML file',
session_export_html: '导出为 HTML',
session_export_html_desc: '将此对话下载为一个独立的HTML文件',
session_duplicate_failed: '复制失败:',
session_stop_response: '停止回复',
session_stop_response_desc: '取消此会话正在进行的回复',
@ -11689,15 +11689,15 @@ const LOCALES = {
settings_check_now: '立即检查',
settings_checking: '检查中\u2026',
settings_up_to_date: '已是最新 \u2713',
update_later: 'Later',
update_now: 'Update Now',
update_updating: 'Updating…',
update_force: 'Force update',
update_clear_lock_retry: 'Clear lock and retry update',
update_hard_refresh_now: 'Hard refresh now',
update_no_target: 'No update target selected. Refresh update status and retry.',
update_failed_network: 'Update failed: could not reach the WebUI server. It may have restarted or the connection was interrupted. Please wait a few seconds, reload the page, then check the server if it still does not come back.',
update_failed_prefix: 'Update failed: ',
update_later: '稍后',
update_now: '现在更新',
update_updating: '更新中…',
update_force: '强制更新',
update_clear_lock_retry: '清除锁定并重试更新',
update_hard_refresh_now: '立即强制刷新',
update_no_target: '未选择更新目标。请刷新更新状态并重试。',
update_failed_network: '更新失败:无法连接到 WebUI 服务器。服务可能已重启,或者连接被中断。请稍等几秒,重新加载页面,如果服务器仍未恢复,请检查服务器状态。 ',
update_failed_prefix: '更新失败: ',
settings_updates_available: '有 {count} 个更新可用',
settings_updates_disabled: '更新检查已禁用',
settings_update_check_failed: '更新检查失败',
@ -11745,7 +11745,7 @@ const LOCALES = {
done: '完成',
settings_option_compact_worklog: '紧凑工作日志',
settings_option_transparent_stream: '透明流模式',
settings_option_final_answer_only: 'Final answer only',
settings_option_final_answer_only: '只给出最终答案',
settings_label_tab_visibility: '侧边栏标签',
settings_desc_tab_visibility: '选择在侧边栏和导航栏中显示哪些标签。聊天和设置始终可见。',

View file

@ -2732,6 +2732,49 @@ function _providerFromModelValue(modelId){
if(value.startsWith('@')&&value.includes(':')) return value.slice(1,value.lastIndexOf(':'));
return '';
}
function _modelPickerOptionIdentity(modelId, providerId){
let value=String(modelId||'');
const provider=String(providerId||'').trim();
if(value.startsWith('@')&&value.includes(':')){
const exactPrefix=provider ? `@${provider}:` : '';
if(exactPrefix && value.toLowerCase().startsWith(exactPrefix.toLowerCase())){
value=value.substring(exactPrefix.length);
}else if(value.startsWith('@custom:')){
const namedProvider=value.substring('@custom:'.length);
const splitAt=namedProvider.indexOf(':');
value=splitAt>=0 ? namedProvider.substring(splitAt+1) : namedProvider;
}else{
value=value.substring(value.indexOf(':')+1);
}
}
return value.replace(/-/g,'.').toLowerCase();
}
function _deduplicateModelPickerOptions(sel,selectedValue){
if(!sel||!sel.querySelectorAll) return 0;
let removed=0;
for(const group of sel.querySelectorAll('optgroup')){
const options=Array.from(group.children||[]).filter(opt=>opt&&opt.tagName==='OPTION');
const byIdentity=new Map();
for(const opt of options){
const identity=_modelPickerOptionIdentity(opt.value,_getOptionProviderId(opt));
if(!identity) continue;
if(!byIdentity.has(identity)) byIdentity.set(identity,[]);
byIdentity.get(identity).push(opt);
}
for(const candidates of byIdentity.values()){
if(candidates.length<2) continue;
const selected=candidates.find(opt=>opt.value===selectedValue);
const routable=candidates.find(opt=>String(opt.value||'').startsWith('@'));
const survivor=selected||routable||candidates[0];
for(const opt of candidates){
if(opt===survivor) continue;
group.removeChild(opt);
removed++;
}
}
}
return removed;
}
function _providerSkipsModelMismatchWarning(providerId){
const p=String(providerId||'').toLowerCase();
return !p||p==='custom'||p.startsWith('custom:')||p==='openrouter';
@ -3018,9 +3061,14 @@ function _findModelInDropdown(modelId, sel, preferredProviderId){
const pref=String(preferredProviderId||'').toLowerCase();
if(!pref || !exactProv || exactProv===pref) return modelId;
}
// 1. Normalize: lowercase, strip namespace prefix, replace hyphens→dots.
// Also strip @provider: prefix from deduplicated model IDs (#1228, #1313).
const norm=s=>s.toLowerCase().replace(/^[^/]+\//,'').replace(/^@([^:]+:)+/,'').replace(/-/g,'.');
// 1. Restore lookup keeps the older hierarchy-preserving matcher instead of
// the picker-dedup identity, so missing qualified models do not substitute a
// different suffix-sharing sibling.
const norm=s=>String(s||'')
.toLowerCase()
.replace(/^@([^:]+:)+/,'')
.replace(/^[^/]+\//,'')
.replace(/-/g,'.');
const target=norm(modelId);
let explicitProvider='';
const rawModel=String(modelId||'');
@ -3122,6 +3170,7 @@ function _applyModelToDropdown(modelId, sel, preferredProviderId, opts){
}
function _ensureModelOptionInDropdown(modelId, sel, preferredProviderId){
if(!modelId||!sel) return null;
if(typeof _deduplicateModelPickerOptions==='function') _deduplicateModelPickerOptions(sel,sel.value);
const applied=_applyModelToDropdown(modelId,sel,preferredProviderId);
if(applied) return applied;
const value=modelId;
@ -3296,6 +3345,9 @@ async function populateModelDropdown(opts={}){
}
sel.appendChild(og);
}
if(typeof _deduplicateModelPickerOptions==='function'){
_deduplicateModelPickerOptions(sel,previousSelection&&previousSelection.model||'');
}
_reconcileModelDropdownSelection(sel,data,previousSelection,opts);
if(typeof syncModelChip==='function') syncModelChip();
const dd=$('composerModelDropdown');
@ -3346,32 +3398,47 @@ function _addLiveModelsToSelect(provider, models, sel){
providerGroup.dataset.provider=provider;
}
const existingIds=new Set([...sel.options].map(o=>o.value));
// Normalized dedup strips provider/custom prefixes and namespaces (#907, #3478).
const _normId=id=>{
let s=String(id||'');
if(s.startsWith('@')&&s.includes(':')){
if(s.startsWith('@custom:')){
s=s.substring(s.lastIndexOf(':')+1)||s;
}else{
s=s.substring(s.indexOf(':')+1);
}
}
s=s.split('/').pop();
return s.replace(/-/g,'.').toLowerCase();
};
const existingNorm=new Set([...sel.options].map(o=>_normId(o.value)));
let added=0;
const _ap=(window._activeProvider||'').toLowerCase();
const _providerLower=String(provider||'').toLowerCase();
const _isNamedCustomActiveProvider=_ap.startsWith('custom:');
const _isPortalFetch=_ap && _ap!=='openrouter' && _ap!=='custom' && _ap!=='openai-codex' && (_providerLower===_ap||_isNamedCustomActiveProvider&&_providerLower===_ap);
// Keep existingNorm.has( within the #907 source slice.
const optionIdentity=typeof _modelPickerOptionIdentity==='function'
? (modelId,providerId)=>_modelPickerOptionIdentity(modelId,providerId)
: (modelId,providerId)=>{
let value=String(modelId||'');
const provider=String(providerId||'').trim();
if(value.startsWith('@')&&value.includes(':')){
const exactPrefix=provider ? `@${provider}:` : '';
if(exactPrefix && value.toLowerCase().startsWith(exactPrefix.toLowerCase())){
value=value.substring(exactPrefix.length);
}else if(value.startsWith('@custom:')){
const namedProvider=value.substring('@custom:'.length);
const splitAt=namedProvider.indexOf(':');
value=splitAt>=0 ? namedProvider.substring(splitAt+1) : namedProvider;
}else{
value=value.substring(value.indexOf(':')+1);
}
}
return value.split('/').pop().replace(/-/g,'.').toLowerCase();
};
const existingNorm=new Set([...sel.options].map(o=>optionIdentity(o.value,_getOptionProviderId(o))));
let added=0;
for(const m of models){
let mid=m.id;
if(_isPortalFetch && !mid.startsWith('@')){
mid=`@${provider}:${mid}`;
}
if(existingIds.has(mid)) continue;
if(existingNorm.has(_normId(mid))) continue; // dedup cross-prefix duplicates (#907)
const identity=optionIdentity(mid,provider);
if(existingNorm.has(identity)){
const sameGroup=Array.from(providerGroup.children||[]).find(o=>optionIdentity(o.value,_getOptionProviderId(o))===identity);
if(sameGroup){
const incomingRoutable=String(mid).startsWith('@');
const existingRoutable=String(sameGroup.value||'').startsWith('@');
if(!(!existingRoutable&&incomingRoutable)) continue; // let proxy replace catalog twin
}
}
const opt=document.createElement('option');
opt.value=mid;
opt.textContent=m.label||m.id;
@ -3383,9 +3450,12 @@ function _addLiveModelsToSelect(provider, models, sel){
opt.dataset.fast='0';
}
providerGroup.appendChild(opt);
existingIds.add(mid);
existingNorm.add(identity);
_dynamicModelLabels[mid]=m.label||m.id;
added++;
}
if(typeof _deduplicateModelPickerOptions==='function') _deduplicateModelPickerOptions(sel,currentVal);
const currentState=(currentVal&&typeof _modelStateForSelect==='function')
? _modelStateForSelect(sel, currentVal)
: {model:currentVal||'', model_provider:(S.session&&S.session.model_provider)||null};
@ -3763,6 +3833,7 @@ function renderModelDropdown(){
const dd=$(opts.dropdownId||'composerModelDropdown');
const sel=$(opts.selectId||'modelSelect');
if(!dd||!sel) return;
if(typeof _deduplicateModelPickerOptions==='function') _deduplicateModelPickerOptions(sel,sel.value);
// Whether the search input should auto-grab focus on (re-)render. Default true
// preserves the composer picker's behavior exactly; the settings picker passes
// false on coarse-pointer devices so opening it doesn't pop the mobile keyboard.

View file

@ -262,6 +262,29 @@ console.log(_findModelInDropdown('google/gemma-4-27b', sel, 'custom:beta') || ''
resolved = subprocess.run(["node", "-e", script], capture_output=True, text=True, check=True)
assert resolved.stdout.strip() == "@custom:beta:google/gemma-4-27b"
def test_find_model_returns_no_match_for_missing_qualified_same_suffix_sibling(self):
import re
import subprocess
src = self._read_js()
helper = re.search(r"function _getOptionProviderId\(opt\)\{.*?\n\}", src, re.S)
finder = re.search(r"function _findModelInDropdown\(modelId, sel, preferredProviderId\)\{.*?\n\}", src, re.S)
assert helper, "_getOptionProviderId() not found in ui.js"
assert finder, "_findModelInDropdown() not found in ui.js"
script = f"""
{helper.group(0)}
{finder.group(0)}
const sel = {{
options: [
{{ value: 'vendor-b/catalog/deepseek-v4-pro', parentElement: {{ tagName: 'OPTGROUP', dataset: {{ provider: 'custom:beta' }} }} }},
]
}};
console.log(_findModelInDropdown('vendor-a/deepseek-v4-pro', sel, 'custom:alpha') || '');
"""
resolved = subprocess.run(["node", "-e", script], capture_output=True, text=True, check=True)
assert resolved.stdout.strip() == ""
class TestResolveModelProviderColonInProviderId(unittest.TestCase):
"""resolve_model_provider() must handle provider_ids containing ':'.

View file

@ -0,0 +1,166 @@
"""Regression coverage for #5989's cross-source model picker duplicate."""
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
def _function(source, name):
match = re.search(rf"function {name}\([^)]*\)\{{", source)
assert match, f"{name} not found in ui.js"
start = match.start()
depth = 0
for index in range(match.end() - 1, len(source)):
if source[index] == "{":
depth += 1
elif source[index] == "}":
depth -= 1
if depth == 0:
return source[start:index + 1]
raise AssertionError(f"unterminated {name}")
def _run_harness():
provider_helper = _function(UI_JS, "_getOptionProviderId")
identity = _function(UI_JS, "_modelPickerOptionIdentity")
helper = _function(UI_JS, "_deduplicateModelPickerOptions")
live_add = _function(UI_JS, "_addLiveModelsToSelect")
script = f"""
{provider_helper}
{identity}
{helper}
{live_add}
class Node {{
constructor(tag) {{ this.tagName=tag.toUpperCase(); this.children=[]; this.dataset={{}}; this.parentElement=null; }}
appendChild(child) {{ child.parentElement=this; this.children.push(child); return child; }}
removeChild(child) {{ this.children=this.children.filter(item=>item!==child); child.parentElement=null; }}
querySelectorAll(selector) {{
if(selector==='optgroup') return this.children.filter(child=>child.tagName==='OPTGROUP');
return [];
}}
get options() {{
return this.tagName==='SELECT'
? this.children.flatMap(child=>child.tagName==='OPTGROUP'?child.children:[child])
: undefined;
}}
}}
globalThis.window={{_activeProvider:'custom:llm-proxy'}};
globalThis.document={{createElement:tag=>new Node(tag)}};
globalThis._dynamicModelLabels={{}};
globalThis._modelStateForSelect=()=>({{model:'',model_provider:null}});
globalThis._applyModelToDropdown=()=>null;
globalThis.S={{session:null}};
function addCatalog(select, value) {{
const group=select.querySelectorAll('optgroup')[0] || (()=>{{
const item=new Node('optgroup'); item.dataset.provider='custom:llm-proxy'; select.appendChild(item); return item;
}})();
const catalog=new Node('option'); catalog.value=value; catalog.textContent=value; group.appendChild(catalog);
}}
function snapshot(select) {{
if(!select.value && select.options[0]) select.value=select.options[0].value;
_deduplicateModelPickerOptions(select,select.value);
return {{groups:select.querySelectorAll('optgroup').map(item=>item.children.map(option=>option.value)),selected:select.value}};
}}
function makeSelect() {{
const select=new Node('select');
const group=new Node('optgroup'); group.dataset.provider='custom:llm-proxy'; select.appendChild(group);
return select;
}}
const liveFirst=makeSelect();
_addLiveModelsToSelect('custom:llm-proxy',[{{id:'x-ai/grok-4.5',label:'Grok 4.5'}}],liveFirst);
addCatalog(liveFirst,'x-ai/grok-4.5');
liveFirst.value=liveFirst.options[0].value;
_addLiveModelsToSelect('custom:llm-proxy',[{{id:'x-ai/grok-4.5',label:'Grok 4.5'}}],liveFirst);
const otherGroup=new Node('optgroup'); otherGroup.dataset.provider='custom:other-proxy';
const other=new Node('option'); other.value='x-ai/grok-4.5'; otherGroup.appendChild(other); liveFirst.appendChild(otherGroup);
const catalogFirst=makeSelect();
addCatalog(catalogFirst,'x-ai/grok-4.5');
_addLiveModelsToSelect('custom:llm-proxy',[{{id:'x-ai/grok-4.5',label:'Grok 4.5'}}],catalogFirst);
const selectedBare=makeSelect();
addCatalog(selectedBare,'x-ai/grok-4.5');
selectedBare.value='x-ai/grok-4.5';
_addLiveModelsToSelect('custom:llm-proxy',[{{id:'x-ai/grok-4.5',label:'Grok 4.5'}}],selectedBare);
const sameSuffix=makeSelect();
addCatalog(sameSuffix,'vendor-a/deepseek-v4-pro');
addCatalog(sameSuffix,'vendor-b/catalog/deepseek-v4-pro');
sameSuffix.value='vendor-a/deepseek-v4-pro';
const sameGroupColonSuffix=makeSelect();
_addLiveModelsToSelect('custom:llm-proxy',[{{id:'@custom:llm-proxy:deepseek/deepseek-r1:free',label:'DeepSeek R1 Free'}}],sameGroupColonSuffix);
_addLiveModelsToSelect('custom:llm-proxy',[{{id:'@custom:llm-proxy:meta-llama/llama-3.3:free',label:'Llama 3.3 Free'}}],sameGroupColonSuffix);
const crossProviderLive=new Node('select');
_addLiveModelsToSelect('custom:a',[{{id:'@custom:a:gpt-4o',label:'GPT-4o A'}}],crossProviderLive);
_addLiveModelsToSelect('custom:b',[{{id:'@custom:b:gpt-4o',label:'GPT-4o B'}}],crossProviderLive);
const unnamespaced=makeSelect();
addCatalog(unnamespaced,'gpt-4o');
_addLiveModelsToSelect('custom:llm-proxy',[{{id:'@custom:llm-proxy:gpt-4o',label:'GPT-4o'}}],unnamespaced);
console.log(JSON.stringify({{
liveFirst:snapshot(liveFirst),
catalogFirst:snapshot(catalogFirst),
selectedBare:snapshot(selectedBare),
sameSuffix:snapshot(sameSuffix),
sameGroupColonSuffix:snapshot(sameGroupColonSuffix),
crossProviderLive:snapshot(crossProviderLive),
unnamespaced:snapshot(unnamespaced),
}}));
"""
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_cross_source_proxy_and_catalog_twin_has_one_routable_row():
result = _run_harness()
assert result["liveFirst"] == {
"groups": [["@custom:llm-proxy:x-ai/grok-4.5"], ["x-ai/grok-4.5"]],
"selected": "@custom:llm-proxy:x-ai/grok-4.5",
}
def test_catalog_first_keeps_the_routable_proxy_row():
assert _run_harness()["catalogFirst"] == {
"groups": [["@custom:llm-proxy:x-ai/grok-4.5"]],
"selected": "@custom:llm-proxy:x-ai/grok-4.5",
}
def test_selected_bare_occurrence_survives_same_group_dedup():
assert _run_harness()["selectedBare"] == {
"groups": [["x-ai/grok-4.5"]],
"selected": "x-ai/grok-4.5",
}
def test_same_suffix_models_in_one_group_do_not_collapse():
assert _run_harness()["sameSuffix"] == {
"groups": [["vendor-a/deepseek-v4-pro", "vendor-b/catalog/deepseek-v4-pro"]],
"selected": "vendor-a/deepseek-v4-pro",
}
def test_same_group_colon_suffixed_proxy_models_remain_distinct():
assert _run_harness()["sameGroupColonSuffix"] == {
"groups": [[
"@custom:llm-proxy:deepseek/deepseek-r1:free",
"@custom:llm-proxy:meta-llama/llama-3.3:free",
]],
"selected": "@custom:llm-proxy:deepseek/deepseek-r1:free",
}
def test_live_models_with_same_identity_survive_in_different_provider_groups():
assert _run_harness()["crossProviderLive"] == {
"groups": [["@custom:a:gpt-4o"], ["@custom:b:gpt-4o"]],
"selected": "@custom:a:gpt-4o",
}
def test_unnamespaced_custom_proxy_value_deduplicates_with_catalog():
assert _run_harness()["unnamespaced"] == {
"groups": [["@custom:llm-proxy:gpt-4o"]],
"selected": "@custom:llm-proxy:gpt-4o",
}