free-coding-models 0.5.57 โ 0.5.58
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/README.md +52 -0
- package/changelog/v0.5.58.md +42 -0
- package/package.json +4 -4
- package/src/core/probe-cache.js +515 -0
- package/src/core/router-daemon.js +62 -1
- package/src/core/utils.js +24 -1
- package/src/tui/app.js +127 -3
- package/src/tui/cli-help.js +3 -0
- package/src/tui/key-handler.js +19 -0
- package/src/tui/render-table.js +24 -2
- package/src/tui/tui-state.js +11 -0
- package/web/dist/assets/{index-BoJ4r2gC.js โ index-4Jq00xKl.js} +2 -2
- package/web/dist/index.html +1 -1
|
@@ -54,6 +54,15 @@ import { sendUsageTelemetry } from './telemetry.js'
|
|
|
54
54
|
import { TIER_ORDER } from './utils.js'
|
|
55
55
|
import { atomicWriteJson, safeJsonParse, sleep, maskApiKey, isRouteableProvider } from './shared-helpers.js'
|
|
56
56
|
import { normalizeRequestBody } from './schema-normalizer.js'
|
|
57
|
+
import {
|
|
58
|
+
loadCache as loadProbeCache,
|
|
59
|
+
flushCache as flushProbeCache,
|
|
60
|
+
recordProbeResults as recordProbeCacheResults,
|
|
61
|
+
getCacheStats as getProbeCacheStats,
|
|
62
|
+
getModelsDueForProbe,
|
|
63
|
+
isCacheFresh as isProbeCacheFresh,
|
|
64
|
+
pruneStaleEntries as pruneProbeCacheStaleEntries,
|
|
65
|
+
} from './probe-cache.js'
|
|
57
66
|
|
|
58
67
|
export const ROUTER_DEFAULT_PORT = 19280
|
|
59
68
|
export const ROUTER_MAX_PORT = 19289
|
|
@@ -870,6 +879,12 @@ class RouterRuntime {
|
|
|
870
879
|
this.webGlobalBenchmarkRunning = false
|
|
871
880
|
this.webGlobalBenchmarkTotal = 0
|
|
872
881
|
this.webGlobalBenchmarkCompleted = 0
|
|
882
|
+
// ๐ Probe-cache (t1): load the persistent probe-cache on boot so the daemon
|
|
883
|
+
// ๐ can skip fresh healthy models during its health-probe loop and write
|
|
884
|
+
// ๐ results back to the same file the CLI TUI uses. See src/core/probe-cache.js.
|
|
885
|
+
this.probeCache = loadProbeCache()
|
|
886
|
+
this.probeCacheDirty = false
|
|
887
|
+
this.probeCacheFlushTimer = null
|
|
873
888
|
this.refreshRouteState()
|
|
874
889
|
}
|
|
875
890
|
|
|
@@ -1023,6 +1038,39 @@ class RouterRuntime {
|
|
|
1023
1038
|
latency_ms: result.latencyMs ?? null,
|
|
1024
1039
|
circuit_state: this.circuit.get(key)?.state || 'UNKNOWN',
|
|
1025
1040
|
})
|
|
1041
|
+
|
|
1042
|
+
// ๐ Probe-cache (t1): mirror the result into the persistent cross-session
|
|
1043
|
+
// ๐ cache so the CLI TUI can skip fresh healthy models and auto-hide broken
|
|
1044
|
+
// ๐ ones. key is `provider/modelId` โ split on the first slash.
|
|
1045
|
+
const slashIdx = key.indexOf('/')
|
|
1046
|
+
if (slashIdx > 0) {
|
|
1047
|
+
const providerKey = key.slice(0, slashIdx)
|
|
1048
|
+
const modelId = key.slice(slashIdx + 1)
|
|
1049
|
+
recordProbeCacheResults(providerKey, [{
|
|
1050
|
+
modelId,
|
|
1051
|
+
status: result.ok ? 'ok' : 'broken',
|
|
1052
|
+
latencyMs: result.latencyMs ?? null,
|
|
1053
|
+
lastError: result.ok ? null : (result.code != null ? String(result.code) : 'error'),
|
|
1054
|
+
}])
|
|
1055
|
+
this.probeCacheDirty = true
|
|
1056
|
+
this.scheduleProbeCacheFlush()
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* ๐ scheduleProbeCacheFlush โ debounced write to disk so we don't thrash the
|
|
1062
|
+
* ๐ filesystem when a probe burst records dozens of results at once.
|
|
1063
|
+
*/
|
|
1064
|
+
scheduleProbeCacheFlush() {
|
|
1065
|
+
if (this.probeCacheFlushTimer) return
|
|
1066
|
+
this.probeCacheFlushTimer = setTimeout(() => {
|
|
1067
|
+
this.probeCacheFlushTimer = null
|
|
1068
|
+
if (this.probeCacheDirty) {
|
|
1069
|
+
flushProbeCache()
|
|
1070
|
+
this.probeCacheDirty = false
|
|
1071
|
+
}
|
|
1072
|
+
}, 2000)
|
|
1073
|
+
if (typeof this.probeCacheFlushTimer.unref === 'function') this.probeCacheFlushTimer.unref()
|
|
1026
1074
|
}
|
|
1027
1075
|
|
|
1028
1076
|
markAuthError(key, detail = 'authentication failed') {
|
|
@@ -1435,6 +1483,10 @@ class RouterRuntime {
|
|
|
1435
1483
|
configPath: CONFIG_PATH,
|
|
1436
1484
|
tokenStatsPath: ROUTER_TOKENS_PATH,
|
|
1437
1485
|
logPath: ROUTER_LOG_PATH,
|
|
1486
|
+
// ๐ Probe-cache (t1): live aggregates from the persistent probe-cache.
|
|
1487
|
+
// ๐ Surfaced so the Web Dashboard + CLI can show cache hit rate + how many
|
|
1488
|
+
// ๐ broken models are currently hidden. Refreshed every /stats call.
|
|
1489
|
+
probeCache: getProbeCacheStats(),
|
|
1438
1490
|
}
|
|
1439
1491
|
}
|
|
1440
1492
|
|
|
@@ -1529,7 +1581,14 @@ class RouterRuntime {
|
|
|
1529
1581
|
if (!set) return
|
|
1530
1582
|
const candidates = this.scoreCandidates(set)
|
|
1531
1583
|
.filter((candidate) => candidate.catalog?.routeable && !candidate.circuit?.stale)
|
|
1532
|
-
|
|
1584
|
+
// ๐ Probe-cache (t1): skip models that are still fresh + ok in the persistent
|
|
1585
|
+
// ๐ cache. Broken models naturally pass through (isProbeCacheFresh returns false
|
|
1586
|
+
// ๐ for them), so recovery detection keeps working unchanged.
|
|
1587
|
+
const filtered = candidates.filter((c) => {
|
|
1588
|
+
if (!c.catalog) return true
|
|
1589
|
+
return !isProbeCacheFresh(c.catalog.providerKey, c.catalog.modelId)
|
|
1590
|
+
})
|
|
1591
|
+
await Promise.allSettled(filtered.map((candidate) => this.probeCandidate(candidate, {
|
|
1533
1592
|
eco: this.routerConfig().probeMode === 'eco',
|
|
1534
1593
|
})))
|
|
1535
1594
|
}
|
|
@@ -3101,12 +3160,14 @@ class RouterRuntime {
|
|
|
3101
3160
|
if (this.probeTimer) clearInterval(this.probeTimer)
|
|
3102
3161
|
if (this.configReloadTimer) clearInterval(this.configReloadTimer)
|
|
3103
3162
|
if (this.tokenFlushTimer) clearInterval(this.tokenFlushTimer)
|
|
3163
|
+
if (this.probeCacheFlushTimer) clearInterval(this.probeCacheFlushTimer)
|
|
3104
3164
|
for (const timeout of this.probeTimeouts) clearTimeout(timeout)
|
|
3105
3165
|
const started = Date.now()
|
|
3106
3166
|
while (this.inFlight > 0 && Date.now() - started < 30000) {
|
|
3107
3167
|
await sleep(100)
|
|
3108
3168
|
}
|
|
3109
3169
|
this.tokenTracker.flush({ force: true })
|
|
3170
|
+
flushProbeCache() // ๐ t1: persist any pending probe-cache deltas before exit
|
|
3110
3171
|
try { this.server?.close() } catch {}
|
|
3111
3172
|
try { unlinkSync(ROUTER_PID_PATH) } catch {}
|
|
3112
3173
|
try { unlinkSync(ROUTER_PORT_PATH) } catch {}
|
package/src/core/utils.js
CHANGED
|
@@ -452,11 +452,16 @@ export function findBestModel(results) {
|
|
|
452
452
|
// --daemon-status, --no-telemetry, --json, --help/-h (case-insensitive)
|
|
453
453
|
// --playground / playground subcommand (open the in-TUI chat playground)
|
|
454
454
|
// - Value flag: --tier <letter> (the next non-flag arg is the tier value)
|
|
455
|
+
// - Probe-cache flags (t1):
|
|
456
|
+
// --reprobe / --no-cache (boolean) โ force-rebuild the probe cache this run
|
|
457
|
+
// --probe-ttl <ms> (value) โ override the 24h default TTL
|
|
458
|
+
// --show-broken (boolean) โ don't auto-hide broken models (one-shot)
|
|
455
459
|
//
|
|
456
460
|
// Returns:
|
|
457
461
|
// { apiKey, bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openCodeWebMode, openClawMode,
|
|
458
462
|
// aiderMode, crushMode, gooseMode, qwenMode, openHandsMode, ampMode,
|
|
459
|
-
// piMode, jcodeMode, copilotMode, forgecodeMode, zcodeMode, noTelemetry, jsonMode, helpMode, tierFilter
|
|
463
|
+
// piMode, jcodeMode, copilotMode, forgecodeMode, zcodeMode, noTelemetry, jsonMode, helpMode, tierFilter,
|
|
464
|
+
// reprobeMode, probeTtlMs, showBrokenMode }
|
|
460
465
|
//
|
|
461
466
|
// ๐ Note: apiKey may be null here โ the main CLI falls back to env vars and saved config.
|
|
462
467
|
export function parseArgs(argv) {
|
|
@@ -486,6 +491,12 @@ export function parseArgs(argv) {
|
|
|
486
491
|
? pingIntervalIdx + 1
|
|
487
492
|
: -1
|
|
488
493
|
|
|
494
|
+
// ๐ --probe-ttl <ms> โ override the 24h probe-cache TTL (power users / debugging)
|
|
495
|
+
const probeTtlIdx = args.findIndex(a => a.toLowerCase() === '--probe-ttl')
|
|
496
|
+
const probeTtlValueIdx = (probeTtlIdx !== -1 && args[probeTtlIdx + 1] && !args[probeTtlIdx + 1].startsWith('--'))
|
|
497
|
+
? probeTtlIdx + 1
|
|
498
|
+
: -1
|
|
499
|
+
|
|
489
500
|
// ๐ --sync-set [name] โ auto-discover and live-probe models into a named router set
|
|
490
501
|
const syncSetIdx = args.findIndex(a => a.toLowerCase() === '--sync-set')
|
|
491
502
|
const syncSetValueIdx = (syncSetIdx !== -1 && args[syncSetIdx + 1] && !args[syncSetIdx + 1].startsWith('--'))
|
|
@@ -499,6 +510,7 @@ export function parseArgs(argv) {
|
|
|
499
510
|
if (originValueIdx !== -1) skipIndices.add(originValueIdx)
|
|
500
511
|
if (pingIntervalValueIdx !== -1) skipIndices.add(pingIntervalValueIdx)
|
|
501
512
|
if (syncSetValueIdx !== -1) skipIndices.add(syncSetValueIdx)
|
|
513
|
+
if (probeTtlValueIdx !== -1) skipIndices.add(probeTtlValueIdx)
|
|
502
514
|
|
|
503
515
|
for (const [i, arg] of args.entries()) {
|
|
504
516
|
if (arg.startsWith('--') || arg === '-h') {
|
|
@@ -574,6 +586,13 @@ export function parseArgs(argv) {
|
|
|
574
586
|
// ๐ --recommend โ launch directly into Smart Recommend mode (Q key equivalent)
|
|
575
587
|
const recommendMode = flags.includes('--recommend')
|
|
576
588
|
|
|
589
|
+
// ๐ Probe-cache flags (t1): --reprobe / --no-cache force a fresh probe pass;
|
|
590
|
+
// ๐ --probe-ttl overrides the 24h default; --show-broken un-hides broken models for this run.
|
|
591
|
+
const reprobeMode = flags.includes('--reprobe') || flags.includes('--no-cache')
|
|
592
|
+
const showBrokenMode = flags.includes('--show-broken')
|
|
593
|
+
const probeTtlRaw = probeTtlValueIdx !== -1 ? args[probeTtlValueIdx] : null
|
|
594
|
+
const probeTtlMs = probeTtlRaw !== null ? parseInt(probeTtlRaw, 10) : null
|
|
595
|
+
|
|
577
596
|
return {
|
|
578
597
|
apiKey,
|
|
579
598
|
bestMode,
|
|
@@ -621,6 +640,10 @@ export function parseArgs(argv) {
|
|
|
621
640
|
devMode,
|
|
622
641
|
syncSetMode,
|
|
623
642
|
syncSetName,
|
|
643
|
+
// ๐ Probe-cache flags (t1) โ see src/core/probe-cache.js
|
|
644
|
+
reprobeMode,
|
|
645
|
+
probeTtlMs: Number.isFinite(probeTtlMs) && probeTtlMs > 0 ? probeTtlMs : null,
|
|
646
|
+
showBrokenMode,
|
|
624
647
|
}
|
|
625
648
|
}
|
|
626
649
|
|
package/src/tui/app.js
CHANGED
|
@@ -128,6 +128,17 @@ import { startExternalTool } from '../core/tool-launchers.js'
|
|
|
128
128
|
import { getToolInstallPlan, installToolWithPlan, isToolInstalled } from '../core/tool-bootstrap.js'
|
|
129
129
|
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels } from '../core/endpoint-installer.js'
|
|
130
130
|
import { loadCache, saveCache, clearCache, getCacheAge } from '../core/cache.js'
|
|
131
|
+
import {
|
|
132
|
+
loadCache as loadProbeCache,
|
|
133
|
+
flushCache as flushProbeCache,
|
|
134
|
+
clearCache as clearProbeCache,
|
|
135
|
+
getModelsDueForProbe,
|
|
136
|
+
isCacheFresh,
|
|
137
|
+
recordProbeResults,
|
|
138
|
+
getCacheStats as getProbeCacheStats,
|
|
139
|
+
pruneStaleEntries as pruneProbeCacheStaleEntries,
|
|
140
|
+
DEFAULT_PROBE_TTL_MS,
|
|
141
|
+
} from '../core/probe-cache.js'
|
|
131
142
|
import { checkConfigSecurity } from '../core/security.js'
|
|
132
143
|
import { buildCliHelpText } from './cli-help.js'
|
|
133
144
|
import { detectActiveTheme, THEME_BG_RGB, getTheme, patchThemeBg } from './theme.js'
|
|
@@ -424,6 +435,78 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
424
435
|
}
|
|
425
436
|
}
|
|
426
437
|
|
|
438
|
+
// ๐ Probe-cache (t1): persistent per-provider health cache with TTL + auto-hide broken.
|
|
439
|
+
// ๐ Lives at ~/.free-coding-models/probe-cache.json, shared with daemon + Tauri surfaces.
|
|
440
|
+
// ๐ Honors CLI flags: --reprobe (force clear), --probe-ttl <ms>, --show-broken (don't hide).
|
|
441
|
+
if (cliArgs.reprobeMode) {
|
|
442
|
+
// ๐ --reprobe / --no-cache: nuke the cache so this run pings everything fresh.
|
|
443
|
+
clearProbeCache()
|
|
444
|
+
}
|
|
445
|
+
const probeCacheTtlMs = cliArgs.probeTtlMs ?? DEFAULT_PROBE_TTL_MS
|
|
446
|
+
state.probeCacheTtlMs = probeCacheTtlMs
|
|
447
|
+
state.showBrokenMode = !!cliArgs.showBrokenMode
|
|
448
|
+
const probeCache = loadProbeCache()
|
|
449
|
+
|
|
450
|
+
// ๐ Prune entries whose modelId is no longer in the catalog (one-time per boot).
|
|
451
|
+
for (const providerKey of new Set(state.results.map(r => r.providerKey))) {
|
|
452
|
+
const liveIds = state.results.filter(r => r.providerKey === providerKey).map(r => r.modelId)
|
|
453
|
+
pruneProbeCacheStaleEntries(providerKey, liveIds)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ๐ Apply probe-cache to results: auto-hide broken, pre-fill fresh ok stats.
|
|
457
|
+
let probeCacheHits = 0
|
|
458
|
+
let probeCacheMisses = 0
|
|
459
|
+
for (const r of state.results) {
|
|
460
|
+
const entry = probeCache?.providers?.[r.providerKey]?.models?.[r.modelId]
|
|
461
|
+
if (!entry) {
|
|
462
|
+
probeCacheMisses++
|
|
463
|
+
continue
|
|
464
|
+
}
|
|
465
|
+
if (entry.status === 'broken') {
|
|
466
|
+
probeCacheHits++
|
|
467
|
+
r.cachedBroken = true
|
|
468
|
+
r.lastProbedAt = entry.lastProbedAt
|
|
469
|
+
r.lastError = entry.lastError ?? null
|
|
470
|
+
// ๐ Auto-hide broken models (unless --show-broken or feature disabled).
|
|
471
|
+
const autoHideEnabled = config.settings?.autoHideBrokenModels !== false
|
|
472
|
+
if (!state.showBrokenMode && autoHideEnabled) {
|
|
473
|
+
r.hidden = true
|
|
474
|
+
r.status = 'down'
|
|
475
|
+
}
|
|
476
|
+
continue
|
|
477
|
+
}
|
|
478
|
+
if (entry.status !== 'ok') {
|
|
479
|
+
probeCacheMisses++
|
|
480
|
+
continue
|
|
481
|
+
}
|
|
482
|
+
if (entry.probeVersion !== 2) {
|
|
483
|
+
// ๐ Stale version โ count as miss, force re-probe this run.
|
|
484
|
+
probeCacheMisses++
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
if (Date.now() - entry.lastProbedAt < probeCacheTtlMs) {
|
|
488
|
+
// ๐ Fresh ok entry โ pre-fill the row so the TUI has data on frame 1.
|
|
489
|
+
probeCacheHits++
|
|
490
|
+
r.fromProbeCache = true
|
|
491
|
+
r.lastProbedAt = entry.lastProbedAt
|
|
492
|
+
const cachedLatency = typeof entry.latencyMs === 'number' ? entry.latencyMs : 0
|
|
493
|
+
r.avg = cachedLatency
|
|
494
|
+
r.p95 = cachedLatency
|
|
495
|
+
r.jitter = 0
|
|
496
|
+
r.stability = 100
|
|
497
|
+
r.uptime = 100
|
|
498
|
+
r.verdict = 'Cached'
|
|
499
|
+
r.status = 'up'
|
|
500
|
+
r.httpCode = '200'
|
|
501
|
+
r.pings = [{ ms: cachedLatency, code: '200' }]
|
|
502
|
+
} else {
|
|
503
|
+
probeCacheMisses++
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
state.probeCacheHits = probeCacheHits
|
|
507
|
+
state.probeCacheMisses = probeCacheMisses
|
|
508
|
+
state.probeCacheBrokenHidden = state.results.filter(r => r.cachedBroken && r.hidden).length
|
|
509
|
+
|
|
427
510
|
// ๐ Define pingModel before JSON mode so `--json` can reuse the same provider-aware
|
|
428
511
|
// ๐ ping path as the interactive TUI without waiting for the PTY/render loop setup.
|
|
429
512
|
pingModel = async (r) => {
|
|
@@ -445,6 +528,26 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
445
528
|
|
|
446
529
|
r.pings.push({ ms, code })
|
|
447
530
|
|
|
531
|
+
// ๐ Probe-cache (t1): record the result so future runs can skip healthy models
|
|
532
|
+
// ๐ and auto-hide broken ones. Uses the module-level in-memory cache; flushed
|
|
533
|
+
// ๐ on exit (see exit()) and on graceful shutdown.
|
|
534
|
+
const probeStatus = code === '200' ? 'ok' : 'broken'
|
|
535
|
+
recordProbeResults(r.providerKey, [{
|
|
536
|
+
modelId: r.modelId,
|
|
537
|
+
status: probeStatus,
|
|
538
|
+
latencyMs: typeof ms === 'number' ? ms : null,
|
|
539
|
+
lastError: probeStatus === 'broken' ? String(code) : null,
|
|
540
|
+
}])
|
|
541
|
+
// ๐ If a previously-broken model comes back ok, un-hide it automatically so the
|
|
542
|
+
// ๐ user sees the recovery without a restart. (Skip when --show-broken keeps them visible.)
|
|
543
|
+
if (probeStatus === 'ok' && r.cachedBroken && !state.showBrokenMode) {
|
|
544
|
+
r.cachedBroken = false
|
|
545
|
+
r.hidden = false
|
|
546
|
+
r.lastError = null
|
|
547
|
+
// ๐ Re-evaluate the broken-hidden counter for the footer chip.
|
|
548
|
+
state.probeCacheBrokenHidden = state.results.filter(x => x.cachedBroken && x.hidden).length
|
|
549
|
+
}
|
|
550
|
+
|
|
448
551
|
if (code === '200') {
|
|
449
552
|
r.status = 'up'
|
|
450
553
|
} else if (code === '000') {
|
|
@@ -541,6 +644,7 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
541
644
|
// ๐ Ensure we always leave alt screen cleanly (Ctrl+C, crash, normal exit)
|
|
542
645
|
const exit = (code = 0) => {
|
|
543
646
|
saveCache(state.results, state.pingMode)
|
|
647
|
+
flushProbeCache()
|
|
544
648
|
clearInterval(ticker)
|
|
545
649
|
clearTimeout(state.pingIntervalObj)
|
|
546
650
|
clearInterval(state.versionRecheckTimer)
|
|
@@ -845,6 +949,11 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
845
949
|
probeTotal: state.probeTotal,
|
|
846
950
|
probeCompleted: state.probeCompleted,
|
|
847
951
|
probeHiddenCount: state.probeHiddenCount,
|
|
952
|
+
// ๐ Probe-cache (t1): footer chip stats
|
|
953
|
+
probeCacheHits: state.probeCacheHits || 0,
|
|
954
|
+
probeCacheMisses: state.probeCacheMisses || 0,
|
|
955
|
+
probeCacheBrokenHidden: state.probeCacheBrokenHidden || 0,
|
|
956
|
+
showBrokenMode: !!state.showBrokenMode,
|
|
848
957
|
}
|
|
849
958
|
if (state.commandPaletteOpen) {
|
|
850
959
|
if (!state.commandPaletteFrozenTable) {
|
|
@@ -970,8 +1079,13 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
970
1079
|
|
|
971
1080
|
// โโ Continuous ping loop โ ping all models every N seconds forever โโโโโโโโโโ
|
|
972
1081
|
|
|
973
|
-
// ๐ Initial ping
|
|
974
|
-
|
|
1082
|
+
// ๐ Initial ping: skip models that are still fresh in the probe-cache (t1).
|
|
1083
|
+
// ๐ They already have valid data pre-filled and will be re-probed when their TTL expires.
|
|
1084
|
+
// ๐ Broken models are always re-probed so recovery is detected.
|
|
1085
|
+
const initialDueModels = state.results.filter(r => !isCacheFresh(r.providerKey, r.modelId, {
|
|
1086
|
+
ttlMs: state.probeCacheTtlMs,
|
|
1087
|
+
}))
|
|
1088
|
+
const initialPing = Promise.all(initialDueModels.map(r => pingModel(r)))
|
|
975
1089
|
|
|
976
1090
|
// ๐ Continuous ping loop with mode-driven cadence.
|
|
977
1091
|
const runPingCycle = async () => {
|
|
@@ -1008,6 +1122,13 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
1008
1122
|
if (!state.config.favorites.includes(favKey)) return
|
|
1009
1123
|
}
|
|
1010
1124
|
|
|
1125
|
+
// ๐ Probe-cache (t1): skip models that are still fresh + ok in the cache.
|
|
1126
|
+
// ๐ Broken models always pass through isCacheFresh() (it returns false for them),
|
|
1127
|
+
// ๐ which gives us automatic recovery detection for free.
|
|
1128
|
+
if (isCacheFresh(r.providerKey, r.modelId, { ttlMs: state.probeCacheTtlMs })) {
|
|
1129
|
+
return
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1011
1132
|
pingModel(r).catch(() => {
|
|
1012
1133
|
// Individual ping failures don't crash the loop
|
|
1013
1134
|
})
|
|
@@ -1031,8 +1152,11 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
1031
1152
|
await initialPing
|
|
1032
1153
|
scheduleAiSpeedScanOnStartup()
|
|
1033
1154
|
|
|
1034
|
-
// ๐ Save
|
|
1155
|
+
// ๐ Save caches after initial pings complete for faster next startup.
|
|
1156
|
+
// ๐ Both caches live side-by-side: the old per-session cache for instant frame-1
|
|
1157
|
+
// ๐ data, and the new persistent probe-cache for cross-session skip + auto-hide.
|
|
1035
1158
|
saveCache(state.results, state.pingMode)
|
|
1159
|
+
flushProbeCache()
|
|
1036
1160
|
|
|
1037
1161
|
// ๐ Background version re-check: poll npm registry every 5 minutes.
|
|
1038
1162
|
// ๐ If a new version appears (wasn't there at startup), update the banner live.
|
package/src/tui/cli-help.js
CHANGED
|
@@ -33,6 +33,9 @@ const ANALYSIS_FLAGS = [
|
|
|
33
33
|
{ flag: '--ping-interval <ms>', description: 'Override ping interval in milliseconds' },
|
|
34
34
|
{ flag: '--hide-unconfigured', description: 'Hide models without configured API keys' },
|
|
35
35
|
{ flag: '--show-unconfigured', description: 'Show all models regardless of API key config' },
|
|
36
|
+
{ flag: '--reprobe, --no-cache', description: 'Force-rebuild the persistent probe-cache this run (skip cached healthy models)' },
|
|
37
|
+
{ flag: '--probe-ttl <ms>', description: 'Override probe-cache TTL (default 24h); healthy models within TTL are not re-pinged' },
|
|
38
|
+
{ flag: '--show-broken', description: "Don't auto-hide models that the probe-cache marked broken (one-shot override)" },
|
|
36
39
|
]
|
|
37
40
|
|
|
38
41
|
const CONFIG_FLAGS = [
|
package/src/tui/key-handler.js
CHANGED
|
@@ -2784,6 +2784,25 @@ export function createKeyHandler(ctx) {
|
|
|
2784
2784
|
return
|
|
2785
2785
|
}
|
|
2786
2786
|
|
|
2787
|
+
// ๐ Shift+B: Toggle visibility of probe-cache-broken models (t1).
|
|
2788
|
+
// ๐ When toggled on, hidden broken rows become visible (greyed/dimmed via render-table).
|
|
2789
|
+
// ๐ When toggled off, they go back to hidden. Persisted for the current session.
|
|
2790
|
+
if (key.name === 'b' && key.shift && !key.ctrl && !key.meta) {
|
|
2791
|
+
const nextShowBroken = !state.showBrokenMode
|
|
2792
|
+
state.showBrokenMode = nextShowBroken
|
|
2793
|
+
// ๐ If enabling, restore all broken rows to visible; if disabling, re-hide them.
|
|
2794
|
+
for (const r of state.results) {
|
|
2795
|
+
if (r.cachedBroken) {
|
|
2796
|
+
r.hidden = !nextShowBroken
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
// ๐ Refresh the footer counter chip + cursor bounds.
|
|
2800
|
+
state.probeCacheBrokenHidden = state.results.filter(x => x.cachedBroken && x.hidden).length
|
|
2801
|
+
applyTierFilter()
|
|
2802
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
2803
|
+
return
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2787
2806
|
// ๐ X clears the active free-text filter set from the command palette.
|
|
2788
2807
|
if (key.name === 'x' && !key.ctrl && !key.meta) {
|
|
2789
2808
|
if (!state.customTextFilter) return
|
package/src/tui/render-table.js
CHANGED
|
@@ -151,6 +151,10 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
151
151
|
* routerFooterTodayTokens?: number,
|
|
152
152
|
* routerFooterAllTimeTokens?: number,
|
|
153
153
|
* routerFooterRequests?: number,
|
|
154
|
+
* probeCacheHits?: number, // t1: number of fresh ok entries served from probe-cache
|
|
155
|
+
* probeCacheMisses?: number, // t1: number of models that needed a live probe
|
|
156
|
+
* probeCacheBrokenHidden?: number, // t1: number of broken models auto-hidden this session
|
|
157
|
+
* showBrokenMode?: boolean, // t1: true when Shift+B has un-hidden broken models
|
|
154
158
|
* }} opts
|
|
155
159
|
* @returns {string}
|
|
156
160
|
*/
|
|
@@ -197,6 +201,10 @@ export function renderTable({
|
|
|
197
201
|
probeTotal = 0,
|
|
198
202
|
probeCompleted = 0,
|
|
199
203
|
probeHiddenCount = 0,
|
|
204
|
+
probeCacheHits = 0,
|
|
205
|
+
probeCacheMisses = 0,
|
|
206
|
+
probeCacheBrokenHidden = 0,
|
|
207
|
+
showBrokenMode = false,
|
|
200
208
|
} = _) {
|
|
201
209
|
// ๐ Filter out hidden models for display
|
|
202
210
|
const visibleResults = results.filter(r => !r.hidden)
|
|
@@ -1199,8 +1207,20 @@ export function renderTable({
|
|
|
1199
1207
|
probeLabel = chalk.bgRgb(120, 60, 60).rgb(255, 200, 200).bold(` ๐ Probe done: ${probeHiddenCount} broken model${probeHiddenCount > 1 ? 's' : ''} hidden `)
|
|
1200
1208
|
}
|
|
1201
1209
|
|
|
1202
|
-
// ๐
|
|
1203
|
-
|
|
1210
|
+
// ๐ Probe-cache chip (t1): `โก N cached ยท ๐ด M broken (Shift+B)`
|
|
1211
|
+
// ๐ Always shown once any probe-cache state exists โ gives the user a hint that
|
|
1212
|
+
// ๐ cache hits are happening (and that broken models are being hidden for them).
|
|
1213
|
+
let probeCacheLabel = ''
|
|
1214
|
+
if (probeCacheHits > 0 || probeCacheBrokenHidden > 0 || showBrokenMode) {
|
|
1215
|
+
const cachedTxt = `โก ${probeCacheHits} cached`
|
|
1216
|
+
const brokenTxt = probeCacheBrokenHidden > 0
|
|
1217
|
+
? ` ๐ด ${probeCacheBrokenHidden} broken${showBrokenMode ? ' (visible)' : ' (Shift+B)'}`
|
|
1218
|
+
: ''
|
|
1219
|
+
probeCacheLabel = chalk.bgRgb(40, 90, 140).rgb(220, 235, 255).bold(` ${cachedTxt}${brokenTxt} `)
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// ๐ Line 3: Speed Test + Global Benchmark + Probe + Probe-cache + Last release
|
|
1223
|
+
if (releaseLabel || speedTestLabel || globalBenchmarkLabel || probeLabel || probeCacheLabel) {
|
|
1204
1224
|
const parts = [
|
|
1205
1225
|
{ text: ' ', key: null },
|
|
1206
1226
|
{ text: speedTestLabel, key: 'a' },
|
|
@@ -1208,6 +1228,8 @@ export function renderTable({
|
|
|
1208
1228
|
{ text: globalBenchmarkLabel, key: 'u' },
|
|
1209
1229
|
{ text: probeLabel ? ' ' : '', key: null },
|
|
1210
1230
|
{ text: probeLabel, key: null },
|
|
1231
|
+
{ text: probeCacheLabel ? ' ' : '', key: null },
|
|
1232
|
+
{ text: probeCacheLabel, key: null },
|
|
1211
1233
|
{ text: ' ', key: null },
|
|
1212
1234
|
{ text: releaseLabel, key: null },
|
|
1213
1235
|
]
|
package/src/tui/tui-state.js
CHANGED
|
@@ -286,6 +286,17 @@ export function createTuiState({
|
|
|
286
286
|
probeCompleted: 0,
|
|
287
287
|
probeHiddenCount: 0,
|
|
288
288
|
|
|
289
|
+
// ๐ Persistent probe-cache (t1): TTL'd cross-session health cache.
|
|
290
|
+
// ๐ - probeCacheHits / Misses: telemetry counters per session (footer chip).
|
|
291
|
+
// ๐ - probeCacheBrokenHidden: live count of broken rows currently hidden.
|
|
292
|
+
// ๐ - probeCacheTtlMs: TTL override from --probe-ttl CLI flag (default 24h).
|
|
293
|
+
// ๐ - showBrokenMode: Shift+B toggle; true = broken rows are visible (dimmed).
|
|
294
|
+
probeCacheHits: 0,
|
|
295
|
+
probeCacheMisses: 0,
|
|
296
|
+
probeCacheBrokenHidden: 0,
|
|
297
|
+
probeCacheTtlMs: null, // ๐ Set in app.js from cliArgs.probeTtlMs || DEFAULT
|
|
298
|
+
showBrokenMode: false,
|
|
299
|
+
|
|
289
300
|
// ๐ Header click flash animation: briefly highlights the clicked column header
|
|
290
301
|
// ๐ with an inverse/bright style for ~250ms (3 frames at 12 FPS).
|
|
291
302
|
headerFlashColumn: null, // ๐ Column name being flashed (null = no flash active)
|