free-coding-models 0.1.5 → 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
@@ -342,6 +342,43 @@ openclaw configure
342
342
 
343
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.
344
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
+
345
382
  ---
346
383
 
347
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.
@@ -812,27 +803,7 @@ async function startOpenClaw(model, apiKey) {
812
803
  }
813
804
 
814
805
  // ─── Helper function to find best model after analysis ────────────────────────
815
- function findBestModel(results) {
816
- // šŸ“– Sort by avg ping (fastest first), then by uptime percentage (most reliable)
817
- const sorted = [...results].sort((a, b) => {
818
- const avgA = getAvg(a)
819
- const avgB = getAvg(b)
820
- const uptimeA = getUptime(a)
821
- const uptimeB = getUptime(b)
822
-
823
- // šŸ“– Priority 1: Models that are up (status === 'up')
824
- if (a.status === 'up' && b.status !== 'up') return -1
825
- if (a.status !== 'up' && b.status === 'up') return 1
826
-
827
- // šŸ“– Priority 2: Fastest average ping
828
- if (avgA !== avgB) return avgA - avgB
829
-
830
- // šŸ“– Priority 3: Highest uptime percentage
831
- return uptimeB - uptimeA
832
- })
833
-
834
- return sorted.length > 0 ? sorted[0] : null
835
- }
806
+ // šŸ“– findBestModel is imported from lib/utils.js
836
807
 
837
808
  // ─── Function to run in fiable mode (10-second analysis then output best model) ──
838
809
  async function runFiableMode(apiKey) {
@@ -891,64 +862,28 @@ async function runFiableMode(apiKey) {
891
862
  process.exit(0)
892
863
  }
893
864
 
894
- // ─── Tier filter helper ────────────────────────────────────────────────────────
895
- // šŸ“– Maps a single tier letter (S, A, B, C) to the full set of matching tier strings.
896
- // šŸ“– --tier S → includes S+ and S
897
- // šŸ“– --tier A → includes A+, A, A-
898
- // šŸ“– --tier B → includes B+, B
899
- // šŸ“– --tier C → includes C only
900
- const TIER_LETTER_MAP = {
901
- 'S': ['S+', 'S'],
902
- 'A': ['A+', 'A', 'A-'],
903
- 'B': ['B+', 'B'],
904
- 'C': ['C'],
905
- }
906
-
907
- function filterByTier(results, tierLetter) {
908
- const letter = tierLetter.toUpperCase()
909
- const allowed = TIER_LETTER_MAP[letter]
910
- 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) {
911
870
  console.error(chalk.red(` āœ– Unknown tier "${tierLetter}". Valid tiers: S, A, B, C`))
912
871
  process.exit(1)
913
872
  }
914
- return results.filter(r => allowed.includes(r.tier))
873
+ return filtered
915
874
  }
916
875
 
917
876
  async function main() {
918
- // šŸ“– Parse CLI arguments properly
919
- const args = process.argv.slice(2)
920
-
921
- // šŸ“– Extract API key (first non-flag argument) and flags
922
- let apiKey = null
923
- const flags = []
924
-
925
- for (const arg of args) {
926
- if (arg.startsWith('--')) {
927
- flags.push(arg.toLowerCase())
928
- } else if (!apiKey) {
929
- apiKey = arg
930
- }
931
- }
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
932
881
 
933
882
  // šŸ“– Priority: CLI arg > env var > saved config > wizard
934
883
  if (!apiKey) {
935
884
  apiKey = process.env.NVIDIA_API_KEY || loadApiKey()
936
885
  }
937
886
 
938
- // šŸ“– Check for CLI flags
939
- const bestMode = flags.includes('--best')
940
- const fiableMode = flags.includes('--fiable')
941
- const openCodeMode = flags.includes('--opencode')
942
- const openClawMode = flags.includes('--openclaw')
943
-
944
- // šŸ“– Parse --tier X flag (e.g. --tier S, --tier A)
945
- // šŸ“– Find "--tier" in flags array, then get the next raw arg as the tier value
946
- let tierFilter = null
947
- const tierIdx = args.findIndex(a => a.toLowerCase() === '--tier')
948
- if (tierIdx !== -1 && args[tierIdx + 1] && !args[tierIdx + 1].startsWith('--')) {
949
- tierFilter = args[tierIdx + 1].toUpperCase()
950
- }
951
-
952
887
  if (!apiKey) {
953
888
  apiKey = await promptApiKey()
954
889
  if (!apiKey) {
@@ -965,6 +900,9 @@ async function main() {
965
900
  await runFiableMode(apiKey)
966
901
  }
967
902
 
903
+ // šŸ“– Check for available update (non-blocking, 5s timeout)
904
+ const latestVersion = await checkForUpdate()
905
+
968
906
  // šŸ“– Determine active mode:
969
907
  // --opencode → opencode
970
908
  // --openclaw → openclaw
@@ -976,7 +914,18 @@ async function main() {
976
914
  mode = 'opencode'
977
915
  } else {
978
916
  // šŸ“– No mode flag given — ask user with the startup menu
979
- 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()
980
929
  }
981
930
 
982
931
  // šŸ“– Filter models to only show top tiers if BEST mode is active
@@ -993,7 +942,7 @@ async function main() {
993
942
 
994
943
  // šŸ“– Apply tier letter filter if --tier X was given
995
944
  if (tierFilter) {
996
- results = filterByTier(results, tierFilter)
945
+ results = filterByTierOrExit(results, tierFilter)
997
946
  }
998
947
 
999
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.5",
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()