free-coding-models 0.3.11 → 0.3.12
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 +19 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +13 -167
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +5 -117
- package/src/endpoint-installer.js +26 -64
- package/src/key-handler.js +56 -437
- package/src/legacy-proxy-cleanup.js +432 -0
- package/src/openclaw.js +69 -108
- package/src/opencode-config.js +48 -0
- package/src/opencode.js +6 -248
- package/src/overlays.js +23 -517
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +10 -18
- package/src/testfcm.js +90 -43
- package/src/token-usage-reader.js +9 -38
- package/src/tool-launchers.js +235 -409
- package/src/tool-metadata.js +0 -7
- package/src/utils.js +3 -68
- package/bin/fcm-proxy-daemon.js +0 -242
- package/src/account-manager.js +0 -634
- package/src/anthropic-translator.js +0 -440
- package/src/daemon-manager.js +0 -527
- package/src/error-classifier.js +0 -157
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-foreground.js +0 -234
- package/src/proxy-server.js +0 -1506
- package/src/proxy-sync.js +0 -591
- package/src/proxy-topology.js +0 -85
- package/src/request-transformer.js +0 -180
- package/src/responses-translator.js +0 -423
- package/src/token-stats.js +0 -320
|
@@ -80,7 +80,6 @@
|
|
|
80
80
|
* - --json: Output results as JSON (for scripting/automation)
|
|
81
81
|
* - --recommend: Open Smart Recommend immediately on startup
|
|
82
82
|
* - --profile <name>: Load a saved config profile before entering the TUI
|
|
83
|
-
* - --clean-proxy / --proxy-clean: Remove persisted fcm-proxy config from OpenCode
|
|
84
83
|
* - --no-telemetry: Disable anonymous usage analytics for this run
|
|
85
84
|
* - --help / -h: Print the full CLI help and exit
|
|
86
85
|
* - --tier S/A/B/C: Filter models by tier letter (S=S+/S, A=A+/A/A-, B=B+/B, C=C)
|
|
@@ -98,18 +97,15 @@ import { randomUUID } from 'crypto'
|
|
|
98
97
|
import { homedir } from 'os'
|
|
99
98
|
import { join, dirname } from 'path'
|
|
100
99
|
import { MODELS, sources } from '../sources.js'
|
|
101
|
-
import { getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore, sortResults, filterByTier, findBestModel, parseArgs, TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, scoreModelForTask, getTopRecommendations, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS, formatCtxWindow, labelFromId,
|
|
102
|
-
import { loadConfig, saveConfig, getApiKey,
|
|
100
|
+
import { getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore, sortResults, filterByTier, findBestModel, parseArgs, TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, scoreModelForTask, getTopRecommendations, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS, formatCtxWindow, labelFromId, formatResultsAsJSON } from '../src/utils.js'
|
|
101
|
+
import { loadConfig, saveConfig, getApiKey, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled, persistApiKeysForProvider } from '../src/config.js'
|
|
103
102
|
import { buildMergedModels } from '../src/model-merger.js'
|
|
104
|
-
import {
|
|
105
|
-
import { loadOpenCodeConfig, saveOpenCodeConfig, syncToOpenCode, restoreOpenCodeBackup, cleanupOpenCodeProxyConfig } from '../src/opencode-sync.js'
|
|
106
|
-
import { syncProxyToTool, cleanupToolConfig, PROXY_SYNCABLE_TOOLS } from '../src/proxy-sync.js'
|
|
103
|
+
import { loadOpenCodeConfig, saveOpenCodeConfig } from '../src/opencode-config.js'
|
|
107
104
|
import { usageForRow as _usageForRow } from '../src/usage-reader.js'
|
|
108
|
-
import { loadRecentLogs } from '../src/log-reader.js'
|
|
109
105
|
import { buildProviderModelTokenKey, loadTokenUsageByProviderModel } from '../src/token-usage-reader.js'
|
|
110
106
|
import { parseOpenRouterResponse, fetchProviderQuota as _fetchProviderQuotaFromModule } from '../src/provider-quota-fetchers.js'
|
|
111
107
|
import { isKnownQuotaTelemetry } from '../src/quota-capabilities.js'
|
|
112
|
-
import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG,
|
|
108
|
+
import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, OVERLAY_PANEL_WIDTH, TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES, msCell, spinCell } from '../src/constants.js'
|
|
113
109
|
import { TIER_COLOR } from '../src/tier-colors.js'
|
|
114
110
|
import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getProviderQuotaPercentCached, usagePlaceholderForProvider } from '../src/ping.js'
|
|
115
111
|
import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
|
|
@@ -118,16 +114,15 @@ import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelem
|
|
|
118
114
|
import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel } from '../src/favorites.js'
|
|
119
115
|
import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification } from '../src/updater.js'
|
|
120
116
|
import { promptApiKey } from '../src/setup.js'
|
|
121
|
-
import { stripAnsi, maskApiKey, displayWidth, padEndDisplay, tintOverlayLines, keepOverlayTargetVisible, sliceOverlayLines, calculateViewport, sortResultsWithPinnedFavorites,
|
|
117
|
+
import { stripAnsi, maskApiKey, displayWidth, padEndDisplay, tintOverlayLines, keepOverlayTargetVisible, sliceOverlayLines, calculateViewport, sortResultsWithPinnedFavorites, adjustScrollOffset } from '../src/render-helpers.js'
|
|
122
118
|
import { renderTable, PROVIDER_COLOR } from '../src/render-table.js'
|
|
123
|
-
import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop
|
|
119
|
+
import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop } from '../src/opencode.js'
|
|
124
120
|
import { startOpenClaw } from '../src/openclaw.js'
|
|
125
121
|
import { createOverlayRenderers } from '../src/overlays.js'
|
|
126
122
|
import { createKeyHandler } from '../src/key-handler.js'
|
|
127
123
|
import { getToolModeOrder, getToolMeta } from '../src/tool-metadata.js'
|
|
128
124
|
import { startExternalTool } from '../src/tool-launchers.js'
|
|
129
|
-
import {
|
|
130
|
-
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels, CONNECTION_MODES } from '../src/endpoint-installer.js'
|
|
125
|
+
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels } from '../src/endpoint-installer.js'
|
|
131
126
|
import { loadCache, saveCache, clearCache, getCacheAge } from '../src/cache.js'
|
|
132
127
|
import { checkConfigSecurity } from '../src/security.js'
|
|
133
128
|
import { buildCliHelpText } from '../src/cli-help.js'
|
|
@@ -220,108 +215,6 @@ async function main() {
|
|
|
220
215
|
config.settings.sortAsc = true
|
|
221
216
|
}
|
|
222
217
|
|
|
223
|
-
if (cliArgs.cleanProxyMode) {
|
|
224
|
-
const cleaned = cleanupOpenCodeProxyConfig()
|
|
225
|
-
console.log()
|
|
226
|
-
console.log(chalk.green(' ✅ OpenCode proxy cleanup complete'))
|
|
227
|
-
console.log(chalk.dim(` Config: ${cleaned.path}`))
|
|
228
|
-
console.log(chalk.dim(` Removed provider: ${cleaned.removedProvider ? 'yes' : 'no'} • Removed default model: ${cleaned.removedModel ? 'yes' : 'no'}`))
|
|
229
|
-
console.log()
|
|
230
|
-
process.exit(0)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 📖 Foreground proxy mode — starts the proxy in the current terminal with live dashboard
|
|
234
|
-
if (cliArgs.proxyForegroundMode) {
|
|
235
|
-
await startForegroundProxy(config, chalk)
|
|
236
|
-
return // 📖 startForegroundProxy keeps the process alive via signal handlers
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// 📖 CLI subcommand: free-coding-models daemon <action>
|
|
240
|
-
const daemonSubcmd = process.argv[2] === 'daemon' ? (process.argv[3] || 'status') : null
|
|
241
|
-
if (daemonSubcmd) {
|
|
242
|
-
const dm = await import('../src/daemon-manager.js')
|
|
243
|
-
if (daemonSubcmd === 'status') {
|
|
244
|
-
const s = await dm.getDaemonStatus()
|
|
245
|
-
console.log()
|
|
246
|
-
if (s.status === 'running') {
|
|
247
|
-
console.log(chalk.greenBright(` 📡 FCM Proxy V2: Running`))
|
|
248
|
-
console.log(chalk.dim(` PID: ${s.info.pid} • Port: ${s.info.port} • Accounts: ${s.info.accountCount} • Version: ${s.info.version}`))
|
|
249
|
-
console.log(chalk.dim(` Started: ${s.info.startedAt}`))
|
|
250
|
-
} else if (s.status === 'stopped') {
|
|
251
|
-
console.log(chalk.yellow(` 📡 FCM Proxy V2: Stopped (service installed but not running)`))
|
|
252
|
-
} else if (s.status === 'stale') {
|
|
253
|
-
console.log(chalk.red(` 📡 FCM Proxy V2: Stale (crashed — PID ${s.info?.pid} no longer alive)`))
|
|
254
|
-
} else if (s.status === 'unhealthy') {
|
|
255
|
-
console.log(chalk.red(` 📡 FCM Proxy V2: Unhealthy (PID alive but health check failed)`))
|
|
256
|
-
} else {
|
|
257
|
-
console.log(chalk.dim(` 📡 FCM Proxy V2: Not installed`))
|
|
258
|
-
console.log(chalk.dim(` Install via: free-coding-models daemon install`))
|
|
259
|
-
}
|
|
260
|
-
console.log()
|
|
261
|
-
process.exit(0)
|
|
262
|
-
}
|
|
263
|
-
if (daemonSubcmd === 'install') {
|
|
264
|
-
const result = dm.installDaemon()
|
|
265
|
-
console.log()
|
|
266
|
-
if (result.success) {
|
|
267
|
-
console.log(chalk.greenBright(' ✅ FCM Proxy V2 background service installed and started!'))
|
|
268
|
-
console.log(chalk.dim(' The proxy will now run automatically at login.'))
|
|
269
|
-
} else {
|
|
270
|
-
console.log(chalk.red(` ❌ Install failed: ${result.error}`))
|
|
271
|
-
}
|
|
272
|
-
console.log()
|
|
273
|
-
process.exit(result.success ? 0 : 1)
|
|
274
|
-
}
|
|
275
|
-
if (daemonSubcmd === 'uninstall') {
|
|
276
|
-
const result = dm.uninstallDaemon()
|
|
277
|
-
console.log()
|
|
278
|
-
if (result.success) {
|
|
279
|
-
console.log(chalk.greenBright(' ✅ FCM Proxy V2 background service uninstalled.'))
|
|
280
|
-
} else {
|
|
281
|
-
console.log(chalk.red(` ❌ Uninstall failed: ${result.error}`))
|
|
282
|
-
}
|
|
283
|
-
console.log()
|
|
284
|
-
process.exit(result.success ? 0 : 1)
|
|
285
|
-
}
|
|
286
|
-
if (daemonSubcmd === 'restart') {
|
|
287
|
-
const result = dm.restartDaemon()
|
|
288
|
-
console.log()
|
|
289
|
-
if (result.success) {
|
|
290
|
-
console.log(chalk.greenBright(' ✅ FCM Proxy V2 service restarted.'))
|
|
291
|
-
} else {
|
|
292
|
-
console.log(chalk.red(` ❌ Restart failed: ${result.error}`))
|
|
293
|
-
}
|
|
294
|
-
console.log()
|
|
295
|
-
process.exit(result.success ? 0 : 1)
|
|
296
|
-
}
|
|
297
|
-
if (daemonSubcmd === 'stop') {
|
|
298
|
-
const result = dm.stopDaemon()
|
|
299
|
-
console.log()
|
|
300
|
-
if (result.success) {
|
|
301
|
-
console.log(chalk.greenBright(' ✅ FCM Proxy V2 service stopped.'))
|
|
302
|
-
console.log(chalk.dim(' The service stays installed and can be restarted later.'))
|
|
303
|
-
} else {
|
|
304
|
-
console.log(chalk.red(` ❌ Stop failed: ${result.error}`))
|
|
305
|
-
}
|
|
306
|
-
console.log()
|
|
307
|
-
process.exit(result.success ? 0 : 1)
|
|
308
|
-
}
|
|
309
|
-
if (daemonSubcmd === 'logs') {
|
|
310
|
-
const logPath = dm.getDaemonLogPath()
|
|
311
|
-
console.log(chalk.dim(` Log file: ${logPath}`))
|
|
312
|
-
try {
|
|
313
|
-
const { execSync } = await import('child_process')
|
|
314
|
-
execSync(`tail -50 "${logPath}"`, { stdio: 'inherit' })
|
|
315
|
-
} catch {
|
|
316
|
-
console.log(chalk.dim(' (no logs yet)'))
|
|
317
|
-
}
|
|
318
|
-
process.exit(0)
|
|
319
|
-
}
|
|
320
|
-
console.log(chalk.red(` Unknown command: ${daemonSubcmd}`))
|
|
321
|
-
console.log(chalk.dim(' Usage: free-coding-models daemon [status|install|uninstall|restart|stop|logs]'))
|
|
322
|
-
process.exit(1)
|
|
323
|
-
}
|
|
324
|
-
|
|
325
218
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
326
219
|
|
|
327
220
|
// 📖 Check if any provider has a key — if not, run the first-time setup wizard
|
|
@@ -338,9 +231,6 @@ async function main() {
|
|
|
338
231
|
}
|
|
339
232
|
}
|
|
340
233
|
|
|
341
|
-
// 📖 Backward-compat: keep apiKey var for startOpenClaw() which still needs it
|
|
342
|
-
let apiKey = getApiKey(config, 'nvidia')
|
|
343
|
-
|
|
344
234
|
// 📖 Default mode: use the last persisted launcher choice when valid,
|
|
345
235
|
// 📖 otherwise fall back to OpenCode CLI.
|
|
346
236
|
let mode = getToolModeOrder().includes(config.settings?.preferredToolMode)
|
|
@@ -354,9 +244,6 @@ async function main() {
|
|
|
354
244
|
aider: cliArgs.aiderMode,
|
|
355
245
|
crush: cliArgs.crushMode,
|
|
356
246
|
goose: cliArgs.gooseMode,
|
|
357
|
-
'claude-code': cliArgs.claudeCodeMode,
|
|
358
|
-
codex: cliArgs.codexMode,
|
|
359
|
-
gemini: cliArgs.geminiMode,
|
|
360
247
|
qwen: cliArgs.qwenMode,
|
|
361
248
|
openhands: cliArgs.openHandsMode,
|
|
362
249
|
amp: cliArgs.ampMode,
|
|
@@ -531,15 +418,6 @@ async function main() {
|
|
|
531
418
|
settingsUpdateState: 'idle', // 📖 'idle'|'checking'|'available'|'up-to-date'|'error'|'installing'
|
|
532
419
|
settingsUpdateLatestVersion: null, // 📖 Latest npm version discovered from manual check
|
|
533
420
|
settingsUpdateError: null, // 📖 Last update-check error message for maintenance row
|
|
534
|
-
settingsProxyPortEditMode: false, // 📖 Whether Settings is editing the preferred proxy port field.
|
|
535
|
-
settingsProxyPortBuffer: '', // 📖 Inline input buffer for the preferred proxy port (0 = auto).
|
|
536
|
-
daemonStatus: 'not-installed', // 📖 Background daemon status: 'running'|'stopped'|'stale'|'unhealthy'|'not-installed'
|
|
537
|
-
daemonInfo: null, // 📖 daemon.json contents when daemon is running
|
|
538
|
-
// 📖 Proxy & Daemon overlay state (opened from Settings)
|
|
539
|
-
proxyDaemonOpen: false, // 📖 Whether the dedicated Proxy & Daemon overlay is active
|
|
540
|
-
proxyDaemonCursor: 0, // 📖 Selected row in the proxy/daemon overlay
|
|
541
|
-
proxyDaemonScrollOffset: 0, // 📖 Vertical scroll offset for the proxy/daemon overlay
|
|
542
|
-
proxyDaemonMessage: null, // 📖 Feedback message { type: 'success'|'warning'|'error', msg: string, ts: number }
|
|
543
421
|
config, // 📖 Live reference to the config object (updated on save)
|
|
544
422
|
visibleSorted: [], // 📖 Cached visible+sorted models — shared between render loop and key handlers
|
|
545
423
|
helpVisible: false, // 📖 Whether the help overlay (K key) is active
|
|
@@ -547,12 +425,12 @@ async function main() {
|
|
|
547
425
|
helpScrollOffset: 0, // 📖 Vertical scroll offset for Help overlay viewport
|
|
548
426
|
// 📖 Install Endpoints overlay state (Y key opens it)
|
|
549
427
|
installEndpointsOpen: false, // 📖 Whether the install-endpoints overlay is active
|
|
550
|
-
installEndpointsPhase: 'providers', // 📖 providers | tools |
|
|
428
|
+
installEndpointsPhase: 'providers', // 📖 providers | tools | scope | models | result
|
|
551
429
|
installEndpointsCursor: 0, // 📖 Selected row within the current install phase
|
|
552
430
|
installEndpointsScrollOffset: 0, // 📖 Vertical scroll offset for the install overlay viewport
|
|
553
431
|
installEndpointsProviderKey: null, // 📖 Selected provider for endpoint installation
|
|
554
432
|
installEndpointsToolMode: null, // 📖 Selected target tool mode
|
|
555
|
-
installEndpointsConnectionMode: null, // 📖
|
|
433
|
+
installEndpointsConnectionMode: null, // 📖 Direct provider path retained for future install flow state.
|
|
556
434
|
installEndpointsScope: null, // 📖 all | selected
|
|
557
435
|
installEndpointsSelectedModelIds: new Set(), // 📖 Multi-select buffer for the selected-models phase
|
|
558
436
|
installEndpointsErrorMsg: null, // 📖 Temporary validation/error message inside the install flow
|
|
@@ -576,22 +454,12 @@ async function main() {
|
|
|
576
454
|
bugReportError: null, // 📖 Last webhook error message
|
|
577
455
|
// 📖 OpenCode sync status (S key in settings)
|
|
578
456
|
settingsSyncStatus: null, // 📖 { type: 'success'|'error', msg: string } — shown in settings footer
|
|
579
|
-
// 📖 Log page overlay state (X key opens it)
|
|
580
|
-
logVisible: false, // 📖 Whether the log page overlay is active
|
|
581
|
-
logScrollOffset: 0, // 📖 Vertical scroll offset for log overlay viewport
|
|
582
|
-
logShowAll: false, // 📖 Show all logs (true) or limited to 500 (false)
|
|
583
457
|
// 📖 Changelog overlay state (N key opens it)
|
|
584
458
|
changelogOpen: false, // 📖 Whether the changelog overlay is active
|
|
585
459
|
changelogScrollOffset: 0, // 📖 Vertical scroll offset for changelog overlay viewport
|
|
586
460
|
changelogPhase: 'index', // 📖 'index' (all versions) | 'details' (specific version)
|
|
587
461
|
changelogCursor: 0, // 📖 Selected row in index phase
|
|
588
462
|
changelogSelectedVersion: null, // 📖 Which version to show details for
|
|
589
|
-
// 📖 Proxy startup status — set by autoStartProxyIfSynced, consumed by Task 3 indicator
|
|
590
|
-
// 📖 null = not configured/not attempted
|
|
591
|
-
// 📖 { phase: 'starting' } — proxy start in progress
|
|
592
|
-
// 📖 { phase: 'running', port, accountCount } — proxy is live
|
|
593
|
-
// 📖 { phase: 'failed', reason } — proxy failed to start
|
|
594
|
-
proxyStartupStatus: null, // 📖 Startup-phase proxy status (null | { phase, ...details })
|
|
595
463
|
}
|
|
596
464
|
|
|
597
465
|
// 📖 Re-clamp viewport on terminal resize
|
|
@@ -661,10 +529,6 @@ async function main() {
|
|
|
661
529
|
}
|
|
662
530
|
}
|
|
663
531
|
|
|
664
|
-
// 📖 Auto-start proxy on launch when proxy auto-sync is enabled for the current tool.
|
|
665
|
-
// 📖 Fire-and-forget: does not block UI startup. state.proxyStartupStatus is updated async.
|
|
666
|
-
void autoStartProxyIfSynced(config, state)
|
|
667
|
-
|
|
668
532
|
// 📖 Load cache if available (for faster startup with cached ping results)
|
|
669
533
|
const cached = loadCache()
|
|
670
534
|
if (cached && cached.models) {
|
|
@@ -874,19 +738,16 @@ async function main() {
|
|
|
874
738
|
PROVIDER_COLOR,
|
|
875
739
|
LOCAL_VERSION,
|
|
876
740
|
getApiKey,
|
|
877
|
-
getProxySettings,
|
|
878
741
|
resolveApiKeys,
|
|
879
742
|
isProviderEnabled,
|
|
880
743
|
TIER_CYCLE,
|
|
881
744
|
SETTINGS_OVERLAY_BG,
|
|
882
745
|
HELP_OVERLAY_BG,
|
|
883
746
|
RECOMMEND_OVERLAY_BG,
|
|
884
|
-
LOG_OVERLAY_BG,
|
|
885
747
|
OVERLAY_PANEL_WIDTH,
|
|
886
748
|
keepOverlayTargetVisible,
|
|
887
749
|
sliceOverlayLines,
|
|
888
750
|
tintOverlayLines,
|
|
889
|
-
loadRecentLogs,
|
|
890
751
|
TASK_TYPES,
|
|
891
752
|
PRIORITY_TYPES,
|
|
892
753
|
CONTEXT_BUDGETS,
|
|
@@ -901,7 +762,6 @@ async function main() {
|
|
|
901
762
|
getConfiguredInstallableProviders,
|
|
902
763
|
getInstallTargetModes,
|
|
903
764
|
getProviderCatalogModels,
|
|
904
|
-
CONNECTION_MODES,
|
|
905
765
|
getToolMeta,
|
|
906
766
|
})
|
|
907
767
|
|
|
@@ -912,7 +772,6 @@ async function main() {
|
|
|
912
772
|
MODELS,
|
|
913
773
|
sources,
|
|
914
774
|
getApiKey,
|
|
915
|
-
getProxySettings,
|
|
916
775
|
resolveApiKeys,
|
|
917
776
|
addApiKey,
|
|
918
777
|
removeApiKey,
|
|
@@ -923,7 +782,6 @@ async function main() {
|
|
|
923
782
|
getInstallTargetModes,
|
|
924
783
|
getProviderCatalogModels,
|
|
925
784
|
installProviderEndpoints,
|
|
926
|
-
CONNECTION_MODES,
|
|
927
785
|
syncFavoriteFlags,
|
|
928
786
|
toggleFavoriteModel,
|
|
929
787
|
sortResultsWithPinnedFavorites,
|
|
@@ -933,19 +791,12 @@ async function main() {
|
|
|
933
791
|
TIER_CYCLE,
|
|
934
792
|
ORIGIN_CYCLE,
|
|
935
793
|
ENV_VAR_NAMES,
|
|
936
|
-
ensureProxyRunning,
|
|
937
|
-
syncToOpenCode,
|
|
938
|
-
cleanupToolConfig,
|
|
939
|
-
restoreOpenCodeBackup,
|
|
940
794
|
checkForUpdateDetailed,
|
|
941
795
|
runUpdate,
|
|
942
796
|
startOpenClaw,
|
|
943
797
|
startOpenCodeDesktop,
|
|
944
798
|
startOpenCode,
|
|
945
|
-
startProxyAndLaunch,
|
|
946
799
|
startExternalTool,
|
|
947
|
-
buildProxyTopologyFromConfig,
|
|
948
|
-
isProxyEnabledForConfig,
|
|
949
800
|
getToolModeOrder,
|
|
950
801
|
startRecommendAnalysis: overlays.startRecommendAnalysis,
|
|
951
802
|
stopRecommendAnalysis: overlays.stopRecommendAnalysis,
|
|
@@ -957,7 +808,6 @@ async function main() {
|
|
|
957
808
|
CONTEXT_BUDGETS,
|
|
958
809
|
toFavoriteKey,
|
|
959
810
|
mergedModels,
|
|
960
|
-
apiKey,
|
|
961
811
|
chalk,
|
|
962
812
|
setPingMode,
|
|
963
813
|
noteUserActivity,
|
|
@@ -996,14 +846,12 @@ async function main() {
|
|
|
996
846
|
refreshAutoPingMode()
|
|
997
847
|
state.frame++
|
|
998
848
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
999
|
-
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen
|
|
849
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen) {
|
|
1000
850
|
const visible = state.results.filter(r => !r.hidden)
|
|
1001
851
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1002
852
|
}
|
|
1003
853
|
const content = state.settingsOpen
|
|
1004
854
|
? overlays.renderSettings()
|
|
1005
|
-
: state.proxyDaemonOpen
|
|
1006
|
-
? overlays.renderProxyDaemon()
|
|
1007
855
|
: state.installEndpointsOpen
|
|
1008
856
|
? overlays.renderInstallEndpoints()
|
|
1009
857
|
: state.recommendOpen
|
|
@@ -1012,11 +860,9 @@ async function main() {
|
|
|
1012
860
|
? overlays.renderFeedback()
|
|
1013
861
|
: state.helpVisible
|
|
1014
862
|
? overlays.renderHelp()
|
|
1015
|
-
: state.logVisible
|
|
1016
|
-
? overlays.renderLog()
|
|
1017
863
|
: state.changelogOpen
|
|
1018
864
|
? overlays.renderChangelog()
|
|
1019
|
-
: 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,
|
|
865
|
+
: 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, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled)
|
|
1020
866
|
process.stdout.write(ALT_HOME + content)
|
|
1021
867
|
if (process.stdout.isTTY) {
|
|
1022
868
|
process.stdout.flush && process.stdout.flush()
|
|
@@ -1027,7 +873,7 @@ async function main() {
|
|
|
1027
873
|
const initialVisible = state.results.filter(r => !r.hidden)
|
|
1028
874
|
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
|
|
1029
875
|
|
|
1030
|
-
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,
|
|
876
|
+
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, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled))
|
|
1031
877
|
if (process.stdout.isTTY) {
|
|
1032
878
|
process.stdout.flush && process.stdout.flush()
|
|
1033
879
|
}
|
|
@@ -1054,7 +900,7 @@ async function main() {
|
|
|
1054
900
|
refreshAutoPingMode()
|
|
1055
901
|
state.lastPingTime = Date.now()
|
|
1056
902
|
|
|
1057
|
-
// 📖 Refresh persisted usage snapshots each cycle so
|
|
903
|
+
// 📖 Refresh persisted usage snapshots each cycle so background usage data appears live in table.
|
|
1058
904
|
// 📖 Freshness-aware: stale snapshots (>30m) are excluded and row reverts to undefined.
|
|
1059
905
|
for (const r of state.results) {
|
|
1060
906
|
const pct = _usageForRow(r.providerKey, r.modelId)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
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,8 +36,7 @@
|
|
|
36
36
|
"type": "module",
|
|
37
37
|
"main": "bin/free-coding-models.js",
|
|
38
38
|
"bin": {
|
|
39
|
-
"free-coding-models": "./bin/free-coding-models.js"
|
|
40
|
-
"fcm-proxy-daemon": "./bin/fcm-proxy-daemon.js"
|
|
39
|
+
"free-coding-models": "./bin/free-coding-models.js"
|
|
41
40
|
},
|
|
42
41
|
"files": [
|
|
43
42
|
"bin/",
|
package/src/cli-help.js
CHANGED
|
@@ -37,26 +37,14 @@ const ANALYSIS_FLAGS = [
|
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
const CONFIG_FLAGS = [
|
|
40
|
-
{ flag: '--proxy', description: 'Start FCM Proxy V2 in foreground with live dashboard (no daemon)' },
|
|
41
40
|
{ flag: '--no-telemetry', description: 'Disable anonymous telemetry for this run' },
|
|
42
|
-
{ flag: '--clean-proxy, --proxy-clean', description: 'Remove persisted fcm-proxy config from OpenCode' },
|
|
43
41
|
{ flag: '--help, -h', description: 'Print this help and exit' },
|
|
44
42
|
]
|
|
45
43
|
|
|
46
|
-
const COMMANDS = [
|
|
47
|
-
{ command: 'daemon status', description: 'Show background FCM Proxy V2 service status' },
|
|
48
|
-
{ command: 'daemon install', description: 'Install and start the background service' },
|
|
49
|
-
{ command: 'daemon uninstall', description: 'Remove the background service' },
|
|
50
|
-
{ command: 'daemon restart', description: 'Restart the background service' },
|
|
51
|
-
{ command: 'daemon stop', description: 'Gracefully stop the background service without uninstalling it' },
|
|
52
|
-
{ command: 'daemon logs', description: 'Print the latest daemon log lines' },
|
|
53
|
-
]
|
|
54
|
-
|
|
55
44
|
const EXAMPLES = [
|
|
56
45
|
'free-coding-models --help',
|
|
57
46
|
'free-coding-models --openclaw --tier S',
|
|
58
47
|
"free-coding-models --json | jq '.[0]'",
|
|
59
|
-
'free-coding-models daemon status',
|
|
60
48
|
]
|
|
61
49
|
|
|
62
50
|
function paint(chalk, formatter, text) {
|
|
@@ -79,7 +67,6 @@ export function buildCliHelpLines({ chalk = null, indent = '', title = 'CLI Help
|
|
|
79
67
|
|
|
80
68
|
lines.push(`${indent}${paint(chalk, chalk?.bold, title)}`)
|
|
81
69
|
lines.push(`${indent}${paint(chalk, chalk?.dim, 'Usage: free-coding-models [apiKey] [options]')}`)
|
|
82
|
-
lines.push(`${indent}${paint(chalk, chalk?.dim, ' free-coding-models daemon [status|install|uninstall|restart|stop|logs]')}`)
|
|
83
70
|
lines.push('')
|
|
84
71
|
lines.push(`${indent}${paint(chalk, chalk?.bold, 'Tool Flags')}`)
|
|
85
72
|
for (const entry of launchFlags) {
|
|
@@ -96,11 +83,6 @@ export function buildCliHelpLines({ chalk = null, indent = '', title = 'CLI Help
|
|
|
96
83
|
lines.push(formatEntry(entry.flag, entry.description, { chalk, indent }))
|
|
97
84
|
}
|
|
98
85
|
lines.push('')
|
|
99
|
-
lines.push(`${indent}${paint(chalk, chalk?.bold, 'Commands')}`)
|
|
100
|
-
for (const entry of COMMANDS) {
|
|
101
|
-
lines.push(formatEntry(entry.command, entry.description, { chalk, indent }))
|
|
102
|
-
}
|
|
103
|
-
lines.push('')
|
|
104
86
|
lines.push(`${indent}${paint(chalk, chalk?.dim, 'Default launcher with no tool flag: OpenCode CLI')}`)
|
|
105
87
|
lines.push(`${indent}${paint(chalk, chalk?.dim, 'Flags can be combined: --openclaw --tier S --json')}`)
|
|
106
88
|
lines.push('')
|
package/src/config.js
CHANGED
|
@@ -87,14 +87,11 @@
|
|
|
87
87
|
* → buildPersistedConfig(incomingConfig, diskConfig, options?) — Merge a live snapshot with the latest disk state safely
|
|
88
88
|
* → replaceConfigContents(targetConfig, nextConfig) — Refresh an in-memory config object from a normalized snapshot
|
|
89
89
|
* → persistApiKeysForProvider(config, providerKey) — Persist one provider's API keys without clobbering the rest of the file
|
|
90
|
-
|
|
91
|
-
* → getProxySettings(config) — Return normalized proxy settings from config
|
|
92
|
-
* → setClaudeProxyModelRouting(config, modelId) — Mirror free-claude-code MODEL/MODEL_* routing onto one selected FCM model
|
|
93
90
|
* → normalizeEndpointInstalls(endpointInstalls) — Keep tracked endpoint installs stable across app versions
|
|
94
91
|
*
|
|
95
92
|
* @exports loadConfig, saveConfig, validateConfigFile, getApiKey, isProviderEnabled
|
|
96
93
|
* @exports addApiKey, removeApiKey, listApiKeys — multi-key management helpers
|
|
97
|
-
* @exports
|
|
94
|
+
* @exports normalizeEndpointInstalls
|
|
98
95
|
* @exports buildPersistedConfig, replaceConfigContents, persistApiKeysForProvider
|
|
99
96
|
* @exports CONFIG_PATH — path to the JSON config file
|
|
100
97
|
*
|
|
@@ -103,14 +100,13 @@
|
|
|
103
100
|
*/
|
|
104
101
|
|
|
105
102
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, renameSync } from 'node:fs'
|
|
106
|
-
import { randomBytes } from 'node:crypto'
|
|
107
103
|
import { homedir } from 'node:os'
|
|
108
104
|
import { join } from 'node:path'
|
|
109
105
|
|
|
110
106
|
// 📖 New JSON config path — stores all providers' API keys + enabled state
|
|
111
107
|
export const CONFIG_PATH = join(homedir(), '.free-coding-models.json')
|
|
112
108
|
|
|
113
|
-
// 📖
|
|
109
|
+
// 📖 Runtime data directory — backups and local snapshots live here.
|
|
114
110
|
export const DAEMON_DATA_DIR = join(homedir(), '.free-coding-models')
|
|
115
111
|
|
|
116
112
|
// 📖 Old plain-text config path — used only for migration
|
|
@@ -213,7 +209,6 @@ function normalizeSettingsSection(settings) {
|
|
|
213
209
|
return {
|
|
214
210
|
...safeSettings,
|
|
215
211
|
hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
|
|
216
|
-
proxy: normalizeProxySettings(safeSettings.proxy),
|
|
217
212
|
disableWidthsWarning: safeSettings.disableWidthsWarning === true,
|
|
218
213
|
}
|
|
219
214
|
}
|
|
@@ -234,7 +229,6 @@ function normalizeProfileSettings(settings) {
|
|
|
234
229
|
return {
|
|
235
230
|
..._emptyProfileSettings(),
|
|
236
231
|
...safeSettings,
|
|
237
|
-
proxy: normalizeProxySettings(safeSettings.proxy),
|
|
238
232
|
disableWidthsWarning: safeSettings.disableWidthsWarning === true,
|
|
239
233
|
}
|
|
240
234
|
}
|
|
@@ -847,117 +841,10 @@ export function _emptyProfileSettings() {
|
|
|
847
841
|
pingInterval: 10000, // 📖 default ms between pings in the steady "normal" mode
|
|
848
842
|
hideUnconfiguredModels: true, // 📖 true = default to providers that are actually configured
|
|
849
843
|
preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
|
|
850
|
-
proxy: normalizeProxySettings(),
|
|
851
844
|
disableWidthsWarning: false, // 📖 Disable widths warning (default off)
|
|
852
845
|
}
|
|
853
846
|
}
|
|
854
847
|
|
|
855
|
-
function normalizeAnthropicRouting(anthropicRouting = null) {
|
|
856
|
-
const normalizeModelId = (value) => {
|
|
857
|
-
if (typeof value !== 'string') return null
|
|
858
|
-
const trimmed = value.trim().replace(/^fcm-proxy\//, '')
|
|
859
|
-
return trimmed || null
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
return {
|
|
863
|
-
// 📖 Mirror free-claude-code naming: MODEL is the fallback, and MODEL_* are
|
|
864
|
-
// 📖 Claude-family overrides. FCM currently pins all four to one selected model.
|
|
865
|
-
model: normalizeModelId(anthropicRouting?.model),
|
|
866
|
-
modelOpus: normalizeModelId(anthropicRouting?.modelOpus),
|
|
867
|
-
modelSonnet: normalizeModelId(anthropicRouting?.modelSonnet),
|
|
868
|
-
modelHaiku: normalizeModelId(anthropicRouting?.modelHaiku),
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
/**
|
|
873
|
-
* 📖 normalizeProxySettings: keep proxy-related preferences stable across old configs,
|
|
874
|
-
* 📖 new installs, and profile switches. Proxy is opt-in by default.
|
|
875
|
-
*
|
|
876
|
-
* 📖 stableToken — persisted bearer token shared between TUI and daemon. Generated once
|
|
877
|
-
* on first access so env files and tool configs remain valid across restarts.
|
|
878
|
-
* 📖 daemonEnabled — opt-in for the always-on background proxy daemon (launchd / systemd).
|
|
879
|
-
* 📖 daemonConsent — ISO timestamp of when user consented to daemon install, or null.
|
|
880
|
-
*
|
|
881
|
-
* @param {object|undefined|null} proxy
|
|
882
|
-
* @returns {{ enabled: boolean, syncToOpenCode: boolean, preferredPort: number, stableToken: string, daemonEnabled: boolean, daemonConsent: string|null, anthropicRouting: { model: string|null, modelOpus: string|null, modelSonnet: string|null, modelHaiku: string|null } }}
|
|
883
|
-
*/
|
|
884
|
-
export function normalizeProxySettings(proxy = null) {
|
|
885
|
-
const preferredPort = Number.isInteger(proxy?.preferredPort) && proxy.preferredPort >= 0 && proxy.preferredPort <= 65535
|
|
886
|
-
? proxy.preferredPort
|
|
887
|
-
: 0
|
|
888
|
-
|
|
889
|
-
// 📖 Generate a stable proxy token once and persist it forever
|
|
890
|
-
const stableToken = (typeof proxy?.stableToken === 'string' && proxy.stableToken.length > 0)
|
|
891
|
-
? proxy.stableToken
|
|
892
|
-
: `fcm_${randomBytes(24).toString('hex')}`
|
|
893
|
-
|
|
894
|
-
return {
|
|
895
|
-
enabled: proxy?.enabled === true,
|
|
896
|
-
syncToOpenCode: proxy?.syncToOpenCode === true,
|
|
897
|
-
preferredPort,
|
|
898
|
-
stableToken,
|
|
899
|
-
daemonEnabled: proxy?.daemonEnabled === true,
|
|
900
|
-
daemonConsent: (typeof proxy?.daemonConsent === 'string' && proxy.daemonConsent.length > 0)
|
|
901
|
-
? proxy.daemonConsent
|
|
902
|
-
: null,
|
|
903
|
-
anthropicRouting: normalizeAnthropicRouting(proxy?.anthropicRouting),
|
|
904
|
-
// 📖 activeTool — legacy field kept only for backward compatibility.
|
|
905
|
-
// 📖 Runtime sync now follows the current Z-selected tool automatically.
|
|
906
|
-
activeTool: (typeof proxy?.activeTool === 'string' && proxy.activeTool.length > 0)
|
|
907
|
-
? proxy.activeTool
|
|
908
|
-
: null,
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* 📖 getProxySettings: return normalized proxy settings from the live config.
|
|
914
|
-
* 📖 This centralizes the opt-in default so launchers do not guess.
|
|
915
|
-
*
|
|
916
|
-
* @param {object} config
|
|
917
|
-
* @returns {{ enabled: boolean, syncToOpenCode: boolean, preferredPort: number }}
|
|
918
|
-
*/
|
|
919
|
-
export function getProxySettings(config) {
|
|
920
|
-
return normalizeProxySettings(config?.settings?.proxy)
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* 📖 Persist the free-claude-code style MODEL / MODEL_OPUS / MODEL_SONNET /
|
|
925
|
-
* 📖 MODEL_HAIKU routing onto one selected proxy model. Claude Code itself then
|
|
926
|
-
* 📖 keeps speaking in fake Claude model ids while the proxy chooses the backend.
|
|
927
|
-
*
|
|
928
|
-
* @param {object} config
|
|
929
|
-
* @param {string} modelId
|
|
930
|
-
* @returns {boolean} true when the normalized proxy settings changed
|
|
931
|
-
*/
|
|
932
|
-
export function setClaudeProxyModelRouting(config, modelId) {
|
|
933
|
-
const normalizedModelId = typeof modelId === 'string' ? modelId.trim().replace(/^fcm-proxy\//, '') : ''
|
|
934
|
-
if (!normalizedModelId) return false
|
|
935
|
-
|
|
936
|
-
if (!config.settings || typeof config.settings !== 'object') config.settings = {}
|
|
937
|
-
|
|
938
|
-
const current = getProxySettings(config)
|
|
939
|
-
const nextAnthropicRouting = {
|
|
940
|
-
model: normalizedModelId,
|
|
941
|
-
modelOpus: normalizedModelId,
|
|
942
|
-
modelSonnet: normalizedModelId,
|
|
943
|
-
modelHaiku: normalizedModelId,
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
const changed = current.enabled !== true
|
|
947
|
-
|| current.anthropicRouting.model !== nextAnthropicRouting.model
|
|
948
|
-
|| current.anthropicRouting.modelOpus !== nextAnthropicRouting.modelOpus
|
|
949
|
-
|| current.anthropicRouting.modelSonnet !== nextAnthropicRouting.modelSonnet
|
|
950
|
-
|| current.anthropicRouting.modelHaiku !== nextAnthropicRouting.modelHaiku
|
|
951
|
-
|
|
952
|
-
config.settings.proxy = {
|
|
953
|
-
...current,
|
|
954
|
-
enabled: true,
|
|
955
|
-
anthropicRouting: nextAnthropicRouting,
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
return changed
|
|
959
|
-
}
|
|
960
|
-
|
|
961
848
|
/**
|
|
962
849
|
* 📖 normalizeEndpointInstalls keeps the endpoint-install tracking list safe to replay.
|
|
963
850
|
*
|
|
@@ -997,9 +884,10 @@ export function normalizeEndpointInstalls(endpointInstalls) {
|
|
|
997
884
|
function _emptyConfig() {
|
|
998
885
|
return {
|
|
999
886
|
apiKeys: {},
|
|
887
|
+
providers: {},
|
|
1000
888
|
favorites: [],
|
|
1001
|
-
|
|
1002
|
-
endpointInstalls:
|
|
889
|
+
telemetry: { enabled: null, consentVersion: 0, anonymousId: null },
|
|
890
|
+
endpointInstalls: [],
|
|
1003
891
|
settings: _emptyProfileSettings(),
|
|
1004
892
|
}
|
|
1005
893
|
}
|