llm-switcher 1.6.3 → 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.
Files changed (3) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/package.json +1 -1
  3. package/ui.html +107 -104
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## Release 1.6.3 — 2026-10-08
4
9
 
5
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-switcher",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "Zero-dependency multi-protocol edge gateway & provider switcher for Claude Code and Codex, with OpenAI-compatible, Anthropic and Vertex upstreams",
5
5
  "keywords": [
6
6
  "llm",
package/ui.html CHANGED
@@ -1105,12 +1105,23 @@
1105
1105
  </div>
1106
1106
  </div>
1107
1107
 
1108
+ <div class="row">
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
+
1108
1119
  <div class="row" style="align-items:flex-start;">
1109
1120
  <div class="who" style="padding-top:0.25rem;">
1110
1121
  <div class="l">Summarization Model</div>
1111
- <div class="meta">Select from discovered/profile models or enter custom ID (empty = deterministic truncation)</div>
1122
+ <div class="meta" id="idle-compact-models-status">Select model from provider URL or enter custom ID (empty = deterministic truncation)</div>
1112
1123
  </div>
1113
- <div style="min-width:18rem; flex:1; max-width:28rem; margin-left:auto; display:flex; flex-direction:column; gap:0.4rem;">
1124
+ <div style="min-width:18rem; flex:1; max-width:26rem; margin-left:auto;">
1114
1125
  <div class="combo" id="combo-compact-model">
1115
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">
1116
1127
  <button type="button" class="combo-toggle" tabindex="-1" aria-label="Show available summary models">
@@ -1118,9 +1129,6 @@
1118
1129
  </button>
1119
1130
  <ul class="combo-list" id="idle-compact-model-list" role="listbox" aria-label="Available summarization models" hidden></ul>
1120
1131
  </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>
1124
1132
  </div>
1125
1133
  </div>
1126
1134
 
@@ -2072,10 +2080,7 @@
2072
2080
  const elKb = document.getElementById('idle-compact-kb');
2073
2081
  if (elKb) elKb.value = (c.minBytes ?? 65536) / 1024;
2074
2082
  const elModel = document.getElementById('idle-compact-model');
2075
- if (elModel) {
2076
- elModel.value = c.model || '';
2077
- updateCompactModelPresets(c.model || '');
2078
- }
2083
+ if (elModel) elModel.value = c.model || '';
2079
2084
  const elCodex = document.getElementById('idle-compact-codex');
2080
2085
  if (elCodex) elCodex.checked = !!c.codex;
2081
2086
  const elKeep = document.getElementById('idle-compact-keep');
@@ -2125,66 +2130,107 @@
2125
2130
  }
2126
2131
  }
2127
2132
 
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;
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();
2135
2163
  }
2136
2164
 
2137
- function setCompactModel(model) {
2138
- const inp = document.getElementById('idle-compact-model');
2139
- if (inp) {
2140
- inp.value = model;
2141
- saveIdleCompact();
2142
- updateCompactModelPresets(model);
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'}`;
2143
2170
  }
2144
2171
  }
2145
2172
 
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() || '';
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);
2151
2179
  }
2180
+ }
2152
2181
 
2153
- const presets = [
2154
- { id: '', label: 'Deterministic (none)' },
2155
- { id: 'haiku', label: 'haiku' },
2156
- { id: 'subagent', label: 'subagent' }
2157
- ];
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
+ }
2158
2192
 
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
- }
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)}`;
2168
2214
  }
2169
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);
2170
2224
  }
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
2225
  }
2181
2226
 
2182
2227
  async function loadIdleCompact() {
2183
- ensureCatalogLoaded();
2228
+ populateCompactSourceProfiles();
2184
2229
  try {
2185
2230
  const r = await api('/api/idle-compact');
2186
2231
  if (r.ok) showIdleCompact((await r.json()).idleCompact);
2187
2232
  } catch { /* the card stays at its defaults; nothing here is required to route */ }
2233
+ await reloadCompactModels();
2188
2234
  }
2189
2235
 
2190
2236
  async function saveIdleCompact() {
@@ -2861,9 +2907,6 @@
2861
2907
  combo.dataset.filter = '1';
2862
2908
  combo.activeIndex = -1;
2863
2909
  openCombo(combo);
2864
- if (input.id === 'idle-compact-model') {
2865
- updateCompactModelPresets(input.value.trim());
2866
- }
2867
2910
  });
2868
2911
  input.addEventListener('keydown', onComboKeydown);
2869
2912
  input.addEventListener('blur', () => closeCombo(combo));
@@ -2971,54 +3014,16 @@
2971
3014
  // 1. Deterministic option (empty string)
2972
3015
  add('', 'Deterministic Truncation (no external call)', 'Pure heuristic context truncation · Zero latency & zero tokens', 'Deterministic');
2973
3016
 
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
- }
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');
3007
3024
  }
3008
3025
  }
3009
3026
 
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
3027
  return items;
3023
3028
  }
3024
3029
 
@@ -3030,13 +3035,13 @@
3030
3035
  const filtered = allItems.filter(item => {
3031
3036
  if (!q) return true;
3032
3037
  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);
3038
+ return item.id.toLowerCase().includes(q) || item.label.toLowerCase().includes(q);
3034
3039
  }).slice(0, 150);
3035
3040
 
3036
3041
  combo.matches = filtered.map(item => item.id);
3037
3042
 
3038
3043
  if (!filtered.length) {
3039
- list.innerHTML = '<li class="combo-empty">No matching model found. The typed model will be used directly.</li>';
3044
+ list.innerHTML = '<li class="combo-empty">No matching model found. The typed model ID will be used directly.</li>';
3040
3045
  return;
3041
3046
  }
3042
3047
 
@@ -3048,8 +3053,7 @@
3048
3053
  const isSelected = item.id === current;
3049
3054
  let badgeClass = 'badge outline';
3050
3055
  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';
3056
+ else if (item.category === 'Upstream Model') badgeClass = 'badge';
3053
3057
 
3054
3058
  const cut = item.id.lastIndexOf('/') + 1;
3055
3059
  const prefix = cut ? `<span class="pfx">${markMatch(item.id.slice(0, cut), q)}</span>` : '';
@@ -3098,7 +3102,6 @@
3098
3102
  updateWindowNotes();
3099
3103
  if (input.id === 'idle-compact-model') {
3100
3104
  saveIdleCompact();
3101
- updateCompactModelPresets(model);
3102
3105
  }
3103
3106
  }
3104
3107