free-coding-models 0.3.6 → 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/src/config.js CHANGED
@@ -59,7 +59,7 @@
59
59
  * "consentVersion": 1,
60
60
  * "anonymousId": "anon_550e8400-e29b-41d4-a716-446655440000"
61
61
  * },
62
- * "activeProfile": "work",
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
- * 📖 Profiles store a snapshot of the user's configuration. Each profile contains:
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
- * → saveAsProfile(config, name) — Snapshot current apiKeys/providers/favorites/settings into a named profile
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 saveAsProfile, loadProfile, listProfiles, deleteProfile
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
- function normalizeProfilesSection(profiles) {
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
- activeProfile: typeof safeConfig.activeProfile === 'string' && safeConfig.activeProfile.trim()
282
- ? safeConfig.activeProfile.trim()
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
- const replaceProfileNames = new Set((options.replaceProfileNames || []).filter((name) => typeof name === 'string' && name.length > 0))
324
- const removedProfileNames = new Set((options.removedProfileNames || []).filter((name) => typeof name === 'string' && name.length > 0))
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, favorites, or profiles they did not touch.
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
- activeProfile: normalizedIncoming.activeProfile,
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: latestConfig.activeProfile ? [latestConfig.activeProfile] : [],
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
- // ─── Config Profiles ──────────────────────────────────────────────────────────
835
+
928
836
 
929
837
  /**
930
- * 📖 _emptyProfileSettings: Default TUI settings stored in a profile.
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
- // 📖 Telemetry consent is explicit. null = not decided yet.
1215
- telemetry: {
1216
- enabled: null,
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
  }
@@ -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: false, skipAccount: true }
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