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 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 any of the 13 supported 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
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.6 brings powerful new features:**
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
- - **Provider colors everywhere** — Provider names are now colored consistently in logs, settings, and the main table for better visual recognition
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
- # Output results as JSON (for scripting/automation)
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
- - **Active tool selector** — Choose which AI coding tool receives proxy config (cycles through all 12 syncable tools)
605
- - **Auto-sync toggle** — Automatically write the `fcm-proxy` provider to the active tool's config when the proxy starts
606
- - **Cleanup** — Remove `fcm-proxy` entries from the active tool's config (works for any syncable tool)
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 in the config file tracks which tool the proxy auto-syncs to (e.g. `"opencode"`, `"aider"`, `"claude-code"`).
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
- All tools are also available as install targets in the **Install Endpoints** flow (`Y` key) install an entire provider catalog into any tool with one flow, choosing between Direct Provider or FCM Proxy V2 connection.
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 the active tool |
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 all 13 supported tools:
1015
- - Config-based: `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, `Goose`, `Pi`, `Aider`, `Amp`, `Gemini`, `Qwen`
1016
- - Env-file based: `Claude Code`, `Codex CLI`, `OpenHands` (writes `~/.fcm-{tool}-env` — source it before launching)
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
- - For env-based tools (Claude Code, Codex, OpenHands), FCM writes a sourceable file at `~/.fcm-{tool}-env` run `source ~/.fcm-claude-code-env` before launching
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
- // 📖 Check for updates in the background (non-blocking, non-forced)
340
- // 📖 The old auto-update on startup caused infinite loops, so we've moved to:
341
- // 📖 1. Optional prompt when a new version is available (user chooses to update or not)
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
- let isOutdated = false
356
+ const isDevMode = existsSync(join(dirname(fileURLToPath(import.meta.url)), '..', '.git'))
345
357
  try {
346
358
  latestVersion = await checkForUpdate()
347
- // 📖 Track update check failures - if it fails 3+ times, mark as outdated
348
- if (!latestVersion && config.settings?.updateCheckFailures >= 3) {
349
- isOutdated = true
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, // 📖 Latest npm version available (null if none or check failed)
450
- isOutdated, // 📖 Set to true if update check failed 3+ times (show red "OUTDATED" footer)
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 if OpenCode config already has an fcm-proxy provider.
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
- if (mode === 'opencode' || mode === 'opencode-desktop') {
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.isOutdated, state.latestVersion)
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.isOutdated, state.latestVersion))
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.0",
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
+ }
@@ -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 — which tool the proxy auto-syncs to (defaults to current Z mode)
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
- * - Claude Code, Codex, OpenHands get a sourceable env file (~/.fcm-{tool}-env)
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
- // 📖 All supported install targets matches TOOL_MODE_ORDER in tool-metadata.js
58
- const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'openclaw', 'crush', 'goose', 'pi', 'aider', 'claude-code', 'codex', 'gemini', 'qwen', 'openhands', 'amp']
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 ANTHROPIC_API_KEY="${effectiveApiKey}"`)
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 {