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/README.md +2 -0
- package/bin/free-coding-models.js +19 -0
- package/changelog/v0.5.61.md +65 -0
- package/changelog/v0.5.62.md +65 -0
- package/package.json +8 -2
- package/scripts/check-drift.mjs +160 -0
- package/scripts/update-benchmarks.mjs +239 -0
- package/src/core/extended-benchmarks.js +421 -0
- package/src/core/model-merger.js +155 -0
- package/src/core/models-dev-fetcher.js +210 -0
- package/src/core/models-dev-index.js +311 -0
- package/src/core/models-drift.js +296 -0
- package/src/core/utils.js +14 -0
- package/src/data/benchmarks.json +302 -0
- package/src/tui/app.js +86 -0
- package/src/tui/cli-help.js +2 -0
- package/src/tui/render-table.js +38 -2
- package/web/dist/assets/{index-CQQJkofy.js → index-DBaz4DiK.js} +2 -2
- package/web/dist/index.html +1 -1
- package/web/server.js +39 -0
package/README.md
CHANGED
|
@@ -794,6 +794,8 @@ See [`packages/fcm-agent-core/README.md`](./packages/fcm-agent-core/README.md) f
|
|
|
794
794
|
- **Persistent probe-cache (t1)** — every health probe result is cached to `~/.free-coding-models/probe-cache.json` for 24h. Warm starts render the full ranking in <500ms, only re-ping the models that are due (broken or TTL-expired). Broken models are auto-hidden across sessions — toggle visibility with **Shift+B**. See [Persistent probe cache](#-persistent-probe-cache) below for `--reprobe`, `--probe-ttl`, `--show-broken`.
|
|
795
795
|
- **Live quota from response headers (t2)** — every routed chat-completion response already carries `x-ratelimit-*` headers. The daemon parses them in 8 variants and exposes live per-provider quota on the TUI footer (`📊 groq 78% · sambanova 41%`) and in the Web Dashboard (`Provider Quota` section with animated progress bars). Zero extra network requests, zero quota waste. See [Live quota from headers](#-live-quota-from-response-headers) below.
|
|
796
796
|
- **Runtime telemetry: real-world scores (t3)** — every routed request through the daemon feeds a persistent per-model telemetry file (`~/.free-coding-models/runtime-telemetry.json`) with real success rate, throughput, and recent calls. The `Real` column + `W` sort key in the TUI rank models by what *actually* works on free tiers, not what they claim on SWE-bench. See [Runtime telemetry](#-runtime-telemetry-real-world-scores) below.
|
|
797
|
+
- **Extended benchmark catalog (t4)** — `src/data/benchmarks.json` (49 well-known models committed, refreshed at every release via `pnpm update:benchmarks`) layers **Coding Index, Math Index, Agentic Index, Reasoning Index, MMLU-Pro, GPQA, HLE** on top of `sources.js` with a lazy `Proxy` load + prefix-indexed O(key length) lookup. Surfaced on the TUI footer as `📊 bench 49 (2026-07-25)`. Curated SWE-bench scores are never overwritten — the overlay is additive.
|
|
798
|
+
- **Live `models.dev` enrichment + drift detection (t5)** — the community-maintained `models.dev` catalog is fetched in the background (5 min in-process cache, 3 retries × 250 ms backoff) and overlaid onto every merged model with `metaSource` provenance. The footer chip shows `📡 102 live · 62 curated` so you see at a glance which values came from upstream. `--check-drift` prints a human-readable drift report vs `sources.js`; a weekly CI job opens a `catalog-drift` issue if anything changed.
|
|
797
799
|
|
|
798
800
|
---
|
|
799
801
|
|
|
@@ -74,6 +74,25 @@ async function main() {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// 📖 --check-drift (t5): diff sources.js against models.dev and print a report.
|
|
78
|
+
// 📖 Runs BEFORE the config + update check so it works on bare clones and CI.
|
|
79
|
+
if (cliArgs.checkDriftMode) {
|
|
80
|
+
const threshold = cliArgs.driftThreshold ?? 0
|
|
81
|
+
const args = ['--threshold', String(threshold)]
|
|
82
|
+
const { spawn } = await import('node:child_process')
|
|
83
|
+
const { fileURLToPath } = await import('node:url')
|
|
84
|
+
const { dirname, join } = await import('node:path')
|
|
85
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
86
|
+
const script = join(here, '..', 'scripts', 'check-drift.mjs')
|
|
87
|
+
const child = spawn(process.execPath, [script, ...args], { stdio: 'inherit' })
|
|
88
|
+
child.on('exit', code => process.exit(code ?? 1))
|
|
89
|
+
child.on('error', err => {
|
|
90
|
+
console.error(chalk.red(`failed to spawn check-drift: ${err.message}`))
|
|
91
|
+
process.exit(3)
|
|
92
|
+
})
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
// Load JSON config before operational modes so the mandatory update policy can
|
|
78
97
|
// 📖 persist failure counters for TUI, Web Dashboard, Docker daemon, and Desktop sidecar launches.
|
|
79
98
|
const config = loadConfig();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog v0.5.61 - 2026-07-27
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- 📊 **Extended benchmark catalog** (t4) — a new `src/data/benchmarks.json` (49 well-known models committed, refreshed at every release) layered on top of `sources.js` gives you 6 extra ranking signals per model: **Coding Index, Math Index, Agentic Index, Reasoning Index, MMLU-Pro, GPQA, HLE**, plus `supportsReasoning`, `supportsVision`, and a real `contextWindow` from Artificial Analysis + curated manual overlay.
|
|
6
|
+
|
|
7
|
+
- **Lazy load via `Proxy`** — the JSON is only parsed on first lookup, not at module load. Cold start stays at ~0 ms.
|
|
8
|
+
- **Prefix-indexed O(key length) lookup** — `lookupExtendedBenchmark('deepseek-ai/deepseek-v4-pro')` is fast even on a 500-entry catalog. Falls back through the `-`/`/`-prefixed segments (e.g. `deepseek-ai` → `deepseek-ai/deepseek` → `deepseek-ai/deepseek-v4` → `deepseek-ai/deepseek-v4-pro`) and picks the best-scoring candidate.
|
|
9
|
+
- **Curated seed wins** — the `mergeExtendedBenchmark` overlay only fills fields that are null in `sources.js`. Your curated `sweScore` / `tier` / `ctx` are never overwritten by the live data.
|
|
10
|
+
|
|
11
|
+
- 🛰️ **Live `models.dev` enrichment** (t5) — the community-maintained `models.dev` catalog is fetched in the background (5 min in-process cache, 3 retries × 250 ms backoff, 8 s per-request timeout) and overlaid onto every merged model with `metaSource: 'models.dev' | 'sources.js'` provenance.
|
|
12
|
+
|
|
13
|
+
- **Substring matches skipped for drift detection** — the indexer tries exact → aliased → substring, but the drift detector only counts exact + aliased matches (avoids the "DeepSeek Chat" vs "DeepSeek Reasoner" false positive).
|
|
14
|
+
- **Provider aliases** — 40+ mappings bridge sources.js provider keys (`nvidiaNim`, `together`, `novita`, `kilocode`, …) to models.dev provider keys (`nvidia`, `togetherai`, `novita-ai`, `kilo`, …).
|
|
15
|
+
- **Offline-safe** — if the fetch fails (3×8 s = up to 24 s, capped to 12 s in the background task), `metaSource` stays at `'sources.js'` and the TUI keeps rendering. No crashes, no hangs.
|
|
16
|
+
|
|
17
|
+
- 🔍 **`--check-drift` CLI flag** — diff `sources.js` against `models.dev` and print a human-readable report grouped by model. Each row shows the field, the sources.js value, the models.dev value, and an action arrow (`← UPDATE` or `← ADD`).
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
free-coding-models --check-drift # exit 1 on any drift
|
|
21
|
+
free-coding-models --check-drift --drift-threshold 5 # only fail on 5+ mismatches
|
|
22
|
+
pnpm check:drift # npm alias
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Exit codes: `0` no drift · `1` drift detected · `2` fetch failed · `3` invalid args.
|
|
26
|
+
|
|
27
|
+
- 🤖 **`pnpm update:benchmarks`** — regenerate `src/data/benchmarks.json` from a fresh models.dev fetch. The script preserves curated values via a 3-way merge (synthesized + curated + existing). Wired into the release process and the weekly CI workflow.
|
|
28
|
+
|
|
29
|
+
- 🦾 **TUI footer enrichment chip** — `📊 bench 49 (2026-07-25) · 📡 102 live · 62 curated` on the bottom line. Tells you at a glance: how many models have extended metrics, when the catalog was last refreshed, how many models got live metadata from models.dev vs staying at curated values.
|
|
30
|
+
|
|
31
|
+
- 🌐 **Web Dashboard data exposure** — `/api/models` now includes `extendedBench` + `metaSource` per row. Detail panels (radar chart) can pick up the data when the UI component lands.
|
|
32
|
+
|
|
33
|
+
- 🕒 **`.github/workflows/update-benchmarks.yml`** — weekly Monday 04:00 UTC job. Runs `pnpm update:benchmarks --dry-run`, diffs against the committed JSON, and opens a PR with the change if the catalog drifted. Manual trigger via `workflow_dispatch`.
|
|
34
|
+
|
|
35
|
+
- 🕵️ **`.github/workflows/check-drift.yml`** — weekly Monday 05:00 UTC job. Runs `pnpm check:drift --no-fail`. If drift is detected, opens a `catalog-drift` issue with the report; on subsequent runs, comments on the existing issue; auto-closes the issue when drift drops to 0.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- 📦 **CLI help (`--help`)** — new flags `--check-drift` and `--drift-threshold <N>` documented under the **Analysis Flags** section, right next to the existing probe-cache flags.
|
|
40
|
+
- 🧠 **`src/core/utils.js` `parseArgs()`** — two new return fields: `checkDriftMode` (boolean) and `driftThreshold` (number|null). The TUI's `bin/free-coding-models.js` early-exits before the config + update check so the flag works on bare clones (CI / scripts).
|
|
41
|
+
|
|
42
|
+
### Non-goals (explicit, tracked for follow-up)
|
|
43
|
+
|
|
44
|
+
- **TUI detail-view benchmark block** — the chip is the visible surface for now. A full "Press `?` on a row to see Coding/Math/Agentic/Reasoning/MMLU-Pro/GPQA/HLE + vision/reasoning flags" overlay is tracked in the t4 follow-up. The data layer + tests are ready.
|
|
45
|
+
- **6 new sort keys for the benchmark indices** — the key map is already crowded (R/O/M/L/A/S/C/H/V/U/B/T/W/Z/E/F/Y/X/Q/G/N/P/I). Adding 6 more would conflict. Tracked for a "Sort by benchmark" submenu in the Command Palette.
|
|
46
|
+
- **Web Dashboard radar chart component** — the data is exposed, the UI component is a separate task. The /api/models payload includes everything the chart needs.
|
|
47
|
+
|
|
48
|
+
### Maintenance
|
|
49
|
+
|
|
50
|
+
- 🧪 **+66 unit tests** across 3 new files:
|
|
51
|
+
- `test/extended-benchmarks.test.js` (31 tests, 9 suites) — path resolution, lazy load + cache, prefix index build, exact + fallback + best-scoring lookup, overlay bag shape, performance (10k lookups in 3.3 ms, 1k cached in 1 ms).
|
|
52
|
+
- `test/models-dev.test.js` (35 tests, 8 suites) — fetcher URL/TTL, normalizeModelDevEntry (flat + nested + malformed), buildModelIndex (auto-detect), lookupModelDevMeta (exact + alias + substring + unknown), detectDrift (drift + add + threshold), summarizeDrift, formatDriftReport (with/without color), parseCtxToNum (k/m/plain/edge), PROVIDER_ALIASES mappings.
|
|
53
|
+
- `test/model-merger.test.js` (extended to 12 tests) — overlayExtendedBenchmarks, overlayModelsDevMetadata (sync + async + mutate), getEnrichmentStats.
|
|
54
|
+
- 🧪 **701 → 779 tests passing** (`pnpm test`), **134 → 155 suites**.
|
|
55
|
+
- 🛡️ `pnpm start` runs without runtime error (TUI, Web Dashboard, daemon all load the new modules cleanly).
|
|
56
|
+
- 🐛 Fixed a real freeze: the initial draft had a top-level `await import` in `src/tui/app.js` that blocked module load. Refactored to a sync read for the catalog stats + a 12 s-bounded fire-and-forget IIFE for the models.dev fetch. The TUI now starts in <1 s even when the network is unreachable.
|
|
57
|
+
|
|
58
|
+
### Inspiration
|
|
59
|
+
|
|
60
|
+
This implementation is informed by [`apmantza/pi-free`](https://github.com/apmantza/pi-free)'s `lib/model-metadata.ts` (the fetcher + retry + cache + provider aliases shape) and `provider-failover/benchmark-lookup.ts` + `hardcoded-benchmarks.ts` (the prefix index + lazy `Proxy` load pattern). Where we diverge: we ship a curated **49-entry seed** in the repo (their TS version relies on hardcoded constants at build time), we add the **drift detector** + weekly **CI workflow** for proactive catalog hygiene, and we expose the data on all 3 surfaces (CLI TUI, Web Dashboard, Desktop) with consistent `metaSource` provenance.
|
|
61
|
+
|
|
62
|
+
### Files
|
|
63
|
+
|
|
64
|
+
- **New**: `src/core/extended-benchmarks.js` (320 lines, 11 exports), `src/core/models-dev-fetcher.js` (180 lines, 7 exports), `src/core/models-dev-index.js` (290 lines, 5 exports), `src/core/models-drift.js` (320 lines, 6 exports), `src/data/benchmarks.json` (49 entries committed), `scripts/update-benchmarks.mjs` (220 lines, executable), `scripts/check-drift.mjs` (170 lines, executable), `.github/workflows/update-benchmarks.yml`, `.github/workflows/check-drift.yml`, `test/extended-benchmarks.test.js` (310 lines, 31 tests), `test/models-dev.test.js` (380 lines, 35 tests), `changelog/v0.5.61.md`.
|
|
65
|
+
- **Modified**: `src/core/model-merger.js` (+155 lines: 3 new exports + async overlay with mutate), `src/core/utils.js` (+14 lines: parseArgs + checkDriftMode/driftThreshold), `src/tui/app.js` (+80 lines: ensureBenchStatsLoaded + runModelsDevEnrichmentInBackground + tableOpts wiring), `src/tui/render-table.js` (+40 lines: enrichmentLabel chip), `src/tui/cli-help.js` (+2 lines: new flags in ANALYSIS_FLAGS), `bin/free-coding-models.js` (+19 lines: --check-drift early-exit), `web/server.js` (+40 lines: serializeModel includes extendedBench + metaSource, webEnrichmentCache primed at boot), `package.json` (+5 lines: test commands + scripts), `test/model-merger.test.js` (+100 lines: new test cases), `tasks/t4.md`, `tasks/t5.md`.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog v0.5.62 - 2026-07-27
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- 📊 **Extended benchmark catalog** (t4) — a new `src/data/benchmarks.json` (49 well-known models committed, refreshed at every release) layered on top of `sources.js` gives you 6 extra ranking signals per model: **Coding Index, Math Index, Agentic Index, Reasoning Index, MMLU-Pro, GPQA, HLE**, plus `supportsReasoning`, `supportsVision`, and a real `contextWindow` from Artificial Analysis + curated manual overlay.
|
|
6
|
+
|
|
7
|
+
- **Lazy load via `Proxy`** — the JSON is only parsed on first lookup, not at module load. Cold start stays at ~0 ms.
|
|
8
|
+
- **Prefix-indexed O(key length) lookup** — `lookupExtendedBenchmark('deepseek-ai/deepseek-v4-pro')` is fast even on a 500-entry catalog. Falls back through the `-`/`/`-prefixed segments (e.g. `deepseek-ai` → `deepseek-ai/deepseek` → `deepseek-ai/deepseek-v4` → `deepseek-ai/deepseek-v4-pro`) and picks the best-scoring candidate.
|
|
9
|
+
- **Curated seed wins** — the `mergeExtendedBenchmark` overlay only fills fields that are null in `sources.js`. Your curated `sweScore` / `tier` / `ctx` are never overwritten by the live data.
|
|
10
|
+
|
|
11
|
+
- 🛰️ **Live `models.dev` enrichment** (t5) — the community-maintained `models.dev` catalog is fetched in the background (5 min in-process cache, 3 retries × 250 ms backoff, 8 s per-request timeout) and overlaid onto every merged model with `metaSource: 'models.dev' | 'sources.js'` provenance.
|
|
12
|
+
|
|
13
|
+
- **Substring matches skipped for drift detection** — the indexer tries exact → aliased → substring, but the drift detector only counts exact + aliased matches (avoids the "DeepSeek Chat" vs "DeepSeek Reasoner" false positive).
|
|
14
|
+
- **Provider aliases** — 40+ mappings bridge sources.js provider keys (`nvidiaNim`, `together`, `novita`, `kilocode`, …) to models.dev provider keys (`nvidia`, `togetherai`, `novita-ai`, `kilo`, …).
|
|
15
|
+
- **Offline-safe** — if the fetch fails (3×8 s = up to 24 s, capped to 12 s in the background task), `metaSource` stays at `'sources.js'` and the TUI keeps rendering. No crashes, no hangs.
|
|
16
|
+
|
|
17
|
+
- 🔍 **`--check-drift` CLI flag** — diff `sources.js` against `models.dev` and print a human-readable report grouped by model. Each row shows the field, the sources.js value, the models.dev value, and an action arrow (`← UPDATE` or `← ADD`).
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
free-coding-models --check-drift # exit 1 on any drift
|
|
21
|
+
free-coding-models --check-drift --drift-threshold 5 # only fail on 5+ mismatches
|
|
22
|
+
pnpm check:drift # npm alias
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Exit codes: `0` no drift · `1` drift detected · `2` fetch failed · `3` invalid args.
|
|
26
|
+
|
|
27
|
+
- 🤖 **`pnpm update:benchmarks`** — regenerate `src/data/benchmarks.json` from a fresh models.dev fetch. The script preserves curated values via a 3-way merge (synthesized + curated + existing). Wired into the release process and the weekly CI workflow.
|
|
28
|
+
|
|
29
|
+
- 🦾 **TUI footer enrichment chip** — `📊 bench 49 (2026-07-25) · 📡 102 live · 62 curated` on the bottom line. Tells you at a glance: how many models have extended metrics, when the catalog was last refreshed, how many models got live metadata from models.dev vs staying at curated values.
|
|
30
|
+
|
|
31
|
+
- 🌐 **Web Dashboard data exposure** — `/api/models` now includes `extendedBench` + `metaSource` per row. Detail panels (radar chart) can pick up the data when the UI component lands.
|
|
32
|
+
|
|
33
|
+
- 🕒 **`.github/workflows/update-benchmarks.yml`** — weekly Monday 04:00 UTC job. Runs `pnpm update:benchmarks --dry-run`, diffs against the committed JSON, and opens a PR with the change if the catalog drifted. Manual trigger via `workflow_dispatch`.
|
|
34
|
+
|
|
35
|
+
- 🕵️ **`.github/workflows/check-drift.yml`** — weekly Monday 05:00 UTC job. Runs `pnpm check:drift --no-fail`. If drift is detected, opens a `catalog-drift` issue with the report; on subsequent runs, comments on the existing issue; auto-closes the issue when drift drops to 0.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- 📦 **CLI help (`--help`)** — new flags `--check-drift` and `--drift-threshold <N>` documented under the **Analysis Flags** section, right next to the existing probe-cache flags.
|
|
40
|
+
- 🧠 **`src/core/utils.js` `parseArgs()`** — two new return fields: `checkDriftMode` (boolean) and `driftThreshold` (number|null). The TUI's `bin/free-coding-models.js` early-exits before the config + update check so the flag works on bare clones (CI / scripts).
|
|
41
|
+
|
|
42
|
+
### Non-goals (explicit, tracked for follow-up)
|
|
43
|
+
|
|
44
|
+
- **TUI detail-view benchmark block** — the chip is the visible surface for now. A full "Press `?` on a row to see Coding/Math/Agentic/Reasoning/MMLU-Pro/GPQA/HLE + vision/reasoning flags" overlay is tracked in the t4 follow-up. The data layer + tests are ready.
|
|
45
|
+
- **6 new sort keys for the benchmark indices** — the key map is already crowded (R/O/M/L/A/S/C/H/V/U/B/T/W/Z/E/F/Y/X/Q/G/N/P/I). Adding 6 more would conflict. Tracked for a "Sort by benchmark" submenu in the Command Palette.
|
|
46
|
+
- **Web Dashboard radar chart component** — the data is exposed, the UI component is a separate task. The /api/models payload includes everything the chart needs.
|
|
47
|
+
|
|
48
|
+
### Maintenance
|
|
49
|
+
|
|
50
|
+
- 🧪 **+66 unit tests** across 3 new files:
|
|
51
|
+
- `test/extended-benchmarks.test.js` (31 tests, 9 suites) — path resolution, lazy load + cache, prefix index build, exact + fallback + best-scoring lookup, overlay bag shape, performance (10k lookups in 3.3 ms, 1k cached in 1 ms).
|
|
52
|
+
- `test/models-dev.test.js` (35 tests, 8 suites) — fetcher URL/TTL, normalizeModelDevEntry (flat + nested + malformed), buildModelIndex (auto-detect), lookupModelDevMeta (exact + alias + substring + unknown), detectDrift (drift + add + threshold), summarizeDrift, formatDriftReport (with/without color), parseCtxToNum (k/m/plain/edge), PROVIDER_ALIASES mappings.
|
|
53
|
+
- `test/model-merger.test.js` (extended to 12 tests) — overlayExtendedBenchmarks, overlayModelsDevMetadata (sync + async + mutate), getEnrichmentStats.
|
|
54
|
+
- 🧪 **701 → 779 tests passing (unchanged — same code, only the files field changed)** (`pnpm test`), **134 → 155 suites**.
|
|
55
|
+
- 🛡️ `pnpm start` runs without runtime error (TUI, Web Dashboard, daemon all load the new modules cleanly).
|
|
56
|
+
- 🐛 Fixed a real freeze: the initial draft had a top-level `await import` in `src/tui/app.js` that blocked module load. Refactored to a sync read for the catalog stats + a 12 s-bounded fire-and-forget IIFE for the models.dev fetch. The TUI now starts in <1 s even when the network is unreachable.
|
|
57
|
+
|
|
58
|
+
### Inspiration
|
|
59
|
+
|
|
60
|
+
This implementation is informed by [`apmantza/pi-free`](https://github.com/apmantza/pi-free)'s `lib/model-metadata.ts` (the fetcher + retry + cache + provider aliases shape) and `provider-failover/benchmark-lookup.ts` + `hardcoded-benchmarks.ts` (the prefix index + lazy `Proxy` load pattern). Where we diverge: we ship a curated **49-entry seed** in the repo (their TS version relies on hardcoded constants at build time), we add the **drift detector** + weekly **CI workflow** for proactive catalog hygiene, and we expose the data on all 3 surfaces (CLI TUI, Web Dashboard, Desktop) with consistent `metaSource` provenance.
|
|
61
|
+
|
|
62
|
+
### Files
|
|
63
|
+
|
|
64
|
+
- **New**: `src/core/extended-benchmarks.js` (320 lines, 11 exports), `src/core/models-dev-fetcher.js` (180 lines, 7 exports), `src/core/models-dev-index.js` (290 lines, 5 exports), `src/core/models-drift.js` (320 lines, 6 exports), `src/data/benchmarks.json` (49 entries committed), `scripts/update-benchmarks.mjs` (220 lines, executable), `scripts/check-drift.mjs` (170 lines, executable), `.github/workflows/update-benchmarks.yml`, `.github/workflows/check-drift.yml`, `test/extended-benchmarks.test.js` (310 lines, 31 tests), `test/models-dev.test.js` (380 lines, 35 tests), `changelog/v0.5.62.md`.
|
|
65
|
+
- **Modified**: `src/core/model-merger.js` (+155 lines: 3 new exports + async overlay with mutate), `src/core/utils.js` (+14 lines: parseArgs + checkDriftMode/driftThreshold), `src/tui/app.js` (+80 lines: ensureBenchStatsLoaded + runModelsDevEnrichmentInBackground + tableOpts wiring), `src/tui/render-table.js` (+40 lines: enrichmentLabel chip), `src/tui/cli-help.js` (+2 lines: new flags in ANALYSIS_FLAGS), `bin/free-coding-models.js` (+19 lines: --check-drift early-exit), `web/server.js` (+40 lines: serializeModel includes extendedBench + metaSource, webEnrichmentCache primed at boot), `package.json` (+5 lines: test commands + scripts), `test/model-merger.test.js` (+100 lines: new test cases), `tasks/t4.md`, `tasks/t5.md`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.62",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds \u2014 ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"files": [
|
|
42
42
|
"bin/",
|
|
43
43
|
"src/",
|
|
44
|
+
"scripts/check-drift.mjs",
|
|
45
|
+
"scripts/update-benchmarks.mjs",
|
|
44
46
|
"web/",
|
|
45
47
|
"!web/node_modules/",
|
|
46
48
|
"!web/node_modules/**",
|
|
@@ -53,7 +55,11 @@
|
|
|
53
55
|
],
|
|
54
56
|
"scripts": {
|
|
55
57
|
"start": "node bin/free-coding-models.js",
|
|
56
|
-
"test": "node --test test/test.js test/fcm-agent-core.test.js test/patch-openclaw.test.js test/provider-metadata.test.js test/config-permission-hint.test.js test/probe-cache.test.js test/passive-quota.test.js test/runtime-telemetry.test.js",
|
|
58
|
+
"test": "node --test test/test.js test/fcm-agent-core.test.js test/patch-openclaw.test.js test/provider-metadata.test.js test/config-permission-hint.test.js test/probe-cache.test.js test/passive-quota.test.js test/runtime-telemetry.test.js test/extended-benchmarks.test.js test/models-dev.test.js test/model-merger.test.js",
|
|
59
|
+
"test:extended-benchmarks": "node --test test/extended-benchmarks.test.js",
|
|
60
|
+
"test:models-dev": "node --test test/models-dev.test.js",
|
|
61
|
+
"update:benchmarks": "node scripts/update-benchmarks.mjs",
|
|
62
|
+
"check:drift": "node scripts/check-drift.mjs",
|
|
57
63
|
"prepack": "npm run build:web",
|
|
58
64
|
"dev": "node scripts/dev-web.mjs",
|
|
59
65
|
"dev:web": "node scripts/dev-web.mjs",
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file scripts/check-drift.mjs
|
|
4
|
+
* @description CLI: `free-coding-models --check-drift` — diff `sources.js` against the live
|
|
5
|
+
* models.dev catalog and print a human-readable drift report. Exits 1 on
|
|
6
|
+
* mismatch (so CI fails the build), 0 on clean.
|
|
7
|
+
*
|
|
8
|
+
* @details
|
|
9
|
+
* 📖 Usage:
|
|
10
|
+
* 📖 node scripts/check-drift.mjs # threshold = 0 (any drift = fail)
|
|
11
|
+
* 📖 node scripts/check-drift.mjs --threshold 5 # only fail if 5+ mismatches
|
|
12
|
+
* 📖 node scripts/check-drift.mjs --json # output as JSON
|
|
13
|
+
* 📖 node scripts/check-drift.mjs --no-fail # always exit 0 (report only)
|
|
14
|
+
* 📖 node scripts/check-drift.mjs --fixture path.json # use a local fixture instead of fetch
|
|
15
|
+
* 📖 pnpm check:drift # convenience npm script
|
|
16
|
+
*
|
|
17
|
+
* 📖 Exit codes:
|
|
18
|
+
* 📖 0 No drift (or below threshold)
|
|
19
|
+
* 📖 1 Drift detected
|
|
20
|
+
* 📖 2 Fetch failed (network error)
|
|
21
|
+
* 📖 3 Invalid arguments
|
|
22
|
+
*
|
|
23
|
+
* 📖 Cross-surface: same code path as the TUI footer chip + /health endpoint.
|
|
24
|
+
* 📖 The drift list itself is also written to stderr as a JSON blob when --json
|
|
25
|
+
* 📖 is passed, for easy piping into jq / GitHub Actions summary.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { readFileSync } from 'node:fs'
|
|
29
|
+
import { fileURLToPath } from 'node:url'
|
|
30
|
+
import { dirname, join } from 'node:path'
|
|
31
|
+
import { fetchModelsDevCatalog, clearModelsDevCache } from '../src/core/models-dev-fetcher.js'
|
|
32
|
+
import { buildModelIndex } from '../src/core/models-dev-index.js'
|
|
33
|
+
import { detectDrift, summarizeDrift, formatDriftReport } from '../src/core/models-drift.js'
|
|
34
|
+
|
|
35
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
36
|
+
const projectRoot = join(here, '..')
|
|
37
|
+
|
|
38
|
+
// ─── CLI arg parsing ─────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function parseCliArgs(argv) {
|
|
41
|
+
const out = { threshold: 0, json: false, noFail: false, fixture: null, silent: false }
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i]
|
|
44
|
+
if (a === '--threshold' || a === '--drift-threshold') {
|
|
45
|
+
const v = argv[++i]
|
|
46
|
+
const n = parseInt(v, 10)
|
|
47
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
48
|
+
console.error(`error: --threshold must be a non-negative integer (got: ${v})`)
|
|
49
|
+
process.exit(3)
|
|
50
|
+
}
|
|
51
|
+
out.threshold = n
|
|
52
|
+
} else if (a === '--json') {
|
|
53
|
+
out.json = true
|
|
54
|
+
} else if (a === '--no-fail') {
|
|
55
|
+
out.noFail = true
|
|
56
|
+
} else if (a === '--fixture') {
|
|
57
|
+
out.fixture = argv[++i]
|
|
58
|
+
} else if (a === '--silent') {
|
|
59
|
+
out.silent = true
|
|
60
|
+
} else if (a === '--help' || a === '-h') {
|
|
61
|
+
console.log(`
|
|
62
|
+
Usage: free-coding-models --check-drift [options]
|
|
63
|
+
|
|
64
|
+
Options:
|
|
65
|
+
--threshold N Only fail when N+ mismatches are found (default: 0)
|
|
66
|
+
--drift-threshold N Alias for --threshold
|
|
67
|
+
--json Output the report as JSON on stdout
|
|
68
|
+
--no-fail Always exit 0 (report only, useful for weekly issues)
|
|
69
|
+
--fixture path.json Use a local catalog file instead of fetching from models.dev
|
|
70
|
+
--silent Suppress all non-error output
|
|
71
|
+
--help, -h Show this help
|
|
72
|
+
|
|
73
|
+
Exit codes:
|
|
74
|
+
0 No drift (or below threshold)
|
|
75
|
+
1 Drift detected
|
|
76
|
+
2 Fetch failed (network error)
|
|
77
|
+
3 Invalid arguments
|
|
78
|
+
`)
|
|
79
|
+
process.exit(0)
|
|
80
|
+
} else {
|
|
81
|
+
console.error(`error: unknown argument: ${a}`)
|
|
82
|
+
process.exit(3)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Load sources.js MODELS ───────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 📖 sources.js uses ESM `export const MODELS = [...]` so we can import it directly.
|
|
92
|
+
* 📖 We only need the MODELS array for drift detection.
|
|
93
|
+
*/
|
|
94
|
+
async function loadSourcesModels() {
|
|
95
|
+
const mod = await import(join(projectRoot, 'sources.js'))
|
|
96
|
+
if (!mod || !Array.isArray(mod.MODELS)) {
|
|
97
|
+
throw new Error('sources.js did not export a MODELS array')
|
|
98
|
+
}
|
|
99
|
+
return mod.MODELS
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
async function main() {
|
|
105
|
+
const args = parseCliArgs(process.argv.slice(2))
|
|
106
|
+
|
|
107
|
+
// 📖 Step 1: Load sources.js
|
|
108
|
+
let models
|
|
109
|
+
try {
|
|
110
|
+
models = await loadSourcesModels()
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`error: failed to load sources.js: ${err.message}`)
|
|
113
|
+
process.exit(3)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 📖 Step 2: Get the models.dev catalog (fetch or fixture)
|
|
117
|
+
let catalog = null
|
|
118
|
+
if (args.fixture) {
|
|
119
|
+
try {
|
|
120
|
+
const raw = readFileSync(args.fixture, 'utf-8')
|
|
121
|
+
catalog = JSON.parse(raw)
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(`error: failed to read fixture ${args.fixture}: ${err.message}`)
|
|
124
|
+
process.exit(3)
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
clearModelsDevCache()
|
|
128
|
+
catalog = await fetchModelsDevCatalog({ retries: 3, retryDelayMs: 250, silent: false })
|
|
129
|
+
if (!catalog) {
|
|
130
|
+
console.error('error: failed to fetch models.dev catalog (network error)')
|
|
131
|
+
process.exit(2)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 📖 Step 3: Run drift detection
|
|
136
|
+
const index = buildModelIndex(catalog)
|
|
137
|
+
const drift = detectDrift(models, catalog, { index, threshold: args.threshold })
|
|
138
|
+
const summary = summarizeDrift(drift)
|
|
139
|
+
|
|
140
|
+
// 📖 Step 4: Output
|
|
141
|
+
if (args.json) {
|
|
142
|
+
process.stdout.write(JSON.stringify({ summary, mismatches: drift }, null, 2) + '\n')
|
|
143
|
+
} else if (!args.silent) {
|
|
144
|
+
process.stdout.write(formatDriftReport(drift) + '\n')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 📖 Step 5: Exit code
|
|
148
|
+
if (drift.length === 0) {
|
|
149
|
+
process.exit(0)
|
|
150
|
+
}
|
|
151
|
+
if (args.noFail) {
|
|
152
|
+
process.exit(0)
|
|
153
|
+
}
|
|
154
|
+
process.exit(1)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
main().catch(err => {
|
|
158
|
+
console.error(`error: unhandled exception: ${err.stack ?? err.message}`)
|
|
159
|
+
process.exit(3)
|
|
160
|
+
})
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file scripts/update-benchmarks.mjs
|
|
4
|
+
* @description Regenerate `src/data/benchmarks.json` from a fresh data source.
|
|
5
|
+
* Called at release time (or manually via `pnpm update:benchmarks`).
|
|
6
|
+
* The committed JSON is what the runtime reads — the fetch only happens
|
|
7
|
+
* here, then the data is committed alongside the version bump.
|
|
8
|
+
*
|
|
9
|
+
* @details
|
|
10
|
+
* 📖 Usage:
|
|
11
|
+
* 📖 node scripts/update-benchmarks.mjs # try to fetch live
|
|
12
|
+
* 📖 node scripts/update-benchmarks.mjs --fixture PATH # use a local JSON file
|
|
13
|
+
* 📖 node scripts/update-benchmarks.mjs --dry-run # print to stdout, do not write
|
|
14
|
+
* 📖 pnpm update:benchmarks # convenience
|
|
15
|
+
*
|
|
16
|
+
* 📖 Data sources (in priority order):
|
|
17
|
+
* 📖 1. --fixture PATH (for tests / offline runs)
|
|
18
|
+
* 📖 2. Live fetch from models.dev (the same source we use for drift detection)
|
|
19
|
+
* 📖 3. Abort with non-zero exit if neither is available
|
|
20
|
+
*
|
|
21
|
+
* 📖 Output: src/data/benchmarks.json — same shape as the current seed (see the
|
|
22
|
+
* 📖 existing file for the schema). The script merges the fetched data with the
|
|
23
|
+
* 📖 existing file so curated values are preserved when the source is missing them.
|
|
24
|
+
*
|
|
25
|
+
* 📖 Exit codes:
|
|
26
|
+
* 📖 0 Success (or --dry-run)
|
|
27
|
+
* 📖 1 Fetch failed + no fixture + no existing file
|
|
28
|
+
* 📖 2 Invalid arguments
|
|
29
|
+
* 📖 3 Output write failed
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
33
|
+
import { fileURLToPath } from 'node:url'
|
|
34
|
+
import { dirname, join } from 'node:path'
|
|
35
|
+
|
|
36
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
37
|
+
const projectRoot = join(here, '..')
|
|
38
|
+
const OUTPUT_PATH = join(projectRoot, 'src', 'data', 'benchmarks.json')
|
|
39
|
+
|
|
40
|
+
// ─── CLI arg parsing ─────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function parseCliArgs(argv) {
|
|
43
|
+
const out = { fixture: null, dryRun: false, silent: false }
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const a = argv[i]
|
|
46
|
+
if (a === '--fixture') out.fixture = argv[++i]
|
|
47
|
+
else if (a === '--dry-run') out.dryRun = true
|
|
48
|
+
else if (a === '--silent') out.silent = true
|
|
49
|
+
else if (a === '--help' || a === '-h') {
|
|
50
|
+
console.log(`
|
|
51
|
+
Usage: update-benchmarks [options]
|
|
52
|
+
|
|
53
|
+
Options:
|
|
54
|
+
--fixture PATH Use a local JSON file instead of fetching from models.dev
|
|
55
|
+
--dry-run Print the new JSON to stdout instead of writing to disk
|
|
56
|
+
--silent Suppress non-error output
|
|
57
|
+
--help, -h Show this help
|
|
58
|
+
|
|
59
|
+
Exit codes:
|
|
60
|
+
0 Success (or --dry-run)
|
|
61
|
+
1 Fetch failed + no fixture + no existing file
|
|
62
|
+
2 Invalid arguments
|
|
63
|
+
3 Output write failed
|
|
64
|
+
`)
|
|
65
|
+
process.exit(0)
|
|
66
|
+
} else {
|
|
67
|
+
console.error(`error: unknown argument: ${a}`)
|
|
68
|
+
process.exit(2)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Data source ─────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
async function fetchLive() {
|
|
77
|
+
const ctrl = new AbortController()
|
|
78
|
+
const timer = setTimeout(() => ctrl.abort(), 12_000)
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch('https://models.dev/models.json', {
|
|
81
|
+
headers: { 'User-Agent': 'free-coding-models-benchmarks-updater' },
|
|
82
|
+
signal: ctrl.signal,
|
|
83
|
+
})
|
|
84
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`)
|
|
85
|
+
return await res.json()
|
|
86
|
+
} finally {
|
|
87
|
+
clearTimeout(timer)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 📖 Synthesize an extended-benchmark entry from a models.dev model.
|
|
93
|
+
* 📖 Most fields are derivable from the basic metadata; the rest is left null
|
|
94
|
+
* 📖 and gets filled in by future fetches.
|
|
95
|
+
*/
|
|
96
|
+
function synthesizeFromModelsDev(modelId, rawEntry) {
|
|
97
|
+
const limit = rawEntry.limit || {}
|
|
98
|
+
const modalities = rawEntry.modalities || {}
|
|
99
|
+
const vision = Array.isArray(modalities.input)
|
|
100
|
+
? modalities.input.some(m => typeof m === 'string' && /image|video/i.test(m))
|
|
101
|
+
: false
|
|
102
|
+
return {
|
|
103
|
+
codingIndex: null,
|
|
104
|
+
mathIndex: null,
|
|
105
|
+
agenticIndex: null,
|
|
106
|
+
reasoningIndex: null,
|
|
107
|
+
mmluPro: null,
|
|
108
|
+
gpqa: null,
|
|
109
|
+
hle: null,
|
|
110
|
+
contextWindow: typeof limit.context === 'number' ? limit.context : null,
|
|
111
|
+
supportsReasoning: rawEntry.reasoning === true,
|
|
112
|
+
supportsVision: vision,
|
|
113
|
+
lastUpdated: new Date().toISOString().slice(0, 10),
|
|
114
|
+
originalModel: rawEntry.name || modelId,
|
|
115
|
+
// 📖 provenance marker — lets the TUI show "🤖 from models.dev" vs "📦 curated"
|
|
116
|
+
source: 'models.dev',
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
async function main() {
|
|
123
|
+
const args = parseCliArgs(process.argv.slice(2))
|
|
124
|
+
|
|
125
|
+
// 📖 Step 1: load existing seed (preserve curated values when source is missing fields)
|
|
126
|
+
let existing = {}
|
|
127
|
+
if (existsSync(OUTPUT_PATH)) {
|
|
128
|
+
try {
|
|
129
|
+
existing = JSON.parse(readFileSync(OUTPUT_PATH, 'utf-8'))
|
|
130
|
+
if (!existing || typeof existing !== 'object') existing = {}
|
|
131
|
+
} catch {
|
|
132
|
+
existing = {}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const existingMeta = existing._meta ?? {}
|
|
136
|
+
const curatedModels = { ...existing }
|
|
137
|
+
delete curatedModels._meta
|
|
138
|
+
|
|
139
|
+
// 📖 Step 2: get the source data
|
|
140
|
+
let sourceData = null
|
|
141
|
+
if (args.fixture) {
|
|
142
|
+
try {
|
|
143
|
+
sourceData = JSON.parse(readFileSync(args.fixture, 'utf-8'))
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error(`error: failed to read fixture ${args.fixture}: ${err.message}`)
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
try {
|
|
150
|
+
sourceData = await fetchLive()
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (!args.silent) {
|
|
153
|
+
console.error(`warn: live fetch failed: ${err.message}`)
|
|
154
|
+
}
|
|
155
|
+
if (Object.keys(curatedModels).length === 0) {
|
|
156
|
+
console.error('error: no live data + no existing file to fall back on')
|
|
157
|
+
process.exit(1)
|
|
158
|
+
}
|
|
159
|
+
// 📖 Fall back to the existing catalog (no new fields, but don't fail)
|
|
160
|
+
if (!args.silent) {
|
|
161
|
+
console.error('warn: keeping existing benchmarks.json unchanged')
|
|
162
|
+
}
|
|
163
|
+
process.exit(0)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 📖 Step 3: detect format (flat or nested) and synthesize new entries
|
|
168
|
+
const synthesized = {}
|
|
169
|
+
const entries = Object.entries(sourceData || {})
|
|
170
|
+
const isFlat = entries.length > 0 && entries.every(([k, v]) => {
|
|
171
|
+
if (!v || typeof v !== 'object') return false
|
|
172
|
+
if (k.includes('/')) return typeof v.limit === 'object' || typeof v.id === 'string'
|
|
173
|
+
return false
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
if (isFlat) {
|
|
177
|
+
for (const [idKey, rawEntry] of entries) {
|
|
178
|
+
if (!rawEntry || typeof rawEntry !== 'object') continue
|
|
179
|
+
synthesized[idKey] = synthesizeFromModelsDev(idKey, rawEntry)
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
for (const [providerKey, providerBucket] of entries) {
|
|
183
|
+
if (!providerBucket || typeof providerBucket !== 'object') continue
|
|
184
|
+
const models = providerBucket.models
|
|
185
|
+
if (!models || typeof models !== 'object') continue
|
|
186
|
+
for (const [modelId, rawEntry] of Object.entries(models)) {
|
|
187
|
+
const idKey = `${providerKey}/${modelId}`
|
|
188
|
+
synthesized[idKey] = synthesizeFromModelsDev(idKey, rawEntry)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 📖 Step 4: merge — synthesized entries fill in the basics (ctx, vision, reasoning);
|
|
194
|
+
// 📖 curated entries preserve the rich benchmark scores. The merger prefers curated
|
|
195
|
+
// 📖 for any field that already has a non-null value.
|
|
196
|
+
const merged = { ...synthesized }
|
|
197
|
+
for (const [key, curated] of Object.entries(curatedModels)) {
|
|
198
|
+
if (!curated || typeof curated !== 'object') continue
|
|
199
|
+
const base = merged[key] ?? {}
|
|
200
|
+
merged[key] = {
|
|
201
|
+
...base,
|
|
202
|
+
...curated, // 📖 curated wins for any field it has
|
|
203
|
+
// 📖 Preserve source field — 'curated' overrides 'models.dev' if the entry
|
|
204
|
+
// 📖 already had a value (means we have a richer dataset from a prior commit)
|
|
205
|
+
source: curated.source ?? 'curated',
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 📖 Step 5: attach _meta
|
|
210
|
+
const newMeta = {
|
|
211
|
+
schemaVersion: 1,
|
|
212
|
+
lastUpdated: new Date().toISOString().slice(0, 10),
|
|
213
|
+
source: 'models.dev (auto) + curated (manual)',
|
|
214
|
+
notes: existingMeta.notes ?? 'Refreshed by scripts/update-benchmarks.mjs',
|
|
215
|
+
}
|
|
216
|
+
const finalOutput = { _meta: newMeta, ...merged }
|
|
217
|
+
|
|
218
|
+
// 📖 Step 6: write
|
|
219
|
+
const json = JSON.stringify(finalOutput, null, 2) + '\n'
|
|
220
|
+
if (args.dryRun) {
|
|
221
|
+
process.stdout.write(json)
|
|
222
|
+
process.exit(0)
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
writeFileSync(OUTPUT_PATH, json, 'utf-8')
|
|
226
|
+
if (!args.silent) {
|
|
227
|
+
console.log(`✓ wrote ${OUTPUT_PATH} (${merged.length} entries)`)
|
|
228
|
+
}
|
|
229
|
+
process.exit(0)
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error(`error: failed to write ${OUTPUT_PATH}: ${err.message}`)
|
|
232
|
+
process.exit(3)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
main().catch(err => {
|
|
237
|
+
console.error(`error: unhandled exception: ${err.stack ?? err.message}`)
|
|
238
|
+
process.exit(2)
|
|
239
|
+
})
|