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.
- package/CHANGELOG.md +78 -0
- package/README.md +112 -39
- package/bin/fcm-proxy-daemon.js +239 -0
- package/bin/free-coding-models.js +105 -23
- package/package.json +3 -2
- package/src/account-manager.js +34 -0
- package/src/anthropic-translator.js +370 -0
- package/src/config.js +24 -1
- package/src/daemon-manager.js +527 -0
- package/src/endpoint-installer.js +187 -2
- package/src/key-handler.js +355 -150
- package/src/opencode.js +30 -32
- package/src/overlays.js +365 -225
- package/src/proxy-server.js +488 -6
- package/src/proxy-sync.js +552 -0
- package/src/proxy-topology.js +80 -0
- package/src/render-table.js +24 -15
- package/src/tool-launchers.js +138 -18
- package/src/tool-metadata.js +14 -14
|
@@ -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,
|
|
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
|
-
// 📖
|
|
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
|
-
// 📖
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
798
|
-
? overlays.
|
|
799
|
-
|
|
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.
|
|
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/",
|
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
|
/**
|