free-coding-models 0.5.60 โ†’ 0.5.62

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/src/tui/app.js CHANGED
@@ -149,6 +149,69 @@ const mergedModels = buildMergedModels(MODELS)
149
149
  const mergedModelByLabel = new Map(mergedModels.map(m => [m.label, m]))
150
150
  setOpenCodeModelData(mergedModels, mergedModelByLabel)
151
151
 
152
+ // ๐Ÿ“– Enrichment (t4 + t5): module-level state. The sync part (extended
153
+ // ๐Ÿ“– benchmark catalog stats) is computed lazily on first read via the
154
+ // ๐Ÿ“– getter, so we don't block module load. The async part (models.dev
155
+ // ๐Ÿ“– fetch) runs inside runApp as a background task โ€” never at module load,
156
+ // ๐Ÿ“– never blocking the TUI from rendering.
157
+ // ๐Ÿ“–
158
+ // ๐Ÿ“– The TUI reads from this object on every render to update the footer chips.
159
+ const moduleEnrichmentStats = {
160
+ bench: { total: 0, lastUpdated: 'unknown', source: 'unknown' },
161
+ modelsDev: { 'sources.js': 0, 'models.dev': 0, cached: false },
162
+ }
163
+ let _benchStatsLoaded = false
164
+ function ensureBenchStatsLoaded() {
165
+ if (_benchStatsLoaded) return
166
+ _benchStatsLoaded = true
167
+ try {
168
+ // ๐Ÿ“– require() works synchronously and is safe at module load because
169
+ // ๐Ÿ“– model-merger.js has no top-level await side effects.
170
+ const { getEnrichmentStats } = require('../core/model-merger.js')
171
+ moduleEnrichmentStats.bench = getEnrichmentStats().extendedBench
172
+ } catch (err) {
173
+ if (process.env.FCM_BENCH_DEBUG) console.warn('[t4] enrichment stats failed:', err?.message ?? err)
174
+ }
175
+ }
176
+
177
+ // ๐Ÿ“– Background enrichment: fetch models.dev and update moduleEnrichmentStats
178
+ // ๐Ÿ“– when it resolves. Fully decoupled from runApp โ€” even if the fetch hangs
179
+ // ๐Ÿ“– for 30s+, the TUI keeps rendering. The chip is updated by the next render
180
+ // ๐Ÿ“– after resolution.
181
+ function runModelsDevEnrichmentInBackground(merged) {
182
+ // ๐Ÿ“– Fire-and-forget; never awaited. If it throws, the catch swallows it.
183
+ ;(async () => {
184
+ try {
185
+ // ๐Ÿ“– Bound the total wait to 12s (worst case 3ร—4s retries). If the
186
+ // ๐Ÿ“– network is truly down, the chip stays at "0 live" and the TUI
187
+ // ๐Ÿ“– is unaffected. Callers can still trigger a refresh via the
188
+ // ๐Ÿ“– settings panel (future work).
189
+ const timeout = new Promise(resolve => setTimeout(() => resolve(null), 12_000))
190
+ const { overlayModelsDevMetadata } = await import('../core/model-merger.js')
191
+ const result = await Promise.race([
192
+ overlayModelsDevMetadata(merged, { mutate: true }),
193
+ timeout,
194
+ ])
195
+ if (!result) {
196
+ if (process.env.FCM_BENCH_DEBUG) console.warn('[t5] models.dev fetch timed out (12s)')
197
+ return
198
+ }
199
+ let live = 0, curated = 0
200
+ for (const m of result) {
201
+ if (m.metaSource === 'models.dev') live++
202
+ else if (m.metaSource === 'sources.js') curated++
203
+ }
204
+ moduleEnrichmentStats.modelsDev = { 'sources.js': curated, 'models.dev': live, cached: true }
205
+ // ๐Ÿ“– Note: state is owned by runApp โ€” we don't have a direct ref here.
206
+ // ๐Ÿ“– The footer reads moduleEnrichmentStats lazily on each render, so
207
+ // ๐Ÿ“– the next render will pick up the new counts. We do NOT trigger a
208
+ // ๐Ÿ“– render from here (we're not in the render context).
209
+ } catch (err) {
210
+ if (process.env.FCM_BENCH_DEBUG) console.warn('[t5] models.dev overlay failed:', err?.message ?? err)
211
+ }
212
+ })()
213
+ }
214
+
152
215
  // ๐Ÿ“– Provider quota cache is managed by lib/provider-quota-fetchers.js (TTL + backoff).
153
216
  // ๐Ÿ“– Usage placeholder logic uses isKnownQuotaTelemetry() from lib/quota-capabilities.js.
154
217
 
@@ -189,6 +252,16 @@ const LOCAL_VERSION = pkg.version
189
252
  // ๐Ÿ“– OpenCode helpers are imported from ../src/opencode.js
190
253
 
191
254
  export async function runApp(cliArgs, config, startupOptions = {}) {
255
+ // ๐Ÿ“– t4: prime the sync extended-benchmark stats so the footer chip is
256
+ // ๐Ÿ“– accurate on the first render (no async, no network โ€” just a sync read).
257
+ ensureBenchStatsLoaded()
258
+
259
+ // ๐Ÿ“– t5: kick off the async models.dev enrichment in the background. This
260
+ // ๐Ÿ“– does NOT block runApp โ€” the TUI starts rendering immediately with
261
+ // ๐Ÿ“– curated values, then re-renders when the models.dev fetch resolves.
262
+ // ๐Ÿ“– If the network is unreachable, the chip just stays at "0 live" forever
263
+ // ๐Ÿ“– (no user-visible freeze). The background task is bounded to 12s.
264
+ runModelsDevEnrichmentInBackground(mergedModels)
192
265
 
193
266
  // ๐Ÿ“– Detect user active terminal theme
194
267
  detectActiveTheme(config.settings?.theme || 'auto')
@@ -505,6 +578,8 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
505
578
  }
506
579
  state.probeCacheHits = probeCacheHits
507
580
  state.probeCacheMisses = probeCacheMisses
581
+ // ๐Ÿ“– Enrichment (t4 + t5): pulled fresh on every render from moduleEnrichmentStats
582
+ // ๐Ÿ“– (see the tableOpts construction below). No need to mirror into state here.
508
583
  state.probeCacheBrokenHidden = state.results.filter(r => r.cachedBroken && r.hidden).length
509
584
 
510
585
  // ๐Ÿ“– Runtime telemetry (t3): load the per-model metrics file + compute the
@@ -971,6 +1046,17 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
971
1046
  probeCacheMisses: state.probeCacheMisses || 0,
972
1047
  probeCacheBrokenHidden: state.probeCacheBrokenHidden || 0,
973
1048
  showBrokenMode: !!state.showBrokenMode,
1049
+ // ๐Ÿ“– Enrichment chips (t4 + t5): benchmark catalog + models.dev provenance
1050
+ // ๐Ÿ“– Read fresh from moduleEnrichmentStats on every render so the async
1051
+ // ๐Ÿ“– models.dev fetch (background, never blocks the TUI) is reflected as
1052
+ // ๐Ÿ“– soon as it resolves โ€” even if the user isn't pressing keys.
1053
+ enrichmentBenchCount: moduleEnrichmentStats.bench.total || 0,
1054
+ enrichmentBenchLastUpdated: moduleEnrichmentStats.bench.lastUpdated || null,
1055
+ metaSourceCounts: {
1056
+ 'sources.js': moduleEnrichmentStats.modelsDev['sources.js'] || 0,
1057
+ 'models.dev': moduleEnrichmentStats.modelsDev['models.dev'] || 0,
1058
+ },
1059
+ modelsDevCacheCached: moduleEnrichmentStats.modelsDev.cached === true,
974
1060
  // ๐Ÿ“– Passive quota (t2): live snapshots from response headers, populated
975
1061
  // ๐Ÿ“– by pings (every ping pre-warms quota for its provider). See
976
1062
  // ๐Ÿ“– src/core/provider-quota-fetchers.js for getAllPassiveQuotas().
@@ -36,6 +36,8 @@ const ANALYSIS_FLAGS = [
36
36
  { flag: '--reprobe, --no-cache', description: 'Force-rebuild the persistent probe-cache this run (skip cached healthy models)' },
37
37
  { flag: '--probe-ttl <ms>', description: 'Override probe-cache TTL (default 24h); healthy models within TTL are not re-pinged' },
38
38
  { flag: '--show-broken', description: "Don't auto-hide models that the probe-cache marked broken (one-shot override)" },
39
+ { flag: '--check-drift', description: 'Diff sources.js against models.dev and print a drift report; exit 1 on mismatch (t5)' },
40
+ { flag: '--drift-threshold <N>', description: 'Only fail --check-drift when N+ mismatches are found (default: 0 = any drift)' },
39
41
  ]
40
42
 
41
43
  const CONFIG_FLAGS = [
@@ -155,6 +155,13 @@ export const PROVIDER_COLOR = new Proxy({}, {
155
155
  * probeCacheMisses?: number, // t1: number of models that needed a live probe
156
156
  * probeCacheBrokenHidden?: number, // t1: number of broken models auto-hidden this session
157
157
  * showBrokenMode?: boolean, // t1: true when Shift+B has un-hidden broken models
158
+ * enrichmentBenchCount?: number, // t4: extended benchmark catalog size (committed JSON)
159
+ * enrichmentBenchLastUpdated?: string, // t4: lastUpdated field from the catalog _meta
160
+ * metaSourceCounts?: { // t5: count of models per metadata source
161
+ * 'sources.js': number,
162
+ * 'models.dev': number,
163
+ * },
164
+ * modelsDevCacheCached?: boolean, // t5: whether the models.dev cache is still within TTL
158
165
  * quota?: Record<string, { // t2: live quota from response headers
159
166
  * remaining: number, limit: number, percent: number,
160
167
  * source: 'header'|'endpoint', lastUpdated: number, windowType?: string,
@@ -209,6 +216,10 @@ export function renderTable({
209
216
  probeCacheMisses = 0,
210
217
  probeCacheBrokenHidden = 0,
211
218
  showBrokenMode = false,
219
+ enrichmentBenchCount = 0,
220
+ enrichmentBenchLastUpdated = null,
221
+ metaSourceCounts = null,
222
+ modelsDevCacheCached = false,
212
223
  quota = {},
213
224
  } = _) {
214
225
  // ๐Ÿ“– Filter out hidden models for display
@@ -1224,6 +1235,29 @@ export function renderTable({
1224
1235
  probeCacheLabel = chalk.bgRgb(40, 90, 140).rgb(220, 235, 255).bold(` ${cachedTxt}${brokenTxt} `)
1225
1236
  }
1226
1237
 
1238
+ // ๐Ÿ“– Enrichment chips (t4 + t5):
1239
+ // ๐Ÿ“– `๐Ÿ“Š bench N (date) ยท ๐Ÿ“ก live K / ๐Ÿ“ฆ cached M`
1240
+ // ๐Ÿ“– Always shown when any enrichment state exists.
1241
+ let enrichmentLabel = ''
1242
+ if ((enrichmentBenchCount && enrichmentBenchCount > 0) || metaSourceCounts) {
1243
+ const parts = []
1244
+ if (enrichmentBenchCount && enrichmentBenchCount > 0) {
1245
+ const dateTxt = enrichmentBenchLastUpdated && enrichmentBenchLastUpdated !== 'unknown'
1246
+ ? ` (${enrichmentBenchLastUpdated})`
1247
+ : ''
1248
+ parts.push(`๐Ÿ“Š bench ${enrichmentBenchCount}${dateTxt}`)
1249
+ }
1250
+ if (metaSourceCounts && (metaSourceCounts['models.dev'] > 0 || metaSourceCounts['sources.js'] > 0)) {
1251
+ const liveTxt = metaSourceCounts['models.dev'] ?? 0
1252
+ const cachedTxt = metaSourceCounts['sources.js'] ?? 0
1253
+ const liveIcon = modelsDevCacheCached ? '๐Ÿ“ก' : '๐Ÿ“ฆ'
1254
+ parts.push(`${liveIcon} ${liveTxt} live ยท ${cachedTxt} curated`)
1255
+ }
1256
+ if (parts.length > 0) {
1257
+ enrichmentLabel = chalk.bgRgb(80, 50, 110).rgb(230, 220, 245).bold(` ${parts.join(' ยท ')} `)
1258
+ }
1259
+ }
1260
+
1227
1261
  // ๐Ÿ“– Passive quota chip (t2): `๐Ÿ“Š groq 78% ยท sambanova 41%` (top-N depleted first).
1228
1262
  // ๐Ÿ“– Shows live rate-limit headers that the daemon / pings collected from upstream
1229
1263
  // ๐Ÿ“– responses. Zero extra network requests โ€” see provider-quota-fetchers.js.
@@ -1243,8 +1277,8 @@ export function renderTable({
1243
1277
  }
1244
1278
  }
1245
1279
 
1246
- // ๐Ÿ“– Line 3: Speed Test + Global Benchmark + Probe + Probe-cache + Quota + Last release
1247
- if (releaseLabel || speedTestLabel || globalBenchmarkLabel || probeLabel || probeCacheLabel || quotaLabel) {
1280
+ // ๐Ÿ“– Line 3: Speed Test + Global Benchmark + Probe + Probe-cache + Enrichment + Quota + Last release
1281
+ if (releaseLabel || speedTestLabel || globalBenchmarkLabel || probeLabel || probeCacheLabel || enrichmentLabel || quotaLabel) {
1248
1282
  const parts = [
1249
1283
  { text: ' ', key: null },
1250
1284
  { text: speedTestLabel, key: 'a' },
@@ -1254,6 +1288,8 @@ export function renderTable({
1254
1288
  { text: probeLabel, key: null },
1255
1289
  { text: probeCacheLabel ? ' ' : '', key: null },
1256
1290
  { text: probeCacheLabel, key: null },
1291
+ { text: enrichmentLabel ? ' ' : '', key: null },
1292
+ { text: enrichmentLabel, key: null },
1257
1293
  { text: quotaLabel ? ' ' : '', key: null },
1258
1294
  { text: quotaLabel, key: null },
1259
1295
  { text: ' ', key: null },