free-coding-models 0.3.9 → 0.3.11
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 +21 -0
- package/bin/free-coding-models.js +31 -31
- package/package.json +1 -1
- package/src/cli-help.js +1 -1
- package/src/config.js +18 -240
- package/src/error-classifier.js +4 -1
- package/src/favorites.js +0 -14
- package/src/key-handler.js +26 -212
- package/src/overlays.js +4 -34
- package/src/proxy-foreground.js +234 -0
- package/src/proxy-server.js +41 -12
- package/src/proxy-sync.js +28 -2
- package/src/render-table.js +6 -17
- package/src/utils.js +7 -11
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 0.3.11
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Added early 404 response when a requested model has no registered accounts, ensuring clear error handling.
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
- **Profile system**: Entire profile system removed to ensure API keys persist permanently across all sessions. No more profile switching causing API key loss.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`--proxy` foreground mode**: New `--proxy` flag starts FCM Proxy V2 in the current terminal with a live dashboard showing status, accounts, provider breakdown, and real-time request log. No daemon install needed — works from dev checkout too.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Claude Code proxy auth**: Proxy now accepts `x-api-key` header (used by Anthropic SDK / Claude Code) in addition to `Authorization: Bearer`
|
|
18
|
+
- **Claude model fallback**: When `anthropicRouting` is all null, Claude model names now fall back to the first available account instead of returning "Model not found"
|
|
19
|
+
- **Proxy sync for Claude Code**: Added `claude-code` to `PROXY_SYNCABLE_TOOLS` so the env file is properly written/updated by proxy sync
|
|
20
|
+
- **Correct env var name**: Claude Code env file now exports `ANTHROPIC_API_KEY` (SDK standard) instead of `ANTHROPIC_AUTH_TOKEN`
|
|
21
|
+
- **Auto-source shell profile**: Claude Code env file is now automatically sourced in `.zshrc` / `.bashrc` / `.bash_profile`
|
|
22
|
+
- **Removed deleted profile functions from tests**: Cleaned up test imports after profile system removal from config.js
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
5
26
|
## 0.3.9
|
|
6
27
|
|
|
7
28
|
### Improved
|
|
@@ -99,7 +99,7 @@ import { homedir } from 'os'
|
|
|
99
99
|
import { join, dirname } from 'path'
|
|
100
100
|
import { MODELS, sources } from '../sources.js'
|
|
101
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, getProxyStatusInfo, formatResultsAsJSON } from '../src/utils.js'
|
|
102
|
-
import { loadConfig, saveConfig, getApiKey, getProxySettings, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled,
|
|
102
|
+
import { loadConfig, saveConfig, getApiKey, getProxySettings, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled, persistApiKeysForProvider } from '../src/config.js'
|
|
103
103
|
import { buildMergedModels } from '../src/model-merger.js'
|
|
104
104
|
import { ProxyServer } from '../src/proxy-server.js'
|
|
105
105
|
import { loadOpenCodeConfig, saveOpenCodeConfig, syncToOpenCode, restoreOpenCodeBackup, cleanupOpenCodeProxyConfig } from '../src/opencode-sync.js'
|
|
@@ -126,6 +126,7 @@ import { createOverlayRenderers } from '../src/overlays.js'
|
|
|
126
126
|
import { createKeyHandler } from '../src/key-handler.js'
|
|
127
127
|
import { getToolModeOrder, getToolMeta } from '../src/tool-metadata.js'
|
|
128
128
|
import { startExternalTool } from '../src/tool-launchers.js'
|
|
129
|
+
import { startForegroundProxy } from '../src/proxy-foreground.js'
|
|
129
130
|
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels, CONNECTION_MODES } from '../src/endpoint-installer.js'
|
|
130
131
|
import { loadCache, saveCache, clearCache, getCacheAge } from '../src/cache.js'
|
|
131
132
|
import { checkConfigSecurity } from '../src/security.js'
|
|
@@ -229,6 +230,12 @@ async function main() {
|
|
|
229
230
|
process.exit(0)
|
|
230
231
|
}
|
|
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
|
+
|
|
232
239
|
// 📖 CLI subcommand: free-coding-models daemon <action>
|
|
233
240
|
const daemonSubcmd = process.argv[2] === 'daemon' ? (process.argv[3] || 'status') : null
|
|
234
241
|
if (daemonSubcmd) {
|
|
@@ -315,19 +322,7 @@ async function main() {
|
|
|
315
322
|
process.exit(1)
|
|
316
323
|
}
|
|
317
324
|
|
|
318
|
-
// 📖
|
|
319
|
-
let startupProfileSettings = null
|
|
320
|
-
if (cliArgs.profileName) {
|
|
321
|
-
startupProfileSettings = loadProfile(config, cliArgs.profileName)
|
|
322
|
-
if (!startupProfileSettings) {
|
|
323
|
-
console.error(chalk.red(` Unknown profile "${cliArgs.profileName}". Available: ${listProfiles(config).join(', ') || '(none)'}`))
|
|
324
|
-
process.exit(1)
|
|
325
|
-
}
|
|
326
|
-
saveConfig(config, {
|
|
327
|
-
replaceApiKeys: true,
|
|
328
|
-
replaceFavorites: true,
|
|
329
|
-
})
|
|
330
|
-
}
|
|
325
|
+
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
331
326
|
|
|
332
327
|
// 📖 Check if any provider has a key — if not, run the first-time setup wizard
|
|
333
328
|
const hasAnyKey = Object.keys(sources).some(pk => !!getApiKey(config, pk))
|
|
@@ -494,15 +489,15 @@ async function main() {
|
|
|
494
489
|
return 'normal'
|
|
495
490
|
}
|
|
496
491
|
|
|
497
|
-
|
|
492
|
+
// 📖 tierFilter: current tier filter letter (null = all, 'S' = S+/S, 'A' = A+/A/A-, etc.)
|
|
498
493
|
const state = {
|
|
499
494
|
results,
|
|
500
495
|
pendingPings: 0,
|
|
501
496
|
frame: 0,
|
|
502
497
|
cursor: 0,
|
|
503
498
|
selectedModel: null,
|
|
504
|
-
sortColumn:
|
|
505
|
-
sortDirection: (
|
|
499
|
+
sortColumn: config.settings?.sortColumn ?? 'avg',
|
|
500
|
+
sortDirection: (config.settings?.sortAsc ?? true) ? 'asc' : 'desc',
|
|
506
501
|
pingInterval: PING_MODE_INTERVALS.speed, // 📖 Effective live interval derived from the active ping mode.
|
|
507
502
|
pingMode: 'speed', // 📖 Current ping mode: speed | normal | slow | forced.
|
|
508
503
|
pingModeSource: 'startup', // 📖 Why this mode is active: startup | manual | auto | idle | activity.
|
|
@@ -516,7 +511,7 @@ async function main() {
|
|
|
516
511
|
tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
|
|
517
512
|
originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
|
|
518
513
|
premiumMode: cliArgs.premiumMode, // 📖 Special elite-only mode: S/S+ only, Health UP only, Perfect/Normal/Slow verdict only.
|
|
519
|
-
hideUnconfiguredModels:
|
|
514
|
+
hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
|
|
520
515
|
disableWidthsWarning: config.settings?.disableWidthsWarning ?? false, // 📖 Disable widths warning toggle (default off)
|
|
521
516
|
scrollOffset: 0, // 📖 First visible model index in viewport
|
|
522
517
|
terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
|
|
@@ -574,10 +569,6 @@ async function main() {
|
|
|
574
569
|
recommendAnalysisTimer: null, // 📖 setInterval handle for the 10s analysis phase
|
|
575
570
|
recommendPingTimer: null, // 📖 setInterval handle for 2 pings/sec during analysis
|
|
576
571
|
recommendedKeys: new Set(), // 📖 Set of "providerKey/modelId" for recommended models (shown in main table)
|
|
577
|
-
// 📖 Config Profiles state
|
|
578
|
-
activeProfile: getActiveProfileName(config), // 📖 Currently loaded profile name (or null)
|
|
579
|
-
profileSaveMode: false, // 📖 Whether the inline "Save profile" name input is active
|
|
580
|
-
profileSaveBuffer: '', // 📖 Typed characters for the profile name being saved
|
|
581
572
|
// 📖 Feedback state (J/I keys open it)
|
|
582
573
|
feedbackOpen: false, // 📖 Whether the feedback overlay is active
|
|
583
574
|
bugReportBuffer: '', // 📖 Typed characters for the feedback message
|
|
@@ -798,6 +789,9 @@ async function main() {
|
|
|
798
789
|
|
|
799
790
|
// 📖 Enter alternate screen — animation runs here, zero scrollback pollution
|
|
800
791
|
process.stdout.write(ALT_ENTER)
|
|
792
|
+
if (process.stdout.isTTY) {
|
|
793
|
+
process.stdout.flush && process.stdout.flush()
|
|
794
|
+
}
|
|
801
795
|
|
|
802
796
|
// 📖 Ensure we always leave alt screen cleanly (Ctrl+C, crash, normal exit)
|
|
803
797
|
const exit = (code = 0) => {
|
|
@@ -806,6 +800,9 @@ async function main() {
|
|
|
806
800
|
clearInterval(ticker)
|
|
807
801
|
clearTimeout(state.pingIntervalObj)
|
|
808
802
|
process.stdout.write(ALT_LEAVE)
|
|
803
|
+
if (process.stdout.isTTY) {
|
|
804
|
+
process.stdout.flush && process.stdout.flush()
|
|
805
|
+
}
|
|
809
806
|
process.exit(code)
|
|
810
807
|
}
|
|
811
808
|
process.on('SIGINT', () => exit(0))
|
|
@@ -813,7 +810,7 @@ async function main() {
|
|
|
813
810
|
|
|
814
811
|
// 📖 originFilterMode: index into ORIGIN_CYCLE, 0=All, then each provider key in order
|
|
815
812
|
const ORIGIN_CYCLE = [null, ...Object.keys(sources)]
|
|
816
|
-
const resolvedTierFilter =
|
|
813
|
+
const resolvedTierFilter = config.settings?.tierFilter
|
|
817
814
|
state.tierFilterMode = resolvedTierFilter ? Math.max(0, TIER_CYCLE.indexOf(resolvedTierFilter)) : 0
|
|
818
815
|
const resolvedOriginFilter = config.settings?.originFilter
|
|
819
816
|
state.originFilterMode = resolvedOriginFilter ? Math.max(0, ORIGIN_CYCLE.indexOf(resolvedOriginFilter)) : 0
|
|
@@ -865,6 +862,9 @@ async function main() {
|
|
|
865
862
|
if (process.stdin.isTTY && resetRawMode) process.stdin.setRawMode(false)
|
|
866
863
|
process.stdin.pause()
|
|
867
864
|
process.stdout.write(ALT_LEAVE)
|
|
865
|
+
if (process.stdout.isTTY) {
|
|
866
|
+
process.stdout.flush && process.stdout.flush()
|
|
867
|
+
}
|
|
868
868
|
}
|
|
869
869
|
|
|
870
870
|
const overlays = createOverlayRenderers(state, {
|
|
@@ -877,7 +877,6 @@ async function main() {
|
|
|
877
877
|
getProxySettings,
|
|
878
878
|
resolveApiKeys,
|
|
879
879
|
isProviderEnabled,
|
|
880
|
-
listProfiles,
|
|
881
880
|
TIER_CYCLE,
|
|
882
881
|
SETTINGS_OVERLAY_BG,
|
|
883
882
|
HELP_OVERLAY_BG,
|
|
@@ -919,11 +918,6 @@ async function main() {
|
|
|
919
918
|
removeApiKey,
|
|
920
919
|
persistApiKeysForProvider,
|
|
921
920
|
isProviderEnabled,
|
|
922
|
-
listProfiles,
|
|
923
|
-
loadProfile,
|
|
924
|
-
deleteProfile,
|
|
925
|
-
saveAsProfile,
|
|
926
|
-
setActiveProfile,
|
|
927
921
|
saveConfig,
|
|
928
922
|
getConfiguredInstallableProviders,
|
|
929
923
|
getInstallTargetModes,
|
|
@@ -1022,15 +1016,21 @@ async function main() {
|
|
|
1022
1016
|
? overlays.renderLog()
|
|
1023
1017
|
: state.changelogOpen
|
|
1024
1018
|
? overlays.renderChangelog()
|
|
1025
|
-
|
|
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, 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)
|
|
1026
1020
|
process.stdout.write(ALT_HOME + content)
|
|
1021
|
+
if (process.stdout.isTTY) {
|
|
1022
|
+
process.stdout.flush && process.stdout.flush()
|
|
1023
|
+
}
|
|
1027
1024
|
}, Math.round(1000 / FPS))
|
|
1028
1025
|
|
|
1029
1026
|
// 📖 Populate visibleSorted before the first frame so Enter works immediately
|
|
1030
1027
|
const initialVisible = state.results.filter(r => !r.hidden)
|
|
1031
1028
|
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
|
|
1032
1029
|
|
|
1033
|
-
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.
|
|
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, 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))
|
|
1031
|
+
if (process.stdout.isTTY) {
|
|
1032
|
+
process.stdout.flush && process.stdout.flush()
|
|
1033
|
+
}
|
|
1034
1034
|
|
|
1035
1035
|
// 📖 If --recommend was passed, auto-open the Smart Recommend overlay on start
|
|
1036
1036
|
if (cliArgs.recommendMode) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
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",
|
package/src/cli-help.js
CHANGED
|
@@ -37,7 +37,7 @@ const ANALYSIS_FLAGS = [
|
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
const CONFIG_FLAGS = [
|
|
40
|
-
{ flag: '--
|
|
40
|
+
{ flag: '--proxy', description: 'Start FCM Proxy V2 in foreground with live dashboard (no daemon)' },
|
|
41
41
|
{ flag: '--no-telemetry', description: 'Disable anonymous telemetry for this run' },
|
|
42
42
|
{ flag: '--clean-proxy, --proxy-clean', description: 'Remove persisted fcm-proxy config from OpenCode' },
|
|
43
43
|
{ flag: '--help, -h', description: 'Print this help and exit' },
|
package/src/config.js
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
* "consentVersion": 1,
|
|
60
60
|
* "anonymousId": "anon_550e8400-e29b-41d4-a716-446655440000"
|
|
61
61
|
* },
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
* "profiles": {
|
|
64
64
|
* "work": { "apiKeys": {...}, "providers": {...}, "favorites": [...], "settings": {...} },
|
|
65
65
|
* "personal": { "apiKeys": {...}, "providers": {...}, "favorites": [...], "settings": {...} },
|
|
@@ -70,15 +70,7 @@
|
|
|
70
70
|
* ]
|
|
71
71
|
* }
|
|
72
72
|
*
|
|
73
|
-
|
|
74
|
-
* - apiKeys: API keys per provider (can differ between work/personal setups)
|
|
75
|
-
* - providers: enabled/disabled state per provider
|
|
76
|
-
* - favorites: list of pinned favorite models
|
|
77
|
-
* - settings: extra TUI preferences (tierFilter, sortColumn, sortAsc, pingInterval, hideUnconfiguredModels, preferredToolMode, proxy)
|
|
78
|
-
*
|
|
79
|
-
* 📖 When a profile is loaded via --profile <name> or Shift+P, the main config's
|
|
80
|
-
* apiKeys/providers/favorites are replaced with the profile's values. The profile
|
|
81
|
-
* data itself stays in the profiles section — it's a named snapshot, not a fork.
|
|
73
|
+
|
|
82
74
|
*
|
|
83
75
|
* 📖 Migration: On first run, if the old plain-text ~/.free-coding-models exists
|
|
84
76
|
* and the new JSON file does not, the old key is auto-migrated as the nvidia key.
|
|
@@ -95,21 +87,14 @@
|
|
|
95
87
|
* → buildPersistedConfig(incomingConfig, diskConfig, options?) — Merge a live snapshot with the latest disk state safely
|
|
96
88
|
* → replaceConfigContents(targetConfig, nextConfig) — Refresh an in-memory config object from a normalized snapshot
|
|
97
89
|
* → persistApiKeysForProvider(config, providerKey) — Persist one provider's API keys without clobbering the rest of the file
|
|
98
|
-
|
|
99
|
-
* → loadProfile(config, name) — Apply a named profile's values onto the live config
|
|
100
|
-
* → listProfiles(config) — Return array of profile names
|
|
101
|
-
* → deleteProfile(config, name) — Remove a named profile
|
|
102
|
-
* → getActiveProfileName(config) — Get the currently active profile name (or null)
|
|
103
|
-
* → setActiveProfile(config, name) — Set which profile is active (null to clear)
|
|
104
|
-
* → _emptyProfileSettings() — Default TUI settings for a profile
|
|
90
|
+
|
|
105
91
|
* → getProxySettings(config) — Return normalized proxy settings from config
|
|
106
92
|
* → setClaudeProxyModelRouting(config, modelId) — Mirror free-claude-code MODEL/MODEL_* routing onto one selected FCM model
|
|
107
93
|
* → normalizeEndpointInstalls(endpointInstalls) — Keep tracked endpoint installs stable across app versions
|
|
108
94
|
*
|
|
109
95
|
* @exports loadConfig, saveConfig, validateConfigFile, getApiKey, isProviderEnabled
|
|
110
96
|
* @exports addApiKey, removeApiKey, listApiKeys — multi-key management helpers
|
|
111
|
-
* @exports
|
|
112
|
-
* @exports getActiveProfileName, setActiveProfile, getProxySettings, setClaudeProxyModelRouting, normalizeEndpointInstalls
|
|
97
|
+
* @exports getProxySettings, setClaudeProxyModelRouting, normalizeEndpointInstalls
|
|
113
98
|
* @exports buildPersistedConfig, replaceConfigContents, persistApiKeysForProvider
|
|
114
99
|
* @exports CONFIG_PATH — path to the JSON config file
|
|
115
100
|
*
|
|
@@ -254,20 +239,7 @@ function normalizeProfileSettings(settings) {
|
|
|
254
239
|
}
|
|
255
240
|
}
|
|
256
241
|
|
|
257
|
-
|
|
258
|
-
if (!isPlainObject(profiles)) return {}
|
|
259
|
-
const normalized = {}
|
|
260
|
-
for (const [profileName, profile] of Object.entries(profiles)) {
|
|
261
|
-
if (!isPlainObject(profile)) continue
|
|
262
|
-
normalized[profileName] = {
|
|
263
|
-
apiKeys: normalizeApiKeysSection(profile.apiKeys),
|
|
264
|
-
providers: normalizeProvidersSection(profile.providers),
|
|
265
|
-
favorites: normalizeFavoriteList(profile.favorites),
|
|
266
|
-
settings: normalizeProfileSettings(profile.settings),
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return normalized
|
|
270
|
-
}
|
|
242
|
+
|
|
271
243
|
|
|
272
244
|
function normalizeConfigShape(config) {
|
|
273
245
|
const safeConfig = isPlainObject(config) ? config : {}
|
|
@@ -278,10 +250,8 @@ function normalizeConfigShape(config) {
|
|
|
278
250
|
favorites: normalizeFavoriteList(safeConfig.favorites),
|
|
279
251
|
telemetry: normalizeTelemetrySection(safeConfig.telemetry),
|
|
280
252
|
endpointInstalls: normalizeEndpointInstalls(safeConfig.endpointInstalls),
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
: null,
|
|
284
|
-
profiles: normalizeProfilesSection(safeConfig.profiles),
|
|
253
|
+
|
|
254
|
+
|
|
285
255
|
}
|
|
286
256
|
}
|
|
287
257
|
|
|
@@ -320,48 +290,13 @@ function mergeEndpointInstalls(diskEndpointInstalls, incomingEndpointInstalls) {
|
|
|
320
290
|
}
|
|
321
291
|
|
|
322
292
|
function mergeProfiles(diskProfiles, incomingProfiles, options = {}) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const normalizedDiskProfiles = normalizeProfilesSection(diskProfiles)
|
|
326
|
-
const normalizedIncomingProfiles = normalizeProfilesSection(incomingProfiles)
|
|
327
|
-
const mergedProfiles = {}
|
|
328
|
-
const profileNames = new Set([
|
|
329
|
-
...Object.keys(normalizedDiskProfiles),
|
|
330
|
-
...Object.keys(normalizedIncomingProfiles),
|
|
331
|
-
])
|
|
332
|
-
|
|
333
|
-
for (const profileName of profileNames) {
|
|
334
|
-
if (removedProfileNames.has(profileName)) continue
|
|
335
|
-
|
|
336
|
-
const diskProfile = normalizedDiskProfiles[profileName]
|
|
337
|
-
const incomingProfile = normalizedIncomingProfiles[profileName]
|
|
338
|
-
if (!incomingProfile) {
|
|
339
|
-
if (diskProfile) mergedProfiles[profileName] = cloneConfigValue(diskProfile)
|
|
340
|
-
continue
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (!diskProfile || replaceProfileNames.has(profileName)) {
|
|
344
|
-
mergedProfiles[profileName] = cloneConfigValue(incomingProfile)
|
|
345
|
-
continue
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
mergedProfiles[profileName] = {
|
|
349
|
-
apiKeys: { ...diskProfile.apiKeys, ...incomingProfile.apiKeys },
|
|
350
|
-
providers: { ...diskProfile.providers, ...incomingProfile.providers },
|
|
351
|
-
favorites: mergeOrderedUniqueStrings(incomingProfile.favorites, diskProfile.favorites),
|
|
352
|
-
settings: normalizeProfileSettings({
|
|
353
|
-
...diskProfile.settings,
|
|
354
|
-
...incomingProfile.settings,
|
|
355
|
-
}),
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return mergedProfiles
|
|
293
|
+
// 📖 Profile system removed - return empty object
|
|
294
|
+
return {}
|
|
360
295
|
}
|
|
361
296
|
|
|
362
297
|
/**
|
|
363
298
|
* 📖 buildPersistedConfig merges the latest disk snapshot with the in-memory config so
|
|
364
|
-
* 📖 stale writers do not accidentally wipe secrets
|
|
299
|
+
* 📖 stale writers do not accidentally wipe secrets or favorites they did not touch.
|
|
365
300
|
*
|
|
366
301
|
* @param {object} incomingConfig
|
|
367
302
|
* @param {object} [diskConfig=_emptyConfig()]
|
|
@@ -389,15 +324,8 @@ export function buildPersistedConfig(incomingConfig, diskConfig = _emptyConfig()
|
|
|
389
324
|
endpointInstalls: options.replaceEndpointInstalls === true
|
|
390
325
|
? cloneConfigValue(normalizedIncoming.endpointInstalls)
|
|
391
326
|
: mergeEndpointInstalls(normalizedDisk.endpointInstalls, normalizedIncoming.endpointInstalls),
|
|
392
|
-
|
|
393
|
-
profiles: mergeProfiles(normalizedDisk.profiles, normalizedIncoming.profiles, {
|
|
394
|
-
replaceProfileNames: options.replaceProfileNames,
|
|
395
|
-
removedProfileNames: options.removedProfileNames,
|
|
396
|
-
}),
|
|
397
|
-
}
|
|
327
|
+
// 📖 Profile system removed - always null
|
|
398
328
|
|
|
399
|
-
if (merged.activeProfile && !merged.profiles[merged.activeProfile]) {
|
|
400
|
-
merged.activeProfile = null
|
|
401
329
|
}
|
|
402
330
|
|
|
403
331
|
return normalizeConfigShape(merged)
|
|
@@ -435,32 +363,12 @@ export function persistApiKeysForProvider(config, providerKey) {
|
|
|
435
363
|
const latestConfig = readStoredConfigSnapshot()
|
|
436
364
|
const normalizedProviderValue = normalizeApiKeyValue(config?.apiKeys?.[providerKey])
|
|
437
365
|
|
|
438
|
-
latestConfig.activeProfile = typeof config?.activeProfile === 'string' && config.activeProfile.trim()
|
|
439
|
-
? config.activeProfile.trim()
|
|
440
|
-
: null
|
|
441
|
-
|
|
442
366
|
if (normalizedProviderValue === null) delete latestConfig.apiKeys[providerKey]
|
|
443
367
|
else latestConfig.apiKeys[providerKey] = cloneConfigValue(normalizedProviderValue)
|
|
444
368
|
|
|
445
|
-
if (latestConfig.activeProfile) {
|
|
446
|
-
if (!latestConfig.profiles[latestConfig.activeProfile]) {
|
|
447
|
-
latestConfig.profiles[latestConfig.activeProfile] = config?.profiles?.[latestConfig.activeProfile]
|
|
448
|
-
? cloneConfigValue(config.profiles[latestConfig.activeProfile])
|
|
449
|
-
: {
|
|
450
|
-
apiKeys: {},
|
|
451
|
-
providers: {},
|
|
452
|
-
favorites: [],
|
|
453
|
-
settings: _emptyProfileSettings(),
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (normalizedProviderValue === null) delete latestConfig.profiles[latestConfig.activeProfile].apiKeys[providerKey]
|
|
458
|
-
else latestConfig.profiles[latestConfig.activeProfile].apiKeys[providerKey] = cloneConfigValue(normalizedProviderValue)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
369
|
const saveResult = saveConfig(latestConfig, {
|
|
462
370
|
replaceApiKeys: true,
|
|
463
|
-
replaceProfileNames:
|
|
371
|
+
replaceProfileNames: [],
|
|
464
372
|
})
|
|
465
373
|
|
|
466
374
|
if (saveResult.success) replaceConfigContents(config, latestConfig)
|
|
@@ -924,13 +832,10 @@ export function isProviderEnabled(config, providerKey) {
|
|
|
924
832
|
return providerConfig.enabled !== false
|
|
925
833
|
}
|
|
926
834
|
|
|
927
|
-
|
|
835
|
+
|
|
928
836
|
|
|
929
837
|
/**
|
|
930
|
-
* 📖 _emptyProfileSettings: Default TUI settings
|
|
931
|
-
*
|
|
932
|
-
* 📖 These settings are saved/restored when switching profiles so each profile
|
|
933
|
-
* can have different sort, filter, and ping preferences.
|
|
838
|
+
* 📖 _emptyProfileSettings: Default TUI settings.
|
|
934
839
|
*
|
|
935
840
|
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, preferredToolMode: string }}
|
|
936
841
|
*/
|
|
@@ -1086,142 +991,15 @@ export function normalizeEndpointInstalls(endpointInstalls) {
|
|
|
1086
991
|
.filter(Boolean)
|
|
1087
992
|
}
|
|
1088
993
|
|
|
1089
|
-
|
|
1090
|
-
* 📖 saveAsProfile: Snapshot the current config state into a named profile.
|
|
1091
|
-
*
|
|
1092
|
-
* 📖 Takes the current apiKeys, providers, favorites, plus explicit TUI settings
|
|
1093
|
-
* and stores them under config.profiles[name]. Does NOT change activeProfile —
|
|
1094
|
-
* call setActiveProfile() separately if you want to switch to this profile.
|
|
1095
|
-
*
|
|
1096
|
-
* 📖 If a profile with the same name exists, it's overwritten.
|
|
1097
|
-
*
|
|
1098
|
-
* @param {object} config — Live config object (will be mutated)
|
|
1099
|
-
* @param {string} name — Profile name (e.g. 'work', 'personal', 'fast')
|
|
1100
|
-
* @param {object} [settings] — TUI settings to save (tierFilter, sortColumn, etc.)
|
|
1101
|
-
* @returns {object} The config object (for chaining)
|
|
1102
|
-
*/
|
|
1103
|
-
export function saveAsProfile(config, name, settings = null) {
|
|
1104
|
-
if (!config.profiles || typeof config.profiles !== 'object') config.profiles = {}
|
|
1105
|
-
config.profiles[name] = {
|
|
1106
|
-
apiKeys: JSON.parse(JSON.stringify(config.apiKeys || {})),
|
|
1107
|
-
providers: JSON.parse(JSON.stringify(config.providers || {})),
|
|
1108
|
-
favorites: [...(config.favorites || [])],
|
|
1109
|
-
settings: settings ? { ..._emptyProfileSettings(), ...settings } : _emptyProfileSettings(),
|
|
1110
|
-
}
|
|
1111
|
-
return config
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/**
|
|
1115
|
-
* 📖 loadProfile: Apply a named profile's values onto the live config.
|
|
1116
|
-
*
|
|
1117
|
-
* 📖 Replaces config.apiKeys, config.providers, config.favorites with the
|
|
1118
|
-
* profile's stored values. Also sets config.activeProfile to the loaded name.
|
|
1119
|
-
*
|
|
1120
|
-
* 📖 Returns the profile's TUI settings so the caller (main CLI) can apply them
|
|
1121
|
-
* to the live state object (sortColumn, tierFilter, etc.).
|
|
1122
|
-
*
|
|
1123
|
-
* 📖 If the profile doesn't exist, returns null (caller should show an error).
|
|
1124
|
-
*
|
|
1125
|
-
* @param {object} config — Live config object (will be mutated)
|
|
1126
|
-
* @param {string} name — Profile name to load
|
|
1127
|
-
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number }|null}
|
|
1128
|
-
* The profile's TUI settings, or null if profile not found
|
|
1129
|
-
*/
|
|
1130
|
-
export function loadProfile(config, name) {
|
|
1131
|
-
const profile = config?.profiles?.[name]
|
|
1132
|
-
if (!profile) return null
|
|
1133
|
-
const nextSettings = profile.settings ? { ..._emptyProfileSettings(), ...profile.settings, proxy: normalizeProxySettings(profile.settings.proxy) } : _emptyProfileSettings()
|
|
1134
|
-
|
|
1135
|
-
// 📖 Deep-copy the profile data into the live config (don't share references)
|
|
1136
|
-
// 📖 IMPORTANT: MERGE apiKeys instead of replacing to preserve keys not in profile
|
|
1137
|
-
// 📖 Profile keys take priority over existing keys (allows profile-specific overrides)
|
|
1138
|
-
const profileApiKeys = profile.apiKeys || {}
|
|
1139
|
-
const mergedApiKeys = { ...config.apiKeys || {}, ...profileApiKeys }
|
|
1140
|
-
config.apiKeys = JSON.parse(JSON.stringify(mergedApiKeys))
|
|
1141
|
-
|
|
1142
|
-
// 📖 For providers, favorites: replace with profile values (these are profile-specific settings)
|
|
1143
|
-
config.providers = JSON.parse(JSON.stringify(profile.providers || {}))
|
|
1144
|
-
config.favorites = [...(profile.favorites || [])]
|
|
1145
|
-
config.settings = nextSettings
|
|
1146
|
-
config.activeProfile = name
|
|
1147
|
-
|
|
1148
|
-
return nextSettings
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
/**
|
|
1152
|
-
* 📖 listProfiles: Get all saved profile names.
|
|
1153
|
-
*
|
|
1154
|
-
* @param {object} config
|
|
1155
|
-
* @returns {string[]} Array of profile names, sorted alphabetically
|
|
1156
|
-
*/
|
|
1157
|
-
export function listProfiles(config) {
|
|
1158
|
-
if (!config?.profiles || typeof config.profiles !== 'object') return []
|
|
1159
|
-
return Object.keys(config.profiles).sort()
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
/**
|
|
1163
|
-
* 📖 deleteProfile: Remove a named profile from the config.
|
|
1164
|
-
*
|
|
1165
|
-
* 📖 If the deleted profile is the active one, clears activeProfile.
|
|
1166
|
-
*
|
|
1167
|
-
* @param {object} config — Live config object (will be mutated)
|
|
1168
|
-
* @param {string} name — Profile name to delete
|
|
1169
|
-
* @returns {boolean} True if the profile existed and was deleted
|
|
1170
|
-
*/
|
|
1171
|
-
export function deleteProfile(config, name) {
|
|
1172
|
-
if (!config?.profiles?.[name]) return false
|
|
1173
|
-
delete config.profiles[name]
|
|
1174
|
-
if (config.activeProfile === name) config.activeProfile = null
|
|
1175
|
-
return true
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
/**
|
|
1179
|
-
* 📖 getActiveProfileName: Get the currently active profile name.
|
|
1180
|
-
*
|
|
1181
|
-
* @param {object} config
|
|
1182
|
-
* @returns {string|null} Profile name, or null if no profile is active
|
|
1183
|
-
*/
|
|
1184
|
-
export function getActiveProfileName(config) {
|
|
1185
|
-
return config?.activeProfile || null
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
/**
|
|
1189
|
-
* 📖 setActiveProfile: Set which profile is active (or null to clear).
|
|
1190
|
-
*
|
|
1191
|
-
* 📖 This just stores the name — it does NOT load the profile's data.
|
|
1192
|
-
* Call loadProfile() first to actually apply the profile's values.
|
|
1193
|
-
*
|
|
1194
|
-
* @param {object} config — Live config object (will be mutated)
|
|
1195
|
-
* @param {string|null} name — Profile name, or null to clear
|
|
1196
|
-
*/
|
|
1197
|
-
export function setActiveProfile(config, name) {
|
|
1198
|
-
config.activeProfile = name || null
|
|
1199
|
-
}
|
|
994
|
+
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1200
995
|
|
|
1201
996
|
// 📖 Internal helper: create a blank config with the right shape
|
|
1202
997
|
function _emptyConfig() {
|
|
1203
998
|
return {
|
|
1204
999
|
apiKeys: {},
|
|
1205
|
-
providers: {},
|
|
1206
|
-
// 📖 Global TUI preferences that should persist even without a named profile.
|
|
1207
|
-
settings: {
|
|
1208
|
-
hideUnconfiguredModels: true,
|
|
1209
|
-
proxy: normalizeProxySettings(),
|
|
1210
|
-
disableWidthsWarning: false, // 📖 Disable widths warning toggle (default off)
|
|
1211
|
-
},
|
|
1212
|
-
// 📖 Pinned favorites rendered at top of the table ("providerKey/modelId").
|
|
1213
1000
|
favorites: [],
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
consentVersion: 0,
|
|
1218
|
-
anonymousId: null,
|
|
1219
|
-
},
|
|
1220
|
-
// 📖 Tracked `Y` installs — used to refresh external tool catalogs automatically.
|
|
1221
|
-
endpointInstalls: [],
|
|
1222
|
-
// 📖 Active profile name — null means no profile is loaded (using raw config).
|
|
1223
|
-
activeProfile: null,
|
|
1224
|
-
// 📖 Named profiles: each is a snapshot of apiKeys + providers + favorites + settings.
|
|
1225
|
-
profiles: {},
|
|
1001
|
+
proxySettings: { enabled: false, routing: {} },
|
|
1002
|
+
endpointInstalls: {},
|
|
1003
|
+
settings: _emptyProfileSettings(),
|
|
1226
1004
|
}
|
|
1227
1005
|
}
|
package/src/error-classifier.js
CHANGED
|
@@ -72,8 +72,11 @@ export function classifyError(statusCode, body, headers) {
|
|
|
72
72
|
return { type: ErrorType.NETWORK_ERROR, retryAfterSec: 5, shouldRetry: true, skipAccount: false }
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// 📖 401/403 from upstream providers: skip this account and try the next one.
|
|
76
|
+
// 📖 A provider may reject a valid key for specific models (e.g. nvidia 403 on gpt-oss-120b)
|
|
77
|
+
// 📖 while another provider with the same proxyModelId may accept it.
|
|
75
78
|
if (statusCode === 401 || statusCode === 403) {
|
|
76
|
-
return { type: ErrorType.AUTH_ERROR, retryAfterSec: null, shouldRetry:
|
|
79
|
+
return { type: ErrorType.AUTH_ERROR, retryAfterSec: null, shouldRetry: true, skipAccount: true }
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
// Provider-level 404/410: model not found / inaccessible / not deployed on this account.
|
package/src/favorites.js
CHANGED
|
@@ -85,34 +85,20 @@ export function syncFavoriteFlags(results, config) {
|
|
|
85
85
|
*/
|
|
86
86
|
export function toggleFavoriteModel(config, providerKey, modelId) {
|
|
87
87
|
const latestConfig = loadConfig()
|
|
88
|
-
latestConfig.activeProfile = typeof config?.activeProfile === 'string' && config.activeProfile.trim()
|
|
89
|
-
? config.activeProfile.trim()
|
|
90
|
-
: latestConfig.activeProfile
|
|
91
88
|
ensureFavoritesConfig(latestConfig)
|
|
92
|
-
if (latestConfig.activeProfile && !latestConfig.profiles?.[latestConfig.activeProfile] && config?.profiles?.[latestConfig.activeProfile]) {
|
|
93
|
-
latestConfig.profiles[latestConfig.activeProfile] = JSON.parse(JSON.stringify(config.profiles[latestConfig.activeProfile]))
|
|
94
|
-
}
|
|
95
89
|
const favoriteKey = toFavoriteKey(providerKey, modelId)
|
|
96
90
|
const existingIndex = latestConfig.favorites.indexOf(favoriteKey)
|
|
97
91
|
if (existingIndex >= 0) {
|
|
98
92
|
latestConfig.favorites.splice(existingIndex, 1)
|
|
99
|
-
if (latestConfig.activeProfile && latestConfig.profiles?.[latestConfig.activeProfile]) {
|
|
100
|
-
latestConfig.profiles[latestConfig.activeProfile].favorites = [...latestConfig.favorites]
|
|
101
|
-
}
|
|
102
93
|
const saveResult = saveConfig(latestConfig, {
|
|
103
94
|
replaceFavorites: true,
|
|
104
|
-
replaceProfileNames: latestConfig.activeProfile ? [latestConfig.activeProfile] : [],
|
|
105
95
|
})
|
|
106
96
|
if (saveResult.success) replaceConfigContents(config, latestConfig)
|
|
107
97
|
return false
|
|
108
98
|
}
|
|
109
99
|
latestConfig.favorites.push(favoriteKey)
|
|
110
|
-
if (latestConfig.activeProfile && latestConfig.profiles?.[latestConfig.activeProfile]) {
|
|
111
|
-
latestConfig.profiles[latestConfig.activeProfile].favorites = [...latestConfig.favorites]
|
|
112
|
-
}
|
|
113
100
|
const saveResult = saveConfig(latestConfig, {
|
|
114
101
|
replaceFavorites: true,
|
|
115
|
-
replaceProfileNames: latestConfig.activeProfile ? [latestConfig.activeProfile] : [],
|
|
116
102
|
})
|
|
117
103
|
if (saveResult.success) replaceConfigContents(config, latestConfig)
|
|
118
104
|
return true
|