free-coding-models 0.1.4 → 0.1.12

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 CHANGED
@@ -313,6 +313,9 @@ Or run without flags and choose **OpenClaw** from the startup menu.
313
313
  "defaults": {
314
314
  "model": {
315
315
  "primary": "nvidia/deepseek-ai/deepseek-v3.2"
316
+ },
317
+ "models": {
318
+ "nvidia/deepseek-ai/deepseek-v3.2": {}
316
319
  }
317
320
  }
318
321
  }
@@ -321,6 +324,8 @@ Or run without flags and choose **OpenClaw** from the startup menu.
321
324
 
322
325
  > ⚠️ **Note:** `providers` must be nested under `models.providers` — not at the config root. A root-level `providers` key is ignored by OpenClaw.
323
326
 
327
+ > ⚠️ **Note:** The model must also be listed in `agents.defaults.models` (the allowlist). Without this entry, OpenClaw rejects the model with *"not allowed"* even if it is set as primary.
328
+
324
329
  ### After updating OpenClaw config
325
330
 
326
331
  OpenClaw's gateway **auto-reloads** config file changes (depending on `gateway.reload.mode`). To apply manually:
@@ -337,6 +342,43 @@ openclaw configure
337
342
 
338
343
  > 💡 **Why use remote NIM models with OpenClaw?** NVIDIA NIM serves models via a fast API — no local GPU required, no VRAM limits, free credits for developers. You get frontier-class coding models (DeepSeek V3, Kimi K2, Qwen3 Coder) without downloading anything.
339
344
 
345
+ ### Patching OpenClaw for full NVIDIA model support
346
+
347
+ **Problem:** By default, OpenClaw only allows a few specific NVIDIA models in its allowlist. If you try to use a model that's not in the list, you'll get this error:
348
+
349
+ ```
350
+ Model "nvidia/mistralai/devstral-2-123b-instruct-2512" is not allowed. Use /models to list providers, or /models <provider> to list models.
351
+ ```
352
+
353
+ **Solution:** Patch OpenClaw's configuration to add ALL 47 NVIDIA models from `free-coding-models` to the allowlist:
354
+
355
+ ```bash
356
+ # From the free-coding-models package directory
357
+ node patch-openclaw.js
358
+ ```
359
+
360
+ This script:
361
+ - Backs up `~/.openclaw/agents/main/agent/models.json` and `~/.openclaw/openclaw.json`
362
+ - Adds all 47 NVIDIA models with proper context window and token limits
363
+ - Preserves existing models and configuration
364
+ - Prints a summary of what was added
365
+
366
+ **After patching:**
367
+
368
+ 1. Restart OpenClaw gateway:
369
+ ```bash
370
+ systemctl --user restart openclaw-gateway
371
+ ```
372
+
373
+ 2. Verify models are available:
374
+ ```bash
375
+ free-coding-models --openclaw
376
+ ```
377
+
378
+ 3. Select any model — no more "not allowed" errors!
379
+
380
+ **Why this is needed:** OpenClaw uses a strict allowlist system to prevent typos and invalid models. The `patch-openclaw.js` script populates the allowlist with all known working NVIDIA models, so you can freely switch between them without manually editing config files.
381
+
340
382
  ---
341
383
 
342
384
  ## ⚙️ How it works
@@ -75,10 +75,44 @@ import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from
75
75
  import { homedir } from 'os'
76
76
  import { join } from 'path'
77
77
  import { MODELS } from '../sources.js'
78
+ import { patchOpenClawModelsJson } from '../patch-openclaw-models.js'
79
+ import { getAvg, getVerdict, getUptime, sortResults, filterByTier, findBestModel, parseArgs, TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP } from '../lib/utils.js'
78
80
 
79
81
  const require = createRequire(import.meta.url)
80
82
  const readline = require('readline')
81
83
 
84
+ // ─── Version check ────────────────────────────────────────────────────────────
85
+ const pkg = require('../package.json')
86
+ const LOCAL_VERSION = pkg.version
87
+
88
+ async function checkForUpdate() {
89
+ try {
90
+ const res = await fetch('https://registry.npmjs.org/free-coding-models/latest', { signal: AbortSignal.timeout(5000) })
91
+ if (!res.ok) return null
92
+ const data = await res.json()
93
+ if (data.version && data.version !== LOCAL_VERSION) return data.version
94
+ } catch {}
95
+ return null
96
+ }
97
+
98
+ function runUpdate() {
99
+ const { execSync } = require('child_process')
100
+ console.log()
101
+ console.log(chalk.bold.cyan(' ⬆ Updating free-coding-models...'))
102
+ console.log()
103
+ try {
104
+ execSync('npm i -g free-coding-models', { stdio: 'inherit' })
105
+ console.log()
106
+ console.log(chalk.green(' ✅ Update complete! Please restart free-coding-models.'))
107
+ console.log()
108
+ } catch {
109
+ console.log()
110
+ console.log(chalk.red(' ✖ Update failed. Try manually: npm i -g free-coding-models'))
111
+ console.log()
112
+ }
113
+ process.exit(0)
114
+ }
115
+
82
116
  // ─── Config path ──────────────────────────────────────────────────────────────
83
117
  const CONFIG_PATH = join(homedir(), '.free-coding-models')
84
118
 
@@ -128,8 +162,8 @@ async function promptApiKey() {
128
162
  // ─── Startup mode selection menu ──────────────────────────────────────────────
129
163
  // 📖 Shown at startup when neither --opencode nor --openclaw flag is given.
130
164
  // 📖 Simple arrow-key selector in normal terminal (not alt screen).
131
- // 📖 Returns 'opencode' or 'openclaw'.
132
- async function promptModeSelection() {
165
+ // 📖 Returns 'opencode', 'openclaw', or 'update'.
166
+ async function promptModeSelection(latestVersion) {
133
167
  const options = [
134
168
  {
135
169
  label: 'OpenCode',
@@ -143,6 +177,14 @@ async function promptModeSelection() {
143
177
  },
144
178
  ]
145
179
 
180
+ if (latestVersion) {
181
+ options.push({
182
+ label: 'Update now',
183
+ icon: '⬆',
184
+ description: `Update free-coding-models to v${latestVersion}`,
185
+ })
186
+ }
187
+
146
188
  return new Promise((resolve) => {
147
189
  let selected = 0
148
190
 
@@ -150,6 +192,10 @@ async function promptModeSelection() {
150
192
  const render = () => {
151
193
  process.stdout.write('\x1b[2J\x1b[H') // clear screen + cursor home
152
194
  console.log()
195
+ if (latestVersion) {
196
+ console.log(chalk.red(` ⚠ New version available (v${latestVersion}), please run npm i -g free-coding-models to install`))
197
+ console.log()
198
+ }
153
199
  console.log(chalk.bold(' ⚡ Free Coding Models') + chalk.dim(' — Choose your tool'))
154
200
  console.log()
155
201
  for (let i = 0; i < options.length; i++) {
@@ -189,7 +235,9 @@ async function promptModeSelection() {
189
235
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
190
236
  process.stdin.removeListener('keypress', onKey)
191
237
  process.stdin.pause()
192
- resolve(selected === 0 ? 'opencode' : 'openclaw')
238
+ const choices = ['opencode', 'openclaw']
239
+ if (latestVersion) choices.push('update')
240
+ resolve(choices[selected])
193
241
  }
194
242
  }
195
243
 
@@ -252,90 +300,8 @@ const spinCell = (f, o = 0) => chalk.dim.yellow(FRAMES[(f + o) % FRAMES.length].
252
300
 
253
301
  // ─── Table renderer ───────────────────────────────────────────────────────────
254
302
 
255
- const TIER_ORDER = ['S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C']
256
- const getAvg = r => {
257
- // 📖 Calculate average only from successful pings (code 200)
258
- // 📖 pings are objects: { ms, code }
259
- const successfulPings = (r.pings || []).filter(p => p.code === '200')
260
- if (successfulPings.length === 0) return Infinity
261
- return Math.round(successfulPings.reduce((a, b) => a + b.ms, 0) / successfulPings.length)
262
- }
263
-
264
- // 📖 Verdict order for sorting
265
- const VERDICT_ORDER = ['Perfect', 'Normal', 'Slow', 'Very Slow', 'Overloaded', 'Unstable', 'Not Active', 'Pending']
266
-
267
- // 📖 Get verdict for a model result
268
- const getVerdict = (r) => {
269
- const avg = getAvg(r)
270
- const wasUpBefore = r.pings.length > 0 && r.pings.some(p => p.code === '200')
271
-
272
- // 📖 429 = rate limited = Overloaded
273
- if (r.httpCode === '429') return 'Overloaded'
274
- if ((r.status === 'timeout' || r.status === 'down') && wasUpBefore) return 'Unstable'
275
- if (r.status === 'timeout' || r.status === 'down') return 'Not Active'
276
- if (avg === Infinity) return 'Pending'
277
- if (avg < 400) return 'Perfect'
278
- if (avg < 1000) return 'Normal'
279
- if (avg < 3000) return 'Slow'
280
- if (avg < 5000) return 'Very Slow'
281
- if (avg < 10000) return 'Unstable'
282
- return 'Unstable'
283
- }
284
-
285
- // 📖 Calculate uptime percentage (successful pings / total pings)
286
- // 📖 Only count code 200 responses
287
- const getUptime = (r) => {
288
- if (r.pings.length === 0) return 0
289
- const successful = r.pings.filter(p => p.code === '200').length
290
- return Math.round((successful / r.pings.length) * 100)
291
- }
292
-
293
- // 📖 Sort results using the same logic as renderTable - used for both display and selection
294
- const sortResults = (results, sortColumn, sortDirection) => {
295
- return [...results].sort((a, b) => {
296
- let cmp = 0
297
-
298
- switch (sortColumn) {
299
- case 'rank':
300
- cmp = a.idx - b.idx
301
- break
302
- case 'tier':
303
- cmp = TIER_ORDER.indexOf(a.tier) - TIER_ORDER.indexOf(b.tier)
304
- break
305
- case 'origin':
306
- cmp = 'NVIDIA NIM'.localeCompare('NVIDIA NIM') // All same for now
307
- break
308
- case 'model':
309
- cmp = a.label.localeCompare(b.label)
310
- break
311
- case 'ping': {
312
- const aLast = a.pings.length > 0 ? a.pings[a.pings.length - 1] : null
313
- const bLast = b.pings.length > 0 ? b.pings[b.pings.length - 1] : null
314
- const aPing = aLast?.code === '200' ? aLast.ms : Infinity
315
- const bPing = bLast?.code === '200' ? bLast.ms : Infinity
316
- cmp = aPing - bPing
317
- break
318
- }
319
- case 'avg':
320
- cmp = getAvg(a) - getAvg(b)
321
- break
322
- case 'status':
323
- cmp = a.status.localeCompare(b.status)
324
- break
325
- case 'verdict': {
326
- const aVerdict = getVerdict(a)
327
- const bVerdict = getVerdict(b)
328
- cmp = VERDICT_ORDER.indexOf(aVerdict) - VERDICT_ORDER.indexOf(bVerdict)
329
- break
330
- }
331
- case 'uptime':
332
- cmp = getUptime(a) - getUptime(b)
333
- break
334
- }
335
-
336
- return sortDirection === 'asc' ? cmp : -cmp
337
- })
338
- }
303
+ // 📖 Core logic functions (getAvg, getVerdict, getUptime, sortResults, etc.)
304
+ // 📖 are imported from lib/utils.js for testability
339
305
 
340
306
  // 📖 renderTable: mode param controls footer hint text (opencode vs openclaw)
341
307
  function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode') {
@@ -660,13 +626,21 @@ async function startOpenCode(model) {
660
626
  const { spawn } = await import('child_process')
661
627
  const child = spawn('opencode', [], {
662
628
  stdio: 'inherit',
663
- shell: false
629
+ shell: true
664
630
  })
665
631
 
666
632
  // 📖 Wait for OpenCode to exit
667
633
  await new Promise((resolve, reject) => {
668
634
  child.on('exit', resolve)
669
- child.on('error', reject)
635
+ child.on('error', (err) => {
636
+ if (err.code === 'ENOENT') {
637
+ console.error(chalk.red('\n ✗ Could not find "opencode" — is it installed and in your PATH?'))
638
+ console.error(chalk.dim(' Install: npm i -g opencode or see https://opencode.ai'))
639
+ resolve(1)
640
+ } else {
641
+ reject(err)
642
+ }
643
+ })
670
644
  })
671
645
  } else {
672
646
  // 📖 NVIDIA NIM not configured - show install prompt and launch
@@ -702,13 +676,21 @@ After installation, you can use: opencode --model nvidia/${model.modelId}`
702
676
  const { spawn } = await import('child_process')
703
677
  const child = spawn('opencode', [], {
704
678
  stdio: 'inherit',
705
- shell: false
679
+ shell: true
706
680
  })
707
681
 
708
682
  // 📖 Wait for OpenCode to exit
709
683
  await new Promise((resolve, reject) => {
710
684
  child.on('exit', resolve)
711
- child.on('error', reject)
685
+ child.on('error', (err) => {
686
+ if (err.code === 'ENOENT') {
687
+ console.error(chalk.red('\n ✗ Could not find "opencode" — is it installed and in your PATH?'))
688
+ console.error(chalk.dim(' Install: npm i -g opencode or see https://opencode.ai'))
689
+ resolve(1)
690
+ } else {
691
+ reject(err)
692
+ }
693
+ })
712
694
  })
713
695
  }
714
696
  }
@@ -755,6 +737,15 @@ async function startOpenClaw(model, apiKey) {
755
737
  console.log(chalk.dim(` 💾 Backup: ${backupPath}`))
756
738
  }
757
739
 
740
+ // 📖 Patch models.json to add all NVIDIA models (fixes "not allowed" errors)
741
+ const patchResult = patchOpenClawModelsJson()
742
+ if (patchResult.wasPatched) {
743
+ console.log(chalk.dim(` ✨ Added ${patchResult.added} NVIDIA models to allowlist (${patchResult.total} total)`))
744
+ if (patchResult.backup) {
745
+ console.log(chalk.dim(` 💾 models.json backup: ${patchResult.backup}`))
746
+ }
747
+ }
748
+
758
749
  // 📖 Ensure models.providers section exists with nvidia NIM block.
759
750
  // 📖 Per OpenClaw docs (docs.openclaw.ai/providers/nvidia), providers MUST be nested under
760
751
  // 📖 "models.providers", NOT at the config root. Root-level "providers" is ignored by OpenClaw.
@@ -781,6 +772,8 @@ async function startOpenClaw(model, apiKey) {
781
772
  }
782
773
  }
783
774
 
775
+ // 📖 Set as the default primary model for all agents.
776
+ // 📖 Format: "provider/model-id" — e.g. "nvidia/deepseek-ai/deepseek-v3.2"
784
777
  // 📖 Set as the default primary model for all agents.
785
778
  // 📖 Format: "provider/model-id" — e.g. "nvidia/deepseek-ai/deepseek-v3.2"
786
779
  if (!config.agents) config.agents = {}
@@ -788,6 +781,12 @@ async function startOpenClaw(model, apiKey) {
788
781
  if (!config.agents.defaults.model) config.agents.defaults.model = {}
789
782
  config.agents.defaults.model.primary = `nvidia/${model.modelId}`
790
783
 
784
+ // 📖 REQUIRED: OpenClaw requires the model to be explicitly listed in agents.defaults.models
785
+ // 📖 (the allowlist). Without this entry, OpenClaw rejects the model with "not allowed".
786
+ // 📖 See: https://docs.openclaw.ai/gateway/configuration-reference
787
+ if (!config.agents.defaults.models) config.agents.defaults.models = {}
788
+ config.agents.defaults.models[`nvidia/${model.modelId}`] = {}
789
+
791
790
  saveOpenClawConfig(config)
792
791
 
793
792
  console.log(chalk.rgb(255, 140, 0)(` ✓ Default model set to: nvidia/${model.modelId}`))
@@ -804,27 +803,7 @@ async function startOpenClaw(model, apiKey) {
804
803
  }
805
804
 
806
805
  // ─── Helper function to find best model after analysis ────────────────────────
807
- function findBestModel(results) {
808
- // 📖 Sort by avg ping (fastest first), then by uptime percentage (most reliable)
809
- const sorted = [...results].sort((a, b) => {
810
- const avgA = getAvg(a)
811
- const avgB = getAvg(b)
812
- const uptimeA = getUptime(a)
813
- const uptimeB = getUptime(b)
814
-
815
- // 📖 Priority 1: Models that are up (status === 'up')
816
- if (a.status === 'up' && b.status !== 'up') return -1
817
- if (a.status !== 'up' && b.status === 'up') return 1
818
-
819
- // 📖 Priority 2: Fastest average ping
820
- if (avgA !== avgB) return avgA - avgB
821
-
822
- // 📖 Priority 3: Highest uptime percentage
823
- return uptimeB - uptimeA
824
- })
825
-
826
- return sorted.length > 0 ? sorted[0] : null
827
- }
806
+ // 📖 findBestModel is imported from lib/utils.js
828
807
 
829
808
  // ─── Function to run in fiable mode (10-second analysis then output best model) ──
830
809
  async function runFiableMode(apiKey) {
@@ -883,64 +862,28 @@ async function runFiableMode(apiKey) {
883
862
  process.exit(0)
884
863
  }
885
864
 
886
- // ─── Tier filter helper ────────────────────────────────────────────────────────
887
- // 📖 Maps a single tier letter (S, A, B, C) to the full set of matching tier strings.
888
- // 📖 --tier S → includes S+ and S
889
- // 📖 --tier A → includes A+, A, A-
890
- // 📖 --tier B → includes B+, B
891
- // 📖 --tier C → includes C only
892
- const TIER_LETTER_MAP = {
893
- 'S': ['S+', 'S'],
894
- 'A': ['A+', 'A', 'A-'],
895
- 'B': ['B+', 'B'],
896
- 'C': ['C'],
897
- }
898
-
899
- function filterByTier(results, tierLetter) {
900
- const letter = tierLetter.toUpperCase()
901
- const allowed = TIER_LETTER_MAP[letter]
902
- if (!allowed) {
865
+ // 📖 filterByTier and TIER_LETTER_MAP are imported from lib/utils.js
866
+ // 📖 Wrapper that exits on invalid tier (utils version returns null instead)
867
+ function filterByTierOrExit(results, tierLetter) {
868
+ const filtered = filterByTier(results, tierLetter)
869
+ if (filtered === null) {
903
870
  console.error(chalk.red(` ✖ Unknown tier "${tierLetter}". Valid tiers: S, A, B, C`))
904
871
  process.exit(1)
905
872
  }
906
- return results.filter(r => allowed.includes(r.tier))
873
+ return filtered
907
874
  }
908
875
 
909
876
  async function main() {
910
- // 📖 Parse CLI arguments properly
911
- const args = process.argv.slice(2)
912
-
913
- // 📖 Extract API key (first non-flag argument) and flags
914
- let apiKey = null
915
- const flags = []
916
-
917
- for (const arg of args) {
918
- if (arg.startsWith('--')) {
919
- flags.push(arg.toLowerCase())
920
- } else if (!apiKey) {
921
- apiKey = arg
922
- }
923
- }
877
+ // 📖 Parse CLI arguments using shared parseArgs utility
878
+ const parsed = parseArgs(process.argv)
879
+ let apiKey = parsed.apiKey
880
+ const { bestMode, fiableMode, openCodeMode, openClawMode, tierFilter } = parsed
924
881
 
925
882
  // 📖 Priority: CLI arg > env var > saved config > wizard
926
883
  if (!apiKey) {
927
884
  apiKey = process.env.NVIDIA_API_KEY || loadApiKey()
928
885
  }
929
886
 
930
- // 📖 Check for CLI flags
931
- const bestMode = flags.includes('--best')
932
- const fiableMode = flags.includes('--fiable')
933
- const openCodeMode = flags.includes('--opencode')
934
- const openClawMode = flags.includes('--openclaw')
935
-
936
- // 📖 Parse --tier X flag (e.g. --tier S, --tier A)
937
- // 📖 Find "--tier" in flags array, then get the next raw arg as the tier value
938
- let tierFilter = null
939
- const tierIdx = args.findIndex(a => a.toLowerCase() === '--tier')
940
- if (tierIdx !== -1 && args[tierIdx + 1] && !args[tierIdx + 1].startsWith('--')) {
941
- tierFilter = args[tierIdx + 1].toUpperCase()
942
- }
943
-
944
887
  if (!apiKey) {
945
888
  apiKey = await promptApiKey()
946
889
  if (!apiKey) {
@@ -957,6 +900,9 @@ async function main() {
957
900
  await runFiableMode(apiKey)
958
901
  }
959
902
 
903
+ // 📖 Check for available update (non-blocking, 5s timeout)
904
+ const latestVersion = await checkForUpdate()
905
+
960
906
  // 📖 Determine active mode:
961
907
  // --opencode → opencode
962
908
  // --openclaw → openclaw
@@ -968,7 +914,18 @@ async function main() {
968
914
  mode = 'opencode'
969
915
  } else {
970
916
  // 📖 No mode flag given — ask user with the startup menu
971
- mode = await promptModeSelection()
917
+ mode = await promptModeSelection(latestVersion)
918
+ }
919
+
920
+ // 📖 Handle "update now" selection from the menu
921
+ if (mode === 'update') {
922
+ runUpdate()
923
+ }
924
+
925
+ // 📖 When using flags (--opencode/--openclaw), show update warning in terminal
926
+ if (latestVersion && (openCodeMode || openClawMode)) {
927
+ console.log(chalk.red(` ⚠ New version available (v${latestVersion}), please run npm i -g free-coding-models to install`))
928
+ console.log()
972
929
  }
973
930
 
974
931
  // 📖 Filter models to only show top tiers if BEST mode is active
@@ -985,7 +942,7 @@ async function main() {
985
942
 
986
943
  // 📖 Apply tier letter filter if --tier X was given
987
944
  if (tierFilter) {
988
- results = filterByTier(results, tierFilter)
945
+ results = filterByTierOrExit(results, tierFilter)
989
946
  }
990
947
 
991
948
  // 📖 Add interactive selection state - cursor index and user's choice
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.1.4",
3
+ "version": "0.1.12",
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",
@@ -41,11 +41,14 @@
41
41
  "files": [
42
42
  "bin/",
43
43
  "sources.js",
44
+ "patch-openclaw.js",
45
+ "patch-openclaw-models.js",
44
46
  "README.md",
45
47
  "LICENSE"
46
48
  ],
47
49
  "scripts": {
48
- "start": "node bin/free-coding-models.js"
50
+ "start": "node bin/free-coding-models.js",
51
+ "test": "node --test test/test.js"
49
52
  },
50
53
  "dependencies": {
51
54
  "chalk": "^5.4.1"
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file patch-openclaw-models.js
4
+ * @description Helper function to patch OpenClaw's models.json with all NVIDIA models
5
+ *
6
+ * This is imported by bin/free-coding-models.js and called automatically
7
+ * when setting a model in OpenClaw mode.
8
+ */
9
+
10
+ import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'fs'
11
+ import { homedir } from 'os'
12
+ import { join } from 'path'
13
+ import { nvidiaNim } from './sources.js'
14
+
15
+ const MODELS_JSON = join(homedir(), '.openclaw', 'agents', 'main', 'agent', 'models.json')
16
+
17
+ /**
18
+ * Patch models.json to add all NVIDIA models from sources.js
19
+ * @returns {Object} { added: number, total: number, wasPatched: boolean }
20
+ */
21
+ export function patchOpenClawModelsJson() {
22
+ // Read existing config
23
+ let modelsConfig
24
+ if (!existsSync(MODELS_JSON)) {
25
+ return { added: 0, total: 0, wasPatched: false, error: 'models.json not found' }
26
+ }
27
+
28
+ try {
29
+ modelsConfig = JSON.parse(readFileSync(MODELS_JSON, 'utf8'))
30
+ } catch (err) {
31
+ return { added: 0, total: 0, wasPatched: false, error: err.message }
32
+ }
33
+
34
+ // Ensure nvidia provider exists
35
+ if (!modelsConfig.providers) modelsConfig.providers = {}
36
+ if (!modelsConfig.providers.nvidia) {
37
+ modelsConfig.providers.nvidia = {
38
+ baseUrl: 'https://integrate.api.nvidia.com/v1',
39
+ api: 'openai-completions',
40
+ models: []
41
+ }
42
+ }
43
+
44
+ // Get existing model IDs
45
+ const existingModelIds = new Set(modelsConfig.providers.nvidia.models.map(m => m.id))
46
+
47
+ // Helper to get model config by tier
48
+ function getModelConfig(tier) {
49
+ if (tier === 'S+' || tier === 'S') {
50
+ return { contextWindow: 128000, maxTokens: 8192 }
51
+ }
52
+ if (tier === 'A+') {
53
+ return { contextWindow: 131072, maxTokens: 4096 }
54
+ }
55
+ if (tier === 'A' || tier === 'A-') {
56
+ return { contextWindow: 131072, maxTokens: 4096 }
57
+ }
58
+ return { contextWindow: 32768, maxTokens: 2048 }
59
+ }
60
+
61
+ // Add all models from sources.js
62
+ let addedCount = 0
63
+ for (const [modelId, label, tier] of nvidiaNim) {
64
+ if (existingModelIds.has(modelId)) {
65
+ continue // Skip already existing models
66
+ }
67
+
68
+ const config = getModelConfig(tier)
69
+ const isThinking = modelId.includes('thinking')
70
+
71
+ modelsConfig.providers.nvidia.models.push({
72
+ id: modelId,
73
+ name: label,
74
+ contextWindow: config.contextWindow,
75
+ maxTokens: config.maxTokens,
76
+ reasoning: isThinking,
77
+ input: ['text'],
78
+ cost: {
79
+ input: 0,
80
+ output: 0,
81
+ cacheRead: 0,
82
+ cacheWrite: 0
83
+ }
84
+ })
85
+
86
+ addedCount++
87
+ }
88
+
89
+ // Only write if we added something
90
+ if (addedCount > 0) {
91
+ // Backup
92
+ const backupPath = `${MODELS_JSON}.backup-${Date.now()}`
93
+ copyFileSync(MODELS_JSON, backupPath)
94
+
95
+ // Write updated config
96
+ writeFileSync(MODELS_JSON, JSON.stringify(modelsConfig, null, 2))
97
+
98
+ return {
99
+ added: addedCount,
100
+ total: modelsConfig.providers.nvidia.models.length,
101
+ wasPatched: true,
102
+ backup: backupPath
103
+ }
104
+ }
105
+
106
+ return {
107
+ added: 0,
108
+ total: modelsConfig.providers.nvidia.models.length,
109
+ wasPatched: false
110
+ }
111
+ }
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file patch-openclaw.js
4
+ * @description Patch OpenClaw to allow all NVIDIA models from free-coding-models
5
+ *
6
+ * This script adds ALL models from sources.js to OpenClaw's allowlist
7
+ * so any NVIDIA model can be used without "not allowed" errors.
8
+ */
9
+
10
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
11
+ import { homedir } from 'os'
12
+ import { join } from 'path'
13
+ import { nvidiaNim } from './sources.js'
14
+
15
+ const MODELS_JSON = join(homedir(), '.openclaw', 'agents', 'main', 'agent', 'models.json')
16
+ const OPENCLAW_JSON = join(homedir(), '.openclaw', 'openclaw.json')
17
+
18
+ console.log('🦞 Patching OpenClaw for full NVIDIA model support...\n')
19
+
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 }
36
+ }
37
+
38
+ // ─── Patch models.json ──────────────────────────────────────────────────────────
39
+ console.log('📄 Patching models.json...')
40
+
41
+ let modelsConfig
42
+ if (existsSync(MODELS_JSON)) {
43
+ try {
44
+ modelsConfig = JSON.parse(readFileSync(MODELS_JSON, 'utf8'))
45
+ } catch (err) {
46
+ console.error(' ✖ Failed to parse models.json:', err.message)
47
+ process.exit(1)
48
+ }
49
+ } else {
50
+ console.error(' ✖ models.json not found at:', MODELS_JSON)
51
+ process.exit(1)
52
+ }
53
+
54
+ // Backup
55
+ const backupPath = `${MODELS_JSON}.backup-${Date.now()}`
56
+ writeFileSync(backupPath, readFileSync(MODELS_JSON))
57
+ console.log(` 💾 Backup: ${backupPath}`)
58
+
59
+ // Ensure nvidia provider exists
60
+ if (!modelsConfig.providers) modelsConfig.providers = {}
61
+ if (!modelsConfig.providers.nvidia) {
62
+ modelsConfig.providers.nvidia = {
63
+ baseUrl: 'https://integrate.api.nvidia.com/v1',
64
+ api: 'openai-completions',
65
+ models: []
66
+ }
67
+ }
68
+
69
+ // Get existing model IDs
70
+ const existingModelIds = new Set(modelsConfig.providers.nvidia.models.map(m => m.id))
71
+
72
+ // Add all models from sources.js
73
+ let addedCount = 0
74
+ for (const [modelId, label, tier] of nvidiaNim) {
75
+ if (existingModelIds.has(modelId)) {
76
+ continue // Skip already existing models
77
+ }
78
+
79
+ const config = getModelConfig(tier)
80
+ const isThinking = modelId.includes('thinking')
81
+
82
+ modelsConfig.providers.nvidia.models.push({
83
+ id: modelId,
84
+ name: label,
85
+ contextWindow: config.contextWindow,
86
+ maxTokens: config.maxTokens,
87
+ reasoning: isThinking,
88
+ input: ['text'],
89
+ cost: {
90
+ input: 0,
91
+ output: 0,
92
+ cacheRead: 0,
93
+ cacheWrite: 0
94
+ }
95
+ })
96
+
97
+ addedCount++
98
+ }
99
+
100
+ // Write back
101
+ writeFileSync(MODELS_JSON, JSON.stringify(modelsConfig, null, 2))
102
+ console.log(` ✅ Added ${addedCount} models to models.json`)
103
+ console.log(` 📊 Total NVIDIA models: ${modelsConfig.providers.nvidia.models.length}`)
104
+
105
+ // ─── Patch openclaw.json ────────────────────────────────────────────────────────
106
+ console.log('\n📄 Patching openclaw.json...')
107
+
108
+ let openclawConfig
109
+ if (existsSync(OPENCLAW_JSON)) {
110
+ try {
111
+ openclawConfig = JSON.parse(readFileSync(OPENCLAW_JSON, 'utf8'))
112
+ } catch (err) {
113
+ console.error(' ✖ Failed to parse openclaw.json:', err.message)
114
+ process.exit(1)
115
+ }
116
+ } else {
117
+ console.error(' ✖ openclaw.json not found at:', OPENCLAW_JSON)
118
+ process.exit(1)
119
+ }
120
+
121
+ // Backup
122
+ const openclawBackupPath = `${OPENCLAW_JSON}.backup-${Date.now()}`
123
+ writeFileSync(openclawBackupPath, readFileSync(OPENCLAW_JSON))
124
+ console.log(` 💾 Backup: ${openclawBackupPath}`)
125
+
126
+ // Ensure models.providers.nvidia exists
127
+ if (!openclawConfig.models) openclawConfig.models = {}
128
+ if (!openclawConfig.models.providers) openclawConfig.models.providers = {}
129
+ if (!openclawConfig.models.providers.nvidia) {
130
+ openclawConfig.models.providers.nvidia = {
131
+ baseUrl: 'https://integrate.api.nvidia.com/v1',
132
+ api: 'openai-completions',
133
+ models: []
134
+ }
135
+ }
136
+
137
+ // Get existing model IDs in openclaw.json
138
+ const existingOpenClawModelIds = new Set(
139
+ (openclawConfig.models.providers.nvidia.models || []).map(m => m.id)
140
+ )
141
+
142
+ // Add all models (simplified config for openclaw.json)
143
+ let addedOpenClawCount = 0
144
+ for (const [modelId, label, tier] of nvidiaNim) {
145
+ if (existingOpenClawModelIds.has(modelId)) {
146
+ continue
147
+ }
148
+
149
+ const config = getModelConfig(tier)
150
+
151
+ openclawConfig.models.providers.nvidia.models.push({
152
+ id: modelId,
153
+ name: label,
154
+ contextWindow: config.contextWindow,
155
+ maxTokens: config.maxTokens
156
+ })
157
+
158
+ addedOpenClawCount++
159
+ }
160
+
161
+ // Write back
162
+ writeFileSync(OPENCLAW_JSON, JSON.stringify(openclawConfig, null, 2))
163
+ console.log(` ✅ Added ${addedOpenClawCount} models to openclaw.json`)
164
+ console.log(` 📊 Total NVIDIA models: ${openclawConfig.models.providers.nvidia.models.length}`)
165
+
166
+ // ─── Summary ────────────────────────────────────────────────────────────────────
167
+ console.log('\n✨ Patch complete!')
168
+ console.log('\n💡 Next steps:')
169
+ console.log(' 1. Restart OpenClaw gateway: systemctl --user restart openclaw-gateway')
170
+ console.log(' 2. Test with: free-coding-models --openclaw')
171
+ console.log(' 3. Select any model - no more "not allowed" errors!')
172
+ console.log()