free-coding-models 0.3.6 β†’ 0.3.9

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
@@ -2,6 +2,17 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## 0.3.9
6
+
7
+ ### Improved
8
+ - **Enhanced `--premium` flag**: Now applies strict elite-only constraints. Shows only **S/S+** tier models with perfect health (**UP**) and a good verdict (**Perfect**, **Normal**, or **Slow**). Models with 429 errors, auth failures, or poor performance are automatically hidden.
9
+ - **Accurate Token Usage Tracking**: The "Used" column now uses the persistent `token-stats.json` file as the source of truth, providing accurate historical totals instead of only the most recent logs.
10
+ - **Enhanced Log Transparency**: The request log page now always shows the requested model and the actual upstream model (e.g., `llama-3.1-405b β†’ meta/llama-3.1-405b-instruct`) whenever they differ.
11
+ - **Pretty Provider Labels**: The request log page now uses human-readable provider labels (e.g., "NVIDIA NIM", "SambaNova") instead of raw internal keys.
12
+ - **Fixed Tier Filtering Family Logic**: Updated `--tier S` behavior to correctly include both **S** and **S+** models (matching documentation).
13
+
14
+ ---
15
+
5
16
  ## 0.3.6
6
17
 
7
18
  ### Added
package/README.md CHANGED
@@ -200,6 +200,30 @@ free-coding-models
200
200
  # Explicitly target OpenCode CLI (TUI + Enter launches OpenCode CLI)
201
201
  free-coding-models --opencode
202
202
 
203
+ ## πŸ“‹ CLI Flags (expanded)
204
+
205
+ The tool now supports a comprehensive set of flags to fine‑tune its behavior. All flags can be combined in any order.
206
+
207
+ | Flag | Type | Description |
208
+ |------|------|-------------|
209
+ | `--best` | boolean | Show only top‑tier models (A+, S, S+). |
210
+ | `--fiable` | boolean | Run a 10β€―s reliability analysis and output the most reliable model. |
211
+ | `--json` | boolean | Output results as JSON for scripting/automation. |
212
+ | `--tier <S|A|B|C>` | value | Filter models by tier family (e.g. `S` shows S+ and S). |
213
+ | `--recommend` | boolean | Open Smart Recommend mode immediately on startup. |
214
+ | `--sort <column>` | value | Sort by a specific column (`rank`, `tier`, `origin`, `model`, `ping`, `avg`, `swe`, `ctx`, `condition`, `verdict`, `uptime`, `stability`, `usage`). |
215
+ | `--desc` / `--asc` | boolean | Set sort direction explicitly (descending or ascending). |
216
+ | `--origin <provider>` | value | Filter models by provider origin (e.g. `nvidia`, `groq`). |
217
+ | `--ping-interval <ms>` | value | Override the ping interval in milliseconds (affects live monitoring speed). |
218
+ | `--hide-unconfigured` | boolean | Hide models whose providers have no configured API key. |
219
+ | `--show-unconfigured` | boolean | Show all models regardless of API key configuration. |
220
+ | `--disable-widths-warning` | boolean | Disable the terminal width warning banner. |
221
+ | `--profile <name>` | value | Load a saved configuration profile before startup. |
222
+ | `--no-telemetry` | boolean | Disable anonymous telemetry for this run. |
223
+ | `--clean-proxy`, `--proxy-clean` | boolean | Remove persisted FCM proxy configuration from OpenCode. |
224
+ | `--help`, `-h` | boolean | Print the complete help text and exit. |
225
+
226
+ These flags are also reflected in the built‑in help (`free-coding-models --help`).
203
227
  # Explicitly target OpenCode Desktop (TUI + Enter sets model & opens Desktop app)
204
228
  free-coding-models --opencode-desktop
205
229
 
@@ -920,6 +944,25 @@ This script:
920
944
 
921
945
  ## πŸ“‹ API Reference
922
946
 
947
+ ### 🎁 Premium Flag
948
+
949
+ The `--premium` flag provides a quick view of only the elite **S/S+ tier** models with perfect health (**UP**) and a good verdict (**Perfect**, **Normal**, or **Slow**). This is useful when you want to focus exclusively on the highest‑quality, most reliable models that are currently available.
950
+
951
+ ```bash
952
+ free-coding-models --premium
953
+ ```
954
+
955
+ What it does under the hood:
956
+ - Sets `tierFilter` to `S` (showing only S+ and S tier models).
957
+ - Filters out any model that is not currently **UP** (hides 429, 410, auth fail, timeouts, etc.).
958
+ - Filters out models with poor verdicts (hides **Spiky**, **Very Slow**, **Overloaded**, **Unstable**, etc.).
959
+ - Forces the sort column to `verdict` with ascending order, so the best‑rated models appear at the top.
960
+ - Leaves other settings untouched, so you can still combine it with flags like `--json` for scripting.
961
+
962
+ You can combine `--premium` with other flags (e.g., `--json --hide-unconfigured`) to further tailor the output.
963
+
964
+ ---
965
+
923
966
  **Environment variables (override config file):**
924
967
 
925
968
  | Variable | Description |
@@ -203,6 +203,22 @@ async function main() {
203
203
  // πŸ“– User declined auto-fix or it failed β€” continue anyway, just warned
204
204
  }
205
205
 
206
+ // πŸ“– Apply CLI overrides for settings
207
+ if (cliArgs.sortColumn) config.settings.sortColumn = cliArgs.sortColumn
208
+ if (cliArgs.sortDirection) config.settings.sortAsc = cliArgs.sortDirection === 'asc'
209
+ if (cliArgs.originFilter) config.settings.originFilter = cliArgs.originFilter
210
+ if (cliArgs.pingInterval) config.settings.pingInterval = cliArgs.pingInterval
211
+ if (cliArgs.hideUnconfigured) config.settings.hideUnconfiguredModels = true
212
+ if (cliArgs.showUnconfigured) config.settings.hideUnconfiguredModels = false
213
+ if (cliArgs.disableWidthsWarning) config.settings.disableWidthsWarning = true
214
+
215
+ // πŸ“– Apply premium mode: show only S‑tier models sorted by verdict
216
+ if (cliArgs.premiumMode) {
217
+ config.settings.tierFilter = 'S'
218
+ config.settings.sortColumn = 'verdict'
219
+ config.settings.sortAsc = true
220
+ }
221
+
206
222
  if (cliArgs.cleanProxyMode) {
207
223
  const cleaned = cleanupOpenCodeProxyConfig()
208
224
  console.log()
@@ -499,8 +515,9 @@ async function main() {
499
515
  mode, // πŸ“– 'opencode' or 'openclaw' β€” controls Enter action
500
516
  tierFilterMode: 0, // πŸ“– Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
501
517
  originFilterMode: 0, // πŸ“– Index into ORIGIN_CYCLE (0=All, then providers)
502
- hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true || config.settings?.hideUnconfiguredModels === true, // πŸ“– Hide providers with no configured API key when true.
503
- disableWidthsWarning: config.settings?.disableWidthsWarning ?? false, // πŸ“– Disable widths warning toggle (default off)
518
+ premiumMode: cliArgs.premiumMode, // πŸ“– Special elite-only mode: S/S+ only, Health UP only, Perfect/Normal/Slow verdict only.
519
+ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true || config.settings?.hideUnconfiguredModels === true, // πŸ“– Hide providers with no configured API key when true.
520
+ disableWidthsWarning: config.settings?.disableWidthsWarning ?? false, // πŸ“– Disable widths warning toggle (default off)
504
521
  scrollOffset: 0, // πŸ“– First visible model index in viewport
505
522
  terminalRows: process.stdout.rows || 24, // πŸ“– Current terminal height
506
523
  terminalCols: process.stdout.columns || 80, // πŸ“– Current terminal width
@@ -756,6 +773,17 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
756
773
  outputResults = outputResults.filter(r => ['S+', 'S', 'A+'].includes(r.tier))
757
774
  }
758
775
 
776
+ // πŸ“– Apply premium mode filter if specified: elite-only (S/S+, UP, Good Verdict)
777
+ if (cliArgs.premiumMode) {
778
+ outputResults = outputResults.filter(r => {
779
+ const isEliteTier = r.tier === 'S' || r.tier === 'S+'
780
+ const isHealthUp = r.status === 'up'
781
+ const verdict = getVerdict(r)
782
+ const isGoodVerdict = ['Perfect', 'Normal', 'Slow'].includes(verdict)
783
+ return isEliteTier && isHealthUp && isGoodVerdict
784
+ })
785
+ }
786
+
759
787
  // πŸ“– Sort by avg ping (ascending)
760
788
  outputResults = sortResults(outputResults, 'avg', 'asc')
761
789
 
@@ -805,9 +833,23 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
805
833
  return
806
834
  }
807
835
  // πŸ“– Apply both tier and origin filters β€” model is hidden if it fails either
808
- const tierHide = activeTier !== null && r.tier !== activeTier
836
+ // πŸ“– TIER_LETTER_MAP is used so --tier S also includes S+ models (tier family behavior).
837
+ const allowedTiers = (activeTier && TIER_LETTER_MAP[activeTier]) ? TIER_LETTER_MAP[activeTier] : [activeTier]
838
+ const tierHide = activeTier !== null && !allowedTiers.includes(r.tier)
809
839
  const originHide = activeOrigin !== null && r.providerKey !== activeOrigin
810
840
  r.hidden = tierHide || originHide
841
+
842
+ // πŸ“– Premium Mode: elite-only constraints (Health UP, Good Verdict, S/S+ only)
843
+ if (state.premiumMode && !r.hidden) {
844
+ const isEliteTier = r.tier === 'S' || r.tier === 'S+'
845
+ const isHealthUp = r.status === 'up'
846
+ const verdict = getVerdict(r)
847
+ const isGoodVerdict = ['Perfect', 'Normal', 'Slow'].includes(verdict)
848
+
849
+ if (!isEliteTier || !isHealthUp || !isGoodVerdict) {
850
+ r.hidden = true
851
+ }
852
+ }
811
853
  })
812
854
  return state.results
813
855
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.6",
3
+ "version": "0.3.9",
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/cli-help.js CHANGED
@@ -26,6 +26,14 @@ const ANALYSIS_FLAGS = [
26
26
  { flag: '--json', description: 'Output results as JSON for scripts/automation' },
27
27
  { flag: '--tier <S|A|B|C>', description: 'Filter models by tier family' },
28
28
  { flag: '--recommend', description: 'Open Smart Recommend immediately on startup' },
29
+ { flag: '--premium', description: 'Show only S/S+ models with perfect health and good verdict' },
30
+ { flag: '--sort <column>', description: 'Sort by column (rank, tier, origin, model, ping, avg, swe, ctx, condition, verdict, uptime, stability, usage)' },
31
+ { flag: '--desc | --asc', description: 'Set sort direction (descending or ascending)' },
32
+ { flag: '--origin <provider>', description: 'Filter models by provider origin' },
33
+ { flag: '--ping-interval <ms>', description: 'Override ping interval in milliseconds' },
34
+ { flag: '--hide-unconfigured', description: 'Hide models without configured API keys' },
35
+ { flag: '--show-unconfigured', description: 'Show all models regardless of API key config' },
36
+ { flag: '--disable-widths-warning', description: 'Disable terminal width warning' },
29
37
  ]
30
38
 
31
39
  const CONFIG_FLAGS = [
package/src/overlays.js CHANGED
@@ -736,7 +736,8 @@ const profileStartIdx = updateRowIdx + 5
736
736
  } catch { /* keep raw */ }
737
737
 
738
738
  const requestedModelLabel = row.requestedModel || ''
739
- const displayModel = row.switched && requestedModelLabel && requestedModelLabel !== row.model
739
+ // πŸ“– Always show "requested β†’ actual" if they differ, not just when switched
740
+ const displayModel = requestedModelLabel && requestedModelLabel !== row.model
740
741
  ? `${requestedModelLabel} β†’ ${row.model}`
741
742
  : row.model
742
743
 
@@ -781,9 +782,11 @@ const profileStartIdx = updateRowIdx + 5
781
782
  const isFailedWithZeroTokens = row.status !== '200' && (!row.tokens || Number(row.tokens) === 0)
782
783
 
783
784
  const timeCell = chalk.dim(timeStr.slice(0, W_TIME).padEnd(W_TIME))
784
- // πŸ“– Color provider the same way as in the main table (row.provider is already the providerKey, e.g. "nvidia")
785
+ // πŸ“– Provider display: Use pretty label if available, otherwise raw key.
786
+ // πŸ“– All these logs are from FCM Proxy V2.
787
+ const providerLabel = PROVIDER_METADATA[row.provider]?.label || row.provider
785
788
  const providerRgb = PROVIDER_COLOR[row.provider] ?? [105, 190, 245]
786
- const provCell = chalk.bold.rgb(...providerRgb)(row.provider.slice(0, W_PROV).padEnd(W_PROV))
789
+ const provCell = chalk.bold.rgb(...providerRgb)(providerLabel.slice(0, W_PROV).padEnd(W_PROV))
787
790
 
788
791
  // πŸ“– Color model based on status - red for failed requests with zero tokens
789
792
  let modelCell
@@ -26,28 +26,70 @@
26
26
  * @see src/render-table.js
27
27
  */
28
28
 
29
+ import { readFileSync, existsSync } from 'node:fs'
30
+ import { join } from 'node:path'
31
+ import { homedir } from 'node:os'
29
32
  import { loadRecentLogs } from './log-reader.js'
30
33
 
34
+ const DEFAULT_DATA_DIR = join(homedir(), '.free-coding-models')
35
+ const STATS_FILE = join(DEFAULT_DATA_DIR, 'token-stats.json')
36
+
31
37
  // πŸ“– buildProviderModelTokenKey keeps provider-scoped totals isolated even when
32
38
  // πŸ“– multiple Origins expose the same model ID.
33
39
  export function buildProviderModelTokenKey(providerKey, modelId) {
34
40
  return `${providerKey}::${modelId}`
35
41
  }
36
42
 
37
- // πŸ“– loadTokenUsageByProviderModel reads the full bounded log history available
38
- // πŸ“– through log-reader and sums tokens per exact provider+model pair.
39
- export function loadTokenUsageByProviderModel({ logFile, limit = 50_000 } = {}) {
40
- const rows = loadRecentLogs({ logFile, limit })
43
+ // πŸ“– loadTokenUsageByProviderModel prioritizes token-stats.json for accurate
44
+ // πŸ“– historical totals. If missing, it falls back to parsing the bounded log history.
45
+ export function loadTokenUsageByProviderModel({ logFile, statsFile = STATS_FILE, limit = 50_000 } = {}) {
46
+ // πŸ“– If a custom logFile is provided (Test Mode), ONLY use that file.
47
+ if (logFile) {
48
+ const testTotals = {}
49
+ const rows = loadRecentLogs({ logFile, limit })
50
+ for (const row of rows) {
51
+ const key = buildProviderModelTokenKey(row.provider, row.model)
52
+ testTotals[key] = (testTotals[key] || 0) + (Number(row.tokens) || 0)
53
+ }
54
+ return testTotals
55
+ }
56
+
41
57
  const totals = {}
42
58
 
43
- for (const row of rows) {
44
- const providerKey = typeof row.provider === 'string' ? row.provider : 'unknown'
45
- const modelId = typeof row.model === 'string' ? row.model : 'unknown'
46
- const tokens = Number(row.tokens) || 0
47
- if (tokens <= 0) continue
59
+ // πŸ“– Phase 1: Try to load from the aggregated stats file (canonical source for totals)
60
+ try {
61
+ if (existsSync(statsFile)) {
62
+ const stats = JSON.parse(readFileSync(statsFile, 'utf8'))
63
+ // πŸ“– Aggregate byAccount entries (which use providerKey/slug/keyIdx as ID)
64
+ // πŸ“– into providerKey::modelId buckets.
65
+ if (stats.byAccount && typeof stats.byAccount === 'object') {
66
+ for (const [accountId, acct] of Object.entries(stats.byAccount)) {
67
+ const tokens = Number(acct.tokens) || 0
68
+ if (tokens <= 0) continue
69
+
70
+ // πŸ“– Extract providerKey and modelId from accountId (provider/model/index)
71
+ const parts = accountId.split('/')
72
+ if (parts.length >= 2) {
73
+ const providerKey = parts[0]
74
+ const modelId = parts[1]
75
+ const key = buildProviderModelTokenKey(providerKey, modelId)
76
+ totals[key] = (totals[key] || 0) + tokens
77
+ }
78
+ }
79
+ }
80
+ }
81
+ } catch (err) {
82
+ // πŸ“– Silently fall back to log parsing if stats file is corrupt or unreadable
83
+ }
48
84
 
49
- const key = buildProviderModelTokenKey(providerKey, modelId)
50
- totals[key] = (totals[key] || 0) + tokens
85
+ // πŸ“– Phase 2: Supplement with recent log entries if totals are still empty
86
+ // πŸ“– (e.g. fresh install or token-stats.json deleted)
87
+ if (Object.keys(totals).length === 0) {
88
+ const rows = loadRecentLogs({ limit })
89
+ for (const row of rows) {
90
+ const key = buildProviderModelTokenKey(row.provider, row.model)
91
+ totals[key] = (totals[key] || 0) + (Number(row.tokens) || 0)
92
+ }
51
93
  }
52
94
 
53
95
  return totals
package/src/utils.js CHANGED
@@ -414,10 +414,29 @@ export function parseArgs(argv) {
414
414
  ? profileIdx + 1
415
415
  : -1
416
416
 
417
+ // New value flags
418
+ const sortIdx = args.findIndex(a => a.toLowerCase() === '--sort')
419
+ const sortValueIdx = (sortIdx !== -1 && args[sortIdx + 1] && !args[sortIdx + 1].startsWith('--'))
420
+ ? sortIdx + 1
421
+ : -1
422
+
423
+ const originIdx = args.findIndex(a => a.toLowerCase() === '--origin')
424
+ const originValueIdx = (originIdx !== -1 && args[originIdx + 1] && !args[originIdx + 1].startsWith('--'))
425
+ ? originIdx + 1
426
+ : -1
427
+
428
+ const pingIntervalIdx = args.findIndex(a => a.toLowerCase() === '--ping-interval')
429
+ const pingIntervalValueIdx = (pingIntervalIdx !== -1 && args[pingIntervalIdx + 1] && !args[pingIntervalIdx + 1].startsWith('--'))
430
+ ? pingIntervalIdx + 1
431
+ : -1
432
+
417
433
  // πŸ“– Set of arg indices that are values for flags (not API keys)
418
434
  const skipIndices = new Set()
419
435
  if (tierValueIdx !== -1) skipIndices.add(tierValueIdx)
420
436
  if (profileValueIdx !== -1) skipIndices.add(profileValueIdx)
437
+ if (sortValueIdx !== -1) skipIndices.add(sortValueIdx)
438
+ if (originValueIdx !== -1) skipIndices.add(originValueIdx)
439
+ if (pingIntervalValueIdx !== -1) skipIndices.add(pingIntervalValueIdx)
421
440
 
422
441
  for (const [i, arg] of args.entries()) {
423
442
  if (arg.startsWith('--') || arg === '-h') {
@@ -448,8 +467,20 @@ export function parseArgs(argv) {
448
467
  const cleanProxyMode = flags.includes('--clean-proxy') || flags.includes('--proxy-clean')
449
468
  const jsonMode = flags.includes('--json')
450
469
  const helpMode = flags.includes('--help') || flags.includes('-h')
470
+ const premiumMode = flags.includes('--premium')
471
+
472
+ // New boolean flags
473
+ const sortDesc = flags.includes('--desc')
474
+ const sortAscFlag = flags.includes('--asc')
475
+ const hideUnconfigured = flags.includes('--hide-unconfigured')
476
+ const showUnconfigured = flags.includes('--show-unconfigured')
477
+ const disableWidthsWarning = flags.includes('--disable-widths-warning')
451
478
 
452
479
  let tierFilter = tierValueIdx !== -1 ? args[tierValueIdx].toUpperCase() : null
480
+ let sortColumn = sortValueIdx !== -1 ? args[sortValueIdx].toLowerCase() : null
481
+ let originFilter = originValueIdx !== -1 ? args[originValueIdx] : null
482
+ let pingInterval = pingIntervalValueIdx !== -1 ? parseInt(args[pingIntervalValueIdx], 10) : null
483
+ let sortDirection = sortDesc ? 'desc' : (sortAscFlag ? 'asc' : null)
453
484
 
454
485
  const profileName = profileValueIdx !== -1 ? args[profileValueIdx] : null
455
486
 
@@ -478,6 +509,14 @@ export function parseArgs(argv) {
478
509
  jsonMode,
479
510
  helpMode,
480
511
  tierFilter,
512
+ sortColumn,
513
+ sortDirection,
514
+ originFilter,
515
+ pingInterval,
516
+ hideUnconfigured,
517
+ showUnconfigured,
518
+ disableWidthsWarning,
519
+ premiumMode,
481
520
  profileName,
482
521
  recommendMode
483
522
  }