free-coding-models 0.3.5 → 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 +24 -0
- package/README.md +67 -1
- package/bin/free-coding-models.js +102 -64
- package/package.json +4 -2
- package/src/cli-help.js +8 -0
- package/src/config.js +332 -37
- package/src/endpoint-installer.js +2 -2
- package/src/favorites.js +31 -10
- package/src/key-handler.js +45 -24
- package/src/overlays.js +6 -3
- package/src/testfcm.js +451 -0
- package/src/token-usage-reader.js +53 -11
- package/src/utils.js +39 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
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
|
+
|
|
16
|
+
## 0.3.6
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **AI `/testfcm` workflow**: Added a repo-local PTY runner, workflow doc, slash-command prompts, and artifact/report directories so an agent can drive the real TUI, launch a tool, send `hi`, and write a Markdown bug report with evidence.
|
|
20
|
+
- **Mock tool verification path**: Added a tiny fake `crush` binary plus `test:fcm:mock` so maintainers can validate the TUI → launcher → prompt plumbing even when a real coding tool is not installed locally.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **`--json` startup crash**: JSON mode now reuses the same provider-aware ping function as the TUI without crashing on `pingModel is not a function`.
|
|
24
|
+
- **Managed endpoint installs no longer resurrect stale disk entries**: install/refresh saves now replace the tracked `endpointInstalls` snapshot so old provider-tool records from another config state do not leak back into the current catalog set.
|
|
25
|
+
- **Favorites persistence is now much harder to break**: favorite toggles now reload the latest disk config before saving, keep the active profile snapshot in sync, and use atomic config writes so pinned rows no longer disappear after unrelated saves or updates.
|
|
26
|
+
- **API key saves no longer clobber the rest of the config**: editing one provider now persists only that provider against the latest on-disk snapshot, preserves rotated extra keys, and stops stale config writes from wiping other saved keys.
|
|
27
|
+
- **Configured Only no longer hides favorites**: starred rows now stay visible and pinned at the top even when the provider has no currently configured key.
|
|
28
|
+
|
|
5
29
|
## 0.3.5
|
|
6
30
|
|
|
7
31
|
### Fixed
|
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ By Vanessa Depraute
|
|
|
84
84
|
- **📊 Token usage tracking** — The proxy logs prompt+completion token usage per exact provider/model pair, and the TUI surfaces that history in the `Used` column and the request log overlay.
|
|
85
85
|
- **📜 Request Log Overlay** — Press `X` to inspect recent proxied requests and token usage for exact provider/model pairs.
|
|
86
86
|
- **📋 Changelog Overlay** — Press `N` to browse all versions in an index, then `Enter` to view details for any version with full scroll support
|
|
87
|
+
- **🧪 AI end-to-end workflow** — Run the repo-local `/testfcm` flow to drive the TUI in a PTY, launch one tool, send `hi`, and generate a Markdown bug report plus raw artifacts under `task/`
|
|
87
88
|
- **🛠 MODEL_NOT_FOUND Rotation** — If a specific provider returns a 404 for a model, the TUI intelligently rotates through other available providers for the same model.
|
|
88
89
|
- **🔄 Auto-retry** — Timeout models keep getting retried, nothing is ever "given up on"
|
|
89
90
|
- **🎮 Interactive selection** — Navigate with arrow keys directly in the table, press Enter to act
|
|
@@ -199,6 +200,30 @@ free-coding-models
|
|
|
199
200
|
# Explicitly target OpenCode CLI (TUI + Enter launches OpenCode CLI)
|
|
200
201
|
free-coding-models --opencode
|
|
201
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`).
|
|
202
227
|
# Explicitly target OpenCode Desktop (TUI + Enter sets model & opens Desktop app)
|
|
203
228
|
free-coding-models --opencode-desktop
|
|
204
229
|
|
|
@@ -235,6 +260,26 @@ free-coding-models --opencode --best
|
|
|
235
260
|
free-coding-models --tier S --json
|
|
236
261
|
```
|
|
237
262
|
|
|
263
|
+
### AI E2E workflow (`/testfcm`)
|
|
264
|
+
|
|
265
|
+
For repo-level validation, this project now ships a repeatable AI-driven manual test flow:
|
|
266
|
+
|
|
267
|
+
- Preferred: `pnpm test:fcm -- --tool crush`
|
|
268
|
+
- Fallback when `pnpm` is unavailable: `npm run test:fcm -- --tool crush`
|
|
269
|
+
- Mock plumbing check: `pnpm test:fcm:mock`
|
|
270
|
+
|
|
271
|
+
What it does:
|
|
272
|
+
|
|
273
|
+
1. Copies your current `~/.free-coding-models.json` into an isolated HOME
|
|
274
|
+
2. Runs a `--json` preflight to catch obvious startup regressions
|
|
275
|
+
3. Starts the real TUI in a PTY via the system `expect` command
|
|
276
|
+
4. Presses `Enter` like a user to launch the chosen tool
|
|
277
|
+
5. Sends `hi`
|
|
278
|
+
6. Captures the response, `request-log.jsonl`, daemon logs, and generated tool config
|
|
279
|
+
7. Writes a Markdown report to `task/reports/` and raw artifacts to `task/artifacts/`
|
|
280
|
+
|
|
281
|
+
The command workflow is documented in [task/TESTFCM-WORKFLOW.md](task/TESTFCM-WORKFLOW.md). Project-local slash commands are also included at [.claude/commands/testfcm.md](.claude/commands/testfcm.md) and [.crush/commands/testfcm.md](.crush/commands/testfcm.md).
|
|
282
|
+
|
|
238
283
|
### Choosing the target tool
|
|
239
284
|
|
|
240
285
|
Running `free-coding-models` with no launcher flag starts in **OpenCode CLI** mode.
|
|
@@ -318,7 +363,8 @@ Press **`P`** to open the Settings screen at any time:
|
|
|
318
363
|
|
|
319
364
|
Manual update is in the same Settings screen (`P`) under **Maintenance** (Enter to check, Enter again to install when an update is available).
|
|
320
365
|
When a newer npm release is known, the main footer also adds a full-width red warning line with the manual recovery command `npm install -g free-coding-models@latest`.
|
|
321
|
-
Favorites are also persisted in the same config file and survive restarts.
|
|
366
|
+
Favorites are also persisted in the same config file and survive restarts, app relaunches, and package updates.
|
|
367
|
+
Favorite rows stay pinned at the top and remain visible even when `Configured Only` mode is enabled.
|
|
322
368
|
The main table now starts in `Configured Only` mode, so if nothing is set up yet you can press `P` and add your first API key immediately.
|
|
323
369
|
|
|
324
370
|
### Environment variable overrides
|
|
@@ -898,6 +944,25 @@ This script:
|
|
|
898
944
|
|
|
899
945
|
## 📋 API Reference
|
|
900
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
|
+
|
|
901
966
|
**Environment variables (override config file):**
|
|
902
967
|
|
|
903
968
|
| Variable | Description |
|
|
@@ -1073,6 +1138,7 @@ Profiles let you save and restore different TUI configurations — useful if you
|
|
|
1073
1138
|
**Managing profiles:**
|
|
1074
1139
|
- Open Settings (**P** key) — scroll down to the **Profiles** section
|
|
1075
1140
|
- **Enter** on a profile row to load it
|
|
1141
|
+
- While a profile is active, edits to favorites and API keys update that active profile immediately
|
|
1076
1142
|
- **Backspace** on a profile row to delete it
|
|
1077
1143
|
|
|
1078
1144
|
Profiles are stored inside `~/.free-coding-models.json` under the `profiles` key.
|
|
@@ -99,7 +99,7 @@ import { homedir } from 'os'
|
|
|
99
99
|
import { join, dirname } from 'path'
|
|
100
100
|
import { MODELS, sources } from '../sources.js'
|
|
101
101
|
import { getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore, sortResults, filterByTier, findBestModel, parseArgs, TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, scoreModelForTask, getTopRecommendations, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS, formatCtxWindow, labelFromId, getProxyStatusInfo, formatResultsAsJSON } from '../src/utils.js'
|
|
102
|
-
import { loadConfig, saveConfig, getApiKey, getProxySettings, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled, saveAsProfile, loadProfile, listProfiles, deleteProfile, getActiveProfileName, setActiveProfile, _emptyProfileSettings } from '../src/config.js'
|
|
102
|
+
import { loadConfig, saveConfig, getApiKey, getProxySettings, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled, saveAsProfile, loadProfile, listProfiles, deleteProfile, getActiveProfileName, setActiveProfile, _emptyProfileSettings, persistApiKeysForProvider } from '../src/config.js'
|
|
103
103
|
import { buildMergedModels } from '../src/model-merger.js'
|
|
104
104
|
import { ProxyServer } from '../src/proxy-server.js'
|
|
105
105
|
import { loadOpenCodeConfig, saveOpenCodeConfig, syncToOpenCode, restoreOpenCodeBackup, cleanupOpenCodeProxyConfig } from '../src/opencode-sync.js'
|
|
@@ -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()
|
|
@@ -307,7 +323,10 @@ async function main() {
|
|
|
307
323
|
console.error(chalk.red(` Unknown profile "${cliArgs.profileName}". Available: ${listProfiles(config).join(', ') || '(none)'}`))
|
|
308
324
|
process.exit(1)
|
|
309
325
|
}
|
|
310
|
-
saveConfig(config
|
|
326
|
+
saveConfig(config, {
|
|
327
|
+
replaceApiKeys: true,
|
|
328
|
+
replaceFavorites: true,
|
|
329
|
+
})
|
|
311
330
|
}
|
|
312
331
|
|
|
313
332
|
// 📖 Check if any provider has a key — if not, run the first-time setup wizard
|
|
@@ -496,8 +515,9 @@ async function main() {
|
|
|
496
515
|
mode, // 📖 'opencode' or 'openclaw' — controls Enter action
|
|
497
516
|
tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
|
|
498
517
|
originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
|
|
499
|
-
|
|
500
|
-
|
|
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)
|
|
501
521
|
scrollOffset: 0, // 📖 First visible model index in viewport
|
|
502
522
|
terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
|
|
503
523
|
terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
|
|
@@ -674,6 +694,52 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
|
|
|
674
694
|
}
|
|
675
695
|
}
|
|
676
696
|
|
|
697
|
+
// 📖 Define pingModel before JSON mode so `--json` can reuse the same provider-aware
|
|
698
|
+
// 📖 ping path as the interactive TUI without waiting for the PTY/render loop setup.
|
|
699
|
+
pingModel = async (r) => {
|
|
700
|
+
state.pendingPings += 1
|
|
701
|
+
r.isPinging = true
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
const providerApiKey = getApiKey(state.config, r.providerKey) ?? null
|
|
705
|
+
const providerUrl = sources[r.providerKey]?.url ?? sources.nvidia.url
|
|
706
|
+
let { code, ms, quotaPercent } = await ping(providerApiKey, r.modelId, r.providerKey, providerUrl)
|
|
707
|
+
|
|
708
|
+
if ((quotaPercent === null || quotaPercent === undefined) && providerApiKey) {
|
|
709
|
+
const providerQuota = await getProviderQuotaPercentCached(r.providerKey, providerApiKey)
|
|
710
|
+
if (typeof providerQuota === 'number' && Number.isFinite(providerQuota)) {
|
|
711
|
+
quotaPercent = providerQuota
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
r.pings.push({ ms, code })
|
|
716
|
+
|
|
717
|
+
if (code === '200') {
|
|
718
|
+
r.status = 'up'
|
|
719
|
+
} else if (code === '000') {
|
|
720
|
+
r.status = 'timeout'
|
|
721
|
+
} else if (code === '401' || code === '403') {
|
|
722
|
+
r.status = providerApiKey ? 'auth_error' : 'noauth'
|
|
723
|
+
r.httpCode = code
|
|
724
|
+
} else {
|
|
725
|
+
r.status = 'down'
|
|
726
|
+
r.httpCode = code
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (typeof quotaPercent === 'number' && Number.isFinite(quotaPercent)) {
|
|
730
|
+
r.usagePercent = quotaPercent
|
|
731
|
+
for (const sibling of state.results) {
|
|
732
|
+
if (sibling.providerKey === r.providerKey && (sibling.usagePercent === undefined || sibling.usagePercent === null)) {
|
|
733
|
+
sibling.usagePercent = quotaPercent
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
} finally {
|
|
738
|
+
r.isPinging = false
|
|
739
|
+
state.pendingPings = Math.max(0, state.pendingPings - 1)
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
677
743
|
// 📖 JSON output mode: skip TUI, output results as JSON after initial pings
|
|
678
744
|
if (cliArgs.jsonMode) {
|
|
679
745
|
console.log(chalk.cyan(' ⚡ Pinging models for JSON output...'))
|
|
@@ -707,6 +773,17 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
|
|
|
707
773
|
outputResults = outputResults.filter(r => ['S+', 'S', 'A+'].includes(r.tier))
|
|
708
774
|
}
|
|
709
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
|
+
|
|
710
787
|
// 📖 Sort by avg ping (ascending)
|
|
711
788
|
outputResults = sortResults(outputResults, 'avg', 'asc')
|
|
712
789
|
|
|
@@ -745,20 +822,34 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
|
|
|
745
822
|
const activeTier = TIER_CYCLE[state.tierFilterMode]
|
|
746
823
|
const activeOrigin = ORIGIN_CYCLE[state.originFilterMode]
|
|
747
824
|
state.results.forEach(r => {
|
|
825
|
+
// 📖 Favorites stay visible and pinned regardless of configured-only, tier, or provider filters.
|
|
826
|
+
if (r.isFavorite) {
|
|
827
|
+
r.hidden = false
|
|
828
|
+
return
|
|
829
|
+
}
|
|
748
830
|
const unconfiguredHide = state.hideUnconfiguredModels && !getApiKey(state.config, r.providerKey)
|
|
749
831
|
if (unconfiguredHide) {
|
|
750
832
|
r.hidden = true
|
|
751
833
|
return
|
|
752
834
|
}
|
|
753
|
-
// 📖 Favorites stay visible regardless of tier/origin filters.
|
|
754
|
-
if (r.isFavorite) {
|
|
755
|
-
r.hidden = false
|
|
756
|
-
return
|
|
757
|
-
}
|
|
758
835
|
// 📖 Apply both tier and origin filters — model is hidden if it fails either
|
|
759
|
-
|
|
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)
|
|
760
839
|
const originHide = activeOrigin !== null && r.providerKey !== activeOrigin
|
|
761
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
|
+
}
|
|
762
853
|
})
|
|
763
854
|
return state.results
|
|
764
855
|
}
|
|
@@ -826,6 +917,7 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
|
|
|
826
917
|
resolveApiKeys,
|
|
827
918
|
addApiKey,
|
|
828
919
|
removeApiKey,
|
|
920
|
+
persistApiKeysForProvider,
|
|
829
921
|
isProviderEnabled,
|
|
830
922
|
listProfiles,
|
|
831
923
|
loadProfile,
|
|
@@ -954,60 +1046,6 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
|
|
|
954
1046
|
|
|
955
1047
|
// ── Continuous ping loop — ping all models every N seconds forever ──────────
|
|
956
1048
|
|
|
957
|
-
// 📖 Single ping function that updates result
|
|
958
|
-
// 📖 Uses per-provider API key and URL from sources.js
|
|
959
|
-
// 📖 If no API key is configured, pings without auth — a 401 still tells us latency + server is up
|
|
960
|
-
pingModel = async (r) => {
|
|
961
|
-
state.pendingPings += 1
|
|
962
|
-
r.isPinging = true
|
|
963
|
-
|
|
964
|
-
try {
|
|
965
|
-
const providerApiKey = getApiKey(state.config, r.providerKey) ?? null
|
|
966
|
-
const providerUrl = sources[r.providerKey]?.url ?? sources.nvidia.url
|
|
967
|
-
let { code, ms, quotaPercent } = await ping(providerApiKey, r.modelId, r.providerKey, providerUrl)
|
|
968
|
-
|
|
969
|
-
if ((quotaPercent === null || quotaPercent === undefined) && providerApiKey) {
|
|
970
|
-
const providerQuota = await getProviderQuotaPercentCached(r.providerKey, providerApiKey)
|
|
971
|
-
if (typeof providerQuota === 'number' && Number.isFinite(providerQuota)) {
|
|
972
|
-
quotaPercent = providerQuota
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// 📖 Store ping result as object with ms and code
|
|
977
|
-
// 📖 ms = actual response time (even for errors like 429)
|
|
978
|
-
// 📖 code = HTTP status code ('200', '429', '500', '000' for timeout)
|
|
979
|
-
r.pings.push({ ms, code })
|
|
980
|
-
|
|
981
|
-
// 📖 Update status based on latest ping
|
|
982
|
-
if (code === '200') {
|
|
983
|
-
r.status = 'up'
|
|
984
|
-
} else if (code === '000') {
|
|
985
|
-
r.status = 'timeout'
|
|
986
|
-
} else if (code === '401' || code === '403') {
|
|
987
|
-
// 📖 Distinguish "no key configured" from "configured key rejected" so the
|
|
988
|
-
// 📖 Health column stays honest when Configured Only mode is enabled.
|
|
989
|
-
r.status = providerApiKey ? 'auth_error' : 'noauth'
|
|
990
|
-
r.httpCode = code
|
|
991
|
-
} else {
|
|
992
|
-
r.status = 'down'
|
|
993
|
-
r.httpCode = code
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (typeof quotaPercent === 'number' && Number.isFinite(quotaPercent)) {
|
|
997
|
-
r.usagePercent = quotaPercent
|
|
998
|
-
// Provider-level fallback: apply latest known quota to sibling rows on same provider.
|
|
999
|
-
for (const sibling of state.results) {
|
|
1000
|
-
if (sibling.providerKey === r.providerKey && (sibling.usagePercent === undefined || sibling.usagePercent === null)) {
|
|
1001
|
-
sibling.usagePercent = quotaPercent
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
} finally {
|
|
1006
|
-
r.isPinging = false
|
|
1007
|
-
state.pendingPings = Math.max(0, state.pendingPings - 1)
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
1049
|
// 📖 Initial ping of all models
|
|
1012
1050
|
const initialPing = Promise.all(state.results.map(r => pingModel(r)))
|
|
1013
1051
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
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",
|
|
@@ -51,7 +51,9 @@
|
|
|
51
51
|
],
|
|
52
52
|
"scripts": {
|
|
53
53
|
"start": "node bin/free-coding-models.js",
|
|
54
|
-
"test": "node --test test/test.js"
|
|
54
|
+
"test": "node --test test/test.js",
|
|
55
|
+
"test:fcm": "node scripts/testfcm-runner.mjs",
|
|
56
|
+
"test:fcm:mock": "node scripts/testfcm-runner.mjs --tool crush --tool-bin-dir test/fixtures/mock-bin"
|
|
55
57
|
},
|
|
56
58
|
"dependencies": {
|
|
57
59
|
"chalk": "^5.4.1"
|
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 = [
|