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 +37 -0
- package/bin/free-coding-models.js +109 -160
- package/package.json +5 -2
- package/patch-openclaw-models.js +111 -0
- package/patch-openclaw.js +172 -0
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 '
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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:
|
|
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',
|
|
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:
|
|
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',
|
|
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
|
-
|
|
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
|
-
//
|
|
895
|
-
// š
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
|
873
|
+
return filtered
|
|
915
874
|
}
|
|
916
875
|
|
|
917
876
|
async function main() {
|
|
918
|
-
// š Parse CLI arguments
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
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 =
|
|
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.
|
|
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()
|