free-coding-models 0.2.15 → 0.3.0

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.
@@ -98,6 +98,7 @@ import { loadConfig, saveConfig, getApiKey, getProxySettings, resolveApiKeys, ad
98
98
  import { buildMergedModels } from '../src/model-merger.js'
99
99
  import { ProxyServer } from '../src/proxy-server.js'
100
100
  import { loadOpenCodeConfig, saveOpenCodeConfig, syncToOpenCode, restoreOpenCodeBackup, cleanupOpenCodeProxyConfig } from '../src/opencode-sync.js'
101
+ import { syncProxyToTool, cleanupToolConfig, PROXY_SYNCABLE_TOOLS } from '../src/proxy-sync.js'
101
102
  import { usageForRow as _usageForRow } from '../src/usage-reader.js'
102
103
  import { loadRecentLogs } from '../src/log-reader.js'
103
104
  import { buildProviderModelTokenKey, loadTokenUsageByProviderModel } from '../src/token-usage-reader.js'
@@ -108,7 +109,7 @@ import { TIER_COLOR } from '../src/tier-colors.js'
108
109
  import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getProviderQuotaPercentCached, usagePlaceholderForProvider } from '../src/ping.js'
109
110
  import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
110
111
  import { PROVIDER_METADATA, ENV_VAR_NAMES, isWindows, isMac } from '../src/provider-metadata.js'
111
- import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry, sendFeatureRequest, sendBugReport } from '../src/telemetry.js'
112
+ import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry, sendBugReport } from '../src/telemetry.js'
112
113
  import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel } from '../src/favorites.js'
113
114
  import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification } from '../src/updater.js'
114
115
  import { promptApiKey } from '../src/setup.js'
@@ -118,9 +119,9 @@ import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop, startProxyAn
118
119
  import { startOpenClaw } from '../src/openclaw.js'
119
120
  import { createOverlayRenderers } from '../src/overlays.js'
120
121
  import { createKeyHandler } from '../src/key-handler.js'
121
- import { getToolModeOrder } from '../src/tool-metadata.js'
122
+ import { getToolModeOrder, getToolMeta } from '../src/tool-metadata.js'
122
123
  import { startExternalTool } from '../src/tool-launchers.js'
123
- import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels } from '../src/endpoint-installer.js'
124
+ import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels, CONNECTION_MODES } from '../src/endpoint-installer.js'
124
125
  import { loadCache, saveCache, clearCache, getCacheAge } from '../src/cache.js'
125
126
  import { checkConfigSecurity } from '../src/security.js'
126
127
 
@@ -140,7 +141,7 @@ const readline = require('readline')
140
141
  const pkg = require('../package.json')
141
142
  const LOCAL_VERSION = pkg.version
142
143
 
143
- // 📖 sendFeatureRequest, sendBugReport → imported from ../src/telemetry.js
144
+ // 📖 sendBugReport → imported from ../src/telemetry.js
144
145
 
145
146
  // 📖 parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig → imported from ../src/telemetry.js
146
147
 
@@ -199,6 +200,80 @@ async function main() {
199
200
  process.exit(0)
200
201
  }
201
202
 
203
+ // 📖 CLI subcommand: free-coding-models daemon <action>
204
+ const daemonSubcmd = process.argv[2] === 'daemon' ? (process.argv[3] || 'status') : null
205
+ if (daemonSubcmd) {
206
+ const dm = await import('../src/daemon-manager.js')
207
+ if (daemonSubcmd === 'status') {
208
+ const s = await dm.getDaemonStatus()
209
+ console.log()
210
+ if (s.status === 'running') {
211
+ console.log(chalk.greenBright(` 📡 FCM Proxy V2: Running`))
212
+ console.log(chalk.dim(` PID: ${s.info.pid} • Port: ${s.info.port} • Accounts: ${s.info.accountCount} • Version: ${s.info.version}`))
213
+ console.log(chalk.dim(` Started: ${s.info.startedAt}`))
214
+ } else if (s.status === 'stopped') {
215
+ console.log(chalk.yellow(` 📡 FCM Proxy V2: Stopped (service installed but not running)`))
216
+ } else if (s.status === 'stale') {
217
+ console.log(chalk.red(` 📡 FCM Proxy V2: Stale (crashed — PID ${s.info?.pid} no longer alive)`))
218
+ } else if (s.status === 'unhealthy') {
219
+ console.log(chalk.red(` 📡 FCM Proxy V2: Unhealthy (PID alive but health check failed)`))
220
+ } else {
221
+ console.log(chalk.dim(` 📡 FCM Proxy V2: Not installed`))
222
+ console.log(chalk.dim(` Install via: free-coding-models daemon install`))
223
+ }
224
+ console.log()
225
+ process.exit(0)
226
+ }
227
+ if (daemonSubcmd === 'install') {
228
+ const result = dm.installDaemon()
229
+ console.log()
230
+ if (result.success) {
231
+ console.log(chalk.greenBright(' ✅ FCM Proxy V2 background service installed and started!'))
232
+ console.log(chalk.dim(' The proxy will now run automatically at login.'))
233
+ } else {
234
+ console.log(chalk.red(` ❌ Install failed: ${result.error}`))
235
+ }
236
+ console.log()
237
+ process.exit(result.success ? 0 : 1)
238
+ }
239
+ if (daemonSubcmd === 'uninstall') {
240
+ const result = dm.uninstallDaemon()
241
+ console.log()
242
+ if (result.success) {
243
+ console.log(chalk.greenBright(' ✅ FCM Proxy V2 background service uninstalled.'))
244
+ } else {
245
+ console.log(chalk.red(` ❌ Uninstall failed: ${result.error}`))
246
+ }
247
+ console.log()
248
+ process.exit(result.success ? 0 : 1)
249
+ }
250
+ if (daemonSubcmd === 'restart') {
251
+ const result = dm.restartDaemon()
252
+ console.log()
253
+ if (result.success) {
254
+ console.log(chalk.greenBright(' ✅ FCM Proxy V2 service restarted.'))
255
+ } else {
256
+ console.log(chalk.red(` ❌ Restart failed: ${result.error}`))
257
+ }
258
+ console.log()
259
+ process.exit(result.success ? 0 : 1)
260
+ }
261
+ if (daemonSubcmd === 'logs') {
262
+ const logPath = dm.getDaemonLogPath()
263
+ console.log(chalk.dim(` Log file: ${logPath}`))
264
+ try {
265
+ const { execSync } = await import('child_process')
266
+ execSync(`tail -50 "${logPath}"`, { stdio: 'inherit' })
267
+ } catch {
268
+ console.log(chalk.dim(' (no logs yet)'))
269
+ }
270
+ process.exit(0)
271
+ }
272
+ console.log(chalk.red(` Unknown command: ${daemonSubcmd}`))
273
+ console.log(chalk.dim(' Usage: free-coding-models daemon [status|install|uninstall|restart|logs]'))
274
+ process.exit(1)
275
+ }
276
+
202
277
  // 📖 If --profile <name> was passed, load that profile into the live config
203
278
  let startupProfileSettings = null
204
279
  if (cliArgs.profileName) {
@@ -382,6 +457,7 @@ async function main() {
382
457
  terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
383
458
  widthWarningStartedAt: (process.stdout.columns || 80) < 166 ? now : null, // 📖 Start the narrow-terminal countdown immediately when booting in a small viewport.
384
459
  widthWarningDismissed: false, // 📖 Esc hides the narrow-terminal warning early for the current narrow-width session.
460
+ widthWarningShowCount: 0, // 📖 Counter for how many times the narrow-terminal warning has been shown (max 2 per session).
385
461
  // 📖 Settings screen state (P key opens it)
386
462
  settingsOpen: false, // 📖 Whether settings overlay is active
387
463
  settingsCursor: 0, // 📖 Which provider row is selected in settings
@@ -396,6 +472,13 @@ async function main() {
396
472
  settingsUpdateError: null, // 📖 Last update-check error message for maintenance row
397
473
  settingsProxyPortEditMode: false, // 📖 Whether Settings is editing the preferred proxy port field.
398
474
  settingsProxyPortBuffer: '', // 📖 Inline input buffer for the preferred proxy port (0 = auto).
475
+ daemonStatus: 'not-installed', // 📖 Background daemon status: 'running'|'stopped'|'stale'|'unhealthy'|'not-installed'
476
+ daemonInfo: null, // 📖 daemon.json contents when daemon is running
477
+ // 📖 Proxy & Daemon overlay state (opened from Settings)
478
+ proxyDaemonOpen: false, // 📖 Whether the dedicated Proxy & Daemon overlay is active
479
+ proxyDaemonCursor: 0, // 📖 Selected row in the proxy/daemon overlay
480
+ proxyDaemonScrollOffset: 0, // 📖 Vertical scroll offset for the proxy/daemon overlay
481
+ proxyDaemonMessage: null, // 📖 Feedback message { type: 'success'|'warning'|'error', msg: string, ts: number }
399
482
  config, // 📖 Live reference to the config object (updated on save)
400
483
  visibleSorted: [], // 📖 Cached visible+sorted models — shared between render loop and key handlers
401
484
  helpVisible: false, // 📖 Whether the help overlay (K key) is active
@@ -403,11 +486,12 @@ async function main() {
403
486
  helpScrollOffset: 0, // 📖 Vertical scroll offset for Help overlay viewport
404
487
  // 📖 Install Endpoints overlay state (Y key opens it)
405
488
  installEndpointsOpen: false, // 📖 Whether the install-endpoints overlay is active
406
- installEndpointsPhase: 'providers', // 📖 providers | tools | scope | models | result
489
+ installEndpointsPhase: 'providers', // 📖 providers | tools | connection | scope | models | result
407
490
  installEndpointsCursor: 0, // 📖 Selected row within the current install phase
408
491
  installEndpointsScrollOffset: 0, // 📖 Vertical scroll offset for the install overlay viewport
409
492
  installEndpointsProviderKey: null, // 📖 Selected provider for endpoint installation
410
493
  installEndpointsToolMode: null, // 📖 Selected target tool mode
494
+ installEndpointsConnectionMode: null, // 📖 'direct' | 'proxy' — how the tool connects to the provider
411
495
  installEndpointsScope: null, // 📖 all | selected
412
496
  installEndpointsSelectedModelIds: new Set(), // 📖 Multi-select buffer for the selected-models phase
413
497
  installEndpointsErrorMsg: null, // 📖 Temporary validation/error message inside the install flow
@@ -428,14 +512,9 @@ async function main() {
428
512
  activeProfile: getActiveProfileName(config), // 📖 Currently loaded profile name (or null)
429
513
  profileSaveMode: false, // 📖 Whether the inline "Save profile" name input is active
430
514
  profileSaveBuffer: '', // 📖 Typed characters for the profile name being saved
431
- // 📖 Feature Request state (J key opens it)
432
- featureRequestOpen: false, // 📖 Whether the feature request overlay is active
433
- featureRequestBuffer: '', // 📖 Typed characters for the feature request message
434
- featureRequestStatus: 'idle', // 📖 'idle'|'sending'|'success'|'error' — webhook send status
435
- featureRequestError: null, // 📖 Last webhook error message
436
- // 📖 Bug Report state (I key opens it)
437
- bugReportOpen: false, // 📖 Whether the bug report overlay is active
438
- bugReportBuffer: '', // 📖 Typed characters for the bug report message
515
+ // 📖 Feedback state (J/I keys open it)
516
+ feedbackOpen: false, // 📖 Whether the feedback overlay is active
517
+ bugReportBuffer: '', // 📖 Typed characters for the feedback message
439
518
  bugReportStatus: 'idle', // 📖 'idle'|'sending'|'success'|'error' — webhook send status
440
519
  bugReportError: null, // 📖 Last webhook error message
441
520
  // 📖 OpenCode sync status (S key in settings)
@@ -467,6 +546,7 @@ async function main() {
467
546
  if (prevCols >= 166 || state.widthWarningDismissed) {
468
547
  state.widthWarningStartedAt = Date.now()
469
548
  state.widthWarningDismissed = false
549
+ state.widthWarningShowCount++ // 📖 Increment counter when showing the warning again
470
550
  } else if (!state.widthWarningStartedAt) {
471
551
  state.widthWarningStartedAt = Date.now()
472
552
  }
@@ -687,6 +767,8 @@ async function main() {
687
767
  getConfiguredInstallableProviders,
688
768
  getInstallTargetModes,
689
769
  getProviderCatalogModels,
770
+ CONNECTION_MODES,
771
+ getToolMeta,
690
772
  })
691
773
 
692
774
  onKeyPress = createKeyHandler({
@@ -711,6 +793,7 @@ async function main() {
711
793
  getInstallTargetModes,
712
794
  getProviderCatalogModels,
713
795
  installProviderEndpoints,
796
+ CONNECTION_MODES,
714
797
  syncFavoriteFlags,
715
798
  toggleFavoriteModel,
716
799
  sortResultsWithPinnedFavorites,
@@ -722,7 +805,7 @@ async function main() {
722
805
  ENV_VAR_NAMES,
723
806
  ensureProxyRunning,
724
807
  syncToOpenCode,
725
- cleanupOpenCodeProxyConfig,
808
+ cleanupToolConfig,
726
809
  restoreOpenCodeBackup,
727
810
  checkForUpdateDetailed,
728
811
  runUpdate,
@@ -736,7 +819,6 @@ async function main() {
736
819
  getToolModeOrder,
737
820
  startRecommendAnalysis: overlays.startRecommendAnalysis,
738
821
  stopRecommendAnalysis: overlays.stopRecommendAnalysis,
739
- sendFeatureRequest,
740
822
  sendBugReport,
741
823
  stopUi,
742
824
  ping,
@@ -784,27 +866,27 @@ async function main() {
784
866
  refreshAutoPingMode()
785
867
  state.frame++
786
868
  // 📖 Cache visible+sorted models each frame so Enter handler always matches the display
787
- if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.featureRequestOpen && !state.bugReportOpen && !state.changelogOpen) {
869
+ if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.proxyDaemonOpen) {
788
870
  const visible = state.results.filter(r => !r.hidden)
789
871
  state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
790
872
  }
791
873
  const content = state.settingsOpen
792
874
  ? overlays.renderSettings()
875
+ : state.proxyDaemonOpen
876
+ ? overlays.renderProxyDaemon()
793
877
  : state.installEndpointsOpen
794
878
  ? overlays.renderInstallEndpoints()
795
879
  : state.recommendOpen
796
880
  ? overlays.renderRecommend()
797
- : state.featureRequestOpen
798
- ? overlays.renderFeatureRequest()
799
- : state.bugReportOpen
800
- ? overlays.renderBugReport()
801
- : state.helpVisible
881
+ : state.feedbackOpen
882
+ ? overlays.renderFeedback()
883
+ : state.helpVisible
802
884
  ? overlays.renderHelp()
803
885
  : state.logVisible
804
886
  ? overlays.renderLog()
805
887
  : state.changelogOpen
806
888
  ? overlays.renderChangelog()
807
- : 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.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true, state.isOutdated, state.latestVersion)
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)
808
890
  process.stdout.write(ALT_HOME + content)
809
891
  }, Math.round(1000 / FPS))
810
892
 
@@ -812,7 +894,7 @@ async function main() {
812
894
  const initialVisible = state.results.filter(r => !r.hidden)
813
895
  state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
814
896
 
815
- 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.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true, state.isOutdated, state.latestVersion))
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))
816
898
 
817
899
  // 📖 If --recommend was passed, auto-open the Smart Recommend overlay on start
818
900
  if (cliArgs.recommendMode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.2.15",
3
+ "version": "0.3.0",
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",
@@ -36,7 +36,8 @@
36
36
  "type": "module",
37
37
  "main": "bin/free-coding-models.js",
38
38
  "bin": {
39
- "free-coding-models": "./bin/free-coding-models.js"
39
+ "free-coding-models": "./bin/free-coding-models.js",
40
+ "fcm-proxy-daemon": "./bin/fcm-proxy-daemon.js"
40
41
  },
41
42
  "files": [
42
43
  "bin/",
@@ -39,6 +39,12 @@ const UNKNOWN_COOLDOWN_BASE_MS = 15 * 60 * 1000 // 15m initial cooldown
39
39
  const UNKNOWN_COOLDOWN_MAX_MS = 6 * 60 * 60 * 1000 // 6h max cooldown
40
40
  const KNOWN_FALLBACK_COOLDOWN_MS = 5 * 60 * 1000 // 5m fallback for known providers
41
41
 
42
+ // 📖 Generic consecutive-failure cooldown constants.
43
+ // When an account accumulates FAILURE_COOLDOWN_THRESHOLD consecutive non-429 failures,
44
+ // it enters a graduated cooldown (30s → 60s → 120s) so the proxy routes around it.
45
+ const FAILURE_COOLDOWN_THRESHOLD = 3
46
+ const FAILURE_COOLDOWN_STEPS_MS = [30_000, 60_000, 120_000]
47
+
42
48
  // ─── Internal: per-account health state ──────────────────────────────────────
43
49
 
44
50
  class AccountHealth {
@@ -85,6 +91,15 @@ class AccountHealth {
85
91
  cooldownUntilMs: 0,
86
92
  probeInFlight: false,
87
93
  }
94
+
95
+ /**
96
+ * 📖 Generic consecutive-failure tracking for non-429 errors.
97
+ * When consecutiveFailures >= FAILURE_COOLDOWN_THRESHOLD, the account enters
98
+ * a graduated cooldown to avoid wasting requests on a failing endpoint.
99
+ */
100
+ this.consecutiveFailures = 0
101
+ this.failureCooldownUntilMs = 0
102
+ this.failureCooldownLevel = 0
88
103
  }
89
104
 
90
105
  /**
@@ -265,6 +280,9 @@ export class AccountManager {
265
280
  const retryAfterTs = this._retryAfterMap.get(acct.id)
266
281
  if (retryAfterTs && Date.now() < retryAfterTs) return false
267
282
 
283
+ // 📖 Generic failure cooldown — blocks account after consecutive non-429 failures
284
+ if (health.failureCooldownUntilMs > 0 && Date.now() < health.failureCooldownUntilMs) return false
285
+
268
286
  // Only exclude when quota is known to be nearly exhausted.
269
287
  // When quotaSignal is 'unknown' (null quotaPercent), we remain available.
270
288
  if (health.quotaSignal === 'known' && health.quotaPercent !== null && health.quotaPercent <= 5) return false
@@ -455,6 +473,17 @@ export class AccountManager {
455
473
  if (classifiedError?.retryAfterSec) {
456
474
  this._retryAfterMap.set(accountId, Date.now() + classifiedError.retryAfterSec * 1000)
457
475
  }
476
+
477
+ // 📖 Generic consecutive-failure cooldown: when an account hits FAILURE_COOLDOWN_THRESHOLD
478
+ // consecutive non-429 failures, put it in graduated cooldown (30s → 60s → 120s)
479
+ // so the proxy routes around it instead of wasting requests.
480
+ health.consecutiveFailures++
481
+ if (health.consecutiveFailures >= FAILURE_COOLDOWN_THRESHOLD) {
482
+ const stepIdx = Math.min(health.failureCooldownLevel, FAILURE_COOLDOWN_STEPS_MS.length - 1)
483
+ health.failureCooldownUntilMs = Date.now() + FAILURE_COOLDOWN_STEPS_MS[stepIdx]
484
+ health.failureCooldownLevel++
485
+ health.consecutiveFailures = 0
486
+ }
458
487
  }
459
488
 
460
489
  /**
@@ -532,6 +561,11 @@ export class AccountManager {
532
561
 
533
562
  // Clear retry-after from the retryAfterMap (no longer cooling down)
534
563
  this._retryAfterMap.delete(accountId)
564
+
565
+ // 📖 Reset generic failure cooldown state on success
566
+ health.consecutiveFailures = 0
567
+ health.failureCooldownUntilMs = 0
568
+ health.failureCooldownLevel = 0
535
569
  }
536
570
 
537
571
  /**