free-coding-models 0.5.27 → 0.5.29
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/bin/free-coding-models.js +10 -2
- package/changelog/v0.5.27.md +2 -0
- package/changelog/v0.5.28.md +5 -0
- package/changelog/v0.5.29.md +19 -0
- package/package.json +1 -1
- package/sources.js +3 -2
- package/src/core/config.js +25 -18
- package/src/core/endpoint-installer.js +5 -9
- package/src/core/legacy-proxy-cleanup.js +2 -0
- package/src/core/model-merger.js +4 -13
- package/src/core/router-daemon.js +160 -43
- package/src/core/router-dashboard.js +63 -25
- package/src/core/shared-helpers.js +117 -0
- package/src/core/sync-set.js +6 -21
- package/src/core/tool-launchers.js +24 -92
- package/src/core/utils.js +26 -100
- package/src/tui/app.js +18 -2
- package/src/tui/command-palette.js +3 -1
- package/src/tui/key-handler.js +229 -23
- package/src/tui/overlays.js +13 -1
- package/src/tui/render-table.js +21 -5
- package/src/tui/tui-state.js +6 -0
- package/web/dist/assets/index-BMU58Jju.js +41 -0
- package/web/dist/assets/index-Dd1jOGEn.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +5 -0
- package/web/src/components/atoms/HealthCell.jsx +12 -7
- package/web/src/components/atoms/HealthCell.module.css +4 -0
- package/web/src/components/atoms/StatusDot.jsx +5 -2
- package/web/src/components/atoms/StatusDot.module.css +12 -7
- package/web/src/components/dashboard/ModelTable.jsx +5 -2
- package/web/src/components/dashboard/ModelTable.module.css +9 -0
- package/web/src/components/router/RouterView.jsx +124 -56
- package/web/src/components/router/RouterView.module.css +135 -1
- package/src/core/product-flags.js +0 -9
- package/web/dist/assets/index-JUPDB-J-.js +0 -41
- package/web/dist/assets/index-avN2GM3H.css +0 -1
package/src/core/utils.js
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* @exports sortResults, filterByTier, findBestModel, parseArgs
|
|
42
42
|
* @exports scoreModelForTask, getTopRecommendations
|
|
43
43
|
* @exports TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS
|
|
44
|
+
* @exports parseCtxToK, parseSweToNum, formatCtxWindow, labelFromId, NEW_MODELS, getVersionStatusInfo, formatResultsAsJSON
|
|
44
45
|
*
|
|
45
46
|
* @see bin/free-coding-models.js — main CLI that imports these utils
|
|
46
47
|
* @see sources.js — model definitions consumed by these functions
|
|
@@ -115,80 +116,27 @@ export const getAvg = (r) => {
|
|
|
115
116
|
//
|
|
116
117
|
// 📖 The "wasUpBefore" check is key — it distinguishes between a model that's
|
|
117
118
|
// temporarily flaky vs one that was never reachable in the first place.
|
|
119
|
+
// 📖 NEW_MODEL_DURATION_MS — how long a model shows the 🆕 badge after being added.
|
|
120
|
+
// 📖 5 days in milliseconds.
|
|
121
|
+
export const NEW_MODEL_DURATION_MS = 5 * 24 * 60 * 60 * 1000
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 📖 Check if a model should display the 🆕 badge.
|
|
125
|
+
* 📖 A model is "new" if its addedDate is within the last 5 days.
|
|
126
|
+
* 📖 `addedDate` comes from sources.js as the optional 6th element of model tuples.
|
|
127
|
+
* @param {string|null|undefined} addedDate — ISO date string (e.g. '2026-06-10')
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
export function isNewModel(addedDate) {
|
|
131
|
+
if (!addedDate || typeof addedDate !== 'string') return false
|
|
132
|
+
const added = Date.parse(addedDate)
|
|
133
|
+
if (!Number.isFinite(added)) return false
|
|
134
|
+
return (Date.now() - added) < NEW_MODEL_DURATION_MS
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 📖 NEW_MODELS kept for backward compat with web/website surfaces that
|
|
138
|
+
// 📖 don't have addedDate. Will be removed once all surfaces migrate.
|
|
118
139
|
export const NEW_MODELS = new Set([
|
|
119
|
-
'nvidia/nemotron-3-ultra-550b-a55b',
|
|
120
|
-
'@cf/meta/llama-3.2-90b-instruct',
|
|
121
|
-
'@cf/mistralai/mistral-7b-instruct-v0.2',
|
|
122
|
-
'@cf/google/gemma-2-9b-it',
|
|
123
|
-
'@cf/anthropic/claude-3-5-sonnet',
|
|
124
|
-
'@cf/openai/gpt-4o-mini',
|
|
125
|
-
'@cf/qwen/qwen3-30b-a3b-fp8',
|
|
126
|
-
'@cf/qwen/qwen2.5-coder-32b-instruct',
|
|
127
|
-
'@cf/meta/llama-3.3-70b-instruct-fp8-fast',
|
|
128
|
-
'@cf/google/gemma-4-26b-a4b-it',
|
|
129
|
-
'@cf/mistralai/mistral-small-3.1-24b-instruct',
|
|
130
|
-
'@cf/ibm/granite-4.0-h-micro',
|
|
131
|
-
'minimaxai/minimax-m2.7',
|
|
132
|
-
'z-ai/glm-5.1',
|
|
133
|
-
'moonshotai/kimi-k2.6',
|
|
134
|
-
'stepfun-ai/step-3.5-flash',
|
|
135
|
-
'stepfun-ai/step-3.7-flash',
|
|
136
|
-
'qwen/qwen3-coder-480b-a35b-instruct',
|
|
137
|
-
'qwen/qwen3.5-397b-a17b',
|
|
138
|
-
'meta/llama-4-maverick-17b-128e-instruct',
|
|
139
|
-
'mistralai/mistral-medium-3.5-128b',
|
|
140
|
-
'mistralai/mistral-small-4-119b-2603',
|
|
141
|
-
'qwen/qwen3.5-122b-a10b',
|
|
142
|
-
'mistralai/mistral-large-3-675b-instruct-2512',
|
|
143
|
-
'nvidia/nemotron-3-super-120b-a12b',
|
|
144
|
-
'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning',
|
|
145
|
-
'google/gemma-4-31b-it',
|
|
146
|
-
'bytedance/seed-oss-36b-instruct',
|
|
147
|
-
'stockmark/stockmark-2-100b-instruct',
|
|
148
|
-
'mistralai/ministral-14b-instruct-2512',
|
|
149
|
-
'meta/llama-3.2-11b-vision-instruct',
|
|
150
|
-
'microsoft/phi-4-mini-instruct',
|
|
151
|
-
'gemma-3-12b-it',
|
|
152
|
-
'nvidia/nemotron-nano-9b-v2',
|
|
153
|
-
'openrouter/owl-alpha',
|
|
154
|
-
'nousresearch/hermes-3-llama-3.1-405b:free',
|
|
155
|
-
'nvidia/nemotron-nano-30b-a3b:free',
|
|
156
|
-
'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
|
|
157
|
-
'meta-llama/llama-3.3-70b-instruct:free',
|
|
158
|
-
'meta-llama/llama-3.2-3b-instruct:free',
|
|
159
|
-
'liquid/lfm-2.5-1.2b-instruct:free',
|
|
160
|
-
'liquid/lfm-2.5-1.2b-thinking:free',
|
|
161
|
-
'qwen3.7-max',
|
|
162
|
-
'qwen3-max',
|
|
163
|
-
'qwen3.6-plus',
|
|
164
|
-
'qwen3-235b-a22b',
|
|
165
|
-
'qwen3.5-plus',
|
|
166
|
-
'qwen3-coder-plus',
|
|
167
|
-
'qwen3-coder-next',
|
|
168
|
-
'qwen3.6-flash',
|
|
169
|
-
'qwen3.5-flash',
|
|
170
|
-
'qwen3-coder-flash',
|
|
171
|
-
'qwen3-32b',
|
|
172
|
-
'qwen3-coder-30b-a3b-instruct',
|
|
173
|
-
'holo2-30b-a3b',
|
|
174
|
-
'llama-3.3-70b-instruct',
|
|
175
|
-
'mistral-small-3.2-24b-instruct-2506',
|
|
176
|
-
'gemma-3-27b-it',
|
|
177
|
-
'qwen3.5-397b-a17b',
|
|
178
|
-
'qwen3-coder-30b-a3b-instruct',
|
|
179
|
-
'gpt-oss-120b',
|
|
180
|
-
'gpt-oss-20b',
|
|
181
|
-
'Meta-Llama-3_3-70B-Instruct',
|
|
182
|
-
'Qwen3-32B',
|
|
183
|
-
'Mistral-Small-3.2-24B-Instruct-2506',
|
|
184
|
-
'Mistral-7B-Instruct-v0.3',
|
|
185
|
-
'Mistral-Nemo-Instruct-2407',
|
|
186
|
-
'Qwen3.5-9B',
|
|
187
|
-
'big-pickle',
|
|
188
|
-
'deepseek-v4-flash-free',
|
|
189
|
-
'mimo-v2.5-free',
|
|
190
|
-
'nemotron-3-super-free',
|
|
191
|
-
'minimax-m3-free'
|
|
192
140
|
]);
|
|
193
141
|
|
|
194
142
|
export const getVerdict = (r) => {
|
|
@@ -376,34 +324,12 @@ export const sortResults = (results, sortColumn, sortDirection, { benchmarkResul
|
|
|
376
324
|
break
|
|
377
325
|
case 'swe': {
|
|
378
326
|
// 📖 Sort by SWE-bench score — higher is better
|
|
379
|
-
|
|
380
|
-
const parseSwe = (score) => {
|
|
381
|
-
if (!score || score === '—') return 0
|
|
382
|
-
const num = parseFloat(score.replace('%', ''))
|
|
383
|
-
return isNaN(num) ? 0 : num
|
|
384
|
-
}
|
|
385
|
-
cmp = parseSwe(a.sweScore) - parseSwe(b.sweScore)
|
|
327
|
+
cmp = parseSweToNum(a.sweScore) - parseSweToNum(b.sweScore)
|
|
386
328
|
break
|
|
387
329
|
}
|
|
388
330
|
case 'ctx': {
|
|
389
|
-
// 📖 Sort by context window size — larger is better
|
|
390
|
-
|
|
391
|
-
const parseCtx = (ctx) => {
|
|
392
|
-
if (!ctx || ctx === '—') return 0
|
|
393
|
-
const str = ctx.toLowerCase()
|
|
394
|
-
// 📖 Handle millions (1m = 1000k)
|
|
395
|
-
if (str.includes('m')) {
|
|
396
|
-
const num = parseFloat(str.replace('m', ''))
|
|
397
|
-
return num * 1000
|
|
398
|
-
}
|
|
399
|
-
// 📖 Handle thousands (128k)
|
|
400
|
-
if (str.includes('k')) {
|
|
401
|
-
const num = parseFloat(str.replace('k', ''))
|
|
402
|
-
return num
|
|
403
|
-
}
|
|
404
|
-
return 0
|
|
405
|
-
}
|
|
406
|
-
cmp = parseCtx(a.ctx) - parseCtx(b.ctx)
|
|
331
|
+
// 📖 Sort by context window size — larger is better (uses parseCtxToK)
|
|
332
|
+
cmp = parseCtxToK(a.ctx) - parseCtxToK(b.ctx)
|
|
407
333
|
break
|
|
408
334
|
}
|
|
409
335
|
case 'condition':
|
|
@@ -727,7 +653,7 @@ export const CONTEXT_BUDGETS = {
|
|
|
727
653
|
|
|
728
654
|
// 📖 parseCtxToK: Convert context window string ("128k", "1m", "200k") into numeric K tokens.
|
|
729
655
|
// 📖 Used by the scoring engine to compare against CONTEXT_BUDGETS thresholds.
|
|
730
|
-
function parseCtxToK(ctx) {
|
|
656
|
+
export function parseCtxToK(ctx) {
|
|
731
657
|
if (!ctx || ctx === '—') return 0
|
|
732
658
|
const str = ctx.toLowerCase()
|
|
733
659
|
if (str.includes('m')) return parseFloat(str.replace('m', '')) * 1000
|
|
@@ -756,7 +682,7 @@ export function labelFromId(id) {
|
|
|
756
682
|
|
|
757
683
|
// 📖 parseSweToNum: Convert SWE-bench score string ("49.2%", "73.1%") into a 0–100 number.
|
|
758
684
|
// 📖 Returns 0 for missing or invalid scores.
|
|
759
|
-
function parseSweToNum(sweScore) {
|
|
685
|
+
export function parseSweToNum(sweScore) {
|
|
760
686
|
if (!sweScore || sweScore === '—') return 0
|
|
761
687
|
const num = parseFloat(sweScore.replace('%', ''))
|
|
762
688
|
return isNaN(num) ? 0 : num
|
package/src/tui/app.js
CHANGED
|
@@ -307,14 +307,26 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
307
307
|
|
|
308
308
|
let results = MODELS
|
|
309
309
|
.filter(([,,,,,providerKey]) => isProviderEnabled(config, providerKey))
|
|
310
|
-
.map(([modelId, label, tier, sweScore, ctx, providerKey], i) => ({
|
|
311
|
-
idx: i + 1, modelId, label, tier, sweScore, ctx, providerKey,
|
|
310
|
+
.map(([modelId, label, tier, sweScore, ctx, providerKey, addedDate], i) => ({
|
|
311
|
+
idx: i + 1, modelId, label, tier, sweScore, ctx, providerKey, addedDate: addedDate || null,
|
|
312
312
|
status: 'pending',
|
|
313
313
|
pings: [], // 📖 All ping results (ms or 'TIMEOUT')
|
|
314
314
|
httpCode: null,
|
|
315
315
|
isPinging: false, // 📖 Per-row live flag so Last Ping can keep last value and show a spinner during refresh.
|
|
316
316
|
hidden: false, // 📖 Simple flag to hide/show models
|
|
317
317
|
}))
|
|
318
|
+
|
|
319
|
+
// 📖 Auto-hide models that were previously marked broken by the 404 probe.
|
|
320
|
+
// 📖 Only applies when autoHideBrokenModels setting is enabled (default).
|
|
321
|
+
if (config.settings?.autoHideBrokenModels !== false && config.hiddenModels instanceof Set) {
|
|
322
|
+
for (const r of results) {
|
|
323
|
+
const modelKey = `${r.providerKey}/${r.modelId}`
|
|
324
|
+
if (config.hiddenModels.has(modelKey)) {
|
|
325
|
+
r.hidden = true
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
318
330
|
syncFavoriteFlags(results, config)
|
|
319
331
|
// 📖 Garbage-collect favorites that reference models no longer in sources.js,
|
|
320
332
|
// 📖 so the router dashboard only shows real, launchable models.
|
|
@@ -826,6 +838,10 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
826
838
|
benchmarkResults: state.benchmarkResults,
|
|
827
839
|
benchmarkRunning: state.benchmarkRunning,
|
|
828
840
|
headerFlashColumn: state.headerFlashColumn,
|
|
841
|
+
probeRunning: state.probeRunning,
|
|
842
|
+
probeTotal: state.probeTotal,
|
|
843
|
+
probeCompleted: state.probeCompleted,
|
|
844
|
+
probeHiddenCount: state.probeHiddenCount,
|
|
829
845
|
}
|
|
830
846
|
if (state.commandPaletteOpen) {
|
|
831
847
|
if (!state.commandPaletteFrozenTable) {
|
|
@@ -200,7 +200,9 @@ const BASE_COMMAND_TREE = [
|
|
|
200
200
|
],
|
|
201
201
|
},
|
|
202
202
|
{ id: 'action-cycle-theme', label: 'Cycle theme', shortcut: 'G', icon: '🌗', description: 'Switch dark/light/auto', keywords: ['theme', 'dark', 'light', 'auto'] },
|
|
203
|
-
{ id: 'action-reset-view', label: 'Reset view', shortcut: 'N', icon: '
|
|
203
|
+
{ id: 'action-reset-view', label: 'Reset view', shortcut: 'N', icon: '\u{1F504}', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
|
|
204
|
+
{ id: 'action-probe-404', label: 'Probe 404 Models', shortcut: 'Ctrl+Shift+P', icon: '\u{1F50D}', description: 'Test all configured models for 404/410 errors. Auto-hides broken models.', keywords: ['probe', '404', 'broken', 'dead', 'health', 'check', 'test', 'verify'] },
|
|
205
|
+
{ id: 'action-toggle-auto-hide-broken', label: 'Toggle Auto-hide Broken Models', icon: '\u{1F6A9}', description: 'Enable/disable auto-hiding of 404/410 models. Unhides all when disabled.', keywords: ['auto', 'hide', 'broken', '404', 'models', 'toggle', 'probe'] },
|
|
204
206
|
],
|
|
205
207
|
},
|
|
206
208
|
// 📖 Pages - directly at root level, not in submenu
|
package/src/tui/key-handler.js
CHANGED
|
@@ -39,6 +39,7 @@ import { join, dirname } from 'node:path'
|
|
|
39
39
|
import { fileURLToPath } from 'node:url'
|
|
40
40
|
import { spawn } from 'node:child_process'
|
|
41
41
|
import { cleanupLegacyProxyArtifacts } from '../core/legacy-proxy-cleanup.js'
|
|
42
|
+
import { sleep } from '../core/shared-helpers.js'
|
|
42
43
|
import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
|
|
43
44
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
44
45
|
import { syncShellEnv, ensureShellRcSource, removeShellEnv } from '../core/shell-env.js'
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
cycleRouterDashboardProbeMode,
|
|
54
55
|
openRouterDashboardOverlay,
|
|
55
56
|
restartRouterDashboardDaemon,
|
|
57
|
+
setDashboardNotice,
|
|
56
58
|
toggleRouterDashboardProbePause,
|
|
57
59
|
} from '../core/router-dashboard.js'
|
|
58
60
|
import {
|
|
@@ -61,6 +63,7 @@ import {
|
|
|
61
63
|
handlePlaygroundKeypress,
|
|
62
64
|
} from '../core/playground.js'
|
|
63
65
|
import { benchmarkModel } from '../core/benchmark.js'
|
|
66
|
+
import { isPackageDevMode } from '../core/updater.js'
|
|
64
67
|
|
|
65
68
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
66
69
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
@@ -76,6 +79,52 @@ const PROVIDER_TEST_MODEL_OVERRIDES = {
|
|
|
76
79
|
const SETTINGS_TEST_MAX_ATTEMPTS = 10
|
|
77
80
|
const SETTINGS_TEST_RETRY_DELAY_MS = 4000
|
|
78
81
|
|
|
82
|
+
// 📖 spawnDaemonCommand — spawns `--daemon-bg` or `--daemon-stop` and captures
|
|
83
|
+
// 📖 the JSON result from stdout so we can surface startup errors to the user.
|
|
84
|
+
// 📖 Falls back to a generic notice when stdout isn't parseable.
|
|
85
|
+
function spawnDaemonCommand(state, args) {
|
|
86
|
+
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
87
|
+
// 📖 Inherit FCM_DEV so the spawned daemon matches the TUI's dev/prod mode.
|
|
88
|
+
// 📖 Without this, a dev-mode TUI (git checkout) spawns a production daemon
|
|
89
|
+
// 📖 that writes to the wrong PID/port files and can't be discovered later.
|
|
90
|
+
// 📖 isPackageDevMode() detects git checkouts even without --dev or FCM_DEV=1.
|
|
91
|
+
const env = { ...process.env }
|
|
92
|
+
if (isPackageDevMode() && !env.FCM_DEV) env.FCM_DEV = '1'
|
|
93
|
+
const child = spawn('node', [binPath, ...args], {
|
|
94
|
+
detached: true,
|
|
95
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
96
|
+
env,
|
|
97
|
+
})
|
|
98
|
+
child.unref()
|
|
99
|
+
|
|
100
|
+
// 📖 Only capture stdout for start commands — stop doesn't need error surfacing.
|
|
101
|
+
if (!args.includes('--daemon-bg')) return
|
|
102
|
+
|
|
103
|
+
let stdout = ''
|
|
104
|
+
let stderr = ''
|
|
105
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk })
|
|
106
|
+
child.stderr?.on('data', (chunk) => { stderr += chunk })
|
|
107
|
+
child.on('close', (code) => {
|
|
108
|
+
if (code === 0) return
|
|
109
|
+
// 📖 Try to parse the JSON result from --daemon-bg for a precise error message.
|
|
110
|
+
let errorMsg = null
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(stdout.trim())
|
|
113
|
+
if (parsed?.error) errorMsg = parsed.error
|
|
114
|
+
} catch {}
|
|
115
|
+
if (!errorMsg && stderr.trim()) {
|
|
116
|
+
// 📖 Common startup crash reasons: port in use, config corruption, etc.
|
|
117
|
+
const lines = stderr.trim().split('\n').filter(l => !l.startsWith('node:'))
|
|
118
|
+
errorMsg = lines.slice(0, 2).join(' — ') || stderr.trim().slice(0, 120)
|
|
119
|
+
}
|
|
120
|
+
if (errorMsg) {
|
|
121
|
+
setDashboardNotice(state, 'error', `Daemon failed to start: ${errorMsg}`, 8000)
|
|
122
|
+
} else if (code !== 0) {
|
|
123
|
+
setDashboardNotice(state, 'error', `Daemon failed to start (exit code ${code})`, 8000)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
79
128
|
// 📖 PROVIDER_AUTH_ENDPOINTS maps provider keys to their auth-check URL + method.
|
|
80
129
|
// 📖 For most providers this is the /models endpoint (returns 200=valid, 401=invalid).
|
|
81
130
|
// 📖 Providers without an auth-check endpoint use null (falls back to chat completion ping).
|
|
@@ -114,11 +163,7 @@ const PROVIDER_AUTH_ENDPOINTS = {
|
|
|
114
163
|
'ollama-cloud': { url: 'https://ollama.com/v1/models', method: 'GET' },
|
|
115
164
|
}
|
|
116
165
|
|
|
117
|
-
// 📖 Sleep
|
|
118
|
-
// 📖 back off between retries without leaking timer logic into the rest of the TUI.
|
|
119
|
-
function sleep(ms) {
|
|
120
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
121
|
-
}
|
|
166
|
+
// 📖 Sleep imported from shared-helpers.js
|
|
122
167
|
|
|
123
168
|
// 📖 testProviderKeyDirect: Fast auth-only check using /v1/account or /v1/models.
|
|
124
169
|
// 📖 Fires 3 parallel probes to get a fast decisive result (auth error vs timeout vs 200).
|
|
@@ -792,6 +837,42 @@ export function createKeyHandler(ctx) {
|
|
|
792
837
|
})
|
|
793
838
|
}
|
|
794
839
|
|
|
840
|
+
// 📖 toggleAutoHideBrokenModels: Toggle auto-hiding of 404/410 models from probe.
|
|
841
|
+
// 📖 When disabling, unhide all previously hidden models so they become visible again.
|
|
842
|
+
function toggleAutoHideBrokenModels() {
|
|
843
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
844
|
+
const wasEnabled = state.config.settings.autoHideBrokenModels !== false
|
|
845
|
+
state.config.settings.autoHideBrokenModels = !wasEnabled
|
|
846
|
+
|
|
847
|
+
if (!wasEnabled) {
|
|
848
|
+
// 📖 Just enabled — models that were hidden before enabling will be picked up on next probe
|
|
849
|
+
} else {
|
|
850
|
+
// 📖 Just disabled — unhide all probe-hidden models so user can see them again
|
|
851
|
+
if (state.config.hiddenModels instanceof Set && state.config.hiddenModels.size > 0) {
|
|
852
|
+
const keysToUnhide = [...state.config.hiddenModels]
|
|
853
|
+
state.config.hiddenModels.clear()
|
|
854
|
+
for (const key of keysToUnhide) {
|
|
855
|
+
const [pk, ...rest] = key.split('/')
|
|
856
|
+
const modelId = rest.join('/')
|
|
857
|
+
const result = state.results.find(r => r.providerKey === pk && r.modelId === modelId)
|
|
858
|
+
if (result) result.hidden = false
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
saveConfig(state.config)
|
|
864
|
+
const hiddenCount = state.config.hiddenModels instanceof Set ? state.config.hiddenModels.size : 0
|
|
865
|
+
state.settingsSyncStatus = {
|
|
866
|
+
type: 'success',
|
|
867
|
+
msg: state.config.settings.autoHideBrokenModels
|
|
868
|
+
? `✅ Auto-hide enabled — broken models (404/410) from Ctrl+Shift+P probe are hidden automatically.${hiddenCount ? ` (${hiddenCount} currently hidden)` : ''}`
|
|
869
|
+
: '✅ Auto-hide disabled — all models are visible. Previously hidden models have been unhidden.',
|
|
870
|
+
}
|
|
871
|
+
trackAppAction('auto_hide_broken_models_toggled', {
|
|
872
|
+
enabled: state.config.settings.autoHideBrokenModels !== false,
|
|
873
|
+
})
|
|
874
|
+
}
|
|
875
|
+
|
|
795
876
|
function toggleShellEnv() {
|
|
796
877
|
if (!state.config.settings) state.config.settings = {}
|
|
797
878
|
const currentlyEnabled = state.config.settings.shellEnvEnabled === true
|
|
@@ -1132,6 +1213,96 @@ export function createKeyHandler(ctx) {
|
|
|
1132
1213
|
return Promise.all(workers).then(() => results)
|
|
1133
1214
|
}
|
|
1134
1215
|
|
|
1216
|
+
// 📖 runBrokenModelProbe: Probe all configured models for 404/broken endpoints.
|
|
1217
|
+
// 📖 Sends a real chat-completion request to each model with an API key.
|
|
1218
|
+
// 📖 Models returning 404 or 410 are auto-hidden when setting is enabled.
|
|
1219
|
+
// 📖 Results flash live in the TUI table — models flip status in real time.
|
|
1220
|
+
async function runBrokenModelProbe(state) {
|
|
1221
|
+
if (state.probeRunning) return
|
|
1222
|
+
|
|
1223
|
+
// 📖 Only probe models where the user has an API key configured
|
|
1224
|
+
const probeable = state.results.filter(r => {
|
|
1225
|
+
const apiKey = getApiKey(state.config, r.providerKey)
|
|
1226
|
+
return !!apiKey
|
|
1227
|
+
})
|
|
1228
|
+
|
|
1229
|
+
if (probeable.length === 0) return
|
|
1230
|
+
|
|
1231
|
+
state.probeRunning = true
|
|
1232
|
+
state.probeTotal = probeable.length
|
|
1233
|
+
state.probeCompleted = 0
|
|
1234
|
+
state.probeHiddenCount = 0
|
|
1235
|
+
|
|
1236
|
+
const autoHide = state.config.settings?.autoHideBrokenModels !== false
|
|
1237
|
+
const HTTP_CODES_404 = new Set([404, '404'])
|
|
1238
|
+
const HTTP_CODES_GONE = new Set([410, '410'])
|
|
1239
|
+
|
|
1240
|
+
const tasks = probeable.map(r => async () => {
|
|
1241
|
+
const apiKey = getApiKey(state.config, r.providerKey) ?? null
|
|
1242
|
+
const providerUrl = sources[r.providerKey]?.url ?? null
|
|
1243
|
+
if (!apiKey || !providerUrl) {
|
|
1244
|
+
state.probeCompleted++
|
|
1245
|
+
return { model: r, ok: false, reason: 'no_key' }
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
try {
|
|
1249
|
+
const { code } = await ping(apiKey, r.modelId, r.providerKey, providerUrl)
|
|
1250
|
+
state.probeCompleted++
|
|
1251
|
+
|
|
1252
|
+
if (HTTP_CODES_404.has(code) || HTTP_CODES_GONE.has(code)) {
|
|
1253
|
+
// 📖 Model is broken (404/410) — mark it
|
|
1254
|
+
r.status = 'down'
|
|
1255
|
+
r.httpCode = String(code)
|
|
1256
|
+
|
|
1257
|
+
if (autoHide) {
|
|
1258
|
+
const modelKey = `${r.providerKey}/${r.modelId}`
|
|
1259
|
+
if (!state.config.hiddenModels) state.config.hiddenModels = new Set()
|
|
1260
|
+
state.config.hiddenModels.add(modelKey)
|
|
1261
|
+
r.hidden = true
|
|
1262
|
+
state.probeHiddenCount++
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return { model: r, ok: false, code, reason: 'broken' }
|
|
1266
|
+
} else if (code === '200') {
|
|
1267
|
+
// 📖 Model is alive — unhide if it was previously hidden by probe
|
|
1268
|
+
const modelKey = `${r.providerKey}/${r.modelId}`
|
|
1269
|
+
if (state.config.hiddenModels?.has(modelKey)) {
|
|
1270
|
+
state.config.hiddenModels.delete(modelKey)
|
|
1271
|
+
r.hidden = false
|
|
1272
|
+
}
|
|
1273
|
+
r.status = 'up'
|
|
1274
|
+
return { model: r, ok: true, code }
|
|
1275
|
+
} else {
|
|
1276
|
+
// 📖 Other codes (401, 429, etc.) — leave status unchanged, don't hide
|
|
1277
|
+
return { model: r, ok: false, code, reason: 'other' }
|
|
1278
|
+
}
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
state.probeCompleted++
|
|
1281
|
+
return { model: r, ok: false, reason: 'error', error: err?.message }
|
|
1282
|
+
}
|
|
1283
|
+
})
|
|
1284
|
+
|
|
1285
|
+
await runWithConcurrency(tasks, 5)
|
|
1286
|
+
|
|
1287
|
+
// 📖 Persist hidden models set to config
|
|
1288
|
+
if (autoHide && state.probeHiddenCount > 0) {
|
|
1289
|
+
saveConfig(state.config)
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
state.probeRunning = false
|
|
1293
|
+
// 📖 Keep totals visible for a few seconds so user can see results
|
|
1294
|
+
setTimeout(() => {
|
|
1295
|
+
if (!state.probeRunning) {
|
|
1296
|
+
state.probeTotal = 0
|
|
1297
|
+
state.probeCompleted = 0
|
|
1298
|
+
// 📖 Don't reset probeHiddenCount immediately — user needs to see how many were hidden
|
|
1299
|
+
setTimeout(() => {
|
|
1300
|
+
state.probeHiddenCount = 0
|
|
1301
|
+
}, 5000)
|
|
1302
|
+
}
|
|
1303
|
+
}, 3000)
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1135
1306
|
// 📖 runGlobalBenchmark: Benchmark all visible models with up to 5 concurrent requests.
|
|
1136
1307
|
// 📖 Results are stored in state.benchmarkResults (same format as individual benchmarks).
|
|
1137
1308
|
async function runGlobalBenchmark(state) {
|
|
@@ -1486,6 +1657,8 @@ export function createKeyHandler(ctx) {
|
|
|
1486
1657
|
case 'action-toggle-favorite': return toggleFavoriteOnSelectedRow()
|
|
1487
1658
|
case 'action-toggle-favorite-mode': return toggleFavoritesDisplayMode()
|
|
1488
1659
|
case 'action-reset-view': return resetViewSettings()
|
|
1660
|
+
case 'action-probe-404': return runBrokenModelProbe(state)
|
|
1661
|
+
case 'action-toggle-auto-hide-broken': return toggleAutoHideBrokenModels()
|
|
1489
1662
|
default:
|
|
1490
1663
|
return
|
|
1491
1664
|
}
|
|
@@ -1512,13 +1685,22 @@ export function createKeyHandler(ctx) {
|
|
|
1512
1685
|
}
|
|
1513
1686
|
|
|
1514
1687
|
// 📖 Ctrl+U: Global AI Speed Benchmark (benchmark all visible models, 5 concurrent)
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1688
|
+
// Also handles the raw \x15 byte as a fallback for terminals where readline doesn't
|
|
1689
|
+
// set key.ctrl properly (same pattern as Ctrl+C → \x03 fallback).
|
|
1517
1690
|
if ((key.ctrl && key.name === 'u') || str === '\x15') {
|
|
1518
1691
|
await runGlobalBenchmark(state)
|
|
1519
1692
|
return
|
|
1520
1693
|
}
|
|
1521
1694
|
|
|
1695
|
+
// 📖 Ctrl+Shift+P: Probe all configured models for 404/broken endpoints.
|
|
1696
|
+
// 📖 Sends a real chat-completion request to every model that has an API key.
|
|
1697
|
+
// 📖 Models returning 404/410 are auto-hidden (when setting is enabled).
|
|
1698
|
+
// 📖 Results flash live in the table — models flip from down→up in real time.
|
|
1699
|
+
if (key.ctrl && key.shift && key.name === 'p') {
|
|
1700
|
+
await runBrokenModelProbe(state)
|
|
1701
|
+
return
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1522
1704
|
// 📖 Command palette captures the keyboard while active.
|
|
1523
1705
|
if (state.commandPaletteOpen) {
|
|
1524
1706
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
@@ -1669,15 +1851,9 @@ export function createKeyHandler(ctx) {
|
|
|
1669
1851
|
// 📖 S: Toggle daemon start/stop
|
|
1670
1852
|
if (key.name === 's') {
|
|
1671
1853
|
const isRunning = state.routerDashboardStatus === 'ready' || state.routerDashboardStatus === 'partial'
|
|
1672
|
-
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
1673
1854
|
const args = isRunning ? ['--daemon-stop'] : ['--daemon-bg']
|
|
1674
|
-
|
|
1675
1855
|
state.routerDashboardStatus = 'loading'
|
|
1676
|
-
|
|
1677
|
-
detached: true,
|
|
1678
|
-
stdio: 'ignore',
|
|
1679
|
-
})
|
|
1680
|
-
child.unref()
|
|
1856
|
+
spawnDaemonCommand(state, args)
|
|
1681
1857
|
return
|
|
1682
1858
|
}
|
|
1683
1859
|
|
|
@@ -1688,15 +1864,9 @@ export function createKeyHandler(ctx) {
|
|
|
1688
1864
|
|
|
1689
1865
|
if ((state.routerDashboardCursorIndex ?? 0) === btnCursor) {
|
|
1690
1866
|
const isRunning = state.routerDashboardStatus === 'ready' || state.routerDashboardStatus === 'partial'
|
|
1691
|
-
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
1692
1867
|
const args = isRunning ? ['--daemon-stop'] : ['--daemon-bg']
|
|
1693
|
-
|
|
1694
1868
|
state.routerDashboardStatus = 'loading'
|
|
1695
|
-
|
|
1696
|
-
detached: true,
|
|
1697
|
-
stdio: 'ignore',
|
|
1698
|
-
})
|
|
1699
|
-
child.unref()
|
|
1869
|
+
spawnDaemonCommand(state, args)
|
|
1700
1870
|
} else if ((state.routerDashboardCursorIndex ?? 0) === installBtnCursor) {
|
|
1701
1871
|
state.routerDashboardOpen = false
|
|
1702
1872
|
state.installEndpointsOpen = true
|
|
@@ -2277,9 +2447,32 @@ export function createKeyHandler(ctx) {
|
|
|
2277
2447
|
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
2278
2448
|
const child = spawn('node', [binPath, '--daemon-bg'], {
|
|
2279
2449
|
detached: true,
|
|
2280
|
-
stdio: 'ignore',
|
|
2450
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2451
|
+
env: { ...process.env, ...(isPackageDevMode() && !process.env.FCM_DEV ? { FCM_DEV: '1' } : {}) },
|
|
2281
2452
|
})
|
|
2282
2453
|
child.unref()
|
|
2454
|
+
|
|
2455
|
+
// 📖 Capture daemon startup output so we can show the real error reason
|
|
2456
|
+
let onboardingStdout = ''
|
|
2457
|
+
let onboardingStderr = ''
|
|
2458
|
+
child.stdout?.on('data', (chunk) => { onboardingStdout += chunk })
|
|
2459
|
+
child.stderr?.on('data', (chunk) => { onboardingStderr += chunk })
|
|
2460
|
+
child.on('close', (code) => {
|
|
2461
|
+
if (code === 0) return
|
|
2462
|
+
let detail = null
|
|
2463
|
+
try {
|
|
2464
|
+
const parsed = JSON.parse(onboardingStdout.trim())
|
|
2465
|
+
if (parsed?.error) detail = parsed.error
|
|
2466
|
+
} catch {}
|
|
2467
|
+
if (!detail && onboardingStderr.trim()) {
|
|
2468
|
+
const lines = onboardingStderr.trim().split('\n').filter(l => !l.startsWith('node:'))
|
|
2469
|
+
detail = lines.slice(0, 2).join(' — ') || onboardingStderr.trim().slice(0, 120)
|
|
2470
|
+
}
|
|
2471
|
+
if (state.routerOnboardingPhase === 'loading') {
|
|
2472
|
+
state.routerOnboardingPhase = 'error'
|
|
2473
|
+
state.routerOnboardingError = detail || `Daemon exited with code ${code}`
|
|
2474
|
+
}
|
|
2475
|
+
})
|
|
2283
2476
|
await new Promise((r) => setTimeout(r, 2000))
|
|
2284
2477
|
if (state.routerOnboardingPhase === 'loading') {
|
|
2285
2478
|
state.routerOnboardingPhase = 'success'
|
|
@@ -2534,7 +2727,8 @@ export function createKeyHandler(ctx) {
|
|
|
2534
2727
|
const themeRowIdx = updateRowIdx + 1
|
|
2535
2728
|
const favoritesModeRowIdx = themeRowIdx + 1
|
|
2536
2729
|
const startupAiSpeedScanRowIdx = favoritesModeRowIdx + 1
|
|
2537
|
-
const
|
|
2730
|
+
const autoHideBrokenModelsRowIdx = startupAiSpeedScanRowIdx + 1
|
|
2731
|
+
const cleanupLegacyProxyRowIdx = autoHideBrokenModelsRowIdx + 1
|
|
2538
2732
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
2539
2733
|
const shellEnvRowIdx = changelogViewRowIdx + 1
|
|
2540
2734
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
@@ -2696,6 +2890,12 @@ export function createKeyHandler(ctx) {
|
|
|
2696
2890
|
return
|
|
2697
2891
|
}
|
|
2698
2892
|
|
|
2893
|
+
// 📖 Auto-hide broken models toggle
|
|
2894
|
+
if (state.settingsCursor === autoHideBrokenModelsRowIdx) {
|
|
2895
|
+
toggleAutoHideBrokenModels()
|
|
2896
|
+
return
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2699
2899
|
if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
|
|
2700
2900
|
runLegacyProxyCleanup()
|
|
2701
2901
|
return
|
|
@@ -2753,6 +2953,11 @@ export function createKeyHandler(ctx) {
|
|
|
2753
2953
|
toggleStartupAiSpeedScan()
|
|
2754
2954
|
return
|
|
2755
2955
|
}
|
|
2956
|
+
// 📖 Auto-hide broken models toggle (space)
|
|
2957
|
+
if (state.settingsCursor === autoHideBrokenModelsRowIdx) {
|
|
2958
|
+
toggleAutoHideBrokenModels()
|
|
2959
|
+
return
|
|
2960
|
+
}
|
|
2756
2961
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
2757
2962
|
|
|
2758
2963
|
// 📖 Toggle enabled/disabled for selected provider
|
|
@@ -2770,6 +2975,7 @@ export function createKeyHandler(ctx) {
|
|
|
2770
2975
|
|| state.settingsCursor === themeRowIdx
|
|
2771
2976
|
|| state.settingsCursor === favoritesModeRowIdx
|
|
2772
2977
|
|| state.settingsCursor === startupAiSpeedScanRowIdx
|
|
2978
|
+
|| state.settingsCursor === autoHideBrokenModelsRowIdx
|
|
2773
2979
|
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
2774
2980
|
|| state.settingsCursor === changelogViewRowIdx
|
|
2775
2981
|
) return
|
package/src/tui/overlays.js
CHANGED
|
@@ -121,7 +121,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
121
121
|
const themeRowIdx = updateRowIdx + 1
|
|
122
122
|
const favoritesModeRowIdx = themeRowIdx + 1
|
|
123
123
|
const startupAiSpeedScanRowIdx = favoritesModeRowIdx + 1
|
|
124
|
-
const
|
|
124
|
+
const autoHideBrokenModelsRowIdx = startupAiSpeedScanRowIdx + 1
|
|
125
|
+
const cleanupLegacyProxyRowIdx = autoHideBrokenModelsRowIdx + 1
|
|
125
126
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
126
127
|
const shellEnvRowIdx = changelogViewRowIdx + 1
|
|
127
128
|
const EL = '\x1b[K'
|
|
@@ -277,6 +278,16 @@ export function createOverlayRenderers(state, deps) {
|
|
|
277
278
|
cursorLineByRow[startupAiSpeedScanRowIdx] = lines.length
|
|
278
279
|
lines.push(state.settingsCursor === startupAiSpeedScanRowIdx ? themeColors.bgCursorSettingsList(startupAiSpeedScanRow) : startupAiSpeedScanRow)
|
|
279
280
|
|
|
281
|
+
// 📖 Auto-hide broken models row: toggles auto-hiding of 404/410 models from probe.
|
|
282
|
+
const autoHideEnabled = state.config.settings?.autoHideBrokenModels !== false
|
|
283
|
+
const hiddenCount = state.config.hiddenModels instanceof Set ? state.config.hiddenModels.size : 0
|
|
284
|
+
const autoHideStatus = autoHideEnabled
|
|
285
|
+
? themeColors.successBold(`✅ Enabled (${hiddenCount} hidden)`)
|
|
286
|
+
: themeColors.errorBold('❌ Disabled')
|
|
287
|
+
const autoHideRow = `${bullet(state.settingsCursor === autoHideBrokenModelsRowIdx)}${themeColors.textBold('Auto-hide Broken Models').padEnd(44)} ${autoHideStatus}`
|
|
288
|
+
cursorLineByRow[autoHideBrokenModelsRowIdx] = lines.length
|
|
289
|
+
lines.push(state.settingsCursor === autoHideBrokenModelsRowIdx ? themeColors.bgCursorSettingsList(autoHideRow) : autoHideRow)
|
|
290
|
+
|
|
280
291
|
if (updateState === 'error' && state.settingsUpdateError) {
|
|
281
292
|
lines.push(themeColors.error(` ${state.settingsUpdateError}`))
|
|
282
293
|
}
|
|
@@ -950,6 +961,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
950
961
|
lines.push(` ${key('Ctrl+P')} Open ⚡️ command palette ${hint('(search and run actions quickly)')}`)
|
|
951
962
|
lines.push(` ${key('Ctrl+A')} AI Speed Test ${hint('(benchmark selected model → time + TPS)')}`)
|
|
952
963
|
lines.push(` ${key('Ctrl+U')} Global AI Speed Test ${hint('(benchmark all models; Settings can auto-run it on startup)')}`)
|
|
964
|
+
lines.push(` ${key('Ctrl+Shift+P')} Probe 404 Models ${hint('(test all configured models; auto-hide broken 404/410)')}`)
|
|
953
965
|
lines.push(` ${key('E')} Cycle filter mode ${hint('(Normal → Configured only → Usable only)')}`)
|
|
954
966
|
lines.push(` ${key('Z')} Cycle tool mode ${hint('(📦 OpenCode → π Pi → 🪼 jcode → 📦 Desktop → 🦞 OpenClaw → 💘 Crush → 🪿 Goose → 🛠 Aider → 🐉 Qwen → 🤲 OpenHands → ⚡ Amp)')}`)
|
|
955
967
|
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(1️⃣2️⃣3️⃣ = router fallback order, capped at 🔟)')}`)
|