free-coding-models 0.3.0 → 0.3.2
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 +29 -20
- package/bin/free-coding-models.js +50 -19
- package/package.json +1 -1
- package/src/anthropic-translator.js +78 -8
- package/src/cli-help.js +108 -0
- package/src/config.js +2 -1
- package/src/endpoint-installer.js +5 -4
- package/src/key-handler.js +31 -34
- package/src/opencode.js +17 -12
- package/src/overlays.js +40 -53
- package/src/proxy-server.js +335 -12
- package/src/proxy-sync.js +16 -4
- package/src/render-helpers.js +4 -2
- package/src/render-table.js +34 -36
- package/src/responses-translator.js +423 -0
- package/src/tool-launchers.js +246 -19
- package/src/utils.js +31 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 0.3.2
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Claude Code model-family routing now mirrors `free-claude-code`**: The proxy remaps Claude's internal model ids like `claude-3-5-sonnet-*`, `claude-3-haiku-*`, `claude-3-opus-*`, `sonnet`, `haiku`, and `default` back to the selected FCM proxy model instead of rejecting them as missing.
|
|
9
|
+
- **Claude Code helper/background requests stay on the selected model**: Launches now pin the Anthropic helper model env vars and encode the selected proxy model inside `ANTHROPIC_AUTH_TOKEN`, so Claude Code has a stable fallback even when it emits internal aliases.
|
|
10
|
+
|
|
11
|
+
## 0.3.1
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **CLI `--help` output**: `free-coding-models --help` now prints the full launcher, analysis, config, and daemon command matrix in a non-interactive format.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Outdated-version footer alert**: The main TUI now shows a full-width red footer line with manual `npm install -g free-coding-models@latest` recovery instructions, but only when a newer npm version is actually known.
|
|
18
|
+
- **Claude Code proxy auth conflict**: Proxy launches now sanitize inherited env vars and use only `ANTHROPIC_AUTH_TOKEN` + `ANTHROPIC_BASE_URL`, matching the `free-claude-code` contract instead of mixing Anthropic auth modes.
|
|
19
|
+
- **Codex CLI proxy routing**: Codex launches now force an explicit custom provider config and the proxy now supports `POST /v1/responses`, so `codex-cli 0.114.0` no longer depends on the broken built-in OAuth/base-url path.
|
|
20
|
+
- **Anthropic token counting**: Added `POST /v1/messages/count_tokens` with a fast local estimate so Claude-compatible clients keep their budgeting flow through FCM Proxy V2.
|
|
21
|
+
- **Gemini proxy failure mode**: Gemini launch now preflights the installed CLI/config, blocks incompatible builds like `0.33.0`, and surfaces `~/.gemini/settings.json` schema errors instead of pretending proxy mode works.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **Proxy auto-sync now follows the current tool**: The proxy overlay no longer asks for a separate active tool; cleanup and auto-sync now target the current `Z` mode whenever that tool supports persisted proxy config.
|
|
25
|
+
- **Install Endpoints (`Y`) is narrower on purpose**: `Claude Code`, `Codex`, and `Gemini` were removed from the install-target menu so the flow only lists tools with a stable persisted-config contract.
|
|
26
|
+
- **Proxy model listing is more Codex-friendly**: `GET /v1/models` now returns both the usual OpenAI `data` array and a `models` array with `slug` fields for clients that expect a richer catalog shape.
|
|
27
|
+
- **Launcher diagnostics now mention the beta state clearly**: Proxy-backed external tools now remind users that the integration is still stabilizing when a launch is blocked.
|
|
28
|
+
|
|
5
29
|
## 0.3.0
|
|
6
30
|
|
|
7
31
|
### Added
|
package/README.md
CHANGED
|
@@ -46,6 +46,9 @@ By Vanessa Depraute
|
|
|
46
46
|
<sub>Ping free coding models from 20 providers in real-time — pick the best one for OpenCode, OpenClaw, or any AI coding assistant</sub>
|
|
47
47
|
</p>
|
|
48
48
|
|
|
49
|
+
> ⚠️ **Beta notice**
|
|
50
|
+
> FCM Proxy V2 support for external tools is still in beta. Claude Code, Codex, Gemini, and the other proxy-backed launchers already work in many setups, but auth and startup edge cases can still fail while the integration stabilizes.
|
|
51
|
+
|
|
49
52
|
<p align="center">
|
|
50
53
|
<img src="demo.gif" alt="free-coding-models demo" width="100%">
|
|
51
54
|
</p>
|
|
@@ -87,7 +90,7 @@ By Vanessa Depraute
|
|
|
87
90
|
- **💻 OpenCode integration** — Auto-detects NIM setup, sets model as default, launches OpenCode
|
|
88
91
|
- **🦞 OpenClaw integration** — Sets selected model as default provider in `~/.openclaw/openclaw.json`
|
|
89
92
|
- **🧰 Public tool launchers** — `Enter` auto-configures and launches 10+ tools: `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, `Goose`, `Aider`, `Claude Code`, `Codex`, `Gemini`, `Qwen`, `OpenHands`, `Amp`, and `Pi`. All tools auto-select the chosen model on launch.
|
|
90
|
-
- **🔌 Install Endpoints flow** — Press `Y` to install one configured provider into
|
|
93
|
+
- **🔌 Install Endpoints flow** — Press `Y` to install one configured provider into the compatible persisted-config tools, with a choice between **Direct Provider** (pure API) or **FCM Proxy V2** (key rotation + usage tracking), then pick all models or a curated subset
|
|
91
94
|
- **📝 Feature Request (J key)** — Send anonymous feedback directly to the project team
|
|
92
95
|
- **🐛 Bug Report (I key)** — Send anonymous bug reports directly to the project team
|
|
93
96
|
- **🎨 Clean output** — Zero scrollback pollution, interface stays open until Ctrl+C
|
|
@@ -179,15 +182,12 @@ bunx free-coding-models YOUR_API_KEY
|
|
|
179
182
|
|
|
180
183
|
### 🆕 What's New
|
|
181
184
|
|
|
182
|
-
**Version 0.2
|
|
183
|
-
|
|
184
|
-
- **`--json` flag** — Output model results as JSON for scripting, CI/CD pipelines, and monitoring dashboards. Perfect for automation: `free-coding-models --tier S --json | jq '.[0].modelId'`
|
|
185
|
-
|
|
186
|
-
- **Persistent ping cache** — Results are cached for 5 minutes between runs. Startup is nearly instant when cache is fresh, and you save API rate limits. Cache file: `~/.free-coding-models.cache.json`
|
|
187
|
-
|
|
188
|
-
- **Config security check** — Automatically warns if your API key config file has insecure permissions and offers one-click auto-fix with `chmod 600`
|
|
185
|
+
**Version 0.3.2 hardens the Claude Code proxy path to match the routing strategy that works in `free-claude-code`:**
|
|
189
186
|
|
|
190
|
-
- **
|
|
187
|
+
- **Claude Code family-model routing is now proxy-side** — FCM remaps Claude's internal family ids such as `claude-3-5-sonnet-*`, `claude-3-haiku-*`, `claude-3-opus-*`, `sonnet`, `haiku`, and `default` back to the selected FCM proxy model.
|
|
188
|
+
- **Claude Code helper model slots are pinned** — FCM now exports the selected model into `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`, and `CLAUDE_CODE_SUBAGENT_MODEL` so background/helper requests stop drifting.
|
|
189
|
+
- **Claude proxy auth now carries the selected model hint** — The launcher encodes the chosen proxy model into `ANTHROPIC_AUTH_TOKEN`, giving the proxy a reliable fallback even when Claude Code ignores the visible `/model` selection.
|
|
190
|
+
- **Proxy support remains beta** — External-tool proxy support is still stabilizing, but Claude Code should now behave much closer to the working `free-claude-code` setup.
|
|
191
191
|
|
|
192
192
|
---
|
|
193
193
|
|
|
@@ -216,11 +216,14 @@ free-coding-models --best
|
|
|
216
216
|
# Analyze for 10 seconds and output the most reliable model
|
|
217
217
|
free-coding-models --fiable
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
# Output results as JSON (for scripting/automation)
|
|
220
220
|
free-coding-models --json
|
|
221
221
|
free-coding-models --tier S --json | jq '.[0].modelId' # Get fastest S-tier model ID
|
|
222
222
|
free-coding-models --json | jq '.[] | select(.avgPing < 500)' # Filter by latency
|
|
223
223
|
|
|
224
|
+
# Print the complete CLI help with every supported flag and daemon command
|
|
225
|
+
free-coding-models --help
|
|
226
|
+
|
|
224
227
|
# Filter models by tier letter
|
|
225
228
|
free-coding-models --tier S # S+ and S only
|
|
226
229
|
free-coding-models --tier A # A+, A, A- only
|
|
@@ -315,6 +318,7 @@ Press **`P`** to open the Settings screen at any time:
|
|
|
315
318
|
Keys are saved to `~/.free-coding-models.json` (permissions `0600`).
|
|
316
319
|
|
|
317
320
|
Manual update is in the same Settings screen (`P`) under **Maintenance** (Enter to check, Enter again to install when an update is available).
|
|
321
|
+
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`.
|
|
318
322
|
Favorites are also persisted in the same config file and survive restarts.
|
|
319
323
|
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.
|
|
320
324
|
|
|
@@ -601,9 +605,9 @@ free-coding-models daemon logs # Show recent service logs
|
|
|
601
605
|
|
|
602
606
|
The dedicated **FCM Proxy V2** overlay (accessible via `J` from main TUI, or Settings → Enter) provides full control:
|
|
603
607
|
|
|
604
|
-
- **
|
|
605
|
-
- **Auto-sync toggle** — Automatically write the `fcm-proxy` provider to the
|
|
606
|
-
- **Cleanup** — Remove `fcm-proxy` entries from the
|
|
608
|
+
- **Current tool hint** — Shows which Z-selected tool will receive persisted proxy config (when that mode supports it)
|
|
609
|
+
- **Auto-sync toggle** — Automatically write the `fcm-proxy` provider to the current tool's config when the proxy starts
|
|
610
|
+
- **Cleanup** — Remove `fcm-proxy` entries from the current tool's config
|
|
607
611
|
- **Status display** — Running/Stopped/Stale/Unhealthy with PID, port, uptime, account/model counts
|
|
608
612
|
- **Version mismatch detection** — warns if service version differs from installed FCM version
|
|
609
613
|
- **Restart** — stop + start via the OS service manager
|
|
@@ -627,7 +631,7 @@ The dedicated **FCM Proxy V2** overlay (accessible via `J` from main TUI, or Set
|
|
|
627
631
|
| `~/.free-coding-models/daemon.json` | Status file (PID, port, token) — written by the background service |
|
|
628
632
|
| `~/.free-coding-models/daemon-stdout.log` | Service output log |
|
|
629
633
|
|
|
630
|
-
The `proxy.activeTool` field
|
|
634
|
+
The `proxy.activeTool` field is now legacy-only. FCM Proxy V2 follows the current **Z-selected** tool automatically whenever that mode supports persisted proxy sync.
|
|
631
635
|
|
|
632
636
|
### Cleanup
|
|
633
637
|
|
|
@@ -680,7 +684,11 @@ Press **Z** to cycle through all 13 tool modes in the TUI, or use flags to start
|
|
|
680
684
|
|
|
681
685
|
⚡ = Requires FCM Proxy V2 background service (press `J` to enable). These tools cannot connect to free providers without the proxy.
|
|
682
686
|
|
|
683
|
-
|
|
687
|
+
Proxy-backed external tool support is still beta. Expect occasional launch/auth rough edges while third-party CLI contracts are still settling.
|
|
688
|
+
|
|
689
|
+
`Codex` is launched through an explicit custom provider config so it stays in API-key mode through the proxy. `Gemini` proxy launches are version-gated: older builds like `0.33.0` are blocked with a clear diagnostic instead of being misconfigured silently.
|
|
690
|
+
|
|
691
|
+
The **Install Endpoints** flow (`Y` key) now targets only the tools with a stable persisted config contract. `Claude Code`, `Codex`, and `Gemini` stay launcher-only and should be started directly from FCM.
|
|
684
692
|
|
|
685
693
|
---
|
|
686
694
|
|
|
@@ -979,7 +987,7 @@ This script:
|
|
|
979
987
|
| `--tier C` | Show only C tier models |
|
|
980
988
|
| `--profile <name>` | Load a saved config profile on startup |
|
|
981
989
|
| `--recommend` | Auto-open Smart Recommend overlay on start |
|
|
982
|
-
| `--clean-proxy` | Remove persisted `fcm-proxy` config from
|
|
990
|
+
| `--clean-proxy` | Remove persisted `fcm-proxy` config from OpenCode |
|
|
983
991
|
|
|
984
992
|
**Keyboard shortcuts (main TUI):**
|
|
985
993
|
- **↑↓** — Navigate models
|
|
@@ -1011,9 +1019,9 @@ Pressing **K** now shows a full in-app reference: main hotkeys, settings hotkeys
|
|
|
1011
1019
|
`Y` opens a dedicated install flow for configured providers. The 5-step flow is:
|
|
1012
1020
|
|
|
1013
1021
|
1. **Provider** — Pick one provider that already has an API key in Settings
|
|
1014
|
-
2. **Tool** — Pick the target tool from
|
|
1015
|
-
- Config-based: `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, `Goose`, `Pi`, `Aider`, `Amp`, `
|
|
1016
|
-
- Env-file based: `
|
|
1022
|
+
2. **Tool** — Pick the target tool from the compatible install targets:
|
|
1023
|
+
- Config-based: `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, `Goose`, `Pi`, `Aider`, `Amp`, `Qwen`
|
|
1024
|
+
- Env-file based: `OpenHands` (writes `~/.fcm-openhands-env` — source it before launching)
|
|
1017
1025
|
3. **Connection Mode** — Choose how the tool connects to the provider:
|
|
1018
1026
|
- **⚡ Direct Provider** — pure API connection, no proxy involved
|
|
1019
1027
|
- **🔄 FCM Proxy V2** — route through FCM Proxy V2 with key rotation and usage tracking
|
|
@@ -1026,7 +1034,8 @@ Important behavior:
|
|
|
1026
1034
|
- `Install all models` is the recommended path because FCM can refresh that catalog automatically on later launches when the provider model list changes
|
|
1027
1035
|
- `Install selected models only` is useful when you want a smaller curated picker inside the target tool
|
|
1028
1036
|
- `OpenCode CLI` and `OpenCode Desktop` share the same `opencode.json`, so the managed provider appears in both
|
|
1029
|
-
-
|
|
1037
|
+
- `Claude Code`, `Codex`, and `Gemini` are launcher-only in this flow for now. Use the normal `Enter` launcher path so FCM can apply the right proxy/runtime contract automatically.
|
|
1038
|
+
- For env-based install targets like `OpenHands`, FCM writes a sourceable helper file at `~/.fcm-{tool}-env`
|
|
1030
1039
|
|
|
1031
1040
|
**Keyboard shortcuts (Settings screen — `P` key):**
|
|
1032
1041
|
- **↑↓** — Navigate providers, maintenance row, and profile rows
|
|
@@ -78,7 +78,11 @@
|
|
|
78
78
|
* - --best: Show only top-tier models (A+, S, S+)
|
|
79
79
|
* - --fiable: Analyze 10s and output the most reliable model
|
|
80
80
|
* - --json: Output results as JSON (for scripting/automation)
|
|
81
|
+
* - --recommend: Open Smart Recommend immediately on startup
|
|
82
|
+
* - --profile <name>: Load a saved config profile before entering the TUI
|
|
83
|
+
* - --clean-proxy / --proxy-clean: Remove persisted fcm-proxy config from OpenCode
|
|
81
84
|
* - --no-telemetry: Disable anonymous usage analytics for this run
|
|
85
|
+
* - --help / -h: Print the full CLI help and exit
|
|
82
86
|
* - --tier S/A/B/C: Filter models by tier letter (S=S+/S, A=A+/A/A-, B=B+/B, C=C)
|
|
83
87
|
*
|
|
84
88
|
* @see {@link https://build.nvidia.com} NVIDIA API key generation
|
|
@@ -88,6 +92,7 @@
|
|
|
88
92
|
|
|
89
93
|
import chalk from 'chalk'
|
|
90
94
|
import { createRequire } from 'module'
|
|
95
|
+
import { fileURLToPath } from 'url'
|
|
91
96
|
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'fs'
|
|
92
97
|
import { randomUUID } from 'crypto'
|
|
93
98
|
import { homedir } from 'os'
|
|
@@ -124,6 +129,7 @@ import { startExternalTool } from '../src/tool-launchers.js'
|
|
|
124
129
|
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels, CONNECTION_MODES } from '../src/endpoint-installer.js'
|
|
125
130
|
import { loadCache, saveCache, clearCache, getCacheAge } from '../src/cache.js'
|
|
126
131
|
import { checkConfigSecurity } from '../src/security.js'
|
|
132
|
+
import { buildCliHelpText } from '../src/cli-help.js'
|
|
127
133
|
|
|
128
134
|
// 📖 mergedModels: cross-provider grouped model list (one entry per label, N providers each)
|
|
129
135
|
// 📖 mergedModelByLabel: fast lookup map from display label → merged model entry
|
|
@@ -173,6 +179,13 @@ const LOCAL_VERSION = pkg.version
|
|
|
173
179
|
async function main() {
|
|
174
180
|
const cliArgs = parseArgs(process.argv)
|
|
175
181
|
|
|
182
|
+
if (cliArgs.helpMode) {
|
|
183
|
+
console.log()
|
|
184
|
+
console.log(buildCliHelpText({ chalk, title: 'free-coding-models' }))
|
|
185
|
+
console.log()
|
|
186
|
+
process.exit(0)
|
|
187
|
+
}
|
|
188
|
+
|
|
176
189
|
// Validate --tier early, before entering alternate screen
|
|
177
190
|
if (cliArgs.tierFilter && !TIER_LETTER_MAP[cliArgs.tierFilter]) {
|
|
178
191
|
console.error(chalk.red(` Unknown tier "${cliArgs.tierFilter}". Valid tiers: S, A, B, C`))
|
|
@@ -336,28 +349,48 @@ async function main() {
|
|
|
336
349
|
ts: new Date().toISOString(),
|
|
337
350
|
})
|
|
338
351
|
|
|
339
|
-
// 📖
|
|
340
|
-
// 📖
|
|
341
|
-
// 📖
|
|
342
|
-
// 📖 2. Display "OUTDATED" in TUI footer if update check fails repeatedly
|
|
352
|
+
// 📖 Auto-update detection: check npm registry for new versions at startup.
|
|
353
|
+
// 📖 If a new version is available, show an interactive prompt (Update / Changelogs / Skip).
|
|
354
|
+
// 📖 Dev mode (git checkout) skips auto-update to avoid infinite relaunch loops.
|
|
343
355
|
let latestVersion = null
|
|
344
|
-
|
|
356
|
+
const isDevMode = existsSync(join(dirname(fileURLToPath(import.meta.url)), '..', '.git'))
|
|
345
357
|
try {
|
|
346
358
|
latestVersion = await checkForUpdate()
|
|
347
|
-
// 📖
|
|
348
|
-
if (
|
|
349
|
-
|
|
359
|
+
// 📖 Reset failure counter on successful check
|
|
360
|
+
if (config.settings?.updateCheckFailures) {
|
|
361
|
+
config.settings.updateCheckFailures = 0
|
|
362
|
+
saveConfig(config)
|
|
350
363
|
}
|
|
351
364
|
} catch (err) {
|
|
352
|
-
// 📖 Silently fail - don't block the app if npm registry is unreachable
|
|
353
|
-
// 📖 But track the failure for outdated detection
|
|
354
365
|
const failures = (config.settings?.updateCheckFailures || 0) + 1
|
|
355
366
|
if (!config.settings) config.settings = {}
|
|
356
367
|
config.settings.updateCheckFailures = Math.min(failures, 3)
|
|
357
|
-
if (failures >= 3) isOutdated = true
|
|
358
368
|
saveConfig(config)
|
|
359
369
|
}
|
|
360
370
|
|
|
371
|
+
// 📖 Show interactive update prompt if a new version is available (skip in dev mode)
|
|
372
|
+
if (latestVersion && !isDevMode) {
|
|
373
|
+
const choice = await promptUpdateNotification(latestVersion)
|
|
374
|
+
if (choice === 'update') {
|
|
375
|
+
runUpdate(latestVersion)
|
|
376
|
+
return // 📖 runUpdate relaunches the process — this line is a safety guard
|
|
377
|
+
} else if (choice === 'changelogs') {
|
|
378
|
+
const { execSync: _exec } = await import('child_process')
|
|
379
|
+
const url = 'https://github.com/vava-nessa/free-coding-models/releases'
|
|
380
|
+
try {
|
|
381
|
+
if (process.platform === 'darwin') _exec(`open ${url}`)
|
|
382
|
+
else if (process.platform === 'linux') _exec(`xdg-open ${url}`)
|
|
383
|
+
else console.log(chalk.dim(` 📋 ${url}`))
|
|
384
|
+
} catch { console.log(chalk.dim(` 📋 ${url}`)) }
|
|
385
|
+
// 📖 After opening changelogs, re-prompt so user can still update or continue
|
|
386
|
+
const choice2 = await promptUpdateNotification(latestVersion)
|
|
387
|
+
if (choice2 === 'update') {
|
|
388
|
+
runUpdate(latestVersion)
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
361
394
|
// 📖 Dynamic OpenRouter free model discovery — fetch live free models from API
|
|
362
395
|
// 📖 Replaces static openrouter entries in MODELS with fresh data.
|
|
363
396
|
// 📖 Fallback: if fetch fails, the static list from sources.js stays intact + warning shown.
|
|
@@ -446,8 +479,8 @@ async function main() {
|
|
|
446
479
|
lastPingTime: now, // 📖 Track when last ping cycle started
|
|
447
480
|
lastUserActivityAt: now, // 📖 Any keypress refreshes this timer; inactivity can force slow mode.
|
|
448
481
|
resumeSpeedOnActivity: false, // 📖 Set after idle slowdown so the next activity restarts a 60s speed burst.
|
|
449
|
-
latestVersion,
|
|
450
|
-
|
|
482
|
+
startupLatestVersion: latestVersion, // 📖 Startup auto-check result reused by the footer banner after "skip update".
|
|
483
|
+
versionAlertsEnabled: !isDevMode, // 📖 Dev checkouts should not tell contributors to upgrade the global npm package.
|
|
451
484
|
mode, // 📖 'opencode' or 'openclaw' — controls Enter action
|
|
452
485
|
tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
|
|
453
486
|
originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
|
|
@@ -604,11 +637,9 @@ async function main() {
|
|
|
604
637
|
}
|
|
605
638
|
}
|
|
606
639
|
|
|
607
|
-
// 📖 Auto-start proxy on launch
|
|
640
|
+
// 📖 Auto-start proxy on launch when proxy auto-sync is enabled for the current tool.
|
|
608
641
|
// 📖 Fire-and-forget: does not block UI startup. state.proxyStartupStatus is updated async.
|
|
609
|
-
|
|
610
|
-
void autoStartProxyIfSynced(config, state)
|
|
611
|
-
}
|
|
642
|
+
void autoStartProxyIfSynced(config, state)
|
|
612
643
|
|
|
613
644
|
// 📖 Load cache if available (for faster startup with cached ping results)
|
|
614
645
|
const cached = loadCache()
|
|
@@ -886,7 +917,7 @@ async function main() {
|
|
|
886
917
|
? overlays.renderLog()
|
|
887
918
|
: state.changelogOpen
|
|
888
919
|
? overlays.renderChangelog()
|
|
889
|
-
: 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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true, state.
|
|
920
|
+
: 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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true, state.startupLatestVersion, state.versionAlertsEnabled)
|
|
890
921
|
process.stdout.write(ALT_HOME + content)
|
|
891
922
|
}, Math.round(1000 / FPS))
|
|
892
923
|
|
|
@@ -894,7 +925,7 @@ async function main() {
|
|
|
894
925
|
const initialVisible = state.results.filter(r => !r.hidden)
|
|
895
926
|
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
|
|
896
927
|
|
|
897
|
-
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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true, state.
|
|
928
|
+
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, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true, state.startupLatestVersion, state.versionAlertsEnabled))
|
|
898
929
|
|
|
899
930
|
// 📖 If --recommend was passed, auto-open the Smart Recommend overlay on start
|
|
900
931
|
if (cliArgs.recommendMode) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|
|
@@ -15,14 +15,51 @@
|
|
|
15
15
|
* → translateAnthropicToOpenAI(body) — Convert Anthropic Messages request → OpenAI chat completions
|
|
16
16
|
* → translateOpenAIToAnthropic(openaiResponse, requestModel) — Convert OpenAI JSON response → Anthropic
|
|
17
17
|
* → createAnthropicSSETransformer(requestModel) — Create a Transform stream for SSE translation
|
|
18
|
+
* → estimateAnthropicTokens(body) — Fast local token estimate for `/v1/messages/count_tokens`
|
|
18
19
|
*
|
|
19
|
-
* @exports translateAnthropicToOpenAI, translateOpenAIToAnthropic, createAnthropicSSETransformer
|
|
20
|
+
* @exports translateAnthropicToOpenAI, translateOpenAIToAnthropic, createAnthropicSSETransformer, estimateAnthropicTokens
|
|
20
21
|
* @see src/proxy-server.js — routes /v1/messages through this translator
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
import { Transform } from 'node:stream'
|
|
24
25
|
import { randomUUID } from 'node:crypto'
|
|
25
26
|
|
|
27
|
+
function normalizeThinkingText(block) {
|
|
28
|
+
if (!block || typeof block !== 'object') return ''
|
|
29
|
+
if (typeof block.text === 'string' && block.text) return block.text
|
|
30
|
+
if (typeof block.thinking === 'string' && block.thinking) return block.thinking
|
|
31
|
+
if (typeof block.summary === 'string' && block.summary) return block.summary
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function contentBlocksToText(blocks, { includeThinking = false } = {}) {
|
|
36
|
+
return blocks
|
|
37
|
+
.map((block) => {
|
|
38
|
+
if (block?.type === 'thinking' && includeThinking) {
|
|
39
|
+
const thinkingText = normalizeThinkingText(block)
|
|
40
|
+
return thinkingText ? `<thinking>${thinkingText}</thinking>` : ''
|
|
41
|
+
}
|
|
42
|
+
if (block?.type === 'redacted_thinking' && includeThinking) {
|
|
43
|
+
return '<thinking>[redacted]</thinking>'
|
|
44
|
+
}
|
|
45
|
+
return block?.type === 'text' ? block.text : ''
|
|
46
|
+
})
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extractReasoningBlocks(message = {}) {
|
|
51
|
+
if (Array.isArray(message.reasoning)) {
|
|
52
|
+
return message.reasoning
|
|
53
|
+
.map((entry) => normalizeThinkingText(entry))
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.map((text) => ({ type: 'thinking', thinking: text }))
|
|
56
|
+
}
|
|
57
|
+
if (typeof message.reasoning_content === 'string' && message.reasoning_content.trim()) {
|
|
58
|
+
return [{ type: 'thinking', thinking: message.reasoning_content.trim() }]
|
|
59
|
+
}
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
|
|
26
63
|
/**
|
|
27
64
|
* 📖 Translate an Anthropic Messages API request body to OpenAI Chat Completions format.
|
|
28
65
|
*
|
|
@@ -48,10 +85,7 @@ export function translateAnthropicToOpenAI(body) {
|
|
|
48
85
|
openaiMessages.push({ role: 'system', content: body.system })
|
|
49
86
|
} else if (Array.isArray(body.system)) {
|
|
50
87
|
// 📖 Anthropic supports system as array of content blocks
|
|
51
|
-
const text = body.system
|
|
52
|
-
.filter(b => b.type === 'text')
|
|
53
|
-
.map(b => b.text)
|
|
54
|
-
.join('\n\n')
|
|
88
|
+
const text = contentBlocksToText(body.system, { includeThinking: true }).join('\n\n')
|
|
55
89
|
if (text) openaiMessages.push({ role: 'system', content: text })
|
|
56
90
|
}
|
|
57
91
|
}
|
|
@@ -65,9 +99,7 @@ export function translateAnthropicToOpenAI(body) {
|
|
|
65
99
|
openaiMessages.push({ role, content: msg.content })
|
|
66
100
|
} else if (Array.isArray(msg.content)) {
|
|
67
101
|
// 📖 Anthropic content blocks: [{type: "text", text: "..."}, {type: "tool_result", ...}]
|
|
68
|
-
const textParts = msg.content
|
|
69
|
-
.filter(b => b.type === 'text')
|
|
70
|
-
.map(b => b.text)
|
|
102
|
+
const textParts = contentBlocksToText(msg.content, { includeThinking: true })
|
|
71
103
|
const toolResults = msg.content.filter(b => b.type === 'tool_result')
|
|
72
104
|
const toolUses = msg.content.filter(b => b.type === 'tool_use')
|
|
73
105
|
|
|
@@ -155,6 +187,10 @@ export function translateOpenAIToAnthropic(openaiResponse, requestModel) {
|
|
|
155
187
|
const message = choice?.message || {}
|
|
156
188
|
const content = []
|
|
157
189
|
|
|
190
|
+
for (const reasoningBlock of extractReasoningBlocks(message)) {
|
|
191
|
+
content.push(reasoningBlock)
|
|
192
|
+
}
|
|
193
|
+
|
|
158
194
|
// 📖 Text content → Anthropic text block
|
|
159
195
|
if (message.content) {
|
|
160
196
|
content.push({ type: 'text', text: message.content })
|
|
@@ -368,3 +404,37 @@ export function createAnthropicSSETransformer(requestModel) {
|
|
|
368
404
|
getUsage: () => ({ input_tokens: inputTokens, output_tokens: outputTokens }),
|
|
369
405
|
}
|
|
370
406
|
}
|
|
407
|
+
|
|
408
|
+
function estimateTokenCountFromText(text) {
|
|
409
|
+
const normalized = String(text || '').trim()
|
|
410
|
+
if (!normalized) return 0
|
|
411
|
+
return Math.ceil(normalized.length / 4)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function estimateAnthropicTokens(body) {
|
|
415
|
+
const openaiBody = translateAnthropicToOpenAI(body)
|
|
416
|
+
const messageTokens = Array.isArray(openaiBody.messages)
|
|
417
|
+
? openaiBody.messages.reduce((total, message) => {
|
|
418
|
+
let nextTotal = total + 4
|
|
419
|
+
if (typeof message.content === 'string') {
|
|
420
|
+
nextTotal += estimateTokenCountFromText(message.content)
|
|
421
|
+
}
|
|
422
|
+
if (Array.isArray(message.tool_calls)) {
|
|
423
|
+
for (const toolCall of message.tool_calls) {
|
|
424
|
+
nextTotal += estimateTokenCountFromText(toolCall.function?.name || '')
|
|
425
|
+
nextTotal += estimateTokenCountFromText(toolCall.function?.arguments || '')
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (typeof message.tool_call_id === 'string') {
|
|
429
|
+
nextTotal += estimateTokenCountFromText(message.tool_call_id)
|
|
430
|
+
}
|
|
431
|
+
return nextTotal
|
|
432
|
+
}, 2)
|
|
433
|
+
: 0
|
|
434
|
+
|
|
435
|
+
const toolTokens = Array.isArray(body?.tools)
|
|
436
|
+
? body.tools.reduce((total, tool) => total + estimateTokenCountFromText(JSON.stringify(tool || {})), 0)
|
|
437
|
+
: 0
|
|
438
|
+
|
|
439
|
+
return messageTokens + toolTokens
|
|
440
|
+
}
|
package/src/cli-help.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/cli-help.js
|
|
3
|
+
* @description Shared CLI help builder for the startup `--help` flag and the in-app help overlay.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* 📖 Keeping CLI help text in one module avoids the classic drift where the TUI overlay
|
|
7
|
+
* 📖 documents one set of flags while `--help` prints another. New flags should be added
|
|
8
|
+
* 📖 here once, then both entry points stay aligned.
|
|
9
|
+
*
|
|
10
|
+
* 📖 The builder accepts an optional `chalk` instance. When omitted, it returns plain text,
|
|
11
|
+
* 📖 which keeps unit tests simple and makes the function safe for non-TTY contexts.
|
|
12
|
+
*
|
|
13
|
+
* @functions
|
|
14
|
+
* → `buildCliHelpLines` — build formatted help lines with optional colors and indentation
|
|
15
|
+
* → `buildCliHelpText` — join the help lines into one printable string
|
|
16
|
+
*
|
|
17
|
+
* @exports buildCliHelpLines, buildCliHelpText
|
|
18
|
+
* @see ./tool-metadata.js — source of truth for launcher modes and their CLI flags
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getToolModeOrder, getToolMeta } from './tool-metadata.js'
|
|
22
|
+
|
|
23
|
+
const ANALYSIS_FLAGS = [
|
|
24
|
+
{ flag: '--best', description: 'Show only top tiers (A+, S, S+)' },
|
|
25
|
+
{ flag: '--fiable', description: 'Run the 10s reliability analysis mode' },
|
|
26
|
+
{ flag: '--json', description: 'Output results as JSON for scripts/automation' },
|
|
27
|
+
{ flag: '--tier <S|A|B|C>', description: 'Filter models by tier family' },
|
|
28
|
+
{ flag: '--recommend', description: 'Open Smart Recommend immediately on startup' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const CONFIG_FLAGS = [
|
|
32
|
+
{ flag: '--profile <name>', description: 'Load a saved config profile before startup' },
|
|
33
|
+
{ flag: '--no-telemetry', description: 'Disable anonymous telemetry for this run' },
|
|
34
|
+
{ flag: '--clean-proxy, --proxy-clean', description: 'Remove persisted fcm-proxy config from OpenCode' },
|
|
35
|
+
{ flag: '--help, -h', description: 'Print this help and exit' },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const COMMANDS = [
|
|
39
|
+
{ command: 'daemon status', description: 'Show background FCM Proxy V2 service status' },
|
|
40
|
+
{ command: 'daemon install', description: 'Install and start the background service' },
|
|
41
|
+
{ command: 'daemon uninstall', description: 'Remove the background service' },
|
|
42
|
+
{ command: 'daemon restart', description: 'Restart the background service' },
|
|
43
|
+
{ command: 'daemon logs', description: 'Print the latest daemon log lines' },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const EXAMPLES = [
|
|
47
|
+
'free-coding-models --help',
|
|
48
|
+
'free-coding-models --openclaw --tier S',
|
|
49
|
+
"free-coding-models --json | jq '.[0]'",
|
|
50
|
+
'free-coding-models daemon status',
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
function paint(chalk, formatter, text) {
|
|
54
|
+
if (!chalk || !formatter) return text
|
|
55
|
+
return formatter(text)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatEntry(label, description, { chalk = null, indent = '', labelWidth = 40 } = {}) {
|
|
59
|
+
const coloredLabel = paint(chalk, chalk?.cyan, label.padEnd(labelWidth))
|
|
60
|
+
const coloredDescription = paint(chalk, chalk?.dim, description)
|
|
61
|
+
return `${indent}${coloredLabel} ${coloredDescription}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildCliHelpLines({ chalk = null, indent = '', title = 'CLI Help' } = {}) {
|
|
65
|
+
const lines = []
|
|
66
|
+
const launchFlags = getToolModeOrder()
|
|
67
|
+
.map((mode) => getToolMeta(mode))
|
|
68
|
+
.filter((meta) => meta.flag)
|
|
69
|
+
.map((meta) => ({ flag: meta.flag, description: `${meta.label} mode` }))
|
|
70
|
+
|
|
71
|
+
lines.push(`${indent}${paint(chalk, chalk?.bold, title)}`)
|
|
72
|
+
lines.push(`${indent}${paint(chalk, chalk?.dim, 'Usage: free-coding-models [apiKey] [options]')}`)
|
|
73
|
+
lines.push(`${indent}${paint(chalk, chalk?.dim, ' free-coding-models daemon [status|install|uninstall|restart|logs]')}`)
|
|
74
|
+
lines.push('')
|
|
75
|
+
lines.push(`${indent}${paint(chalk, chalk?.bold, 'Tool Flags')}`)
|
|
76
|
+
for (const entry of launchFlags) {
|
|
77
|
+
lines.push(formatEntry(entry.flag, entry.description, { chalk, indent }))
|
|
78
|
+
}
|
|
79
|
+
lines.push('')
|
|
80
|
+
lines.push(`${indent}${paint(chalk, chalk?.bold, 'Analysis Flags')}`)
|
|
81
|
+
for (const entry of ANALYSIS_FLAGS) {
|
|
82
|
+
lines.push(formatEntry(entry.flag, entry.description, { chalk, indent }))
|
|
83
|
+
}
|
|
84
|
+
lines.push('')
|
|
85
|
+
lines.push(`${indent}${paint(chalk, chalk?.bold, 'Config & Maintenance')}`)
|
|
86
|
+
for (const entry of CONFIG_FLAGS) {
|
|
87
|
+
lines.push(formatEntry(entry.flag, entry.description, { chalk, indent }))
|
|
88
|
+
}
|
|
89
|
+
lines.push('')
|
|
90
|
+
lines.push(`${indent}${paint(chalk, chalk?.bold, 'Commands')}`)
|
|
91
|
+
for (const entry of COMMANDS) {
|
|
92
|
+
lines.push(formatEntry(entry.command, entry.description, { chalk, indent }))
|
|
93
|
+
}
|
|
94
|
+
lines.push('')
|
|
95
|
+
lines.push(`${indent}${paint(chalk, chalk?.dim, 'Default launcher with no tool flag: OpenCode CLI')}`)
|
|
96
|
+
lines.push(`${indent}${paint(chalk, chalk?.dim, 'Flags can be combined: --openclaw --tier S --json')}`)
|
|
97
|
+
lines.push('')
|
|
98
|
+
lines.push(`${indent}${paint(chalk, chalk?.bold, 'Examples')}`)
|
|
99
|
+
for (const example of EXAMPLES) {
|
|
100
|
+
lines.push(`${indent}${paint(chalk, chalk?.cyan, example)}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lines
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function buildCliHelpText(options = {}) {
|
|
107
|
+
return buildCliHelpLines(options).join('\n')
|
|
108
|
+
}
|
package/src/config.js
CHANGED
|
@@ -679,7 +679,8 @@ export function normalizeProxySettings(proxy = null) {
|
|
|
679
679
|
daemonConsent: (typeof proxy?.daemonConsent === 'string' && proxy.daemonConsent.length > 0)
|
|
680
680
|
? proxy.daemonConsent
|
|
681
681
|
: null,
|
|
682
|
-
// 📖 activeTool —
|
|
682
|
+
// 📖 activeTool — legacy field kept only for backward compatibility.
|
|
683
|
+
// 📖 Runtime sync now follows the current Z-selected tool automatically.
|
|
683
684
|
activeTool: (typeof proxy?.activeTool === 'string' && proxy.activeTool.length > 0)
|
|
684
685
|
? proxy.activeTool
|
|
685
686
|
: null,
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* - Amp gets ~/.config/amp/settings.json
|
|
28
28
|
* - Gemini gets ~/.gemini/settings.json
|
|
29
29
|
* - Qwen gets ~/.qwen/settings.json with modelProviders
|
|
30
|
-
* -
|
|
30
|
+
* - OpenHands gets a sourceable env file (~/.fcm-openhands-env)
|
|
31
31
|
*
|
|
32
32
|
* @functions
|
|
33
33
|
* → `getConfiguredInstallableProviders` — list configured providers that support direct endpoint installs
|
|
@@ -54,8 +54,9 @@ import { ENV_VAR_NAMES, PROVIDER_METADATA } from './provider-metadata.js'
|
|
|
54
54
|
import { getToolMeta } from './tool-metadata.js'
|
|
55
55
|
|
|
56
56
|
const DIRECT_INSTALL_UNSUPPORTED_PROVIDERS = new Set(['replicate', 'zai'])
|
|
57
|
-
// 📖
|
|
58
|
-
|
|
57
|
+
// 📖 Install Endpoints only lists tools whose persisted config shape is actually supported here.
|
|
58
|
+
// 📖 Claude Code, Codex, and Gemini stay launcher-only until their proxy/runtime setup is stable.
|
|
59
|
+
const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'openclaw', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp']
|
|
59
60
|
|
|
60
61
|
// 📖 Connection modes: direct (pure provider) vs FCM proxy (rotates keys)
|
|
61
62
|
export const CONNECTION_MODES = [
|
|
@@ -528,7 +529,7 @@ function installIntoEnvBasedTool(providerKey, models, apiKey, toolMode, paths, c
|
|
|
528
529
|
if (connectionMode === 'proxy') {
|
|
529
530
|
// 📖 Point to proxy base (not /v1) — Claude Code adds /v1/messages itself
|
|
530
531
|
const proxyBase = effectiveBaseUrl.replace(/\/v1$/, '')
|
|
531
|
-
envLines.push(`export
|
|
532
|
+
envLines.push(`export ANTHROPIC_AUTH_TOKEN="${effectiveApiKey}"`)
|
|
532
533
|
envLines.push(`export ANTHROPIC_BASE_URL="${proxyBase}"`)
|
|
533
534
|
envLines.push(`export ANTHROPIC_MODEL="${effectiveModelId}"`)
|
|
534
535
|
} else {
|