free-coding-models 0.5.17 → 0.5.19

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,12 @@
1
+ # Changelog v0.5.18 - 2026-06-09
2
+
3
+ ### Fixed
4
+ - Fixed the mandatory startup updater loop when the active `free-coding-models` binary is installed under one npm prefix but another Node/npm manager shadows `npm` in `PATH`. The updater now detects the npm prefix that owns the currently running package, installs into that exact prefix, and verifies that the active package version changed before relaunching.
5
+ - Improved updater fallback instructions so manual recovery commands include the owning npm binary and `--prefix` target when needed, instead of pointing users at a different global install.
6
+ - Stabilized the router dashboard SSE endpoints so `/api/events` and `/stream/events` flush an initial frame immediately and tests/clients do not hang while waiting for the first broadcast.
7
+
8
+ ### Changed
9
+ - Restored the normal startup update path and live OpenRouter/release-date startup behavior after the temporary local debug bypass used to diagnose the update loop.
10
+
11
+ ### Added
12
+ - Added regression coverage for npm-prefix-aware update command generation so future updater changes do not reintroduce the same cross-prefix loop.
@@ -0,0 +1,25 @@
1
+ # Changelog v0.5.19 - 2026-06-10
2
+
3
+ ### Added
4
+ - **nvidia**: Added `nvidia/nemotron-3-ultra-550b-a55b` (S+, 1M context)
5
+ - **groq**: Added 7 new specialized models:
6
+ - `whisper-large-v3`, `whisper-large-v3-turbo` (audio transcription)
7
+ - `canopylabs/orpheus-arabic-saudi`, `canopylabs/orpheus-v1-english` (Arabic/English speech)
8
+ - `meta-llama/llama-prompt-guard-2-22m`, `meta-llama/llama-prompt-guard-2-86m` (security guardrails)
9
+ - `openai/gpt-oss-safeguard-20b` (safety)
10
+
11
+ ### Fixed
12
+ - **nvidia**: Corrected context windows for three models:
13
+ - `deepseek-ai/deepseek-v4-pro`: 128k → 1M
14
+ - `deepseek-ai/deepseek-v4-flash`: 128k → 1M
15
+ - `mistralai/mistral-small-4-119b-2603`: 128k → 256k
16
+ - **nvidia**: Removed deprecated model `z-ai/glm5` (replaced by `z-ai/glm-5.1`)
17
+
18
+ ### Changed
19
+ - Model counts: nvidia stable at 27, groq increased from 8 to 15.
20
+ - Total catalog size: 161 models across 16 providers.
21
+
22
+ ### Notes
23
+ - This update reflects the latest provider catalogs as of June 10, 2026.
24
+ - All existing models were verified against official docs; only minor configuration drift was found.
25
+ - The `audit_state.json` was updated with fresh fingerprints.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
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",
package/sources.js CHANGED
@@ -44,19 +44,19 @@ export const nvidiaNim = [
44
44
  ['minimaxai/minimax-m2.7', 'MiniMax M2.7', 'S+', '80.2%', '200k'],
45
45
  ['z-ai/glm-5.1', 'GLM 5.1', 'S+', '77.8%', '128k'],
46
46
  ['moonshotai/kimi-k2.6', 'Kimi K2.6', 'S+', '76.8%', '256k'],
47
- ['deepseek-ai/deepseek-v4-pro', 'DeepSeek V4 Pro', 'S+', '73.1%', '128k'],
48
- ['deepseek-ai/deepseek-v4-flash', 'DeepSeek V4 Flash', 'S+', '72.0%', '128k'],
49
- ['z-ai/glm5', 'GLM 5', 'S+', '73.8%', '200k'],
47
+ ['deepseek-ai/deepseek-v4-pro', 'DeepSeek V4 Pro', 'S+', '73.1%', '1M'],
48
+ ['deepseek-ai/deepseek-v4-flash', 'DeepSeek V4 Flash', 'S+', '72.0%', '1M'],
50
49
  ['stepfun-ai/step-3.5-flash', 'Step 3.5 Flash', 'S+', '74.4%', '256k'],
51
50
  ['stepfun-ai/step-3.7-flash', 'Step 3.7 Flash', 'S+', '74.4%', '256k'],
52
51
  ['qwen/qwen3-coder-480b-a35b-instruct', 'Qwen3 Coder 480B', 'S+', '70.6%', '256k'],
52
+ ['nvidia/nemotron-3-ultra-550b-a55b', 'Nemotron 3 Ultra', 'S+', '-', '1M'],
53
53
  // ── S tier — SWE-bench Verified 60–70% ──
54
54
  ['qwen/qwen3-next-80b-a3b-instruct', 'Qwen3 80B Instruct', 'S', '65.0%', '128k'],
55
55
  ['qwen/qwen3.5-397b-a17b', 'Qwen3.5 400B VLM', 'S', '68.0%', '128k'],
56
56
  ['openai/gpt-oss-120b', 'GPT OSS 120B', 'S', '60.0%', '128k'],
57
57
  ['meta/llama-4-maverick-17b-128e-instruct', 'Llama 4 Maverick', 'S', '62.0%', '1M'],
58
58
  ['mistralai/mistral-medium-3.5-128b', 'Mistral Medium 3.5', 'S', '66.0%', '128k'],
59
- ['mistralai/mistral-small-4-119b-2603', 'Mistral Small 4', 'S', '60.0%', '128k'],
59
+ ['mistralai/mistral-small-4-119b-2603', 'Mistral Small 4', 'S', '60.0%', '256k'],
60
60
  ['qwen/qwen3.5-122b-a10b', 'Qwen3.5 122B', 'S', '64.0%', '128k'],
61
61
  // ── A+ tier — SWE-bench Verified 50–60% ──
62
62
  ['mistralai/mistral-large-3-675b-instruct-2512', 'Mistral Large 675B', 'A+', '58.0%', '256k'],
@@ -80,6 +80,7 @@ export const nvidiaNim = [
80
80
  // llama-3.3-70b-instruct (404), mixtral-8x22b-instruct-v0.1 (404),
81
81
  // granite-34b-code-instruct (deprecated), llama-4-scout-17b-16e-instruct (deprecated, not in nvidiaNim),
82
82
  // llama-3.1-8b-instruct (404) → replaced by llama-3.2-11b-vision-instruct
83
+ // Removed (2026-06-10): z-ai/glm5 (replaced by z-ai/glm-5.1)
83
84
  ]
84
85
 
85
86
  // 📖 Groq source - https://console.groq.com
@@ -93,6 +94,13 @@ export const groq = [
93
94
  ['qwen/qwen3-32b', 'Qwen3 32B', 'A+', '50.0%', '131k'],
94
95
  ['groq/compound', 'Groq Compound', 'A', '45.0%', '131k'],
95
96
  ['groq/compound-mini', 'Groq Compound Mini', 'B+', '32.0%', '131k'],
97
+ ['whisper-large-v3', 'Whisper', 'C', '-', '-'],
98
+ ['whisper-large-v3-turbo', 'Whisper Large V3 Turbo', 'C', '-', '-'],
99
+ ['canopylabs/orpheus-arabic-saudi', 'Canopy Labs Orpheus Arabic Saudi', 'C', '-', '4k'],
100
+ ['canopylabs/orpheus-v1-english', 'Canopy Labs Orpheus V1 English', 'C', '-', '4k'],
101
+ ['meta-llama/llama-prompt-guard-2-22m', 'Llama Prompt Guard 2 22M', 'C', '-', '512'],
102
+ ['meta-llama/llama-prompt-guard-2-86m', 'Prompt Guard 2 86M', 'C', '-', '512'],
103
+ ['openai/gpt-oss-safeguard-20b', 'Safety GPT OSS 20B', 'C', '-', '131k'],
96
104
  ]
97
105
 
98
106
  // 📖 Cerebras source - https://cloud.cerebras.ai
@@ -2508,6 +2508,8 @@ class RouterRuntime {
2508
2508
  Connection: 'keep-alive',
2509
2509
  'x-request-id': requestId,
2510
2510
  })
2511
+ res.flushHeaders?.()
2512
+ res.write(': connected\n\n')
2511
2513
  res.write(`event: hello\ndata: ${JSON.stringify(this.statusPayload())}\n\n`)
2512
2514
  this.sseClients.add(res)
2513
2515
  req.on('close', () => this.sseClients.delete(res))
@@ -2643,6 +2645,8 @@ class RouterRuntime {
2643
2645
  'Connection': 'keep-alive',
2644
2646
  'x-request-id': requestId,
2645
2647
  })
2648
+ res.flushHeaders?.()
2649
+ res.write(': connected\n\n')
2646
2650
  res.write(`data: ${JSON.stringify(getWebStatePayload(this))}\n\n`)
2647
2651
  this.sseClients.add(res)
2648
2652
  req.on('close', () => this.sseClients.delete(res))
@@ -28,6 +28,7 @@
28
28
  *
29
29
  * @functions
30
30
  * → detectPackageManager() — Detect which PM owns the current installation
31
+ * → resolveCurrentNpmInstallTarget() — Detect the npm prefix that owns the active package
31
32
  * → getInstallArgs(pm, version) — Build correct { bin, args } per package manager
32
33
  * → getManualInstallCmd(pm, version) — Human-readable install command string for error messages
33
34
  * → checkForUpdateDetailed() — Fetch npm latest with explicit error info
@@ -36,7 +37,7 @@
36
37
  * → enforceMandatoryStartupUpdate() — Mandatory startup self-update with two-failure fallback
37
38
  * → runUpdate(latestVersion) — Install new version via detected PM + relaunch
38
39
  * @exports
39
- * detectPackageManager, getInstallArgs, getManualInstallCmd,
40
+ * detectPackageManager, resolveCurrentNpmInstallTarget, getInstallArgs, getManualInstallCmd,
40
41
  * checkForUpdateDetailed, checkForUpdate, isPackageDevMode,
41
42
  * enforceMandatoryStartupUpdate, runUpdate, fetchLastReleaseDate
42
43
  *
@@ -47,15 +48,42 @@ import chalk from 'chalk'
47
48
  import { createRequire } from 'module'
48
49
  import { fileURLToPath } from 'url'
49
50
  import { dirname, join } from 'path'
50
- import { accessSync, constants, existsSync } from 'fs'
51
+ import { accessSync, constants, existsSync, readFileSync } from 'fs'
51
52
 
52
53
  const require = createRequire(import.meta.url)
53
54
  const readline = require('readline')
54
55
  const pkg = require('../../package.json')
55
56
  const LOCAL_VERSION = pkg.version
56
57
  const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
58
+ const PACKAGE_NAME = 'free-coding-models'
57
59
  export const UPDATE_FAILURE_THRESHOLD = 2
58
60
 
61
+ /**
62
+ * 📖 resolveCurrentNpmInstallTarget: detect the npm prefix that owns this exact
63
+ * 📖 running package, not just the first `npm` found in PATH. Users can run FCM
64
+ * 📖 from one global install while another Node manager shadows `npm`; updating
65
+ * 📖 the shadow prefix leaves the active binary stale and creates an update loop.
66
+ * @param {string} [packageRoot]
67
+ * @returns {{ packageRoot: string, prefix: string, bin: string } | null}
68
+ */
69
+ export function resolveCurrentNpmInstallTarget(packageRoot = PACKAGE_ROOT) {
70
+ const normalizedRoot = String(packageRoot || '').replace(/\\/g, '/')
71
+ const suffix = `/lib/node_modules/${PACKAGE_NAME}`
72
+ if (!normalizedRoot.endsWith(suffix)) return null
73
+
74
+ const prefix = packageRoot.slice(0, packageRoot.length - suffix.length)
75
+ if (!prefix) return null
76
+
77
+ const npmBinName = process.platform === 'win32' ? 'npm.cmd' : 'npm'
78
+ const npmBin = join(prefix, 'bin', npmBinName)
79
+
80
+ return {
81
+ packageRoot,
82
+ prefix,
83
+ bin: existsSync(npmBin) ? npmBin : 'npm',
84
+ }
85
+ }
86
+
59
87
  /**
60
88
  * 📖 detectPackageManager: figure out which package manager owns the current installation.
61
89
  * 📖 Checks import.meta.url (package install path), process.argv[1] (script entry),
@@ -141,27 +169,48 @@ export function buildOutdatedWarningMessage(latestVersion, failures = UPDATE_FAI
141
169
  * 📖 Each PM has different syntax for global install — this normalises them.
142
170
  * @param {'npm' | 'bun' | 'pnpm' | 'yarn'} pm
143
171
  * @param {string} version
172
+ * @param {{ prefix?: string, bin?: string }} [options]
144
173
  * @returns {{ bin: string, args: string[] }}
145
174
  */
146
- export function getInstallArgs(pm, version) {
147
- const pkg = `free-coding-models@${version}`
175
+ export function getInstallArgs(pm, version, options = {}) {
176
+ const pkg = `${PACKAGE_NAME}@${version}`
148
177
  switch (pm) {
149
178
  case 'bun': return { bin: 'bun', args: ['add', '-g', pkg] }
150
179
  case 'pnpm': return { bin: 'pnpm', args: ['add', '-g', pkg] }
151
180
  case 'yarn': return { bin: 'yarn', args: ['global', 'add', pkg] }
152
- default: return { bin: 'npm', args: ['i', '-g', pkg, '--prefer-online'] }
181
+ default: {
182
+ const args = ['i', '-g']
183
+ if (options.prefix) args.push('--prefix', options.prefix)
184
+ args.push(pkg, '--prefer-online')
185
+ return { bin: options.bin || 'npm', args }
186
+ }
153
187
  }
154
188
  }
155
189
 
190
+ function shellQuoteArg(arg) {
191
+ const value = String(arg)
192
+ if (/^[A-Za-z0-9_./:@+=,-]+$/.test(value)) return value
193
+ return `'${value.replace(/'/g, "'\\''")}'`
194
+ }
195
+
196
+ function getCurrentInstallOptions(pm = detectPackageManager()) {
197
+ const installTarget = pm === 'npm' ? resolveCurrentNpmInstallTarget() : null
198
+ return pm === 'npm' && installTarget ? {
199
+ prefix: installTarget.prefix,
200
+ bin: installTarget.bin,
201
+ } : {}
202
+ }
203
+
156
204
  /**
157
205
  * 📖 getManualInstallCmd: human-readable command string for error / fallback messages.
158
206
  * @param {'npm' | 'bun' | 'pnpm' | 'yarn'} pm
159
207
  * @param {string} version
208
+ * @param {{ prefix?: string, bin?: string }} [options]
160
209
  * @returns {string}
161
210
  */
162
- export function getManualInstallCmd(pm, version) {
163
- const { bin, args } = getInstallArgs(pm, version)
164
- return `${bin} ${args.join(' ')}`
211
+ export function getManualInstallCmd(pm, version, options = {}) {
212
+ const { bin, args } = getInstallArgs(pm, version, options)
213
+ return [bin, ...args].map(shellQuoteArg).join(' ')
165
214
  }
166
215
 
167
216
  /**
@@ -262,14 +311,16 @@ export async function enforceMandatoryStartupUpdate(config, options = {}) {
262
311
  base.allowedOutdated = true
263
312
  base.warningMessage = buildOutdatedWarningMessage(latestVersion, failures)
264
313
  console.log(chalk.red(` ${base.warningMessage}`))
265
- console.log(chalk.dim(` Manual update: ${getManualInstallCmd(detectPackageManager(), latestVersion)}`))
314
+ const pm = detectPackageManager()
315
+ console.log(chalk.dim(` Manual update: ${getManualInstallCmd(pm, latestVersion, getCurrentInstallOptions(pm))}`))
266
316
  console.log()
267
317
  return base
268
318
  }
269
319
 
270
320
  base.blocked = true
271
321
  console.log(chalk.red(' ✖ Mandatory update failed. FCM will retry on the next launch.'))
272
- console.log(chalk.dim(` Attempt ${failures}/${UPDATE_FAILURE_THRESHOLD}. Manual update: ${getManualInstallCmd(detectPackageManager(), latestVersion)}`))
322
+ const pm = detectPackageManager()
323
+ console.log(chalk.dim(` Attempt ${failures}/${UPDATE_FAILURE_THRESHOLD}. Manual update: ${getManualInstallCmd(pm, latestVersion, getCurrentInstallOptions(pm))}`))
273
324
  console.log()
274
325
  return base
275
326
  }
@@ -305,9 +356,10 @@ export async function fetchLastReleaseDate() {
305
356
  * 📖 Bun installs to ~/.bun/install/global/ (always user-writable) so sudo is never needed.
306
357
  * 📖 For npm/pnpm/yarn we probe their global root/prefix paths and check writability.
307
358
  * @param {'npm' | 'bun' | 'pnpm' | 'yarn'} pm
359
+ * @param {{ packageRoot: string, prefix: string, bin: string } | null} [installTarget]
308
360
  * @returns {{ needsSudo: boolean, checkedPath: string|null }}
309
361
  */
310
- function detectGlobalInstallPermission(pm) {
362
+ function detectGlobalInstallPermission(pm, installTarget = null) {
311
363
  if (pm === 'bun') {
312
364
  return { needsSudo: false, checkedPath: null }
313
365
  }
@@ -326,13 +378,20 @@ function detectGlobalInstallPermission(pm) {
326
378
  if (dir) candidates.push(dir)
327
379
  } catch {}
328
380
  } else {
381
+ if (installTarget?.prefix) {
382
+ candidates.push(join(installTarget.prefix, 'lib', 'node_modules'))
383
+ candidates.push(installTarget.prefix)
384
+ }
385
+
386
+ const npmBin = installTarget?.bin || 'npm'
387
+
329
388
  try {
330
- const npmRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
389
+ const npmRoot = execFileSync(npmBin, ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
331
390
  if (npmRoot) candidates.push(npmRoot)
332
391
  } catch {}
333
392
 
334
393
  try {
335
- const npmPrefix = execFileSync('npm', ['prefix', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
394
+ const npmPrefix = execFileSync(npmBin, ['prefix', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
336
395
  if (npmPrefix) candidates.push(npmPrefix)
337
396
  } catch {}
338
397
  }
@@ -348,6 +407,28 @@ function detectGlobalInstallPermission(pm) {
348
407
  return { needsSudo: false, checkedPath: candidates[0] || null }
349
408
  }
350
409
 
410
+ function readCurrentPackageVersion() {
411
+ try {
412
+ const data = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'))
413
+ return typeof data.version === 'string' ? data.version : null
414
+ } catch {
415
+ return null
416
+ }
417
+ }
418
+
419
+ function verifyCurrentInstallUpdated(latestVersion, installTarget) {
420
+ if (!installTarget) return
421
+
422
+ const installedVersion = readCurrentPackageVersion()
423
+ if (installedVersion === latestVersion) return
424
+
425
+ const actual = installedVersion ? `v${installedVersion}` : 'an unknown version'
426
+ throw new Error(
427
+ `Update command completed, but the active install at ${installTarget.packageRoot} still reports ${actual}. ` +
428
+ 'The package manager likely installed into another global prefix.'
429
+ )
430
+ }
431
+
351
432
  /**
352
433
  * 📖 hasSudoCommand: lightweight guard so we don't suggest sudo on systems where it does not exist.
353
434
  * @returns {boolean}
@@ -402,11 +483,16 @@ function relaunchCurrentProcess() {
402
483
  * 📖 installUpdateCommand: run global install using the detected package manager, optionally prefixed with sudo.
403
484
  * @param {string} latestVersion
404
485
  * @param {boolean} useSudo
486
+ * @param {{ packageRoot: string, prefix: string, bin: string } | null} [installTarget]
405
487
  */
406
- function installUpdateCommand(latestVersion, useSudo) {
488
+ function installUpdateCommand(latestVersion, useSudo, installTarget = null) {
407
489
  const { execFileSync } = require('child_process')
408
490
  const pm = detectPackageManager()
409
- const { bin, args } = getInstallArgs(pm, latestVersion)
491
+ const installOptions = pm === 'npm' && installTarget ? {
492
+ prefix: installTarget.prefix,
493
+ bin: installTarget.bin,
494
+ } : {}
495
+ const { bin, args } = getInstallArgs(pm, latestVersion, installOptions)
410
496
 
411
497
  if (useSudo) {
412
498
  execFileSync('sudo', [bin, ...args], { stdio: 'inherit', shell: false })
@@ -433,7 +519,12 @@ export function runUpdate(latestVersion, options = {}) {
433
519
  console.log()
434
520
 
435
521
  const pm = detectPackageManager()
436
- const { needsSudo, checkedPath } = detectGlobalInstallPermission(pm)
522
+ const installTarget = pm === 'npm' ? resolveCurrentNpmInstallTarget() : null
523
+ const installOptions = pm === 'npm' && installTarget ? {
524
+ prefix: installTarget.prefix,
525
+ bin: installTarget.bin,
526
+ } : {}
527
+ const { needsSudo, checkedPath } = detectGlobalInstallPermission(pm, installTarget)
437
528
  const sudoAvailable = process.platform !== 'win32' && hasSudoCommand()
438
529
  let lastError = null
439
530
 
@@ -444,7 +535,8 @@ export function runUpdate(latestVersion, options = {}) {
444
535
  }
445
536
 
446
537
  try {
447
- installUpdateCommand(latestVersion, needsSudo && sudoAvailable)
538
+ installUpdateCommand(latestVersion, needsSudo && sudoAvailable, installTarget)
539
+ verifyCurrentInstallUpdated(latestVersion, installTarget)
448
540
  console.log()
449
541
  console.log(chalk.green(` ✅ Update complete! Version ${latestVersion} installed.`))
450
542
  console.log()
@@ -452,13 +544,14 @@ export function runUpdate(latestVersion, options = {}) {
452
544
  return { ok: true }
453
545
  } catch (err) {
454
546
  lastError = err
455
- const manualCmd = getManualInstallCmd(pm, latestVersion)
547
+ const manualCmd = getManualInstallCmd(pm, latestVersion, installOptions)
456
548
  console.log()
457
549
  if (isPermissionError(err) && !needsSudo && sudoAvailable) {
458
550
  console.log(chalk.yellow(` ⚠ Permission denied during ${pm} global install. Retrying with sudo...`))
459
551
  console.log()
460
552
  try {
461
- installUpdateCommand(latestVersion, true)
553
+ installUpdateCommand(latestVersion, true, installTarget)
554
+ verifyCurrentInstallUpdated(latestVersion, installTarget)
462
555
  console.log()
463
556
  console.log(chalk.green(` ✅ Update complete with sudo! Version ${latestVersion} installed.`))
464
557
  console.log()
@@ -484,5 +577,3 @@ export function runUpdate(latestVersion, options = {}) {
484
577
  if (exitOnFailure) process.exit(1)
485
578
  return { ok: false, error: lastError }
486
579
  }
487
-
488
-