free-coding-models 0.3.51 → 0.3.52

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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.3.52] - 2026-04-18
2
+
3
+ ### Added
4
+
5
+ - **OpenCode WebUI Support** — Added `--opencode-web` flag to open the OpenCode WebUI dashboard after configuring the selected model. This mirrors the existing `--opencode-desktop` behavior.
6
+
1
7
  ## [0.3.51] - 2026-04-11
2
8
 
3
9
  ### Changed
package/README.md CHANGED
@@ -11,10 +11,12 @@
11
11
  </p>
12
12
 
13
13
  <h1 align="center">free-coding-models</h1>
14
+
15
+
14
16
 
15
17
  <p align="center">
16
18
  <strong>Find the fastest free coding model in seconds</strong><br>
17
- <sub>Ping 238 models across 25 AI Free providers in real-time </sub><br> <sub> Install Free API endpoints to your favorite AI coding tool: <br>📦 OpenCode, 🦞 OpenClaw, 💘 Crush, 🪿 Goose, 🛠 Aider, 🐉 Qwen Code, 🤲 OpenHands, ⚡ Amp, 🔮 Hermes, ▶️ Continue, 🧠 Cline, 🛠️ Xcode, π Pi, 🦘 Rovo or ♊ Gemini in one keystroke</sub>
19
+ <sub>Ping 238 models across 25 AI Free providers in real-time </sub><br> <sub> Install Free API endpoints to your favorite AI coding tool: <br>📦 OpenCode, 📦 OpenCode Desktop, 📦 OpenCode WebUI, 🦞 OpenClaw, 💘 Crush, 🪿 Goose, 🛠 Aider, 🐉 Qwen Code, 🤲 OpenHands, ⚡ Amp, 🔮 Hermes, ▶️ Continue, 🧠 Cline, 🛠️ Xcode, π Pi, 🦘 Rovo or ♊ Gemini in one keystroke</sub>
18
20
  </p>
19
21
 
20
22
 
@@ -260,6 +262,7 @@ When launching the web dashboard, `free-coding-models` prefers `http://localhost
260
262
  |------|----------|
261
263
  | `--opencode` | 📦 OpenCode CLI |
262
264
  | `--opencode-desktop` | 📦 OpenCode Desktop |
265
+ | `--opencode-web` | 📦 OpenCode WebUI |
263
266
  | `--openclaw` | 🦞 OpenClaw |
264
267
  | `--crush` | 💘 Crush |
265
268
  | `--goose` | 🪿 Goose |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.51",
3
+ "version": "0.3.52",
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/src/app.js CHANGED
@@ -105,7 +105,7 @@ import { usageForRow as _usageForRow } from '../src/usage-reader.js'
105
105
  import { buildProviderModelTokenKey, loadTokenUsageByProviderModel } from '../src/token-usage-reader.js'
106
106
  import { parseOpenRouterResponse, fetchProviderQuota as _fetchProviderQuotaFromModule } from '../src/provider-quota-fetchers.js'
107
107
  import { isKnownQuotaTelemetry } from '../src/quota-capabilities.js'
108
- import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, OVERLAY_PANEL_WIDTH, TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES, WIDTH_WARNING_MIN_COLS, msCell, spinCell } from '../src/constants.js'
108
+ import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, VERDICT_CYCLE, HEALTH_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, OVERLAY_PANEL_WIDTH, TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES, WIDTH_WARNING_MIN_COLS, msCell, spinCell } from '../src/constants.js'
109
109
  import { TIER_COLOR } from '../src/tier-colors.js'
110
110
  import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getProviderQuotaPercentCached, usagePlaceholderForProvider } from '../src/ping.js'
111
111
  import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
@@ -117,7 +117,7 @@ import { promptApiKey } from '../src/setup.js'
117
117
  import { syncShellEnv, ensureShellRcSource, promptShellEnvMigration, removeShellEnv } from '../src/shell-env.js'
118
118
  import { stripAnsi, maskApiKey, displayWidth, padEndDisplay, tintOverlayLines, keepOverlayTargetVisible, sliceOverlayLines, calculateViewport, sortResultsWithPinnedFavorites, adjustScrollOffset } from '../src/render-helpers.js'
119
119
  import { renderTable, PROVIDER_COLOR } from '../src/render-table.js'
120
- import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop } from '../src/opencode.js'
120
+ import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop, startOpenCodeWeb } from '../src/opencode.js'
121
121
  import { startOpenClaw } from '../src/openclaw.js'
122
122
  import { createOverlayRenderers } from '../src/overlays.js'
123
123
  import { createKeyHandler, createMouseEventHandler } from '../src/key-handler.js'
@@ -252,6 +252,7 @@ export async function runApp(cliArgs, config) {
252
252
  const flagByMode = {
253
253
  opencode: cliArgs.openCodeMode,
254
254
  'opencode-desktop': cliArgs.openCodeDesktopMode,
255
+ 'opencode-web': cliArgs.openCodeWebMode,
255
256
  openclaw: cliArgs.openClawMode,
256
257
  aider: cliArgs.aiderMode,
257
258
  crush: cliArgs.crushMode,
@@ -423,6 +424,8 @@ export async function runApp(cliArgs, config) {
423
424
  mode, // 📖 'opencode' or 'openclaw' — controls Enter action
424
425
  tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
425
426
  originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
427
+ verdictFilterMode: 0, // 📖 Index into VERDICT_CYCLE (0=All, then verdicts)
428
+ healthFilterMode: 0, // 📖 Index into HEALTH_CYCLE (0=All, then health states)
426
429
  hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
427
430
  favoritesPinnedAndSticky: config.settings?.favoritesPinnedAndSticky === true, // 📖 false by default: favorites follow normal sort/filter rules until Y enables pinned+sticky mode.
428
431
  footerHidden: config.settings?.footerHidden === true, // 📖 true = footer is collapsed to a single toggle hint
@@ -739,6 +742,8 @@ export async function runApp(cliArgs, config) {
739
742
  function applyTierFilter() {
740
743
  const activeTier = TIER_CYCLE[state.tierFilterMode]
741
744
  const activeOrigin = ORIGIN_CYCLE[state.originFilterMode]
745
+ const activeVerdict = VERDICT_CYCLE[state.verdictFilterMode]
746
+ const activeHealth = HEALTH_CYCLE[state.healthFilterMode]
742
747
  state.results.forEach(r => {
743
748
  // 📖 Sticky-favorites mode keeps favorites visible regardless of configured-only, tier, or provider filters.
744
749
  if (state.favoritesPinnedAndSticky && r.isFavorite) {
@@ -754,12 +759,16 @@ export async function runApp(cliArgs, config) {
754
759
  r.hidden = true
755
760
  return
756
761
  }
757
- // 📖 Apply both tier and origin filters — model is hidden if it fails either
758
- // 📖 TIER_LETTER_MAP is used so --tier S also includes S+ models (tier family behavior).
762
+ // 📖 Apply tier, origin, verdict, and health filters — model is hidden if it fails any
759
763
  const allowedTiers = (activeTier && TIER_LETTER_MAP[activeTier]) ? TIER_LETTER_MAP[activeTier] : [activeTier]
760
764
  const tierHide = activeTier !== null && !allowedTiers.includes(r.tier)
761
765
  const originHide = activeOrigin !== null && r.providerKey !== activeOrigin
762
- if (tierHide || originHide) {
766
+ // 📖 Verdict filter: match against getVerdict(r) when active
767
+ const rVerdict = getVerdict(r)
768
+ const verdictHide = activeVerdict !== null && rVerdict !== activeVerdict
769
+ // 📖 Health filter: match against r.status when active
770
+ const healthHide = activeHealth !== null && r.status !== activeHealth
771
+ if (tierHide || originHide || verdictHide || healthHide) {
763
772
  r.hidden = true
764
773
  return
765
774
  }
@@ -864,6 +873,7 @@ export async function runApp(cliArgs, config) {
864
873
  runUpdate,
865
874
  startOpenClaw,
866
875
  startOpenCodeDesktop,
876
+ startOpenCodeWeb,
867
877
  startOpenCode,
868
878
  startExternalTool,
869
879
  getToolModeOrder,
@@ -1042,7 +1052,10 @@ export async function runApp(cliArgs, config) {
1042
1052
  state.versionAlertsEnabled,
1043
1053
  state.favoritesPinnedAndSticky,
1044
1054
  state.customTextFilter,
1045
- state.lastReleaseDate
1055
+ state.lastReleaseDate,
1056
+ state.footerHidden,
1057
+ state.verdictFilterMode,
1058
+ state.healthFilterMode
1046
1059
  )
1047
1060
  }
1048
1061
  tableContent = state.commandPaletteFrozenTable
@@ -1077,7 +1090,10 @@ export async function runApp(cliArgs, config) {
1077
1090
  state.versionAlertsEnabled,
1078
1091
  state.favoritesPinnedAndSticky,
1079
1092
  state.customTextFilter,
1080
- state.lastReleaseDate
1093
+ state.lastReleaseDate,
1094
+ state.footerHidden,
1095
+ state.verdictFilterMode,
1096
+ state.healthFilterMode
1081
1097
  )
1082
1098
  }
1083
1099
 
@@ -1121,7 +1137,7 @@ export async function runApp(cliArgs, config) {
1121
1137
  pinFavorites: state.favoritesPinnedAndSticky,
1122
1138
  })
1123
1139
 
1124
- process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, state.footerHidden))
1140
+ process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, state.footerHidden, state.verdictFilterMode, state.healthFilterMode))
1125
1141
  if (process.stdout.isTTY) {
1126
1142
  process.stdout.flush && process.stdout.flush()
1127
1143
  }
@@ -20,6 +20,7 @@ import { TOOL_METADATA, TOOL_MODE_ORDER } from './tool-metadata.js'
20
20
  const TOOL_MODE_DESCRIPTIONS = {
21
21
  opencode: 'Launch in OpenCode CLI with the selected model.',
22
22
  'opencode-desktop': 'Set model in shared config, then open OpenCode Desktop.',
23
+ 'opencode-web': 'Set model in shared config, then open OpenCode WebUI.',
23
24
  openclaw: 'Set default model in OpenClaw and launch it.',
24
25
  crush: 'Launch Crush with this provider/model pair.',
25
26
  goose: 'Launch Goose and preselect the active model.',
package/src/constants.js CHANGED
@@ -77,6 +77,14 @@ export const FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','
77
77
  // 📖 Index 0 = no filter (show all), then each tier name in descending quality order.
78
78
  export const TIER_CYCLE = [null, 'S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C']
79
79
 
80
+ // 📖 VERDICT_CYCLE: cycles through health verdict labels (0=All, then each verdict).
81
+ // 📖 Based on VERDICT_ORDER from utils.js — includes all possible getVerdict() values.
82
+ export const VERDICT_CYCLE = [null, 'Perfect', 'Normal', 'Slow', 'Spiky', 'Very Slow', 'Overloaded', 'Unstable', 'Not Active', 'Pending']
83
+
84
+ // 📖 HEALTH_CYCLE: cycles through ping status states (0=All, then each status).
85
+ // 📖 Based on the status values in the app: up, timeout, down, auth_error, noauth, pending.
86
+ export const HEALTH_CYCLE = [null, 'up', 'timeout', 'down', 'auth_error', 'noauth', 'pending']
87
+
80
88
  // 📖 Overlay background chalk functions — each overlay panel has a distinct tint
81
89
  // 📖 so users can tell Settings, Help, Recommend, and Log panels apart at a glance.
82
90
  export const SETTINGS_OVERLAY_BG = chalk.bgRgb(0, 0, 0)
@@ -93,8 +101,8 @@ export const WIDTH_WARNING_MIN_COLS = 80
93
101
 
94
102
  // 📖 Table row-budget constants — must stay in sync with renderTable()'s actual output.
95
103
  // 📖 If this drifts, model rows overflow and can push the title row out of view.
96
- export const TABLE_HEADER_LINES = 4 // 📖 title, spacer, column headers, separator
97
- export const TABLE_FOOTER_LINES = 5 // 📖 spacer, hints line 1, hints line 2, spacer, credit+links
104
+ export const TABLE_HEADER_LINES = 5 // 📖 title, filter bar, spacer, column headers, separator
105
+ export const TABLE_FOOTER_LINES = 1 // 📖 single toggle-hint line when collapsed, full footer otherwise
98
106
  export const TABLE_FIXED_LINES = TABLE_HEADER_LINES + TABLE_FOOTER_LINES
99
107
 
100
108
  // ─── Small cell-formatting helpers ────────────────────────────────────────────
@@ -39,7 +39,7 @@ import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
39
39
  import { cycleThemeSetting, detectActiveTheme } from './theme.js'
40
40
  import { syncShellEnv, ensureShellRcSource, removeShellEnv } from './shell-env.js'
41
41
  import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntries } from './command-palette.js'
42
- import { WIDTH_WARNING_MIN_COLS } from './constants.js'
42
+ import { WIDTH_WARNING_MIN_COLS, VERDICT_CYCLE, HEALTH_CYCLE } from './constants.js'
43
43
  import { scanAllToolConfigs, softDeleteModel } from './installed-models-manager.js'
44
44
  import { startExternalTool } from './tool-launchers.js'
45
45
 
@@ -264,6 +264,7 @@ export function createKeyHandler(ctx) {
264
264
  runUpdate,
265
265
  startOpenClaw,
266
266
  startOpenCodeDesktop,
267
+ startOpenCodeWeb,
267
268
  startOpenCode,
268
269
  startExternalTool,
269
270
  getToolModeOrder,
@@ -495,6 +496,8 @@ export function createKeyHandler(ctx) {
495
496
  exitCode = await startOpenClaw(userSelected, state.config, { launchCli: true })
496
497
  } else if (state.mode === 'opencode-desktop') {
497
498
  exitCode = await startOpenCodeDesktop(userSelected, state.config)
499
+ } else if (state.mode === 'opencode-web') {
500
+ exitCode = await startOpenCodeWeb(userSelected, state.config)
498
501
  } else if (state.mode === 'opencode') {
499
502
  exitCode = await startOpenCode(userSelected, state.config)
500
503
  } else {
@@ -2414,6 +2417,7 @@ export function createKeyHandler(ctx) {
2414
2417
  if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
2415
2418
  state.config.settings.footerHidden = state.footerHidden
2416
2419
  saveConfig(state.config)
2420
+ state.frame++ // 📖 Force immediate re-render
2417
2421
  return
2418
2422
  }
2419
2423
 
@@ -2449,6 +2453,24 @@ export function createKeyHandler(ctx) {
2449
2453
  return
2450
2454
  }
2451
2455
 
2456
+ // 📖 Verdict filter key: V = cycle through each verdict (All → Perfect → Normal → Slow → ... → All)
2457
+ if (key.name === 'v') {
2458
+ state.verdictFilterMode = (state.verdictFilterMode + 1) % VERDICT_CYCLE.length
2459
+ applyTierFilter()
2460
+ refreshVisibleSorted({ resetCursor: true })
2461
+ persistUiSettings()
2462
+ return
2463
+ }
2464
+
2465
+ // 📖 Health filter key: H = cycle through each health status (All → Up → Timeout → Down → ... → All)
2466
+ if (key.name === 'h') {
2467
+ state.healthFilterMode = (state.healthFilterMode + 1) % HEALTH_CYCLE.length
2468
+ applyTierFilter()
2469
+ refreshVisibleSorted({ resetCursor: true })
2470
+ persistUiSettings()
2471
+ return
2472
+ }
2473
+
2452
2474
  // 📖 Help overlay key: Ctrl+H = toggle help overlay
2453
2475
  if (key.ctrl && key.name === 'h') {
2454
2476
  state.helpVisible = !state.helpVisible
package/src/opencode.js CHANGED
@@ -543,6 +543,80 @@ export async function startOpenCode(model, fcmConfig) {
543
543
  await spawnOpenCode(['--model', modelRef], providerKey, fcmConfig)
544
544
  }
545
545
 
546
+ // ─── Start OpenCode Web ───────────────────────────────────────────────────────
547
+
548
+ export async function startOpenCodeWeb(model, fcmConfig) {
549
+ const providerKey = model.providerKey ?? 'nvidia'
550
+ const ocModelId = getOpenCodeModelId(providerKey, model.modelId)
551
+ const modelRef = `${providerKey}/${ocModelId}`
552
+
553
+ const launchWeb = async () => {
554
+ const { exec } = await import('child_process')
555
+ const url = 'https://opencode.ai'
556
+ let command
557
+ if (isMac) {
558
+ command = `open "${url}"`
559
+ } else if (isWindows) {
560
+ command = `start "${url}"`
561
+ } else {
562
+ command = `xdg-open "${url}"`
563
+ }
564
+ exec(command, (err) => {
565
+ if (err) {
566
+ console.error(chalk.red(' Could not open OpenCode WebUI'))
567
+ console.error(chalk.dim(` Please visit ${url} manually`))
568
+ }
569
+ })
570
+ }
571
+
572
+ // 📖 Mirror OpenCode Desktop behavior: set the model in opencode.json
573
+ const config = loadOpenCodeConfig()
574
+ const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
575
+
576
+ if (existsSync(getOpenCodeConfigPath())) {
577
+ copyFileSync(getOpenCodeConfigPath(), backupPath)
578
+ console.log(chalk.dim(` Backup: ${backupPath}`))
579
+ }
580
+
581
+ if (!config.provider) config.provider = {}
582
+
583
+ // 📖 Provider-specific config setup (same as CLI/Desktop)
584
+ if (providerKey === 'nvidia' && !config.provider.nvidia) {
585
+ config.provider.nvidia = {
586
+ npm: '@ai-sdk/openai-compatible',
587
+ name: 'NVIDIA NIM',
588
+ options: { baseURL: 'https://integrate.api.nvidia.com/v1', apiKey: '{env:NVIDIA_API_KEY}' },
589
+ models: {}
590
+ }
591
+ } else if (providerKey === 'groq' && !config.provider.groq) {
592
+ config.provider.groq = { options: { apiKey: '{env:GROQ_API_KEY}' }, models: {} }
593
+ } else if (providerKey === 'cerebras' && !config.provider.cerebras) {
594
+ config.provider.cerebras = {
595
+ npm: '@ai-sdk/openai-compatible',
596
+ name: 'Cerebras',
597
+ options: { baseURL: 'https://api.cerebras.ai/v1', apiKey: '{env:CEREBRAS_API_KEY}' },
598
+ models: {}
599
+ }
600
+ }
601
+ // ... other providers are handled as they are selected
602
+
603
+ console.log(chalk.green(` Setting ${chalk.bold(model.label)} as default (mirroring Desktop)...`))
604
+
605
+ if (providerKey !== 'opencode-zen' && config.provider[providerKey]) {
606
+ if (!config.provider[providerKey].models) config.provider[providerKey].models = {}
607
+ config.provider[providerKey].models[ocModelId] = { name: model.label }
608
+ }
609
+
610
+ config.model = providerKey === 'opencode-zen' ? `opencode/${ocModelId}` : modelRef
611
+ saveOpenCodeConfig(config)
612
+
613
+ console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
614
+ console.log(chalk.dim(' Opening OpenCode WebUI...'))
615
+ console.log()
616
+
617
+ await launchWeb()
618
+ }
619
+
546
620
  // ─── Start OpenCode Desktop ───────────────────────────────────────────────────
547
621
 
548
622
  export async function startOpenCodeDesktop(model, fcmConfig) {