llm-switcher 1.6.2 → 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/ui.html +214 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog — LLM Switcher
|
|
2
2
|
|
|
3
|
+
## Release 1.6.3 — 2026-10-08
|
|
4
|
+
|
|
5
|
+
- **Interactive Summarization Model Selection & Presets:** Replace raw text input for Idle Compaction's summarization model with a fully searchable combobox dropdown and quick 1-click preset chips. Users can pick directly from deterministic heuristic mode (`""`), slot aliases (`haiku`, `subagent`, `sonnet`, `main`), configured profile models (`zencore/*`, `codex/*`), and discovered catalog models, or type to filter in real-time. Selection automatically saves and updates active highlights across both light and dark themes.
|
|
6
|
+
|
|
3
7
|
## Release 1.6.2 — 2026-10-08
|
|
4
8
|
|
|
5
9
|
- **Per-Model Slot Target Thinking Effort:** Support granular reasoning effort configuration per model slot (`profile.modelEfforts`). Individual slot selectors in the dashboard allow setting target effort levels (`auto`, `none`, `low`, `medium`, `high`, `xhigh`, `max`) independently for each model tier (e.g. Sonnet, Opus, Haiku, Codex roles) with automatic fallback to profile-level defaults.
|
package/package.json
CHANGED
package/ui.html
CHANGED
|
@@ -551,6 +551,7 @@
|
|
|
551
551
|
|
|
552
552
|
/* Model slot combobox: one listbox per slot, provider prefix dimmed, match in the accent. */
|
|
553
553
|
.combo { position: relative; }
|
|
554
|
+
.combo[data-open] { z-index: 50; }
|
|
554
555
|
.slot-note { font-size: 0.72rem; color: var(--faint); } /* Kept in the page when empty: a live region added later is often not read. */
|
|
555
556
|
.slot-note:empty { margin-top: -0.25rem; }
|
|
556
557
|
.combo > input { padding-right: 2.25rem; font-family: var(--font-mono); font-size: 0.8rem; }
|
|
@@ -1104,13 +1105,22 @@
|
|
|
1104
1105
|
</div>
|
|
1105
1106
|
</div>
|
|
1106
1107
|
|
|
1107
|
-
<div class="row">
|
|
1108
|
-
<div class="who">
|
|
1108
|
+
<div class="row" style="align-items:flex-start;">
|
|
1109
|
+
<div class="who" style="padding-top:0.25rem;">
|
|
1109
1110
|
<div class="l">Summarization Model</div>
|
|
1110
|
-
<div class="meta">
|
|
1111
|
+
<div class="meta">Select from discovered/profile models or enter custom ID (empty = deterministic truncation)</div>
|
|
1111
1112
|
</div>
|
|
1112
|
-
<div style="min-width:
|
|
1113
|
-
<
|
|
1113
|
+
<div style="min-width:18rem; flex:1; max-width:28rem; margin-left:auto; display:flex; flex-direction:column; gap:0.4rem;">
|
|
1114
|
+
<div class="combo" id="combo-compact-model">
|
|
1115
|
+
<input type="text" id="idle-compact-model" placeholder="empty = deterministic truncation" maxlength="512" onchange="saveIdleCompact()" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="idle-compact-model-list" autocomplete="off" spellcheck="false" style="font-family:var(--font-mono); font-size:0.8rem;" title="Upstream model used to generate turn summary text">
|
|
1116
|
+
<button type="button" class="combo-toggle" tabindex="-1" aria-label="Show available summary models">
|
|
1117
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>
|
|
1118
|
+
</button>
|
|
1119
|
+
<ul class="combo-list" id="idle-compact-model-list" role="listbox" aria-label="Available summarization models" hidden></ul>
|
|
1120
|
+
</div>
|
|
1121
|
+
<div id="compact-model-presets" style="display:flex; flex-wrap:wrap; align-items:center; gap:0.35rem;">
|
|
1122
|
+
<!-- Quick Presets rendered dynamically -->
|
|
1123
|
+
</div>
|
|
1114
1124
|
</div>
|
|
1115
1125
|
</div>
|
|
1116
1126
|
|
|
@@ -2062,7 +2072,10 @@
|
|
|
2062
2072
|
const elKb = document.getElementById('idle-compact-kb');
|
|
2063
2073
|
if (elKb) elKb.value = (c.minBytes ?? 65536) / 1024;
|
|
2064
2074
|
const elModel = document.getElementById('idle-compact-model');
|
|
2065
|
-
if (elModel)
|
|
2075
|
+
if (elModel) {
|
|
2076
|
+
elModel.value = c.model || '';
|
|
2077
|
+
updateCompactModelPresets(c.model || '');
|
|
2078
|
+
}
|
|
2066
2079
|
const elCodex = document.getElementById('idle-compact-codex');
|
|
2067
2080
|
if (elCodex) elCodex.checked = !!c.codex;
|
|
2068
2081
|
const elKeep = document.getElementById('idle-compact-keep');
|
|
@@ -2112,7 +2125,62 @@
|
|
|
2112
2125
|
}
|
|
2113
2126
|
}
|
|
2114
2127
|
|
|
2128
|
+
async function ensureCatalogLoaded() {
|
|
2129
|
+
if (cachedCatalog) return cachedCatalog;
|
|
2130
|
+
try {
|
|
2131
|
+
const res = await api('/api/catalog');
|
|
2132
|
+
if (res.ok) cachedCatalog = await res.json();
|
|
2133
|
+
} catch {}
|
|
2134
|
+
return cachedCatalog;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function setCompactModel(model) {
|
|
2138
|
+
const inp = document.getElementById('idle-compact-model');
|
|
2139
|
+
if (inp) {
|
|
2140
|
+
inp.value = model;
|
|
2141
|
+
saveIdleCompact();
|
|
2142
|
+
updateCompactModelPresets(model);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function updateCompactModelPresets(currentModel) {
|
|
2147
|
+
const container = document.getElementById('compact-model-presets');
|
|
2148
|
+
if (!container) return;
|
|
2149
|
+
if (currentModel === undefined) {
|
|
2150
|
+
currentModel = document.getElementById('idle-compact-model')?.value.trim() || '';
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const presets = [
|
|
2154
|
+
{ id: '', label: 'Deterministic (none)' },
|
|
2155
|
+
{ id: 'haiku', label: 'haiku' },
|
|
2156
|
+
{ id: 'subagent', label: 'subagent' }
|
|
2157
|
+
];
|
|
2158
|
+
|
|
2159
|
+
const profiles = state.config?.profiles || {};
|
|
2160
|
+
let added = 0;
|
|
2161
|
+
for (const p of Object.values(profiles)) {
|
|
2162
|
+
if (p.defaultModels) {
|
|
2163
|
+
for (const [slot, m] of Object.entries(p.defaultModels)) {
|
|
2164
|
+
if (m && added < 2 && !presets.some(x => x.id === m)) {
|
|
2165
|
+
presets.push({ id: m, label: m.includes('/') ? m.split('/')[1] : m });
|
|
2166
|
+
added++;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
container.innerHTML = `<span class="hint" style="font-size:0.75rem; align-self:center; margin-right:0.2rem;">Quick Presets:</span>` +
|
|
2173
|
+
presets.map(p => {
|
|
2174
|
+
const active = (p.id === currentModel);
|
|
2175
|
+
const style = active
|
|
2176
|
+
? 'padding:0.18rem 0.5rem; font-size:0.75rem; border-color:var(--accent); color:var(--accent); background:var(--accent-soft); font-weight:600;'
|
|
2177
|
+
: 'padding:0.18rem 0.5rem; font-size:0.75rem;';
|
|
2178
|
+
return `<button type="button" class="btn btn-outline btn-sm" style="${style}" onclick="setCompactModel('${escapeHtml(p.id)}')">${escapeHtml(p.label)}</button>`;
|
|
2179
|
+
}).join('');
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2115
2182
|
async function loadIdleCompact() {
|
|
2183
|
+
ensureCatalogLoaded();
|
|
2116
2184
|
try {
|
|
2117
2185
|
const r = await api('/api/idle-compact');
|
|
2118
2186
|
if (r.ok) showIdleCompact((await r.json()).idleCompact);
|
|
@@ -2789,7 +2857,14 @@
|
|
|
2789
2857
|
function wireCombo(combo) {
|
|
2790
2858
|
const input = combo.querySelector('input');
|
|
2791
2859
|
combo.activeIndex = -1;
|
|
2792
|
-
input.addEventListener('input', () => {
|
|
2860
|
+
input.addEventListener('input', () => {
|
|
2861
|
+
combo.dataset.filter = '1';
|
|
2862
|
+
combo.activeIndex = -1;
|
|
2863
|
+
openCombo(combo);
|
|
2864
|
+
if (input.id === 'idle-compact-model') {
|
|
2865
|
+
updateCompactModelPresets(input.value.trim());
|
|
2866
|
+
}
|
|
2867
|
+
});
|
|
2793
2868
|
input.addEventListener('keydown', onComboKeydown);
|
|
2794
2869
|
input.addEventListener('blur', () => closeCombo(combo));
|
|
2795
2870
|
// Keep the focus in the field while the list or the arrow is used.
|
|
@@ -2853,6 +2928,12 @@
|
|
|
2853
2928
|
const list = combo.querySelector('.combo-list');
|
|
2854
2929
|
const q = combo.dataset.filter ? input.value.trim().toLowerCase() : '';
|
|
2855
2930
|
const current = input.value.trim();
|
|
2931
|
+
|
|
2932
|
+
if (input.id === 'idle-compact-model') {
|
|
2933
|
+
renderCompactModelOptions(combo, q, current);
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2856
2937
|
const matches = loadedModels.filter(m => !q || m.toLowerCase().includes(q)).slice(0, 200);
|
|
2857
2938
|
combo.matches = matches;
|
|
2858
2939
|
if (!loadedModels.length) {
|
|
@@ -2876,6 +2957,119 @@
|
|
|
2876
2957
|
setComboActive(combo, combo.activeIndex);
|
|
2877
2958
|
}
|
|
2878
2959
|
|
|
2960
|
+
function getAvailableCompactModels() {
|
|
2961
|
+
const items = [];
|
|
2962
|
+
const seen = new Set();
|
|
2963
|
+
|
|
2964
|
+
const add = (id, label, desc, category) => {
|
|
2965
|
+
if (!seen.has(id)) {
|
|
2966
|
+
seen.add(id);
|
|
2967
|
+
items.push({ id, label: label || id, desc: desc || '', category });
|
|
2968
|
+
}
|
|
2969
|
+
};
|
|
2970
|
+
|
|
2971
|
+
// 1. Deterministic option (empty string)
|
|
2972
|
+
add('', 'Deterministic Truncation (no external call)', 'Pure heuristic context truncation · Zero latency & zero tokens', 'Deterministic');
|
|
2973
|
+
|
|
2974
|
+
// 2. Profile role/slot aliases (recommended for routing)
|
|
2975
|
+
add('haiku', 'haiku (Claude Haiku Slot)', 'Auto-resolves to active profile Claude Haiku model', 'Slot Aliases');
|
|
2976
|
+
add('subagent', 'subagent (Codex Subagent Slot)', 'Auto-resolves to active profile Codex Subagent model', 'Slot Aliases');
|
|
2977
|
+
add('sonnet', 'sonnet (Claude Sonnet Slot)', 'Auto-resolves to active profile Claude Sonnet model', 'Slot Aliases');
|
|
2978
|
+
add('main', 'main (Codex Main Slot)', 'Auto-resolves to active profile Codex Main model', 'Slot Aliases');
|
|
2979
|
+
|
|
2980
|
+
// 3. Models configured in current profiles
|
|
2981
|
+
const profiles = state.config?.profiles || {};
|
|
2982
|
+
for (const [profKey, p] of Object.entries(profiles)) {
|
|
2983
|
+
const profName = p.name || profKey;
|
|
2984
|
+
if (p.defaultModels) {
|
|
2985
|
+
for (const [slot, m] of Object.entries(p.defaultModels)) {
|
|
2986
|
+
if (m) add(m, m, `${profName} · ${slot} slot`, 'Profile Models');
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
if (Array.isArray(p.publicModels)) {
|
|
2990
|
+
for (const m of p.publicModels) {
|
|
2991
|
+
if (m) add(m, m, `${profName}`, 'Profile Models');
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
// 4. Discovered Catalog models
|
|
2997
|
+
if (cachedCatalog) {
|
|
2998
|
+
if (Array.isArray(cachedCatalog.claude?.models)) {
|
|
2999
|
+
for (const m of cachedCatalog.claude.models) {
|
|
3000
|
+
if (m.id) add(m.id, m.id, `Claude Catalog · ${m.tier || ''}`, 'Catalog');
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
if (Array.isArray(cachedCatalog.codex?.models)) {
|
|
3004
|
+
for (const m of cachedCatalog.codex.models) {
|
|
3005
|
+
if (m.id) add(m.id, m.id, `Codex Catalog · ${m.role || ''}`, 'Catalog');
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// 5. Popular lightweight models
|
|
3011
|
+
const popular = [
|
|
3012
|
+
{ id: 'zencore/muse-spark-1.3-contributor-free', desc: 'ZenCore contributor fast model' },
|
|
3013
|
+
{ id: 'zencore/exo-free', desc: 'ZenCore free pool fast model' },
|
|
3014
|
+
{ id: 'claude-3-5-haiku-20241022', desc: 'Anthropic lightweight summarizer' },
|
|
3015
|
+
{ id: 'gpt-4o-mini', desc: 'OpenAI low-cost summarizer' },
|
|
3016
|
+
{ id: 'gemini-2.5-flash', desc: 'Google Gemini fast summarizer' }
|
|
3017
|
+
];
|
|
3018
|
+
for (const p of popular) {
|
|
3019
|
+
add(p.id, p.id, p.desc, 'Recommended');
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
return items;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
function renderCompactModelOptions(combo, q, current) {
|
|
3026
|
+
const input = combo.querySelector('input');
|
|
3027
|
+
const list = combo.querySelector('.combo-list');
|
|
3028
|
+
const allItems = getAvailableCompactModels();
|
|
3029
|
+
|
|
3030
|
+
const filtered = allItems.filter(item => {
|
|
3031
|
+
if (!q) return true;
|
|
3032
|
+
if (item.id === '' && ('deterministic'.includes(q) || 'none'.includes(q) || 'empty'.includes(q) || 'heuristic'.includes(q))) return true;
|
|
3033
|
+
return item.id.toLowerCase().includes(q) || item.label.toLowerCase().includes(q) || item.desc.toLowerCase().includes(q);
|
|
3034
|
+
}).slice(0, 150);
|
|
3035
|
+
|
|
3036
|
+
combo.matches = filtered.map(item => item.id);
|
|
3037
|
+
|
|
3038
|
+
if (!filtered.length) {
|
|
3039
|
+
list.innerHTML = '<li class="combo-empty">No matching model found. The typed model will be used directly.</li>';
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
if (!(combo.activeIndex >= 0 && combo.activeIndex < filtered.length)) {
|
|
3044
|
+
combo.activeIndex = filtered.findIndex(item => item.id === current);
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
list.innerHTML = filtered.map((item, i) => {
|
|
3048
|
+
const isSelected = item.id === current;
|
|
3049
|
+
let badgeClass = 'badge outline';
|
|
3050
|
+
if (item.category === 'Deterministic') badgeClass = 'badge ok';
|
|
3051
|
+
else if (item.category === 'Slot Aliases') badgeClass = 'badge accent';
|
|
3052
|
+
else if (item.category === 'Profile Models') badgeClass = 'badge';
|
|
3053
|
+
|
|
3054
|
+
const cut = item.id.lastIndexOf('/') + 1;
|
|
3055
|
+
const prefix = cut ? `<span class="pfx">${markMatch(item.id.slice(0, cut), q)}</span>` : '';
|
|
3056
|
+
const mainText = item.id ? `${prefix}${markMatch(item.id.slice(cut), q)}` : markMatch(item.label, q);
|
|
3057
|
+
|
|
3058
|
+
return `<li class="combo-opt" role="option" id="${input.id}-opt-${i}" data-i="${i}" aria-selected="${i === combo.activeIndex}" style="display:flex; flex-direction:column; gap:0.18rem; padding:0.45rem 0.6rem; border-bottom:1px solid rgba(255,255,255,0.04);" onmousedown="event.preventDefault(); chooseComboOption(this)">`
|
|
3059
|
+
+ `<div style="display:flex; align-items:center; justify-content:space-between; gap:0.5rem; width:100%;">`
|
|
3060
|
+
+ `<span class="id" style="font-weight:600;">${mainText}</span>`
|
|
3061
|
+
+ `<div style="display:flex; align-items:center; gap:0.4rem; flex:none;">`
|
|
3062
|
+
+ `<span class="${badgeClass}" style="font-size:0.68rem; padding:0.1rem 0.35rem;">${escapeHtml(item.category)}</span>`
|
|
3063
|
+
+ (isSelected ? '<span class="cur" style="font-weight:600;">active</span>' : '')
|
|
3064
|
+
+ `</div>`
|
|
3065
|
+
+ `</div>`
|
|
3066
|
+
+ (item.desc && item.id ? `<div style="font-size:0.72rem; color:var(--muted); font-family:var(--font-sans);">${escapeHtml(item.desc)}</div>` : '')
|
|
3067
|
+
+ `</li>`;
|
|
3068
|
+
}).join('');
|
|
3069
|
+
|
|
3070
|
+
setComboActive(combo, combo.activeIndex);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
2879
3073
|
function setComboActive(combo, i) {
|
|
2880
3074
|
const input = combo.querySelector('input');
|
|
2881
3075
|
combo.activeIndex = i;
|
|
@@ -2894,12 +3088,18 @@
|
|
|
2894
3088
|
|
|
2895
3089
|
function chooseComboOption(li) {
|
|
2896
3090
|
const combo = li.closest('.combo');
|
|
2897
|
-
const
|
|
2898
|
-
|
|
2899
|
-
|
|
3091
|
+
const idx = Number(li.dataset.i);
|
|
3092
|
+
const model = combo?.matches?.[idx];
|
|
3093
|
+
if (model === undefined) return;
|
|
3094
|
+
const input = combo.querySelector('input');
|
|
3095
|
+
input.value = model;
|
|
2900
3096
|
formDirty = true;
|
|
2901
3097
|
closeCombo(combo);
|
|
2902
3098
|
updateWindowNotes();
|
|
3099
|
+
if (input.id === 'idle-compact-model') {
|
|
3100
|
+
saveIdleCompact();
|
|
3101
|
+
updateCompactModelPresets(model);
|
|
3102
|
+
}
|
|
2903
3103
|
}
|
|
2904
3104
|
|
|
2905
3105
|
function onComboKeydown(e) {
|
|
@@ -3241,6 +3441,10 @@
|
|
|
3241
3441
|
document.getElementById('p-url')?.addEventListener('input', syncEffortOptions);
|
|
3242
3442
|
document.getElementById('p-tool')?.addEventListener('change', syncEffortOptions);
|
|
3243
3443
|
|
|
3444
|
+
// Wire Idle Compaction summarization model combobox
|
|
3445
|
+
const compactCombo = document.getElementById('combo-compact-model');
|
|
3446
|
+
if (compactCombo) wireCombo(compactCombo);
|
|
3447
|
+
|
|
3244
3448
|
// Trigger initial route
|
|
3245
3449
|
handleRoute();
|
|
3246
3450
|
|