free-coding-models 0.3.4 → 0.3.6
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 +19 -0
- package/README.md +28 -6
- package/bin/free-coding-models.js +57 -61
- package/package.json +4 -2
- 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/proxy-server.js +27 -10
- package/src/testfcm.js +451 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 0.3.6
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **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.
|
|
9
|
+
- **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.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **`--json` startup crash**: JSON mode now reuses the same provider-aware ping function as the TUI without crashing on `pingModel is not a function`.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
18
|
+
## 0.3.5
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Claude Code beta-route compatibility**: FCM Proxy V2 now matches routes on the URL pathname, so Anthropic requests like `/v1/messages?beta=true` and `/v1/messages/count_tokens?beta=true` resolve correctly instead of failing with a fake “selected model may not exist” error.
|
|
22
|
+
- **Claude proxy parity with `free-claude-code`**: The Claude integration was revalidated against the real `claude` binary, and the proxy-side Claude alias mapping now reaches the upstream provider again in the exact `free-claude-code` style flow.
|
|
23
|
+
|
|
5
24
|
## 0.3.4
|
|
6
25
|
|
|
7
26
|
### Added
|
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
|
|
@@ -182,12 +183,11 @@ bunx free-coding-models YOUR_API_KEY
|
|
|
182
183
|
|
|
183
184
|
### 🆕 What's New
|
|
184
185
|
|
|
185
|
-
**Version 0.3.
|
|
186
|
+
**Version 0.3.5 fixes the main Claude Code proxy compatibility bug found in real-world use:**
|
|
186
187
|
|
|
187
|
-
- **
|
|
188
|
-
-
|
|
189
|
-
- **The
|
|
190
|
-
- **Malformed config sections are normalized safely on load** — corrupted `apiKeys`, `providers`, or `settings` values no longer leak through as broken runtime objects.
|
|
188
|
+
- **Claude Code beta-route requests now work** — the proxy accepts Anthropic URLs like `/v1/messages?beta=true` and `/v1/messages/count_tokens?beta=true`, which is how recent Claude Code builds really call the API.
|
|
189
|
+
- **Claude proxy flow now behaves like `free-claude-code` on the routing layer** — fake Claude model ids still map proxy-side to the selected free backend model, but the route matcher no longer breaks before that mapping can run.
|
|
190
|
+
- **The fix was validated against the real `claude` binary** — not just unit tests. The exact failure `selected model (claude-sonnet-4-6) may not exist` is now gone in local end-to-end repro.
|
|
191
191
|
|
|
192
192
|
---
|
|
193
193
|
|
|
@@ -236,6 +236,26 @@ free-coding-models --opencode --best
|
|
|
236
236
|
free-coding-models --tier S --json
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
+
### AI E2E workflow (`/testfcm`)
|
|
240
|
+
|
|
241
|
+
For repo-level validation, this project now ships a repeatable AI-driven manual test flow:
|
|
242
|
+
|
|
243
|
+
- Preferred: `pnpm test:fcm -- --tool crush`
|
|
244
|
+
- Fallback when `pnpm` is unavailable: `npm run test:fcm -- --tool crush`
|
|
245
|
+
- Mock plumbing check: `pnpm test:fcm:mock`
|
|
246
|
+
|
|
247
|
+
What it does:
|
|
248
|
+
|
|
249
|
+
1. Copies your current `~/.free-coding-models.json` into an isolated HOME
|
|
250
|
+
2. Runs a `--json` preflight to catch obvious startup regressions
|
|
251
|
+
3. Starts the real TUI in a PTY via the system `expect` command
|
|
252
|
+
4. Presses `Enter` like a user to launch the chosen tool
|
|
253
|
+
5. Sends `hi`
|
|
254
|
+
6. Captures the response, `request-log.jsonl`, daemon logs, and generated tool config
|
|
255
|
+
7. Writes a Markdown report to `task/reports/` and raw artifacts to `task/artifacts/`
|
|
256
|
+
|
|
257
|
+
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).
|
|
258
|
+
|
|
239
259
|
### Choosing the target tool
|
|
240
260
|
|
|
241
261
|
Running `free-coding-models` with no launcher flag starts in **OpenCode CLI** mode.
|
|
@@ -319,7 +339,8 @@ Press **`P`** to open the Settings screen at any time:
|
|
|
319
339
|
|
|
320
340
|
Manual update is in the same Settings screen (`P`) under **Maintenance** (Enter to check, Enter again to install when an update is available).
|
|
321
341
|
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`.
|
|
322
|
-
Favorites are also persisted in the same config file and survive restarts.
|
|
342
|
+
Favorites are also persisted in the same config file and survive restarts, app relaunches, and package updates.
|
|
343
|
+
Favorite rows stay pinned at the top and remain visible even when `Configured Only` mode is enabled.
|
|
323
344
|
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.
|
|
324
345
|
|
|
325
346
|
### Environment variable overrides
|
|
@@ -1074,6 +1095,7 @@ Profiles let you save and restore different TUI configurations — useful if you
|
|
|
1074
1095
|
**Managing profiles:**
|
|
1075
1096
|
- Open Settings (**P** key) — scroll down to the **Profiles** section
|
|
1076
1097
|
- **Enter** on a profile row to load it
|
|
1098
|
+
- While a profile is active, edits to favorites and API keys update that active profile immediately
|
|
1077
1099
|
- **Backspace** on a profile row to delete it
|
|
1078
1100
|
|
|
1079
1101
|
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'
|
|
@@ -307,7 +307,10 @@ async function main() {
|
|
|
307
307
|
console.error(chalk.red(` Unknown profile "${cliArgs.profileName}". Available: ${listProfiles(config).join(', ') || '(none)'}`))
|
|
308
308
|
process.exit(1)
|
|
309
309
|
}
|
|
310
|
-
saveConfig(config
|
|
310
|
+
saveConfig(config, {
|
|
311
|
+
replaceApiKeys: true,
|
|
312
|
+
replaceFavorites: true,
|
|
313
|
+
})
|
|
311
314
|
}
|
|
312
315
|
|
|
313
316
|
// 📖 Check if any provider has a key — if not, run the first-time setup wizard
|
|
@@ -674,6 +677,52 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
|
|
|
674
677
|
}
|
|
675
678
|
}
|
|
676
679
|
|
|
680
|
+
// 📖 Define pingModel before JSON mode so `--json` can reuse the same provider-aware
|
|
681
|
+
// 📖 ping path as the interactive TUI without waiting for the PTY/render loop setup.
|
|
682
|
+
pingModel = async (r) => {
|
|
683
|
+
state.pendingPings += 1
|
|
684
|
+
r.isPinging = true
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
const providerApiKey = getApiKey(state.config, r.providerKey) ?? null
|
|
688
|
+
const providerUrl = sources[r.providerKey]?.url ?? sources.nvidia.url
|
|
689
|
+
let { code, ms, quotaPercent } = await ping(providerApiKey, r.modelId, r.providerKey, providerUrl)
|
|
690
|
+
|
|
691
|
+
if ((quotaPercent === null || quotaPercent === undefined) && providerApiKey) {
|
|
692
|
+
const providerQuota = await getProviderQuotaPercentCached(r.providerKey, providerApiKey)
|
|
693
|
+
if (typeof providerQuota === 'number' && Number.isFinite(providerQuota)) {
|
|
694
|
+
quotaPercent = providerQuota
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
r.pings.push({ ms, code })
|
|
699
|
+
|
|
700
|
+
if (code === '200') {
|
|
701
|
+
r.status = 'up'
|
|
702
|
+
} else if (code === '000') {
|
|
703
|
+
r.status = 'timeout'
|
|
704
|
+
} else if (code === '401' || code === '403') {
|
|
705
|
+
r.status = providerApiKey ? 'auth_error' : 'noauth'
|
|
706
|
+
r.httpCode = code
|
|
707
|
+
} else {
|
|
708
|
+
r.status = 'down'
|
|
709
|
+
r.httpCode = code
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (typeof quotaPercent === 'number' && Number.isFinite(quotaPercent)) {
|
|
713
|
+
r.usagePercent = quotaPercent
|
|
714
|
+
for (const sibling of state.results) {
|
|
715
|
+
if (sibling.providerKey === r.providerKey && (sibling.usagePercent === undefined || sibling.usagePercent === null)) {
|
|
716
|
+
sibling.usagePercent = quotaPercent
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
} finally {
|
|
721
|
+
r.isPinging = false
|
|
722
|
+
state.pendingPings = Math.max(0, state.pendingPings - 1)
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
677
726
|
// 📖 JSON output mode: skip TUI, output results as JSON after initial pings
|
|
678
727
|
if (cliArgs.jsonMode) {
|
|
679
728
|
console.log(chalk.cyan(' ⚡ Pinging models for JSON output...'))
|
|
@@ -745,16 +794,16 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
|
|
|
745
794
|
const activeTier = TIER_CYCLE[state.tierFilterMode]
|
|
746
795
|
const activeOrigin = ORIGIN_CYCLE[state.originFilterMode]
|
|
747
796
|
state.results.forEach(r => {
|
|
797
|
+
// 📖 Favorites stay visible and pinned regardless of configured-only, tier, or provider filters.
|
|
798
|
+
if (r.isFavorite) {
|
|
799
|
+
r.hidden = false
|
|
800
|
+
return
|
|
801
|
+
}
|
|
748
802
|
const unconfiguredHide = state.hideUnconfiguredModels && !getApiKey(state.config, r.providerKey)
|
|
749
803
|
if (unconfiguredHide) {
|
|
750
804
|
r.hidden = true
|
|
751
805
|
return
|
|
752
806
|
}
|
|
753
|
-
// 📖 Favorites stay visible regardless of tier/origin filters.
|
|
754
|
-
if (r.isFavorite) {
|
|
755
|
-
r.hidden = false
|
|
756
|
-
return
|
|
757
|
-
}
|
|
758
807
|
// 📖 Apply both tier and origin filters — model is hidden if it fails either
|
|
759
808
|
const tierHide = activeTier !== null && r.tier !== activeTier
|
|
760
809
|
const originHide = activeOrigin !== null && r.providerKey !== activeOrigin
|
|
@@ -826,6 +875,7 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
|
|
|
826
875
|
resolveApiKeys,
|
|
827
876
|
addApiKey,
|
|
828
877
|
removeApiKey,
|
|
878
|
+
persistApiKeysForProvider,
|
|
829
879
|
isProviderEnabled,
|
|
830
880
|
listProfiles,
|
|
831
881
|
loadProfile,
|
|
@@ -954,60 +1004,6 @@ hideUnconfiguredModels: startupProfileSettings?.hideUnconfiguredModels === true
|
|
|
954
1004
|
|
|
955
1005
|
// ── Continuous ping loop — ping all models every N seconds forever ──────────
|
|
956
1006
|
|
|
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
1007
|
// 📖 Initial ping of all models
|
|
1012
1008
|
const initialPing = Promise.all(state.results.map(r => pingModel(r)))
|
|
1013
1009
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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"
|