llm-switcher 1.6.2 → 1.6.4
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 +9 -0
- package/package.json +1 -1
- package/ui.html +214 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog — LLM Switcher
|
|
2
2
|
|
|
3
|
+
## Release 1.6.4 — 2026-10-08
|
|
4
|
+
|
|
5
|
+
- **Dynamic Model Provider Source for Idle Compaction:** Query live model listings directly from the selected profile's `baseURL` via `/models` instead of hardcoding provider URLs or static model lists.
|
|
6
|
+
- **Clean Model Combobox without Slot Aliases:** Eliminate hardcoded quick preset buttons, popular model arrays, and slot aliases from the summarization model combobox. Models are loaded dynamically from the upstream provider, sorted alphabetically with context limits, with support for instant search filtering and deterministic heuristic truncation (`""`).
|
|
7
|
+
|
|
8
|
+
## Release 1.6.3 — 2026-10-08
|
|
9
|
+
|
|
10
|
+
- **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.
|
|
11
|
+
|
|
3
12
|
## Release 1.6.2 — 2026-10-08
|
|
4
13
|
|
|
5
14
|
- **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; }
|
|
@@ -1106,11 +1107,28 @@
|
|
|
1106
1107
|
|
|
1107
1108
|
<div class="row">
|
|
1108
1109
|
<div class="who">
|
|
1110
|
+
<div class="l">Model Provider Source</div>
|
|
1111
|
+
<div class="meta" id="idle-compact-source-meta">Profile Base URL queried via /models to list upstream models</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
<div style="display:flex; align-items:center; gap:0.5rem; margin-left:auto; min-width:18rem; max-width:26rem;">
|
|
1114
|
+
<select id="idle-compact-source-profile" onchange="onCompactSourceProfileChange()" style="flex:1; font-size:0.8rem;"></select>
|
|
1115
|
+
<button type="button" class="btn btn-outline btn-sm" id="btn-reload-compact-models" onclick="reloadCompactModels(true)" title="Fetch live models from provider Base URL">Reload</button>
|
|
1116
|
+
</div>
|
|
1117
|
+
</div>
|
|
1118
|
+
|
|
1119
|
+
<div class="row" style="align-items:flex-start;">
|
|
1120
|
+
<div class="who" style="padding-top:0.25rem;">
|
|
1109
1121
|
<div class="l">Summarization Model</div>
|
|
1110
|
-
<div class="meta">
|
|
1122
|
+
<div class="meta" id="idle-compact-models-status">Select model from provider URL or enter custom ID (empty = deterministic truncation)</div>
|
|
1111
1123
|
</div>
|
|
1112
|
-
<div style="min-width:
|
|
1113
|
-
<
|
|
1124
|
+
<div style="min-width:18rem; flex:1; max-width:26rem; margin-left:auto;">
|
|
1125
|
+
<div class="combo" id="combo-compact-model">
|
|
1126
|
+
<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">
|
|
1127
|
+
<button type="button" class="combo-toggle" tabindex="-1" aria-label="Show available summary models">
|
|
1128
|
+
<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>
|
|
1129
|
+
</button>
|
|
1130
|
+
<ul class="combo-list" id="idle-compact-model-list" role="listbox" aria-label="Available summarization models" hidden></ul>
|
|
1131
|
+
</div>
|
|
1114
1132
|
</div>
|
|
1115
1133
|
</div>
|
|
1116
1134
|
|
|
@@ -2112,11 +2130,107 @@
|
|
|
2112
2130
|
}
|
|
2113
2131
|
}
|
|
2114
2132
|
|
|
2133
|
+
let compactLoadedModels = [];
|
|
2134
|
+
let compactLoadedLimits = {};
|
|
2135
|
+
let compactSourceProfileKey = '';
|
|
2136
|
+
|
|
2137
|
+
function populateCompactSourceProfiles() {
|
|
2138
|
+
const sel = document.getElementById('idle-compact-source-profile');
|
|
2139
|
+
if (!sel) return;
|
|
2140
|
+
const profiles = state.config?.profiles || {};
|
|
2141
|
+
const activeClaude = state.config?.activeProfiles?.claude;
|
|
2142
|
+
const activeCodex = state.config?.activeProfiles?.codex;
|
|
2143
|
+
const keys = Object.keys(profiles);
|
|
2144
|
+
|
|
2145
|
+
if (!compactSourceProfileKey || !profiles[compactSourceProfileKey]) {
|
|
2146
|
+
compactSourceProfileKey = activeClaude || activeCodex || keys[0] || '';
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
if (!keys.length) {
|
|
2150
|
+
sel.innerHTML = '<option value="">(No profiles configured)</option>';
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
sel.innerHTML = keys.map(k => {
|
|
2155
|
+
const p = profiles[k];
|
|
2156
|
+
const isActive = (k === activeClaude || k === activeCodex);
|
|
2157
|
+
const prefix = isActive ? '★ ' : '';
|
|
2158
|
+
const selected = (k === compactSourceProfileKey) ? 'selected' : '';
|
|
2159
|
+
return `<option value="${escapeHtml(k)}" ${selected}>${escapeHtml(prefix + (p.name || k))} (${escapeHtml(p.baseURL || 'no URL')})</option>`;
|
|
2160
|
+
}).join('');
|
|
2161
|
+
|
|
2162
|
+
updateCompactSourceMeta();
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
function updateCompactSourceMeta() {
|
|
2166
|
+
const meta = document.getElementById('idle-compact-source-meta');
|
|
2167
|
+
const p = state.config?.profiles?.[compactSourceProfileKey];
|
|
2168
|
+
if (meta && p) {
|
|
2169
|
+
meta.textContent = `${p.baseURL || 'No Base URL configured'} · ${p.outFormat || p.mode || 'convert'}`;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
async function onCompactSourceProfileChange() {
|
|
2174
|
+
const sel = document.getElementById('idle-compact-source-profile');
|
|
2175
|
+
if (sel) {
|
|
2176
|
+
compactSourceProfileKey = sel.value;
|
|
2177
|
+
updateCompactSourceMeta();
|
|
2178
|
+
await reloadCompactModels(true);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
async function reloadCompactModels(force = false) {
|
|
2183
|
+
const p = state.config?.profiles?.[compactSourceProfileKey];
|
|
2184
|
+
const statusEl = document.getElementById('idle-compact-models-status');
|
|
2185
|
+
const reloadBtn = document.getElementById('btn-reload-compact-models');
|
|
2186
|
+
if (!p || !p.baseURL) {
|
|
2187
|
+
compactLoadedModels = [];
|
|
2188
|
+
compactLoadedLimits = {};
|
|
2189
|
+
if (statusEl) statusEl.textContent = 'Configure a Base URL on the profile to load live models';
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
if (statusEl) statusEl.textContent = `Loading models from ${p.baseURL}...`;
|
|
2194
|
+
if (reloadBtn) reloadBtn.disabled = true;
|
|
2195
|
+
|
|
2196
|
+
try {
|
|
2197
|
+
const res = await api('/api/fetch-models', {
|
|
2198
|
+
method: 'POST',
|
|
2199
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2200
|
+
body: JSON.stringify({ baseURL: p.baseURL, apiKey: MASKED_KEY, key: compactSourceProfileKey })
|
|
2201
|
+
});
|
|
2202
|
+
const data = await res.json();
|
|
2203
|
+
if (data.ok && Array.isArray(data.models)) {
|
|
2204
|
+
compactLoadedModels = data.models;
|
|
2205
|
+
compactLoadedLimits = data.limits || {};
|
|
2206
|
+
if (statusEl) {
|
|
2207
|
+
statusEl.textContent = `${compactLoadedModels.length} models loaded from ${p.baseURL}. Select from list or enter model ID.`;
|
|
2208
|
+
}
|
|
2209
|
+
} else {
|
|
2210
|
+
compactLoadedModels = [];
|
|
2211
|
+
compactLoadedLimits = {};
|
|
2212
|
+
if (statusEl) {
|
|
2213
|
+
statusEl.textContent = `Models not loaded: ${modelsError(data.error)}`;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
} catch (err) {
|
|
2217
|
+
compactLoadedModels = [];
|
|
2218
|
+
compactLoadedLimits = {};
|
|
2219
|
+
if (statusEl) statusEl.textContent = `Failed to load models: ${err.message}`;
|
|
2220
|
+
} finally {
|
|
2221
|
+
if (reloadBtn) reloadBtn.disabled = false;
|
|
2222
|
+
const combo = document.getElementById('combo-compact-model');
|
|
2223
|
+
if (combo && combo.hasAttribute('data-open')) renderComboOptions(combo);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2115
2227
|
async function loadIdleCompact() {
|
|
2228
|
+
populateCompactSourceProfiles();
|
|
2116
2229
|
try {
|
|
2117
2230
|
const r = await api('/api/idle-compact');
|
|
2118
2231
|
if (r.ok) showIdleCompact((await r.json()).idleCompact);
|
|
2119
2232
|
} catch { /* the card stays at its defaults; nothing here is required to route */ }
|
|
2233
|
+
await reloadCompactModels();
|
|
2120
2234
|
}
|
|
2121
2235
|
|
|
2122
2236
|
async function saveIdleCompact() {
|
|
@@ -2789,7 +2903,11 @@
|
|
|
2789
2903
|
function wireCombo(combo) {
|
|
2790
2904
|
const input = combo.querySelector('input');
|
|
2791
2905
|
combo.activeIndex = -1;
|
|
2792
|
-
input.addEventListener('input', () => {
|
|
2906
|
+
input.addEventListener('input', () => {
|
|
2907
|
+
combo.dataset.filter = '1';
|
|
2908
|
+
combo.activeIndex = -1;
|
|
2909
|
+
openCombo(combo);
|
|
2910
|
+
});
|
|
2793
2911
|
input.addEventListener('keydown', onComboKeydown);
|
|
2794
2912
|
input.addEventListener('blur', () => closeCombo(combo));
|
|
2795
2913
|
// Keep the focus in the field while the list or the arrow is used.
|
|
@@ -2853,6 +2971,12 @@
|
|
|
2853
2971
|
const list = combo.querySelector('.combo-list');
|
|
2854
2972
|
const q = combo.dataset.filter ? input.value.trim().toLowerCase() : '';
|
|
2855
2973
|
const current = input.value.trim();
|
|
2974
|
+
|
|
2975
|
+
if (input.id === 'idle-compact-model') {
|
|
2976
|
+
renderCompactModelOptions(combo, q, current);
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2856
2980
|
const matches = loadedModels.filter(m => !q || m.toLowerCase().includes(q)).slice(0, 200);
|
|
2857
2981
|
combo.matches = matches;
|
|
2858
2982
|
if (!loadedModels.length) {
|
|
@@ -2876,6 +3000,80 @@
|
|
|
2876
3000
|
setComboActive(combo, combo.activeIndex);
|
|
2877
3001
|
}
|
|
2878
3002
|
|
|
3003
|
+
function getAvailableCompactModels() {
|
|
3004
|
+
const items = [];
|
|
3005
|
+
const seen = new Set();
|
|
3006
|
+
|
|
3007
|
+
const add = (id, label, desc, category) => {
|
|
3008
|
+
if (!seen.has(id)) {
|
|
3009
|
+
seen.add(id);
|
|
3010
|
+
items.push({ id, label: label || id, desc: desc || '', category });
|
|
3011
|
+
}
|
|
3012
|
+
};
|
|
3013
|
+
|
|
3014
|
+
// 1. Deterministic option (empty string)
|
|
3015
|
+
add('', 'Deterministic Truncation (no external call)', 'Pure heuristic context truncation · Zero latency & zero tokens', 'Deterministic');
|
|
3016
|
+
|
|
3017
|
+
// 2. Models fetched live from the provider Base URL
|
|
3018
|
+
const prof = state.config?.profiles?.[compactSourceProfileKey];
|
|
3019
|
+
if (Array.isArray(compactLoadedModels) && compactLoadedModels.length) {
|
|
3020
|
+
for (const m of compactLoadedModels) {
|
|
3021
|
+
const l = compactLoadedLimits[m];
|
|
3022
|
+
const info = l?.context ? `${Math.round(l.context / 1000)}k context` : (prof?.name || 'Upstream');
|
|
3023
|
+
add(m, m, info, 'Upstream Model');
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
return items;
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
function renderCompactModelOptions(combo, q, current) {
|
|
3031
|
+
const input = combo.querySelector('input');
|
|
3032
|
+
const list = combo.querySelector('.combo-list');
|
|
3033
|
+
const allItems = getAvailableCompactModels();
|
|
3034
|
+
|
|
3035
|
+
const filtered = allItems.filter(item => {
|
|
3036
|
+
if (!q) return true;
|
|
3037
|
+
if (item.id === '' && ('deterministic'.includes(q) || 'none'.includes(q) || 'empty'.includes(q) || 'heuristic'.includes(q))) return true;
|
|
3038
|
+
return item.id.toLowerCase().includes(q) || item.label.toLowerCase().includes(q);
|
|
3039
|
+
}).slice(0, 150);
|
|
3040
|
+
|
|
3041
|
+
combo.matches = filtered.map(item => item.id);
|
|
3042
|
+
|
|
3043
|
+
if (!filtered.length) {
|
|
3044
|
+
list.innerHTML = '<li class="combo-empty">No matching model found. The typed model ID will be used directly.</li>';
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
if (!(combo.activeIndex >= 0 && combo.activeIndex < filtered.length)) {
|
|
3049
|
+
combo.activeIndex = filtered.findIndex(item => item.id === current);
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
list.innerHTML = filtered.map((item, i) => {
|
|
3053
|
+
const isSelected = item.id === current;
|
|
3054
|
+
let badgeClass = 'badge outline';
|
|
3055
|
+
if (item.category === 'Deterministic') badgeClass = 'badge ok';
|
|
3056
|
+
else if (item.category === 'Upstream Model') badgeClass = 'badge';
|
|
3057
|
+
|
|
3058
|
+
const cut = item.id.lastIndexOf('/') + 1;
|
|
3059
|
+
const prefix = cut ? `<span class="pfx">${markMatch(item.id.slice(0, cut), q)}</span>` : '';
|
|
3060
|
+
const mainText = item.id ? `${prefix}${markMatch(item.id.slice(cut), q)}` : markMatch(item.label, q);
|
|
3061
|
+
|
|
3062
|
+
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)">`
|
|
3063
|
+
+ `<div style="display:flex; align-items:center; justify-content:space-between; gap:0.5rem; width:100%;">`
|
|
3064
|
+
+ `<span class="id" style="font-weight:600;">${mainText}</span>`
|
|
3065
|
+
+ `<div style="display:flex; align-items:center; gap:0.4rem; flex:none;">`
|
|
3066
|
+
+ `<span class="${badgeClass}" style="font-size:0.68rem; padding:0.1rem 0.35rem;">${escapeHtml(item.category)}</span>`
|
|
3067
|
+
+ (isSelected ? '<span class="cur" style="font-weight:600;">active</span>' : '')
|
|
3068
|
+
+ `</div>`
|
|
3069
|
+
+ `</div>`
|
|
3070
|
+
+ (item.desc && item.id ? `<div style="font-size:0.72rem; color:var(--muted); font-family:var(--font-sans);">${escapeHtml(item.desc)}</div>` : '')
|
|
3071
|
+
+ `</li>`;
|
|
3072
|
+
}).join('');
|
|
3073
|
+
|
|
3074
|
+
setComboActive(combo, combo.activeIndex);
|
|
3075
|
+
}
|
|
3076
|
+
|
|
2879
3077
|
function setComboActive(combo, i) {
|
|
2880
3078
|
const input = combo.querySelector('input');
|
|
2881
3079
|
combo.activeIndex = i;
|
|
@@ -2894,12 +3092,17 @@
|
|
|
2894
3092
|
|
|
2895
3093
|
function chooseComboOption(li) {
|
|
2896
3094
|
const combo = li.closest('.combo');
|
|
2897
|
-
const
|
|
2898
|
-
|
|
2899
|
-
|
|
3095
|
+
const idx = Number(li.dataset.i);
|
|
3096
|
+
const model = combo?.matches?.[idx];
|
|
3097
|
+
if (model === undefined) return;
|
|
3098
|
+
const input = combo.querySelector('input');
|
|
3099
|
+
input.value = model;
|
|
2900
3100
|
formDirty = true;
|
|
2901
3101
|
closeCombo(combo);
|
|
2902
3102
|
updateWindowNotes();
|
|
3103
|
+
if (input.id === 'idle-compact-model') {
|
|
3104
|
+
saveIdleCompact();
|
|
3105
|
+
}
|
|
2903
3106
|
}
|
|
2904
3107
|
|
|
2905
3108
|
function onComboKeydown(e) {
|
|
@@ -3241,6 +3444,10 @@
|
|
|
3241
3444
|
document.getElementById('p-url')?.addEventListener('input', syncEffortOptions);
|
|
3242
3445
|
document.getElementById('p-tool')?.addEventListener('change', syncEffortOptions);
|
|
3243
3446
|
|
|
3447
|
+
// Wire Idle Compaction summarization model combobox
|
|
3448
|
+
const compactCombo = document.getElementById('combo-compact-model');
|
|
3449
|
+
if (compactCombo) wireCombo(compactCombo);
|
|
3450
|
+
|
|
3244
3451
|
// Trigger initial route
|
|
3245
3452
|
handleRoute();
|
|
3246
3453
|
|