free-coding-models 0.5.53 โ†’ 0.5.55

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.
@@ -0,0 +1,18 @@
1
+ # Changelog v0.5.54 - 2026-07-25
2
+
3
+ ### Fixed
4
+ - ๐Ÿ› ๏ธ **`patch-openclaw.js` now respects real model metadata** ([#133](https://github.com/vava-nessa/free-coding-models/issues/133)) โ€” the script was destructuring only 3 of 5 fields from each `nvidiaNim` tuple (`[modelId, label, tier, sweScore, ctx]`), so it dropped the real `ctx` and fell back to a hardcoded `contextWindow` per tier (e.g. all S+ models got 128000, even `DeepSeek V4 Flash` which actually has 1M context). Now it parses the real `ctx` string (`'200k'` โ†’ 200000, `'1M'` โ†’ 1_000_000) and persists `tier`, `sweScore`, and a `source` field for traceability.
5
+
6
+ - ๐Ÿงญ **fcm_router now shows a real label in the TUI** ([#140](https://github.com/vava-nessa/free-coding-models/issues/140)) โ€” added a `fcm_router` entry to `PROVIDER_METADATA` so every UI surface (Setup wizard, Settings panel, tool launchers, TUI filters) resolves "Smart Router Daemon" instead of the raw key `fcm_router` (which users were reading as "Unknown"). The local `getProviderLabel()` in `endpoint-installer.js` was the only place that knew about `fcm_router`; it now falls through to `PROVIDER_METADATA` so all callers stay in sync.
7
+
8
+ ### Maintenance
9
+ - ๐Ÿงน **Closed 3 stale issues that were already fixed** in older releases (verified by `git log` on the relevant code paths):
10
+ - [#116](https://github.com/vava-nessa/free-coding-models/issues/116) โ€” Docker API 404s (fixed in v0.5.28, commit `2c07925`)
11
+ - [#117](https://github.com/vava-nessa/free-coding-models/issues/117) โ€” "pending" status confusing (fixed in v0.5.28, same commit)
12
+ - [#118](https://github.com/vava-nessa/free-coding-models/issues/118) โ€” Probe loop silent stall (fixed in v0.5.36, commit `f0dd3d8`)
13
+ - ๐Ÿงช **+18 new unit tests** for `patch-openclaw.js` parseCtx/defaultMaxTokens (11) and `provider-metadata.js` fcm_router lookup (7). All test runs go through `pnpm test` (no extra setup needed).
14
+
15
+ ### Pre-flight checks
16
+ - 584/584 tests pass (`pnpm test`)
17
+ - `vite build` succeeds (260 modules, ~283ms)
18
+ - CLI loads cleanly (`free-coding-models --help`)
@@ -0,0 +1,17 @@
1
+ # Changelog v0.5.55 - 2026-07-25
2
+
3
+ ### Fixed
4
+ - ๐Ÿณ **Docker "Tool mode save failed" โ€” clear root cause + actionable fix** ([#119](https://github.com/vava-nessa/free-coding-models/issues/119)) โ€” when the `fcm-data` volume was created with files owned by a different UID (older images, host bind-mounts), `chmod -R 777` had no effect because Linux checks file ownership, not mode bits. Two-layer fix:
5
+ 1. `src/core/config.js` โ€” `saveConfig()` now detects `EACCES` / `EPERM` and surfaces a friendly hint via the new exported `formatPermissionHint()` function. The hint explains *why* `chmod` is the wrong tool and gives the exact `docker compose down && docker volume rm <vol> && docker compose up` fix.
6
+ 2. `docker-entrypoint.sh` โ€” proactive ownership check at container startup. If a config file is owned by a different UID than the running `fcm` user, the entrypoint logs a loud `โš ๏ธ WARNING` block *before* the first save attempt fails with a confusing error.
7
+
8
+ ### Closed (upstream, no FCM-side fix possible)
9
+ - ๐Ÿ“Œ **Caveman CLI install fails on Node 26** ([#101](https://github.com/vava-nessa/free-coding-models/issues/101)) โ€” this is upstream in `@juliusbrussee/caveman-code`. `better-sqlite3 v11.10.0` doesn't compile against Node.js v26 (V8 API churn: `GetPrototype` โ†’ `GetPrototypeV2`, etc.). FCM surfaces the npm error verbatim but cannot fix caveman-code's deps. Workaround: use Node v22 or v24 (both LTS, both supported by `better-sqlite3@11`).
10
+
11
+ ### Maintenance
12
+ - ๐Ÿงช **+6 new unit tests** for `formatPermissionHint()` (`test/config-permission-hint.test.js`) โ€” covers empty string for non-permission errors, EACCES/EPERM detection, Docker volume mention, "chmod is not the fix" wording, multi-line output.
13
+
14
+ ### Pre-flight checks
15
+ - 590/590 tests pass (`pnpm test`)
16
+ - `vite build` succeeds
17
+ - All three surfaces (CLI, web dashboard, Docker) compatible
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.5.53",
3
+ "version": "0.5.55",
4
4
  "description": "Find the fastest coding LLM models in seconds โ€” ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
5
5
  "keywords": [
6
6
  "nvidia",
@@ -53,7 +53,7 @@
53
53
  ],
54
54
  "scripts": {
55
55
  "start": "node bin/free-coding-models.js",
56
- "test": "node --test test/test.js test/fcm-agent-core.test.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",
57
57
  "prepack": "npm run build:web",
58
58
  "dev": "node scripts/dev-web.mjs",
59
59
  "dev:web": "node scripts/dev-web.mjs",
package/patch-openclaw.js CHANGED
@@ -18,21 +18,24 @@ const OPENCLAW_JSON = join(homedir(), '.openclaw', 'openclaw.json')
18
18
  console.log('๐Ÿฆž Patching OpenClaw for full NVIDIA model support...\n')
19
19
 
20
20
  // โ”€โ”€โ”€ Helper functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
21
- function getModelConfig(tier) {
22
- // S+/S tier: largest context
23
- if (tier === 'S+' || tier === 'S') {
24
- return { contextWindow: 128000, maxTokens: 8192 }
25
- }
26
- // A+ tier
27
- if (tier === 'A+') {
28
- return { contextWindow: 131072, maxTokens: 4096 }
29
- }
30
- // A/A- tier
31
- if (tier === 'A' || tier === 'A-') {
32
- return { contextWindow: 131072, maxTokens: 4096 }
33
- }
34
- // B+/B/C tier: smaller context
35
- return { contextWindow: 32768, maxTokens: 2048 }
21
+ // ๐Ÿ“– Parse a context-window string from sources.js: "200k" โ†’ 200_000, "1M" โ†’ 1_000_000.
22
+ // Falls back to 8192 if the format is unexpected.
23
+ function parseCtx(ctxStr) {
24
+ if (typeof ctxStr !== 'string') return 8192
25
+ const m = ctxStr.trim().match(/^(\d+(?:\.\d+)?)([kKmM]?)$/)
26
+ if (!m) return 8192
27
+ const n = Number(m[1])
28
+ const unit = m[2]
29
+ if (unit === 'M' || unit === 'm') return Math.round(n * 1_000_000)
30
+ if (unit === 'K' || unit === 'k') return Math.round(n * 1000)
31
+ return Math.round(n)
32
+ }
33
+
34
+ // ๐Ÿ“– Choose a sensible maxTokens from the real contextWindow.
35
+ // Rule: ~5% of context, clamped between 2k and 16k.
36
+ function defaultMaxTokens(ctx) {
37
+ const pct = Math.round(ctx * 0.05)
38
+ return Math.max(2048, Math.min(16384, pct))
36
39
  }
37
40
 
38
41
  // โ”€โ”€โ”€ Patch models.json โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -70,22 +73,29 @@ if (!modelsConfig.providers.nvidia) {
70
73
  const existingModelIds = new Set(modelsConfig.providers.nvidia.models.map(m => m.id))
71
74
 
72
75
  // Add all models from sources.js
76
+ // ๐Ÿ“– Each nvidiaNim tuple is: [modelId, label, tier, sweScore, ctx] โ€” see sources.js:39
73
77
  let addedCount = 0
74
- for (const [modelId, label, tier] of nvidiaNim) {
78
+ for (const [modelId, label, tier, sweScore, ctx] of nvidiaNim) {
75
79
  if (existingModelIds.has(modelId)) {
76
80
  continue // Skip already existing models
77
81
  }
78
82
 
79
- const config = getModelConfig(tier)
83
+ const contextWindow = parseCtx(ctx)
84
+ const maxTokens = defaultMaxTokens(contextWindow)
80
85
  const isThinking = modelId.includes('thinking')
81
86
 
82
87
  modelsConfig.providers.nvidia.models.push({
83
88
  id: modelId,
84
89
  name: label,
85
- contextWindow: config.contextWindow,
86
- maxTokens: config.maxTokens,
90
+ contextWindow,
91
+ maxTokens,
87
92
  reasoning: isThinking,
88
93
  input: ['text'],
94
+ // ๐Ÿ“– OpenClaw ignores unknown fields, but we persist the real metadata
95
+ // so anyone reading models.json can see the source-of-truth values.
96
+ tier,
97
+ sweScore,
98
+ source: 'free-coding-models/sources.js',
89
99
  cost: {
90
100
  input: 0,
91
101
  output: 0,
@@ -760,7 +760,16 @@ export function saveConfig(config, options = {}) {
760
760
  // ๐Ÿ“– Write failed - explicit error instead of silent failure
761
761
  let errorMsg = `Failed to write config: ${writeError.message}`
762
762
  try { unlinkSync(tempPath) } catch { /* ignore temp cleanup failures */ }
763
-
763
+
764
+ // ๐Ÿ“– Detect permission errors and surface a Docker-specific hint. This
765
+ // ๐Ÿ“– happens when a volume was created with files owned by another UID
766
+ // ๐Ÿ“– (e.g. older images running as root, or a host bind-mount where the
767
+ // ๐Ÿ“– directory is owned by a different user). chmod 777 does NOT fix this
768
+ // ๐Ÿ“– because Linux checks file ownership, not mode bits, for write access.
769
+ if (writeError?.code === 'EACCES' || writeError?.code === 'EPERM') {
770
+ errorMsg += formatPermissionHint(writeError)
771
+ }
772
+
764
773
  // ๐Ÿ“– Try to restore from backup if we have one
765
774
  if (backupCreated) {
766
775
  try {
@@ -775,6 +784,37 @@ export function saveConfig(config, options = {}) {
775
784
  }
776
785
  }
777
786
 
787
+ /**
788
+ * ๐Ÿ“– formatPermissionHint โ€” build a friendly hint for the most common Docker
789
+ * ๐Ÿ“– permission failure (config file owned by a different UID than the running
790
+ * ๐Ÿ“– process). Returns an empty string for non-permission errors.
791
+ *
792
+ * ๐Ÿ“– Common symptom (issue #119): "Tool mode save failed" in the web dashboard
793
+ * ๐Ÿ“– even after `chmod -R 777`, because chmod doesn't change ownership.
794
+ */
795
+ export function formatPermissionHint(writeError) {
796
+ // ๐Ÿ“– Only emit the hint for actual permission errors โ€” silently no-op for
797
+ // ๐Ÿ“– anything else so the caller can safely concatenate the result.
798
+ if (writeError?.code !== 'EACCES' && writeError?.code !== 'EPERM') return ''
799
+ const lines = []
800
+ lines.push('')
801
+ lines.push(' ๐Ÿ’ก This is almost always a file-ownership issue, not a chmod issue.')
802
+ lines.push(' The config file is owned by a different user than the one running FCM.')
803
+ try {
804
+ const stat = statSync(CONFIG_PATH)
805
+ lines.push(` File owner UID: ${stat.uid}, current process UID: ${process.getuid?.() ?? 'n/a'}`)
806
+ } catch { /* stat may also fail */ }
807
+ lines.push('')
808
+ lines.push(' Fix in Docker:')
809
+ lines.push(' docker compose down')
810
+ lines.push(' docker volume rm <project>_fcm-data # or the volume name from `docker volume ls`')
811
+ lines.push(' docker compose up # recreates the volume with the right UID')
812
+ lines.push('')
813
+ lines.push(' Fix on host (bind mount):')
814
+ lines.push(' sudo chown $(id -u):$(id -g) ~/.free-coding-models.json')
815
+ return '\n' + lines.join('\n')
816
+ }
817
+
778
818
  /**
779
819
  * ๐Ÿ“– createBackup: Creates a timestamped backup of the current config file.
780
820
  * ๐Ÿ“– Keeps only the 5 most recent backups to avoid disk space issues.
@@ -136,7 +136,9 @@ function getManagedProviderId(providerKey) {
136
136
  }
137
137
 
138
138
  function getProviderLabel(providerKey) {
139
- if (providerKey === 'fcm_router') return 'Smart Router Daemon'
139
+ // ๐Ÿ“– fcm_router is now in PROVIDER_METADATA (provider-metadata.js) so all
140
+ // ๐Ÿ“– surfaces resolve to the same label. Issue #140 surfaced because this
141
+ // ๐Ÿ“– helper was the only place that knew about fcm_router.
140
142
  return PROVIDER_METADATA[providerKey]?.label || sources[providerKey]?.name || providerKey
141
143
  }
142
144
 
@@ -79,6 +79,18 @@ export const OPENCODE_MODEL_MAP = {
79
79
  // ๐Ÿ“– `rateLimits` gives a quick reminder of the free-tier quota without opening a browser.
80
80
  // ๐Ÿ“– `paidProviderNote` marks providers that require credits/billing despite exposing free/trial/free-tagged models.
81
81
  export const PROVIDER_METADATA = {
82
+ // ๐Ÿ“– fcm_router is the local Smart Router Daemon โ€” it's not a remote provider
83
+ // ๐Ÿ“– with an API key or signup URL, but several UI surfaces look it up here
84
+ // ๐Ÿ“– to render a human label. Without this entry, the label resolves to the
85
+ // ๐Ÿ“– raw key `fcm_router` (which users read as "Unknown" โ€” see issue #140).
86
+ fcm_router: {
87
+ label: 'Smart Router Daemon',
88
+ color: chalk.rgb(80, 200, 120),
89
+ signupUrl: null,
90
+ signupHint: null,
91
+ rateLimits: 'Local daemon on http://127.0.0.1:19280 โ€” start with Shift+S',
92
+ noKeyNeeded: true,
93
+ },
82
94
  nvidia: {
83
95
  label: 'NVIDIA NIM',
84
96
  color: chalk.rgb(178, 235, 190),