free-coding-models 0.2.17 → 0.3.1
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 +71 -0
- package/README.md +118 -44
- package/bin/fcm-proxy-daemon.js +239 -0
- package/bin/free-coding-models.js +146 -37
- package/package.json +3 -2
- package/src/account-manager.js +34 -0
- package/src/anthropic-translator.js +440 -0
- package/src/cli-help.js +108 -0
- package/src/config.js +25 -1
- package/src/daemon-manager.js +527 -0
- package/src/endpoint-installer.js +45 -19
- package/src/key-handler.js +324 -148
- package/src/opencode.js +47 -44
- package/src/overlays.js +282 -207
- package/src/proxy-server.js +746 -10
- package/src/proxy-sync.js +564 -0
- package/src/proxy-topology.js +80 -0
- package/src/render-helpers.js +4 -2
- package/src/render-table.js +56 -49
- package/src/responses-translator.js +423 -0
- package/src/tool-launchers.js +343 -26
- package/src/utils.js +31 -8
|
@@ -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'
|
|
@@ -98,6 +103,7 @@ import { loadConfig, saveConfig, getApiKey, getProxySettings, resolveApiKeys, ad
|
|
|
98
103
|
import { buildMergedModels } from '../src/model-merger.js'
|
|
99
104
|
import { ProxyServer } from '../src/proxy-server.js'
|
|
100
105
|
import { loadOpenCodeConfig, saveOpenCodeConfig, syncToOpenCode, restoreOpenCodeBackup, cleanupOpenCodeProxyConfig } from '../src/opencode-sync.js'
|
|
106
|
+
import { syncProxyToTool, cleanupToolConfig, PROXY_SYNCABLE_TOOLS } from '../src/proxy-sync.js'
|
|
101
107
|
import { usageForRow as _usageForRow } from '../src/usage-reader.js'
|
|
102
108
|
import { loadRecentLogs } from '../src/log-reader.js'
|
|
103
109
|
import { buildProviderModelTokenKey, loadTokenUsageByProviderModel } from '../src/token-usage-reader.js'
|
|
@@ -108,7 +114,7 @@ import { TIER_COLOR } from '../src/tier-colors.js'
|
|
|
108
114
|
import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getProviderQuotaPercentCached, usagePlaceholderForProvider } from '../src/ping.js'
|
|
109
115
|
import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
|
|
110
116
|
import { PROVIDER_METADATA, ENV_VAR_NAMES, isWindows, isMac } from '../src/provider-metadata.js'
|
|
111
|
-
import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry,
|
|
117
|
+
import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry, sendBugReport } from '../src/telemetry.js'
|
|
112
118
|
import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel } from '../src/favorites.js'
|
|
113
119
|
import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification } from '../src/updater.js'
|
|
114
120
|
import { promptApiKey } from '../src/setup.js'
|
|
@@ -123,6 +129,7 @@ import { startExternalTool } from '../src/tool-launchers.js'
|
|
|
123
129
|
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels, CONNECTION_MODES } from '../src/endpoint-installer.js'
|
|
124
130
|
import { loadCache, saveCache, clearCache, getCacheAge } from '../src/cache.js'
|
|
125
131
|
import { checkConfigSecurity } from '../src/security.js'
|
|
132
|
+
import { buildCliHelpText } from '../src/cli-help.js'
|
|
126
133
|
|
|
127
134
|
// 📖 mergedModels: cross-provider grouped model list (one entry per label, N providers each)
|
|
128
135
|
// 📖 mergedModelByLabel: fast lookup map from display label → merged model entry
|
|
@@ -140,7 +147,7 @@ const readline = require('readline')
|
|
|
140
147
|
const pkg = require('../package.json')
|
|
141
148
|
const LOCAL_VERSION = pkg.version
|
|
142
149
|
|
|
143
|
-
// 📖
|
|
150
|
+
// 📖 sendBugReport → imported from ../src/telemetry.js
|
|
144
151
|
|
|
145
152
|
// 📖 parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig → imported from ../src/telemetry.js
|
|
146
153
|
|
|
@@ -172,6 +179,13 @@ const LOCAL_VERSION = pkg.version
|
|
|
172
179
|
async function main() {
|
|
173
180
|
const cliArgs = parseArgs(process.argv)
|
|
174
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
|
+
|
|
175
189
|
// Validate --tier early, before entering alternate screen
|
|
176
190
|
if (cliArgs.tierFilter && !TIER_LETTER_MAP[cliArgs.tierFilter]) {
|
|
177
191
|
console.error(chalk.red(` Unknown tier "${cliArgs.tierFilter}". Valid tiers: S, A, B, C`))
|
|
@@ -199,6 +213,80 @@ async function main() {
|
|
|
199
213
|
process.exit(0)
|
|
200
214
|
}
|
|
201
215
|
|
|
216
|
+
// 📖 CLI subcommand: free-coding-models daemon <action>
|
|
217
|
+
const daemonSubcmd = process.argv[2] === 'daemon' ? (process.argv[3] || 'status') : null
|
|
218
|
+
if (daemonSubcmd) {
|
|
219
|
+
const dm = await import('../src/daemon-manager.js')
|
|
220
|
+
if (daemonSubcmd === 'status') {
|
|
221
|
+
const s = await dm.getDaemonStatus()
|
|
222
|
+
console.log()
|
|
223
|
+
if (s.status === 'running') {
|
|
224
|
+
console.log(chalk.greenBright(` 📡 FCM Proxy V2: Running`))
|
|
225
|
+
console.log(chalk.dim(` PID: ${s.info.pid} • Port: ${s.info.port} • Accounts: ${s.info.accountCount} • Version: ${s.info.version}`))
|
|
226
|
+
console.log(chalk.dim(` Started: ${s.info.startedAt}`))
|
|
227
|
+
} else if (s.status === 'stopped') {
|
|
228
|
+
console.log(chalk.yellow(` 📡 FCM Proxy V2: Stopped (service installed but not running)`))
|
|
229
|
+
} else if (s.status === 'stale') {
|
|
230
|
+
console.log(chalk.red(` 📡 FCM Proxy V2: Stale (crashed — PID ${s.info?.pid} no longer alive)`))
|
|
231
|
+
} else if (s.status === 'unhealthy') {
|
|
232
|
+
console.log(chalk.red(` 📡 FCM Proxy V2: Unhealthy (PID alive but health check failed)`))
|
|
233
|
+
} else {
|
|
234
|
+
console.log(chalk.dim(` 📡 FCM Proxy V2: Not installed`))
|
|
235
|
+
console.log(chalk.dim(` Install via: free-coding-models daemon install`))
|
|
236
|
+
}
|
|
237
|
+
console.log()
|
|
238
|
+
process.exit(0)
|
|
239
|
+
}
|
|
240
|
+
if (daemonSubcmd === 'install') {
|
|
241
|
+
const result = dm.installDaemon()
|
|
242
|
+
console.log()
|
|
243
|
+
if (result.success) {
|
|
244
|
+
console.log(chalk.greenBright(' ✅ FCM Proxy V2 background service installed and started!'))
|
|
245
|
+
console.log(chalk.dim(' The proxy will now run automatically at login.'))
|
|
246
|
+
} else {
|
|
247
|
+
console.log(chalk.red(` ❌ Install failed: ${result.error}`))
|
|
248
|
+
}
|
|
249
|
+
console.log()
|
|
250
|
+
process.exit(result.success ? 0 : 1)
|
|
251
|
+
}
|
|
252
|
+
if (daemonSubcmd === 'uninstall') {
|
|
253
|
+
const result = dm.uninstallDaemon()
|
|
254
|
+
console.log()
|
|
255
|
+
if (result.success) {
|
|
256
|
+
console.log(chalk.greenBright(' ✅ FCM Proxy V2 background service uninstalled.'))
|
|
257
|
+
} else {
|
|
258
|
+
console.log(chalk.red(` ❌ Uninstall failed: ${result.error}`))
|
|
259
|
+
}
|
|
260
|
+
console.log()
|
|
261
|
+
process.exit(result.success ? 0 : 1)
|
|
262
|
+
}
|
|
263
|
+
if (daemonSubcmd === 'restart') {
|
|
264
|
+
const result = dm.restartDaemon()
|
|
265
|
+
console.log()
|
|
266
|
+
if (result.success) {
|
|
267
|
+
console.log(chalk.greenBright(' ✅ FCM Proxy V2 service restarted.'))
|
|
268
|
+
} else {
|
|
269
|
+
console.log(chalk.red(` ❌ Restart failed: ${result.error}`))
|
|
270
|
+
}
|
|
271
|
+
console.log()
|
|
272
|
+
process.exit(result.success ? 0 : 1)
|
|
273
|
+
}
|
|
274
|
+
if (daemonSubcmd === 'logs') {
|
|
275
|
+
const logPath = dm.getDaemonLogPath()
|
|
276
|
+
console.log(chalk.dim(` Log file: ${logPath}`))
|
|
277
|
+
try {
|
|
278
|
+
const { execSync } = await import('child_process')
|
|
279
|
+
execSync(`tail -50 "${logPath}"`, { stdio: 'inherit' })
|
|
280
|
+
} catch {
|
|
281
|
+
console.log(chalk.dim(' (no logs yet)'))
|
|
282
|
+
}
|
|
283
|
+
process.exit(0)
|
|
284
|
+
}
|
|
285
|
+
console.log(chalk.red(` Unknown command: ${daemonSubcmd}`))
|
|
286
|
+
console.log(chalk.dim(' Usage: free-coding-models daemon [status|install|uninstall|restart|logs]'))
|
|
287
|
+
process.exit(1)
|
|
288
|
+
}
|
|
289
|
+
|
|
202
290
|
// 📖 If --profile <name> was passed, load that profile into the live config
|
|
203
291
|
let startupProfileSettings = null
|
|
204
292
|
if (cliArgs.profileName) {
|
|
@@ -261,28 +349,48 @@ async function main() {
|
|
|
261
349
|
ts: new Date().toISOString(),
|
|
262
350
|
})
|
|
263
351
|
|
|
264
|
-
// 📖
|
|
265
|
-
// 📖
|
|
266
|
-
// 📖
|
|
267
|
-
// 📖 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.
|
|
268
355
|
let latestVersion = null
|
|
269
|
-
|
|
356
|
+
const isDevMode = existsSync(join(dirname(fileURLToPath(import.meta.url)), '..', '.git'))
|
|
270
357
|
try {
|
|
271
358
|
latestVersion = await checkForUpdate()
|
|
272
|
-
// 📖
|
|
273
|
-
if (
|
|
274
|
-
|
|
359
|
+
// 📖 Reset failure counter on successful check
|
|
360
|
+
if (config.settings?.updateCheckFailures) {
|
|
361
|
+
config.settings.updateCheckFailures = 0
|
|
362
|
+
saveConfig(config)
|
|
275
363
|
}
|
|
276
364
|
} catch (err) {
|
|
277
|
-
// 📖 Silently fail - don't block the app if npm registry is unreachable
|
|
278
|
-
// 📖 But track the failure for outdated detection
|
|
279
365
|
const failures = (config.settings?.updateCheckFailures || 0) + 1
|
|
280
366
|
if (!config.settings) config.settings = {}
|
|
281
367
|
config.settings.updateCheckFailures = Math.min(failures, 3)
|
|
282
|
-
if (failures >= 3) isOutdated = true
|
|
283
368
|
saveConfig(config)
|
|
284
369
|
}
|
|
285
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
|
+
|
|
286
394
|
// 📖 Dynamic OpenRouter free model discovery — fetch live free models from API
|
|
287
395
|
// 📖 Replaces static openrouter entries in MODELS with fresh data.
|
|
288
396
|
// 📖 Fallback: if fetch fails, the static list from sources.js stays intact + warning shown.
|
|
@@ -371,8 +479,8 @@ async function main() {
|
|
|
371
479
|
lastPingTime: now, // 📖 Track when last ping cycle started
|
|
372
480
|
lastUserActivityAt: now, // 📖 Any keypress refreshes this timer; inactivity can force slow mode.
|
|
373
481
|
resumeSpeedOnActivity: false, // 📖 Set after idle slowdown so the next activity restarts a 60s speed burst.
|
|
374
|
-
latestVersion,
|
|
375
|
-
|
|
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.
|
|
376
484
|
mode, // 📖 'opencode' or 'openclaw' — controls Enter action
|
|
377
485
|
tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
|
|
378
486
|
originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
|
|
@@ -382,6 +490,7 @@ async function main() {
|
|
|
382
490
|
terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
|
|
383
491
|
widthWarningStartedAt: (process.stdout.columns || 80) < 166 ? now : null, // 📖 Start the narrow-terminal countdown immediately when booting in a small viewport.
|
|
384
492
|
widthWarningDismissed: false, // 📖 Esc hides the narrow-terminal warning early for the current narrow-width session.
|
|
493
|
+
widthWarningShowCount: 0, // 📖 Counter for how many times the narrow-terminal warning has been shown (max 2 per session).
|
|
385
494
|
// 📖 Settings screen state (P key opens it)
|
|
386
495
|
settingsOpen: false, // 📖 Whether settings overlay is active
|
|
387
496
|
settingsCursor: 0, // 📖 Which provider row is selected in settings
|
|
@@ -396,6 +505,13 @@ async function main() {
|
|
|
396
505
|
settingsUpdateError: null, // 📖 Last update-check error message for maintenance row
|
|
397
506
|
settingsProxyPortEditMode: false, // 📖 Whether Settings is editing the preferred proxy port field.
|
|
398
507
|
settingsProxyPortBuffer: '', // 📖 Inline input buffer for the preferred proxy port (0 = auto).
|
|
508
|
+
daemonStatus: 'not-installed', // 📖 Background daemon status: 'running'|'stopped'|'stale'|'unhealthy'|'not-installed'
|
|
509
|
+
daemonInfo: null, // 📖 daemon.json contents when daemon is running
|
|
510
|
+
// 📖 Proxy & Daemon overlay state (opened from Settings)
|
|
511
|
+
proxyDaemonOpen: false, // 📖 Whether the dedicated Proxy & Daemon overlay is active
|
|
512
|
+
proxyDaemonCursor: 0, // 📖 Selected row in the proxy/daemon overlay
|
|
513
|
+
proxyDaemonScrollOffset: 0, // 📖 Vertical scroll offset for the proxy/daemon overlay
|
|
514
|
+
proxyDaemonMessage: null, // 📖 Feedback message { type: 'success'|'warning'|'error', msg: string, ts: number }
|
|
399
515
|
config, // 📖 Live reference to the config object (updated on save)
|
|
400
516
|
visibleSorted: [], // 📖 Cached visible+sorted models — shared between render loop and key handlers
|
|
401
517
|
helpVisible: false, // 📖 Whether the help overlay (K key) is active
|
|
@@ -429,14 +545,9 @@ async function main() {
|
|
|
429
545
|
activeProfile: getActiveProfileName(config), // 📖 Currently loaded profile name (or null)
|
|
430
546
|
profileSaveMode: false, // 📖 Whether the inline "Save profile" name input is active
|
|
431
547
|
profileSaveBuffer: '', // 📖 Typed characters for the profile name being saved
|
|
432
|
-
// 📖
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
featureRequestStatus: 'idle', // 📖 'idle'|'sending'|'success'|'error' — webhook send status
|
|
436
|
-
featureRequestError: null, // 📖 Last webhook error message
|
|
437
|
-
// 📖 Bug Report state (I key opens it)
|
|
438
|
-
bugReportOpen: false, // 📖 Whether the bug report overlay is active
|
|
439
|
-
bugReportBuffer: '', // 📖 Typed characters for the bug report message
|
|
548
|
+
// 📖 Feedback state (J/I keys open it)
|
|
549
|
+
feedbackOpen: false, // 📖 Whether the feedback overlay is active
|
|
550
|
+
bugReportBuffer: '', // 📖 Typed characters for the feedback message
|
|
440
551
|
bugReportStatus: 'idle', // 📖 'idle'|'sending'|'success'|'error' — webhook send status
|
|
441
552
|
bugReportError: null, // 📖 Last webhook error message
|
|
442
553
|
// 📖 OpenCode sync status (S key in settings)
|
|
@@ -468,6 +579,7 @@ async function main() {
|
|
|
468
579
|
if (prevCols >= 166 || state.widthWarningDismissed) {
|
|
469
580
|
state.widthWarningStartedAt = Date.now()
|
|
470
581
|
state.widthWarningDismissed = false
|
|
582
|
+
state.widthWarningShowCount++ // 📖 Increment counter when showing the warning again
|
|
471
583
|
} else if (!state.widthWarningStartedAt) {
|
|
472
584
|
state.widthWarningStartedAt = Date.now()
|
|
473
585
|
}
|
|
@@ -525,11 +637,9 @@ async function main() {
|
|
|
525
637
|
}
|
|
526
638
|
}
|
|
527
639
|
|
|
528
|
-
// 📖 Auto-start proxy on launch
|
|
640
|
+
// 📖 Auto-start proxy on launch when proxy auto-sync is enabled for the current tool.
|
|
529
641
|
// 📖 Fire-and-forget: does not block UI startup. state.proxyStartupStatus is updated async.
|
|
530
|
-
|
|
531
|
-
void autoStartProxyIfSynced(config, state)
|
|
532
|
-
}
|
|
642
|
+
void autoStartProxyIfSynced(config, state)
|
|
533
643
|
|
|
534
644
|
// 📖 Load cache if available (for faster startup with cached ping results)
|
|
535
645
|
const cached = loadCache()
|
|
@@ -726,7 +836,7 @@ async function main() {
|
|
|
726
836
|
ENV_VAR_NAMES,
|
|
727
837
|
ensureProxyRunning,
|
|
728
838
|
syncToOpenCode,
|
|
729
|
-
|
|
839
|
+
cleanupToolConfig,
|
|
730
840
|
restoreOpenCodeBackup,
|
|
731
841
|
checkForUpdateDetailed,
|
|
732
842
|
runUpdate,
|
|
@@ -740,7 +850,6 @@ async function main() {
|
|
|
740
850
|
getToolModeOrder,
|
|
741
851
|
startRecommendAnalysis: overlays.startRecommendAnalysis,
|
|
742
852
|
stopRecommendAnalysis: overlays.stopRecommendAnalysis,
|
|
743
|
-
sendFeatureRequest,
|
|
744
853
|
sendBugReport,
|
|
745
854
|
stopUi,
|
|
746
855
|
ping,
|
|
@@ -788,27 +897,27 @@ async function main() {
|
|
|
788
897
|
refreshAutoPingMode()
|
|
789
898
|
state.frame++
|
|
790
899
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
791
|
-
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.
|
|
900
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.proxyDaemonOpen) {
|
|
792
901
|
const visible = state.results.filter(r => !r.hidden)
|
|
793
902
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
794
903
|
}
|
|
795
904
|
const content = state.settingsOpen
|
|
796
905
|
? overlays.renderSettings()
|
|
906
|
+
: state.proxyDaemonOpen
|
|
907
|
+
? overlays.renderProxyDaemon()
|
|
797
908
|
: state.installEndpointsOpen
|
|
798
909
|
? overlays.renderInstallEndpoints()
|
|
799
910
|
: state.recommendOpen
|
|
800
911
|
? overlays.renderRecommend()
|
|
801
|
-
: state.
|
|
802
|
-
? overlays.
|
|
803
|
-
|
|
804
|
-
? overlays.renderBugReport()
|
|
805
|
-
: state.helpVisible
|
|
912
|
+
: state.feedbackOpen
|
|
913
|
+
? overlays.renderFeedback()
|
|
914
|
+
: state.helpVisible
|
|
806
915
|
? overlays.renderHelp()
|
|
807
916
|
: state.logVisible
|
|
808
917
|
? overlays.renderLog()
|
|
809
918
|
: state.changelogOpen
|
|
810
919
|
? overlays.renderChangelog()
|
|
811
|
-
: 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.
|
|
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)
|
|
812
921
|
process.stdout.write(ALT_HOME + content)
|
|
813
922
|
}, Math.round(1000 / FPS))
|
|
814
923
|
|
|
@@ -816,7 +925,7 @@ async function main() {
|
|
|
816
925
|
const initialVisible = state.results.filter(r => !r.hidden)
|
|
817
926
|
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
|
|
818
927
|
|
|
819
|
-
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.
|
|
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))
|
|
820
929
|
|
|
821
930
|
// 📖 If --recommend was passed, auto-open the Smart Recommend overlay on start
|
|
822
931
|
if (cliArgs.recommendMode) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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/",
|
package/src/account-manager.js
CHANGED
|
@@ -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
|
/**
|