@kikkimo/claude-launcher 2.5.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +23 -13
- package/claude-launcher +1244 -432
- package/docs/README-zh.md +23 -13
- package/lib/api-manager.js +629 -70
- package/lib/auth/password-input.js +8 -4
- package/lib/auth/password-validator.js +83 -48
- package/lib/i18n/index.js +4 -3
- package/lib/i18n/language-manager.js +4 -3
- package/lib/i18n/locales/de.js +229 -13
- package/lib/i18n/locales/en.js +235 -13
- package/lib/i18n/locales/es.js +229 -13
- package/lib/i18n/locales/fr.js +229 -13
- package/lib/i18n/locales/it.js +229 -13
- package/lib/i18n/locales/ja.js +229 -13
- package/lib/i18n/locales/ko.js +229 -13
- package/lib/i18n/locales/pt.js +229 -13
- package/lib/i18n/locales/ru.js +229 -13
- package/lib/i18n/locales/zh-TW.js +229 -13
- package/lib/i18n/locales/zh.js +235 -13
- package/lib/launcher.js +167 -110
- package/lib/presets/providers.js +143 -39
- package/lib/ui/api-editor.js +668 -0
- package/lib/ui/i18n-labels.js +16 -0
- package/lib/ui/interactive-table.js +216 -99
- package/lib/ui/menu.js +79 -62
- package/lib/ui/prompts.js +168 -139
- package/lib/ui/screen.js +125 -0
- package/lib/utils/stdin-manager.js +11 -9
- package/lib/utils/version-checker.js +65 -4
- package/lib/validators.js +102 -1
- package/package.json +2 -2
- package/docs/superpowers/plans/2026-03-31-update-models-and-auto-mode.md +0 -1414
- package/docs/superpowers/specs/2026-03-31-update-models-and-auto-mode-design.md +0 -187
package/lib/api-manager.js
CHANGED
|
@@ -6,49 +6,210 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const { encrypt, decrypt } = require('./crypto');
|
|
9
|
-
const { validateBaseUrl, validateAuthToken, validateModel } = require('./validators');
|
|
9
|
+
const { validateBaseUrl, validateAuthToken, validateModel, validateApiName } = require('./validators');
|
|
10
|
+
const screen = require('./ui/screen');
|
|
11
|
+
|
|
12
|
+
class DuplicateApiError extends Error {
|
|
13
|
+
constructor(existingApi) {
|
|
14
|
+
super(`Duplicate API: ${existingApi.name}`);
|
|
15
|
+
this.name = 'DuplicateApiError';
|
|
16
|
+
this.code = 'DUPLICATE_API';
|
|
17
|
+
this.existingApiId = existingApi.id;
|
|
18
|
+
this.existingApiName = existingApi.name;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MODEL_CONFIG_LABELS = {
|
|
23
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: 'Regular Model (Sonnet)',
|
|
24
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: 'Heavy Model (Opus)',
|
|
25
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'Fast Model (Haiku)',
|
|
26
|
+
CLAUDE_CODE_SUBAGENT_MODEL: 'Subagent Model',
|
|
27
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION: 'Custom Model',
|
|
28
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: 'Custom Model Name',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const RUNTIME_CONFIG_LABELS = {
|
|
32
|
+
API_TIMEOUT_MS: 'Request Timeout',
|
|
33
|
+
CLAUDE_CODE_ATTRIBUTION_HEADER: 'Output Attribution',
|
|
34
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 'Reduce Non-Essential Traffic',
|
|
35
|
+
CLAUDE_CODE_EFFORT_LEVEL: 'Reasoning Effort',
|
|
36
|
+
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: 'Experimental Features',
|
|
37
|
+
CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: 'Disable Non-Streaming Fallback',
|
|
38
|
+
};
|
|
10
39
|
|
|
11
40
|
class ApiManager {
|
|
12
41
|
constructor() {
|
|
13
42
|
this.configFile = path.join(os.homedir(), '.claude-launcher-apis.json');
|
|
14
|
-
|
|
43
|
+
const { config, migrated } = this.loadConfig();
|
|
44
|
+
this.config = config;
|
|
45
|
+
if (migrated) {
|
|
46
|
+
this.saveConfig();
|
|
47
|
+
}
|
|
15
48
|
}
|
|
16
49
|
|
|
17
50
|
/**
|
|
18
51
|
* Load configuration from encrypted file
|
|
19
52
|
*/
|
|
20
53
|
loadConfig() {
|
|
54
|
+
let migrated = false;
|
|
21
55
|
try {
|
|
22
56
|
if (fs.existsSync(this.configFile)) {
|
|
23
57
|
const encryptedData = fs.readFileSync(this.configFile, 'utf8');
|
|
24
58
|
const decrypted = decrypt(encryptedData);
|
|
25
59
|
if (decrypted.success) {
|
|
26
60
|
const config = JSON.parse(decrypted.value);
|
|
27
|
-
|
|
28
|
-
if (!config.hasOwnProperty('
|
|
29
|
-
config.exportPassword = null;
|
|
30
|
-
}
|
|
31
|
-
if (!config.hasOwnProperty('passwordSkipped')) {
|
|
32
|
-
config.passwordSkipped = false;
|
|
33
|
-
}
|
|
61
|
+
if (!config.hasOwnProperty('exportPassword')) config.exportPassword = null;
|
|
62
|
+
if (!config.hasOwnProperty('passwordSkipped')) config.passwordSkipped = false;
|
|
34
63
|
|
|
35
|
-
|
|
64
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
65
|
+
for (const api of config.apis || []) {
|
|
66
|
+
migrated = this._migrateApiEntry(api, PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS) || migrated;
|
|
67
|
+
}
|
|
68
|
+
return { config, migrated };
|
|
36
69
|
}
|
|
37
70
|
}
|
|
38
71
|
} catch (error) {
|
|
39
|
-
|
|
72
|
+
screen.debug(`[!] Could not load API config: ${error.message}`);
|
|
40
73
|
}
|
|
41
74
|
|
|
42
75
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
76
|
+
config: {
|
|
77
|
+
apis: [],
|
|
78
|
+
activeIndex: -1,
|
|
79
|
+
version: '2.0.0',
|
|
80
|
+
createdAt: new Date().toISOString(),
|
|
81
|
+
exportPassword: null,
|
|
82
|
+
passwordSkipped: false,
|
|
83
|
+
},
|
|
84
|
+
migrated: false,
|
|
49
85
|
};
|
|
50
86
|
}
|
|
51
87
|
|
|
88
|
+
_migrateApiEntry(api, MODEL_KEYS, RUNTIME_KEYS) {
|
|
89
|
+
const before = JSON.stringify({
|
|
90
|
+
modelEnvVars: api.modelEnvVars,
|
|
91
|
+
_autoModelEnvVars: api._autoModelEnvVars,
|
|
92
|
+
runtimeEnvVars: api.runtimeEnvVars,
|
|
93
|
+
_runtimeEnvSources: api._runtimeEnvSources,
|
|
94
|
+
customEnvVars: api.customEnvVars,
|
|
95
|
+
smallFastModel: api.smallFastModel,
|
|
96
|
+
_autoFilledModel: api._autoFilledModel,
|
|
97
|
+
});
|
|
98
|
+
this._normalizeApiFields(api);
|
|
99
|
+
const after = JSON.stringify({
|
|
100
|
+
modelEnvVars: api.modelEnvVars,
|
|
101
|
+
_autoModelEnvVars: api._autoModelEnvVars,
|
|
102
|
+
runtimeEnvVars: api.runtimeEnvVars,
|
|
103
|
+
_runtimeEnvSources: api._runtimeEnvSources,
|
|
104
|
+
customEnvVars: api.customEnvVars,
|
|
105
|
+
smallFastModel: api.smallFastModel,
|
|
106
|
+
_autoFilledModel: api._autoFilledModel,
|
|
107
|
+
});
|
|
108
|
+
return before !== after;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_normalizeApiFields(api) {
|
|
112
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
113
|
+
const { getProvider } = require('./presets/providers');
|
|
114
|
+
|
|
115
|
+
const effectiveModel = api._autoFilledModel || api.model;
|
|
116
|
+
const hadAutoModelEnvVars = !!api._autoModelEnvVars;
|
|
117
|
+
|
|
118
|
+
const providerConfig = getProvider(api.provider);
|
|
119
|
+
let template;
|
|
120
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
121
|
+
template = providerConfig.modelEnvTemplate.getValues(effectiveModel);
|
|
122
|
+
} else {
|
|
123
|
+
template = {};
|
|
124
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) template[k] = effectiveModel;
|
|
125
|
+
template.smallFastModel = effectiveModel;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let smallFastWasFixed = false;
|
|
129
|
+
|
|
130
|
+
// _autoModelEnvVars — full rebuild via template
|
|
131
|
+
if (!api._autoModelEnvVars) {
|
|
132
|
+
api._autoModelEnvVars = { ...template };
|
|
133
|
+
} else {
|
|
134
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
135
|
+
if (!(k in api._autoModelEnvVars) || typeof api._autoModelEnvVars[k] !== 'string') {
|
|
136
|
+
api._autoModelEnvVars[k] = template[k] || '';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const k of Object.keys(api._autoModelEnvVars)) {
|
|
140
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(k) && k !== 'smallFastModel') {
|
|
141
|
+
delete api._autoModelEnvVars[k];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!('smallFastModel' in api._autoModelEnvVars) || typeof api._autoModelEnvVars.smallFastModel !== 'string') {
|
|
145
|
+
api._autoModelEnvVars.smallFastModel = template.smallFastModel;
|
|
146
|
+
smallFastWasFixed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// modelEnvVars — fill with template actual values (NOT "")
|
|
151
|
+
if (!api.modelEnvVars) {
|
|
152
|
+
api.modelEnvVars = {};
|
|
153
|
+
}
|
|
154
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
155
|
+
if (!(k in api.modelEnvVars) || typeof api.modelEnvVars[k] !== 'string') {
|
|
156
|
+
api.modelEnvVars[k] = template[k] || '';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const k of Object.keys(api.modelEnvVars)) {
|
|
160
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(k)) {
|
|
161
|
+
delete api.modelEnvVars[k];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// smallFastModel — sync with template
|
|
166
|
+
if (!api.smallFastModel || typeof api.smallFastModel !== 'string'
|
|
167
|
+
|| !hadAutoModelEnvVars || smallFastWasFixed) {
|
|
168
|
+
api.smallFastModel = template.smallFastModel;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// runtimeEnvVars — fill "" not provider values
|
|
172
|
+
if (!api.runtimeEnvVars) {
|
|
173
|
+
api.runtimeEnvVars = {};
|
|
174
|
+
}
|
|
175
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
176
|
+
if (!(k in api.runtimeEnvVars) || typeof api.runtimeEnvVars[k] !== 'string') {
|
|
177
|
+
api.runtimeEnvVars[k] = '';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// _runtimeEnvSources — missing → "auto"
|
|
182
|
+
if (!api._runtimeEnvSources) {
|
|
183
|
+
api._runtimeEnvSources = {};
|
|
184
|
+
}
|
|
185
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
186
|
+
if (!(k in api._runtimeEnvSources)) {
|
|
187
|
+
api._runtimeEnvSources[k] = 'auto';
|
|
188
|
+
}
|
|
189
|
+
if (api._runtimeEnvSources[k] !== 'auto' && api._runtimeEnvSources[k] !== 'manual') {
|
|
190
|
+
api._runtimeEnvSources[k] = 'auto';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// runtime/source conflict resolution
|
|
195
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
196
|
+
if (api.runtimeEnvVars[k] !== '' && api._runtimeEnvSources[k] === 'auto') {
|
|
197
|
+
api.runtimeEnvVars[k] = '';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// customEnvVars
|
|
202
|
+
if (!api.customEnvVars) {
|
|
203
|
+
api.customEnvVars = {};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (api._autoFilledModel) {
|
|
207
|
+
delete api._autoFilledModel;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return api;
|
|
211
|
+
}
|
|
212
|
+
|
|
52
213
|
/**
|
|
53
214
|
* Save configuration to encrypted file
|
|
54
215
|
*/
|
|
@@ -60,11 +221,11 @@ class ApiManager {
|
|
|
60
221
|
fs.writeFileSync(this.configFile, encrypted.value);
|
|
61
222
|
return true;
|
|
62
223
|
} else {
|
|
63
|
-
|
|
224
|
+
screen.debug(`[!] Failed to save API config: ${encrypted.error}`);
|
|
64
225
|
return false;
|
|
65
226
|
}
|
|
66
227
|
} catch (error) {
|
|
67
|
-
|
|
228
|
+
screen.debug(`[!] Error saving API config: ${error.message}`);
|
|
68
229
|
return false;
|
|
69
230
|
}
|
|
70
231
|
}
|
|
@@ -96,6 +257,10 @@ class ApiManager {
|
|
|
96
257
|
* Add a new API configuration
|
|
97
258
|
*/
|
|
98
259
|
addApi(baseUrl, authToken, model, name, provider = 'custom') {
|
|
260
|
+
if (this.config.apis.length >= 99) {
|
|
261
|
+
throw new Error('Maximum 99 APIs supported. Remove unused APIs before adding new ones.');
|
|
262
|
+
}
|
|
263
|
+
|
|
99
264
|
// Validate inputs
|
|
100
265
|
const urlValidation = validateBaseUrl(baseUrl);
|
|
101
266
|
if (!urlValidation.valid) {
|
|
@@ -115,7 +280,7 @@ class ApiManager {
|
|
|
115
280
|
// Check for duplicates
|
|
116
281
|
const duplicate = this.checkDuplicate(baseUrl, authToken, model);
|
|
117
282
|
if (duplicate.isDuplicate) {
|
|
118
|
-
throw new
|
|
283
|
+
throw new DuplicateApiError(duplicate.existing);
|
|
119
284
|
}
|
|
120
285
|
|
|
121
286
|
// Encrypt the auth token before storing
|
|
@@ -124,6 +289,32 @@ class ApiManager {
|
|
|
124
289
|
throw new Error(`Failed to encrypt auth token: ${encryptedToken.error}`);
|
|
125
290
|
}
|
|
126
291
|
|
|
292
|
+
// Compute model env template values
|
|
293
|
+
const { getProvider } = require('./presets/providers');
|
|
294
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
295
|
+
const providerConfig = getProvider(provider);
|
|
296
|
+
let templateValues;
|
|
297
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
298
|
+
templateValues = providerConfig.modelEnvTemplate.getValues(modelValidation.value);
|
|
299
|
+
} else {
|
|
300
|
+
templateValues = {};
|
|
301
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = modelValidation.value;
|
|
302
|
+
templateValues.smallFastModel = modelValidation.value;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const modelEnvVars = {};
|
|
306
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
307
|
+
modelEnvVars[k] = templateValues[k] || '';
|
|
308
|
+
}
|
|
309
|
+
const _autoModelEnvVars = { ...templateValues };
|
|
310
|
+
|
|
311
|
+
const runtimeEnvVars = {};
|
|
312
|
+
const _runtimeEnvSources = {};
|
|
313
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
314
|
+
runtimeEnvVars[k] = '';
|
|
315
|
+
_runtimeEnvSources[k] = 'auto';
|
|
316
|
+
}
|
|
317
|
+
|
|
127
318
|
const newApi = {
|
|
128
319
|
id: Date.now().toString(),
|
|
129
320
|
name: name || `API-${this.config.apis.length + 1}`,
|
|
@@ -131,15 +322,18 @@ class ApiManager {
|
|
|
131
322
|
baseUrl: urlValidation.value,
|
|
132
323
|
authToken: encryptedToken.value,
|
|
133
324
|
model: modelValidation.value,
|
|
134
|
-
smallFastModel:
|
|
325
|
+
smallFastModel: templateValues.smallFastModel,
|
|
135
326
|
createdAt: new Date().toISOString(),
|
|
136
|
-
// Existing statistics fields
|
|
137
327
|
lastUsed: null,
|
|
138
328
|
usageCount: 0,
|
|
139
|
-
// New statistics fields
|
|
140
329
|
successCount: 0,
|
|
141
330
|
failCount: 0,
|
|
142
|
-
lastError: null
|
|
331
|
+
lastError: null,
|
|
332
|
+
modelEnvVars,
|
|
333
|
+
_autoModelEnvVars,
|
|
334
|
+
runtimeEnvVars,
|
|
335
|
+
_runtimeEnvSources,
|
|
336
|
+
customEnvVars: {},
|
|
143
337
|
};
|
|
144
338
|
|
|
145
339
|
this.config.apis.push(newApi);
|
|
@@ -241,17 +435,227 @@ class ApiManager {
|
|
|
241
435
|
* @returns {Object} The updated API object
|
|
242
436
|
*/
|
|
243
437
|
updateApiModel(apiId, newModel) {
|
|
438
|
+
return this.updateApiField(apiId, 'model', newModel);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Update a single field of an API configuration with validation
|
|
443
|
+
* @param {string} apiId - The API id
|
|
444
|
+
* @param {string} field - Field name: 'name', 'provider', 'baseUrl', 'model'
|
|
445
|
+
* @param {string} value - New value
|
|
446
|
+
* @returns {Object} The updated API object
|
|
447
|
+
*/
|
|
448
|
+
updateApiField(apiId, field, value) {
|
|
449
|
+
const allowedFields = ['name', 'provider', 'baseUrl', 'model'];
|
|
450
|
+
if (!allowedFields.includes(field)) {
|
|
451
|
+
throw new Error(`Field '${field}' is not allowed. Allowed: ${allowedFields.join(', ')}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
244
454
|
const index = this.config.apis.findIndex(api => api.id === apiId);
|
|
245
455
|
if (index === -1) {
|
|
246
456
|
throw new Error(`API not found: ${apiId}`);
|
|
247
457
|
}
|
|
248
458
|
|
|
249
|
-
this.config.apis[index]
|
|
250
|
-
|
|
459
|
+
const api = this.config.apis[index];
|
|
460
|
+
|
|
461
|
+
// Manager-level validation
|
|
462
|
+
switch (field) {
|
|
463
|
+
case 'name': {
|
|
464
|
+
if (!value || value.trim() === '') {
|
|
465
|
+
throw new Error('Name cannot be empty when editing');
|
|
466
|
+
}
|
|
467
|
+
const nameValidation = validateApiName(value);
|
|
468
|
+
if (!nameValidation.valid) {
|
|
469
|
+
throw new Error(`Invalid name: ${nameValidation.error}`);
|
|
470
|
+
}
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
case 'provider': {
|
|
474
|
+
const { getAllProviders } = require('./presets/providers');
|
|
475
|
+
const validIds = getAllProviders().map(p => p.id);
|
|
476
|
+
if (!validIds.includes(value)) {
|
|
477
|
+
throw new Error(`Unknown provider: ${value}. Valid: ${validIds.join(', ')}`);
|
|
478
|
+
}
|
|
479
|
+
return this.updateApiProvider(apiId, value).api;
|
|
480
|
+
}
|
|
481
|
+
case 'baseUrl': {
|
|
482
|
+
const urlValidation = validateBaseUrl(value);
|
|
483
|
+
if (!urlValidation.valid) {
|
|
484
|
+
throw new Error(`Invalid URL: ${urlValidation.error}`);
|
|
485
|
+
}
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
case 'model': {
|
|
489
|
+
const modelValidation = validateModel(value);
|
|
490
|
+
if (!modelValidation.valid) {
|
|
491
|
+
throw new Error(`Invalid model: ${modelValidation.error}`);
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Duplicate check for uniqueness-affecting fields
|
|
498
|
+
if (field === 'baseUrl' || field === 'model') {
|
|
499
|
+
const checkUrl = field === 'baseUrl' ? value : api.baseUrl;
|
|
500
|
+
const checkModel = field === 'model' ? value : api.model;
|
|
501
|
+
const decryptedToken = decrypt(api.authToken);
|
|
502
|
+
const tokenValue = decryptedToken.success ? decryptedToken.value : '';
|
|
503
|
+
|
|
504
|
+
// Check against all OTHER apis (exclude self)
|
|
505
|
+
const duplicate = this.config.apis.find((other, idx) => {
|
|
506
|
+
if (idx === index) return false;
|
|
507
|
+
const otherToken = decrypt(other.authToken);
|
|
508
|
+
const otherTokenValue = otherToken.success ? otherToken.value : '';
|
|
509
|
+
return other.baseUrl === checkUrl &&
|
|
510
|
+
otherTokenValue === tokenValue &&
|
|
511
|
+
other.model === checkModel;
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
if (duplicate) {
|
|
515
|
+
throw new Error(`Duplicate configuration: URL + Token + Model already exists for API '${duplicate.name}'`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Apply update
|
|
520
|
+
api[field] = value.trim();
|
|
521
|
+
if (field === 'model') {
|
|
522
|
+
const { getProvider } = require('./presets/providers');
|
|
523
|
+
const { PREDEFINED_MODEL_ENV_KEYS } = require('./validators');
|
|
524
|
+
const providerConfig = getProvider(api.provider);
|
|
525
|
+
let templateVals;
|
|
526
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
527
|
+
templateVals = providerConfig.modelEnvTemplate.getValues(value.trim());
|
|
528
|
+
} else {
|
|
529
|
+
templateVals = {};
|
|
530
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateVals[k] = value.trim();
|
|
531
|
+
templateVals.smallFastModel = value.trim();
|
|
532
|
+
}
|
|
533
|
+
if (api._autoModelEnvVars) {
|
|
534
|
+
if (!api.modelEnvVars) api.modelEnvVars = {};
|
|
535
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
536
|
+
if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) {
|
|
537
|
+
api.modelEnvVars[k] = templateVals[k] || '';
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) {
|
|
541
|
+
api.smallFastModel = templateVals.smallFastModel;
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
// No snapshot means all fields are auto — overwrite all
|
|
545
|
+
if (!api.modelEnvVars) api.modelEnvVars = {};
|
|
546
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
547
|
+
api.modelEnvVars[k] = templateVals[k] || '';
|
|
548
|
+
}
|
|
549
|
+
api.smallFastModel = templateVals.smallFastModel;
|
|
550
|
+
}
|
|
551
|
+
api._autoModelEnvVars = { ...templateVals };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
this.saveConfig();
|
|
555
|
+
return api;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
updateModelEnvVar(apiId, key, value) {
|
|
559
|
+
const { PREDEFINED_MODEL_ENV_KEYS } = require('./validators');
|
|
560
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(key)) throw new Error(`"${key}" is not a predefined model env key`);
|
|
561
|
+
if (typeof value !== 'string') throw new Error('model env value must be a string');
|
|
562
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
563
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
564
|
+
const api = this.config.apis[index];
|
|
565
|
+
const autoValue = api._autoModelEnvVars && typeof api._autoModelEnvVars[key] === 'string'
|
|
566
|
+
? api._autoModelEnvVars[key]
|
|
567
|
+
: '';
|
|
568
|
+
api.modelEnvVars[key] = value === '' ? autoValue : value;
|
|
569
|
+
this.saveConfig();
|
|
570
|
+
return api;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
updateRuntimeEnvVar(apiId, key, value, options = {}) {
|
|
574
|
+
const { PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue } = require('./validators');
|
|
575
|
+
if (!PREDEFINED_RUNTIME_KEYS.includes(key)) throw new Error(`"${key}" is not a predefined runtime env key`);
|
|
576
|
+
const validation = validateRuntimeEnvValue(key, value);
|
|
577
|
+
if (!validation.valid) throw new Error(`Invalid value for ${key}: ${validation.error}`);
|
|
578
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
579
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
580
|
+
if (options.source === 'auto') {
|
|
581
|
+
this.config.apis[index].runtimeEnvVars[key] = '';
|
|
582
|
+
this.config.apis[index]._runtimeEnvSources[key] = 'auto';
|
|
583
|
+
} else if (options.source === 'manual') {
|
|
584
|
+
this.config.apis[index].runtimeEnvVars[key] = value;
|
|
585
|
+
this.config.apis[index]._runtimeEnvSources[key] = 'manual';
|
|
586
|
+
} else {
|
|
587
|
+
this.config.apis[index].runtimeEnvVars[key] = value;
|
|
588
|
+
this.config.apis[index]._runtimeEnvSources[key] = (value === '') ? 'auto' : 'manual';
|
|
589
|
+
}
|
|
590
|
+
this.saveConfig();
|
|
591
|
+
return this.config.apis[index];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
setCustomEnvVar(apiId, key, value) {
|
|
595
|
+
const { validateEnvKey } = require('./validators');
|
|
596
|
+
const kv = validateEnvKey(key);
|
|
597
|
+
if (!kv.valid) throw new Error(`Custom env key "${key}" is reserved or invalid`);
|
|
598
|
+
if (typeof value !== 'string') throw new Error('Custom env value must be a string');
|
|
599
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
600
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
601
|
+
this.config.apis[index].customEnvVars[key] = value;
|
|
602
|
+
this.saveConfig();
|
|
603
|
+
return this.config.apis[index];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
deleteCustomEnvVar(apiId, key) {
|
|
607
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
608
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
609
|
+
delete this.config.apis[index].customEnvVars[key];
|
|
251
610
|
this.saveConfig();
|
|
252
611
|
return this.config.apis[index];
|
|
253
612
|
}
|
|
254
613
|
|
|
614
|
+
updateApiProvider(apiId, newProviderId) {
|
|
615
|
+
const { getProvider, getAllProviders, detectProvider } = require('./presets/providers');
|
|
616
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
617
|
+
const validIds = getAllProviders().map(p => p.id);
|
|
618
|
+
if (!validIds.includes(newProviderId)) throw new Error(`Unknown provider: ${newProviderId}`);
|
|
619
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
620
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
621
|
+
const api = this.config.apis[index];
|
|
622
|
+
const newProvider = getProvider(newProviderId);
|
|
623
|
+
const warnings = [];
|
|
624
|
+
if (!newProvider.models.includes(api.model)) {
|
|
625
|
+
warnings.push({ code: 'MODEL_NOT_IN_PROVIDER', messageArgs: { model: api.model, providerName: newProvider.name } });
|
|
626
|
+
}
|
|
627
|
+
if (detectProvider(api.baseUrl) !== newProviderId) {
|
|
628
|
+
warnings.push({ code: 'BASE_URL_NOT_UPDATED', messageArgs: { baseUrl: api.baseUrl } });
|
|
629
|
+
}
|
|
630
|
+
if (api._runtimeEnvSources) {
|
|
631
|
+
for (const [key, source] of Object.entries(api._runtimeEnvSources)) {
|
|
632
|
+
if (source === 'auto' && PREDEFINED_RUNTIME_KEYS.includes(key)) api.runtimeEnvVars[key] = '';
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
let templateValues;
|
|
636
|
+
if (newProvider.modelEnvTemplate) {
|
|
637
|
+
templateValues = newProvider.modelEnvTemplate.getValues(api.model);
|
|
638
|
+
} else {
|
|
639
|
+
templateValues = {};
|
|
640
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = api.model;
|
|
641
|
+
templateValues.smallFastModel = api.model;
|
|
642
|
+
}
|
|
643
|
+
if (api._autoModelEnvVars) {
|
|
644
|
+
if (!api.modelEnvVars) api.modelEnvVars = {};
|
|
645
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
646
|
+
if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) api.modelEnvVars[k] = templateValues[k] || '';
|
|
647
|
+
}
|
|
648
|
+
if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) api.smallFastModel = templateValues.smallFastModel;
|
|
649
|
+
}
|
|
650
|
+
api._autoModelEnvVars = { ...templateValues };
|
|
651
|
+
if (detectProvider(api.baseUrl) !== newProviderId && warnings.some(w => w.code === 'MODEL_NOT_IN_PROVIDER')) {
|
|
652
|
+
warnings.push({ code: 'MIXED_PROVIDER_CONFIG', messageArgs: { providerId: newProviderId, baseUrl: api.baseUrl, model: api.model } });
|
|
653
|
+
}
|
|
654
|
+
api.provider = newProviderId;
|
|
655
|
+
this.saveConfig();
|
|
656
|
+
return { api, warnings };
|
|
657
|
+
}
|
|
658
|
+
|
|
255
659
|
/**
|
|
256
660
|
* Record a successful API launch
|
|
257
661
|
* @returns {Object|null} The updated API object or null
|
|
@@ -289,6 +693,40 @@ class ApiManager {
|
|
|
289
693
|
return null;
|
|
290
694
|
}
|
|
291
695
|
|
|
696
|
+
/**
|
|
697
|
+
* Record a launch attempt (optimistic success)
|
|
698
|
+
* Call rollbackLaunchAttempt() if a pre-launch sync error occurs
|
|
699
|
+
* @returns {Object|null} The updated API object or null
|
|
700
|
+
*/
|
|
701
|
+
recordLaunchAttempt() {
|
|
702
|
+
const activeApi = this.getActiveApi();
|
|
703
|
+
if (activeApi) {
|
|
704
|
+
const index = this.config.activeIndex;
|
|
705
|
+
this.config.apis[index].lastUsed = new Date().toISOString();
|
|
706
|
+
this.config.apis[index].usageCount = (this.config.apis[index].usageCount || 0) + 1;
|
|
707
|
+
this.config.apis[index].successCount = (this.config.apis[index].successCount || 0) + 1;
|
|
708
|
+
this.config.apis[index].lastError = null;
|
|
709
|
+
this.saveConfig();
|
|
710
|
+
return this.config.apis[index];
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Rollback an optimistic launch attempt on pre-launch sync error
|
|
717
|
+
* @param {string} errorMessage - The error message
|
|
718
|
+
*/
|
|
719
|
+
rollbackLaunchAttempt(errorMessage) {
|
|
720
|
+
const activeApi = this.getActiveApi();
|
|
721
|
+
if (activeApi) {
|
|
722
|
+
const index = this.config.activeIndex;
|
|
723
|
+
this.config.apis[index].successCount = Math.max(0, (this.config.apis[index].successCount || 0) - 1);
|
|
724
|
+
this.config.apis[index].failCount = (this.config.apis[index].failCount || 0) + 1;
|
|
725
|
+
this.config.apis[index].lastError = errorMessage;
|
|
726
|
+
this.saveConfig();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
292
730
|
/**
|
|
293
731
|
* Get statistics about API usage
|
|
294
732
|
*/
|
|
@@ -456,14 +894,16 @@ class ApiManager {
|
|
|
456
894
|
* Export configuration as plaintext JSON (already authenticated)
|
|
457
895
|
*/
|
|
458
896
|
exportConfigAuthenticated() {
|
|
459
|
-
// Create export data with plaintext API keys
|
|
460
897
|
const exportData = {
|
|
898
|
+
configVersion: 2,
|
|
461
899
|
version: this.config.version,
|
|
900
|
+
warning: 'This file contains plaintext API keys and custom environment variables. Handle with care.',
|
|
462
901
|
exportedAt: new Date().toISOString(),
|
|
463
902
|
apis: this.config.apis.map(api => {
|
|
464
903
|
const decrypted = decrypt(api.authToken);
|
|
904
|
+
const { _autoFilledModel, ...safe } = api;
|
|
465
905
|
return {
|
|
466
|
-
...
|
|
906
|
+
...safe,
|
|
467
907
|
authToken: decrypted.success ? decrypted.value : '***DECRYPTION_FAILED***'
|
|
468
908
|
};
|
|
469
909
|
}),
|
|
@@ -500,84 +940,203 @@ class ApiManager {
|
|
|
500
940
|
processImportData(configData) {
|
|
501
941
|
let imported = 0;
|
|
502
942
|
let skipped = 0;
|
|
943
|
+
const warnings = [];
|
|
944
|
+
const skippedItems = [];
|
|
503
945
|
|
|
504
946
|
if (!configData.apis || !Array.isArray(configData.apis)) {
|
|
505
947
|
throw new Error('Invalid configuration format - no APIs found');
|
|
506
948
|
}
|
|
507
949
|
|
|
950
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue, RESERVED_ENV_KEYS } = require('./validators');
|
|
951
|
+
|
|
508
952
|
configData.apis.forEach(importApi => {
|
|
509
|
-
|
|
953
|
+
if (this.config.apis.length >= 99) {
|
|
954
|
+
skipped++;
|
|
955
|
+
skippedItems.push({ apiName: importApi.name || 'Unknown', reason: 'Maximum 99 APIs reached' });
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
510
959
|
try {
|
|
511
|
-
// Validate Base URL
|
|
512
960
|
const urlValidation = validateBaseUrl(importApi.baseUrl);
|
|
513
961
|
if (!urlValidation.valid) {
|
|
514
|
-
|
|
515
|
-
skipped++;
|
|
516
|
-
return;
|
|
962
|
+
skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: urlValidation.error }); return;
|
|
517
963
|
}
|
|
518
964
|
|
|
519
|
-
// Validate Auth Token (skip validation for placeholder tokens)
|
|
520
965
|
if (importApi.authToken !== '***REQUIRES_MANUAL_INPUT***') {
|
|
521
966
|
const tokenValidation = validateAuthToken(importApi.authToken);
|
|
522
967
|
if (!tokenValidation.valid) {
|
|
523
|
-
|
|
524
|
-
skipped++;
|
|
525
|
-
return;
|
|
968
|
+
skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: tokenValidation.error }); return;
|
|
526
969
|
}
|
|
527
970
|
}
|
|
528
971
|
|
|
529
|
-
// Validate Model
|
|
530
972
|
const modelValidation = validateModel(importApi.model);
|
|
531
973
|
if (!modelValidation.valid) {
|
|
532
|
-
|
|
533
|
-
skipped++;
|
|
534
|
-
return;
|
|
974
|
+
skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: modelValidation.error }); return;
|
|
535
975
|
}
|
|
536
976
|
|
|
537
|
-
// Check for duplicates using the same logic as addApi
|
|
538
977
|
const importToken = importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
|
|
539
978
|
const duplicate = this.checkDuplicate(importApi.baseUrl, importToken, importApi.model);
|
|
540
979
|
|
|
541
980
|
if (duplicate.isDuplicate) {
|
|
542
981
|
skipped++;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
982
|
+
skippedItems.push({ apiName: importApi.name || 'Unknown', reason: 'Duplicate configuration' });
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Clean modelEnvVars: whitelist only
|
|
987
|
+
const cleanedModelEnvVars = {};
|
|
988
|
+
if (importApi.modelEnvVars) {
|
|
989
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
990
|
+
const v = importApi.modelEnvVars[k];
|
|
991
|
+
cleanedModelEnvVars[k] = (typeof v === 'string') ? v : '';
|
|
992
|
+
}
|
|
993
|
+
for (const k of Object.keys(importApi.modelEnvVars)) {
|
|
994
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(k)) {
|
|
995
|
+
warnings.push({ code: 'UNKNOWN_MODEL_ENV_KEY', apiName: importApi.name || 'Unknown', key: k });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Clean runtimeEnvVars: whitelist + validate
|
|
1001
|
+
const cleanedRuntimeEnvVars = {};
|
|
1002
|
+
const cleanedRuntimeEnvSources = {};
|
|
1003
|
+
if (importApi.runtimeEnvVars) {
|
|
1004
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
1005
|
+
let v = importApi.runtimeEnvVars[k];
|
|
1006
|
+
if (typeof v !== 'string') v = '';
|
|
1007
|
+
if (v !== '' && !validateRuntimeEnvValue(k, v).valid) {
|
|
1008
|
+
warnings.push({ code: 'INVALID_RUNTIME_ENV_VALUE', apiName: importApi.name || 'Unknown', key: k });
|
|
1009
|
+
v = '';
|
|
1010
|
+
}
|
|
1011
|
+
cleanedRuntimeEnvVars[k] = v;
|
|
1012
|
+
const src = (importApi._runtimeEnvSources || {})[k];
|
|
1013
|
+
cleanedRuntimeEnvSources[k] = (src === 'manual' && v !== '') ? 'manual' : 'auto';
|
|
1014
|
+
}
|
|
1015
|
+
for (const k of Object.keys(importApi.runtimeEnvVars)) {
|
|
1016
|
+
if (!PREDEFINED_RUNTIME_KEYS.includes(k)) {
|
|
1017
|
+
warnings.push({ code: 'UNKNOWN_RUNTIME_ENV_KEY', apiName: importApi.name || 'Unknown', key: k });
|
|
1018
|
+
}
|
|
550
1019
|
}
|
|
1020
|
+
}
|
|
551
1021
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
this.config.apis.push(newApi);
|
|
565
|
-
imported++;
|
|
566
|
-
|
|
567
|
-
// Set as active if this is the first API
|
|
568
|
-
if (this.config.apis.length === 1) {
|
|
569
|
-
this.config.activeIndex = 0;
|
|
1022
|
+
// Clean customEnvVars: skip reserved/predefined
|
|
1023
|
+
const cleanedCustomEnvVars = {};
|
|
1024
|
+
if (importApi.customEnvVars) {
|
|
1025
|
+
const allP = new Set([...RESERVED_ENV_KEYS, ...PREDEFINED_RUNTIME_KEYS, ...PREDEFINED_MODEL_ENV_KEYS]);
|
|
1026
|
+
for (const [k, v] of Object.entries(importApi.customEnvVars)) {
|
|
1027
|
+
if (allP.has(k)) {
|
|
1028
|
+
warnings.push({ code: 'CUSTOM_ENV_KEY_RESERVED', apiName: importApi.name || 'Unknown', key: k });
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (typeof v === 'string') cleanedCustomEnvVars[k] = v;
|
|
570
1032
|
}
|
|
571
1033
|
}
|
|
1034
|
+
|
|
1035
|
+
const tokenToEncrypt =
|
|
1036
|
+
importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
|
|
1037
|
+
const encryptedResult = encrypt(tokenToEncrypt);
|
|
1038
|
+
if (!encryptedResult.success) {
|
|
1039
|
+
skipped++;
|
|
1040
|
+
skippedItems.push({
|
|
1041
|
+
apiName: importApi.name || 'Unknown',
|
|
1042
|
+
reason: `Failed to encrypt auth token: ${encryptedResult.error}`
|
|
1043
|
+
});
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const encryptedToken = encryptedResult.value;
|
|
1047
|
+
|
|
1048
|
+
const newApi = {
|
|
1049
|
+
id: Date.now() + Math.random(),
|
|
1050
|
+
name: importApi.name || `Imported API ${this.config.apis.length + 1}`,
|
|
1051
|
+
baseUrl: urlValidation.value,
|
|
1052
|
+
authToken: encryptedToken,
|
|
1053
|
+
model: modelValidation.value,
|
|
1054
|
+
provider: importApi.provider || 'custom',
|
|
1055
|
+
smallFastModel: importApi.smallFastModel || importApi.model,
|
|
1056
|
+
createdAt: new Date().toISOString(),
|
|
1057
|
+
lastUsed: null,
|
|
1058
|
+
usageCount: 0,
|
|
1059
|
+
modelEnvVars: cleanedModelEnvVars,
|
|
1060
|
+
runtimeEnvVars: cleanedRuntimeEnvVars,
|
|
1061
|
+
_runtimeEnvSources: cleanedRuntimeEnvSources,
|
|
1062
|
+
customEnvVars: cleanedCustomEnvVars,
|
|
1063
|
+
_autoFilledModel: importApi._autoFilledModel,
|
|
1064
|
+
};
|
|
1065
|
+
if (importApi._autoModelEnvVars) {
|
|
1066
|
+
newApi._autoModelEnvVars = importApi._autoModelEnvVars;
|
|
1067
|
+
}
|
|
1068
|
+
if (importApi.successCount !== undefined) newApi.successCount = importApi.successCount;
|
|
1069
|
+
if (importApi.failCount !== undefined) newApi.failCount = importApi.failCount;
|
|
1070
|
+
if (importApi.lastError !== undefined) newApi.lastError = importApi.lastError;
|
|
1071
|
+
|
|
1072
|
+
this._normalizeApiFields(newApi);
|
|
1073
|
+
this.config.apis.push(newApi);
|
|
1074
|
+
imported++;
|
|
1075
|
+
if (this.config.apis.length === 1) this.config.activeIndex = 0;
|
|
572
1076
|
} catch (error) {
|
|
573
|
-
console.warn(`Skipping API "${importApi.name || 'Unknown'}" - Validation error: ${error.message}`);
|
|
574
1077
|
skipped++;
|
|
1078
|
+
skippedItems.push({ apiName: importApi.name || 'Unknown', reason: error.message });
|
|
575
1079
|
}
|
|
576
1080
|
});
|
|
577
1081
|
|
|
578
1082
|
this.saveConfig();
|
|
579
|
-
return { imported, skipped };
|
|
1083
|
+
return { imported, skipped, warnings, skippedItems };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// --- Draft methods (no persistence, for pre-create config editing) ---
|
|
1087
|
+
|
|
1088
|
+
static buildApiDraft(provider, baseUrl, authToken, model, name) {
|
|
1089
|
+
const { getProvider } = require('./presets/providers');
|
|
1090
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
1091
|
+
const providerConfig = getProvider(provider);
|
|
1092
|
+
let templateValues;
|
|
1093
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
1094
|
+
templateValues = providerConfig.modelEnvTemplate.getValues(model);
|
|
1095
|
+
} else {
|
|
1096
|
+
templateValues = {};
|
|
1097
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = model;
|
|
1098
|
+
templateValues.smallFastModel = model;
|
|
1099
|
+
}
|
|
1100
|
+
const modelEnvVars = {};
|
|
1101
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) modelEnvVars[k] = templateValues[k] || '';
|
|
1102
|
+
const _autoModelEnvVars = { ...templateValues };
|
|
1103
|
+
const runtimeEnvVars = {};
|
|
1104
|
+
const _runtimeEnvSources = {};
|
|
1105
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) { runtimeEnvVars[k] = ''; _runtimeEnvSources[k] = 'auto'; }
|
|
1106
|
+
return { provider, baseUrl, authToken, model, name,
|
|
1107
|
+
smallFastModel: templateValues.smallFastModel,
|
|
1108
|
+
modelEnvVars, _autoModelEnvVars,
|
|
1109
|
+
runtimeEnvVars, _runtimeEnvSources, customEnvVars: {} };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
static applyDraftEnvChange(draft, section, key, value) {
|
|
1113
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue, validateEnvKey } = require('./validators');
|
|
1114
|
+
if (section === 'model') {
|
|
1115
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(key)) throw new Error(`Unknown model env key: ${key}`);
|
|
1116
|
+
if (typeof value !== 'string') throw new Error('model env value must be string');
|
|
1117
|
+
draft.modelEnvVars[key] = (value === '') ? (draft._autoModelEnvVars[key] || '') : value;
|
|
1118
|
+
} else if (section === 'runtime') {
|
|
1119
|
+
if (!PREDEFINED_RUNTIME_KEYS.includes(key)) throw new Error(`Unknown runtime env key: ${key}`);
|
|
1120
|
+
const v = validateRuntimeEnvValue(key, value);
|
|
1121
|
+
if (!v.valid) throw new Error(`Invalid: ${v.error}`);
|
|
1122
|
+
draft.runtimeEnvVars[key] = value;
|
|
1123
|
+
draft._runtimeEnvSources[key] = (value === '') ? 'auto' : 'manual';
|
|
1124
|
+
} else if (section === 'custom') {
|
|
1125
|
+
const kv = validateEnvKey(key);
|
|
1126
|
+
if (!kv.valid) throw new Error(`Invalid custom key: ${kv.error}`);
|
|
1127
|
+
if (typeof value !== 'string') throw new Error('custom env value must be string');
|
|
1128
|
+
draft.customEnvVars[key] = value;
|
|
1129
|
+
}
|
|
1130
|
+
return draft;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
static deleteDraftCustomEnvVar(draft, key) {
|
|
1134
|
+
delete draft.customEnvVars[key];
|
|
1135
|
+
return draft;
|
|
580
1136
|
}
|
|
581
1137
|
}
|
|
582
1138
|
|
|
583
|
-
module.exports = ApiManager;
|
|
1139
|
+
module.exports = ApiManager;
|
|
1140
|
+
module.exports.DuplicateApiError = DuplicateApiError;
|
|
1141
|
+
module.exports.MODEL_CONFIG_LABELS = MODEL_CONFIG_LABELS;
|
|
1142
|
+
module.exports.RUNTIME_CONFIG_LABELS = RUNTIME_CONFIG_LABELS;
|