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/CHANGELOG.md +32 -0
- package/README.md +43 -0
- package/bin/free-coding-models.js +75 -33
- package/package.json +1 -1
- package/src/cli-help.js +9 -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 +10 -37
- 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/token-usage-reader.js +53 -11
- package/src/utils.js +44 -9
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
|