@kikkimo/claude-launcher 3.0.0 → 3.2.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 +43 -0
- package/README.md +7 -4
- package/claude-launcher +634 -38
- package/docs/README-zh.md +7 -4
- package/docs/superpowers/plans/2026-06-14-provider-model-upgrade.md +949 -0
- package/docs/superpowers/specs/2026-06-14-provider-model-upgrade-design.md +292 -0
- package/lib/api-manager.js +527 -67
- package/lib/i18n/locales/de.js +144 -6
- package/lib/i18n/locales/en.js +150 -6
- package/lib/i18n/locales/es.js +144 -6
- package/lib/i18n/locales/fr.js +144 -6
- package/lib/i18n/locales/it.js +144 -6
- package/lib/i18n/locales/ja.js +144 -6
- package/lib/i18n/locales/ko.js +144 -6
- package/lib/i18n/locales/pt.js +144 -6
- package/lib/i18n/locales/ru.js +144 -6
- package/lib/i18n/locales/zh-TW.js +144 -6
- package/lib/i18n/locales/zh.js +150 -6
- package/lib/launcher.js +46 -17
- package/lib/presets/providers.js +191 -61
- package/lib/ui/api-editor.js +668 -210
- package/lib/ui/i18n-labels.js +16 -0
- package/lib/ui/menu.js +19 -13
- package/lib/ui/screen.js +125 -125
- package/lib/utils/version-checker.js +6 -5
- package/lib/validators.js +102 -1
- package/package.json +2 -2
package/lib/api-manager.js
CHANGED
|
@@ -9,31 +9,63 @@ const { encrypt, decrypt } = require('./crypto');
|
|
|
9
9
|
const { validateBaseUrl, validateAuthToken, validateModel, validateApiName } = require('./validators');
|
|
10
10
|
const screen = require('./ui/screen');
|
|
11
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
|
+
};
|
|
39
|
+
|
|
12
40
|
class ApiManager {
|
|
13
41
|
constructor() {
|
|
14
42
|
this.configFile = path.join(os.homedir(), '.claude-launcher-apis.json');
|
|
15
|
-
|
|
43
|
+
const { config, migrated } = this.loadConfig();
|
|
44
|
+
this.config = config;
|
|
45
|
+
if (migrated) {
|
|
46
|
+
this.saveConfig();
|
|
47
|
+
}
|
|
16
48
|
}
|
|
17
49
|
|
|
18
50
|
/**
|
|
19
51
|
* Load configuration from encrypted file
|
|
20
52
|
*/
|
|
21
53
|
loadConfig() {
|
|
54
|
+
let migrated = false;
|
|
22
55
|
try {
|
|
23
56
|
if (fs.existsSync(this.configFile)) {
|
|
24
57
|
const encryptedData = fs.readFileSync(this.configFile, 'utf8');
|
|
25
58
|
const decrypted = decrypt(encryptedData);
|
|
26
59
|
if (decrypted.success) {
|
|
27
60
|
const config = JSON.parse(decrypted.value);
|
|
28
|
-
|
|
29
|
-
if (!config.hasOwnProperty('
|
|
30
|
-
config.exportPassword = null;
|
|
31
|
-
}
|
|
32
|
-
if (!config.hasOwnProperty('passwordSkipped')) {
|
|
33
|
-
config.passwordSkipped = false;
|
|
34
|
-
}
|
|
61
|
+
if (!config.hasOwnProperty('exportPassword')) config.exportPassword = null;
|
|
62
|
+
if (!config.hasOwnProperty('passwordSkipped')) config.passwordSkipped = false;
|
|
35
63
|
|
|
36
|
-
|
|
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 };
|
|
37
69
|
}
|
|
38
70
|
}
|
|
39
71
|
} catch (error) {
|
|
@@ -41,15 +73,169 @@ class ApiManager {
|
|
|
41
73
|
}
|
|
42
74
|
|
|
43
75
|
return {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
50
85
|
};
|
|
51
86
|
}
|
|
52
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
|
+
// template 漂移检测:provider template 升级后首次加载旧配置时,_autoModelEnvVars
|
|
172
|
+
// 仍是旧快照。检测到漂移则刷新仍等于旧快照的 tier 字段(保留用户手动覆盖),
|
|
173
|
+
// 复用 updateApiField() 的保留覆盖模式。_migrateApiEntry 的 before/after 比较
|
|
174
|
+
// 已覆盖 modelEnvVars/_autoModelEnvVars/smallFastModel,会据此返回 migrated=true
|
|
175
|
+
// 并由构造函数统一 saveConfig()。
|
|
176
|
+
if (hadAutoModelEnvVars) {
|
|
177
|
+
let drifted = false;
|
|
178
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
179
|
+
if (api._autoModelEnvVars[k] !== template[k]) { drifted = true; break; }
|
|
180
|
+
}
|
|
181
|
+
if (!drifted && api._autoModelEnvVars.smallFastModel !== template.smallFastModel) {
|
|
182
|
+
drifted = true;
|
|
183
|
+
}
|
|
184
|
+
if (drifted) {
|
|
185
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
186
|
+
if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) {
|
|
187
|
+
api.modelEnvVars[k] = template[k] || '';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) {
|
|
191
|
+
api.smallFastModel = template.smallFastModel;
|
|
192
|
+
}
|
|
193
|
+
api._autoModelEnvVars = { ...template };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// runtimeEnvVars — fill "" not provider values
|
|
198
|
+
if (!api.runtimeEnvVars) {
|
|
199
|
+
api.runtimeEnvVars = {};
|
|
200
|
+
}
|
|
201
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
202
|
+
if (!(k in api.runtimeEnvVars) || typeof api.runtimeEnvVars[k] !== 'string') {
|
|
203
|
+
api.runtimeEnvVars[k] = '';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// _runtimeEnvSources — missing → "auto"
|
|
208
|
+
if (!api._runtimeEnvSources) {
|
|
209
|
+
api._runtimeEnvSources = {};
|
|
210
|
+
}
|
|
211
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
212
|
+
if (!(k in api._runtimeEnvSources)) {
|
|
213
|
+
api._runtimeEnvSources[k] = 'auto';
|
|
214
|
+
}
|
|
215
|
+
if (api._runtimeEnvSources[k] !== 'auto' && api._runtimeEnvSources[k] !== 'manual') {
|
|
216
|
+
api._runtimeEnvSources[k] = 'auto';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// runtime/source conflict resolution
|
|
221
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
222
|
+
if (api.runtimeEnvVars[k] !== '' && api._runtimeEnvSources[k] === 'auto') {
|
|
223
|
+
api.runtimeEnvVars[k] = '';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// customEnvVars
|
|
228
|
+
if (!api.customEnvVars) {
|
|
229
|
+
api.customEnvVars = {};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (api._autoFilledModel) {
|
|
233
|
+
delete api._autoFilledModel;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return api;
|
|
237
|
+
}
|
|
238
|
+
|
|
53
239
|
/**
|
|
54
240
|
* Save configuration to encrypted file
|
|
55
241
|
*/
|
|
@@ -120,7 +306,7 @@ class ApiManager {
|
|
|
120
306
|
// Check for duplicates
|
|
121
307
|
const duplicate = this.checkDuplicate(baseUrl, authToken, model);
|
|
122
308
|
if (duplicate.isDuplicate) {
|
|
123
|
-
throw new
|
|
309
|
+
throw new DuplicateApiError(duplicate.existing);
|
|
124
310
|
}
|
|
125
311
|
|
|
126
312
|
// Encrypt the auth token before storing
|
|
@@ -129,6 +315,32 @@ class ApiManager {
|
|
|
129
315
|
throw new Error(`Failed to encrypt auth token: ${encryptedToken.error}`);
|
|
130
316
|
}
|
|
131
317
|
|
|
318
|
+
// Compute model env template values
|
|
319
|
+
const { getProvider } = require('./presets/providers');
|
|
320
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
321
|
+
const providerConfig = getProvider(provider);
|
|
322
|
+
let templateValues;
|
|
323
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
324
|
+
templateValues = providerConfig.modelEnvTemplate.getValues(modelValidation.value);
|
|
325
|
+
} else {
|
|
326
|
+
templateValues = {};
|
|
327
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = modelValidation.value;
|
|
328
|
+
templateValues.smallFastModel = modelValidation.value;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const modelEnvVars = {};
|
|
332
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
333
|
+
modelEnvVars[k] = templateValues[k] || '';
|
|
334
|
+
}
|
|
335
|
+
const _autoModelEnvVars = { ...templateValues };
|
|
336
|
+
|
|
337
|
+
const runtimeEnvVars = {};
|
|
338
|
+
const _runtimeEnvSources = {};
|
|
339
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
340
|
+
runtimeEnvVars[k] = '';
|
|
341
|
+
_runtimeEnvSources[k] = 'auto';
|
|
342
|
+
}
|
|
343
|
+
|
|
132
344
|
const newApi = {
|
|
133
345
|
id: Date.now().toString(),
|
|
134
346
|
name: name || `API-${this.config.apis.length + 1}`,
|
|
@@ -136,15 +348,18 @@ class ApiManager {
|
|
|
136
348
|
baseUrl: urlValidation.value,
|
|
137
349
|
authToken: encryptedToken.value,
|
|
138
350
|
model: modelValidation.value,
|
|
139
|
-
smallFastModel:
|
|
351
|
+
smallFastModel: templateValues.smallFastModel,
|
|
140
352
|
createdAt: new Date().toISOString(),
|
|
141
|
-
// Existing statistics fields
|
|
142
353
|
lastUsed: null,
|
|
143
354
|
usageCount: 0,
|
|
144
|
-
// New statistics fields
|
|
145
355
|
successCount: 0,
|
|
146
356
|
failCount: 0,
|
|
147
|
-
lastError: null
|
|
357
|
+
lastError: null,
|
|
358
|
+
modelEnvVars,
|
|
359
|
+
_autoModelEnvVars,
|
|
360
|
+
runtimeEnvVars,
|
|
361
|
+
_runtimeEnvSources,
|
|
362
|
+
customEnvVars: {},
|
|
148
363
|
};
|
|
149
364
|
|
|
150
365
|
this.config.apis.push(newApi);
|
|
@@ -287,7 +502,7 @@ class ApiManager {
|
|
|
287
502
|
if (!validIds.includes(value)) {
|
|
288
503
|
throw new Error(`Unknown provider: ${value}. Valid: ${validIds.join(', ')}`);
|
|
289
504
|
}
|
|
290
|
-
|
|
505
|
+
return this.updateApiProvider(apiId, value).api;
|
|
291
506
|
}
|
|
292
507
|
case 'baseUrl': {
|
|
293
508
|
const urlValidation = validateBaseUrl(value);
|
|
@@ -330,13 +545,143 @@ class ApiManager {
|
|
|
330
545
|
// Apply update
|
|
331
546
|
api[field] = value.trim();
|
|
332
547
|
if (field === 'model') {
|
|
333
|
-
|
|
548
|
+
const { getProvider } = require('./presets/providers');
|
|
549
|
+
const { PREDEFINED_MODEL_ENV_KEYS } = require('./validators');
|
|
550
|
+
const providerConfig = getProvider(api.provider);
|
|
551
|
+
let templateVals;
|
|
552
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
553
|
+
templateVals = providerConfig.modelEnvTemplate.getValues(value.trim());
|
|
554
|
+
} else {
|
|
555
|
+
templateVals = {};
|
|
556
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateVals[k] = value.trim();
|
|
557
|
+
templateVals.smallFastModel = value.trim();
|
|
558
|
+
}
|
|
559
|
+
if (api._autoModelEnvVars) {
|
|
560
|
+
if (!api.modelEnvVars) api.modelEnvVars = {};
|
|
561
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
562
|
+
if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) {
|
|
563
|
+
api.modelEnvVars[k] = templateVals[k] || '';
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) {
|
|
567
|
+
api.smallFastModel = templateVals.smallFastModel;
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
// No snapshot means all fields are auto — overwrite all
|
|
571
|
+
if (!api.modelEnvVars) api.modelEnvVars = {};
|
|
572
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
573
|
+
api.modelEnvVars[k] = templateVals[k] || '';
|
|
574
|
+
}
|
|
575
|
+
api.smallFastModel = templateVals.smallFastModel;
|
|
576
|
+
}
|
|
577
|
+
api._autoModelEnvVars = { ...templateVals };
|
|
334
578
|
}
|
|
335
579
|
|
|
336
580
|
this.saveConfig();
|
|
337
581
|
return api;
|
|
338
582
|
}
|
|
339
583
|
|
|
584
|
+
updateModelEnvVar(apiId, key, value) {
|
|
585
|
+
const { PREDEFINED_MODEL_ENV_KEYS } = require('./validators');
|
|
586
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(key)) throw new Error(`"${key}" is not a predefined model env key`);
|
|
587
|
+
if (typeof value !== 'string') throw new Error('model env value must be a string');
|
|
588
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
589
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
590
|
+
const api = this.config.apis[index];
|
|
591
|
+
const autoValue = api._autoModelEnvVars && typeof api._autoModelEnvVars[key] === 'string'
|
|
592
|
+
? api._autoModelEnvVars[key]
|
|
593
|
+
: '';
|
|
594
|
+
api.modelEnvVars[key] = value === '' ? autoValue : value;
|
|
595
|
+
this.saveConfig();
|
|
596
|
+
return api;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
updateRuntimeEnvVar(apiId, key, value, options = {}) {
|
|
600
|
+
const { PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue } = require('./validators');
|
|
601
|
+
if (!PREDEFINED_RUNTIME_KEYS.includes(key)) throw new Error(`"${key}" is not a predefined runtime env key`);
|
|
602
|
+
const validation = validateRuntimeEnvValue(key, value);
|
|
603
|
+
if (!validation.valid) throw new Error(`Invalid value for ${key}: ${validation.error}`);
|
|
604
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
605
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
606
|
+
if (options.source === 'auto') {
|
|
607
|
+
this.config.apis[index].runtimeEnvVars[key] = '';
|
|
608
|
+
this.config.apis[index]._runtimeEnvSources[key] = 'auto';
|
|
609
|
+
} else if (options.source === 'manual') {
|
|
610
|
+
this.config.apis[index].runtimeEnvVars[key] = value;
|
|
611
|
+
this.config.apis[index]._runtimeEnvSources[key] = 'manual';
|
|
612
|
+
} else {
|
|
613
|
+
this.config.apis[index].runtimeEnvVars[key] = value;
|
|
614
|
+
this.config.apis[index]._runtimeEnvSources[key] = (value === '') ? 'auto' : 'manual';
|
|
615
|
+
}
|
|
616
|
+
this.saveConfig();
|
|
617
|
+
return this.config.apis[index];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
setCustomEnvVar(apiId, key, value) {
|
|
621
|
+
const { validateEnvKey } = require('./validators');
|
|
622
|
+
const kv = validateEnvKey(key);
|
|
623
|
+
if (!kv.valid) throw new Error(`Custom env key "${key}" is reserved or invalid`);
|
|
624
|
+
if (typeof value !== 'string') throw new Error('Custom env value must be a string');
|
|
625
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
626
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
627
|
+
this.config.apis[index].customEnvVars[key] = value;
|
|
628
|
+
this.saveConfig();
|
|
629
|
+
return this.config.apis[index];
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
deleteCustomEnvVar(apiId, key) {
|
|
633
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
634
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
635
|
+
delete this.config.apis[index].customEnvVars[key];
|
|
636
|
+
this.saveConfig();
|
|
637
|
+
return this.config.apis[index];
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
updateApiProvider(apiId, newProviderId) {
|
|
641
|
+
const { getProvider, getAllProviders, detectProvider } = require('./presets/providers');
|
|
642
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
643
|
+
const validIds = getAllProviders().map(p => p.id);
|
|
644
|
+
if (!validIds.includes(newProviderId)) throw new Error(`Unknown provider: ${newProviderId}`);
|
|
645
|
+
const index = this.config.apis.findIndex(a => a.id === apiId);
|
|
646
|
+
if (index === -1) throw new Error(`API not found: ${apiId}`);
|
|
647
|
+
const api = this.config.apis[index];
|
|
648
|
+
const newProvider = getProvider(newProviderId);
|
|
649
|
+
const warnings = [];
|
|
650
|
+
if (!newProvider.models.includes(api.model)) {
|
|
651
|
+
warnings.push({ code: 'MODEL_NOT_IN_PROVIDER', messageArgs: { model: api.model, providerName: newProvider.name } });
|
|
652
|
+
}
|
|
653
|
+
if (detectProvider(api.baseUrl) !== newProviderId) {
|
|
654
|
+
warnings.push({ code: 'BASE_URL_NOT_UPDATED', messageArgs: { baseUrl: api.baseUrl } });
|
|
655
|
+
}
|
|
656
|
+
if (api._runtimeEnvSources) {
|
|
657
|
+
for (const [key, source] of Object.entries(api._runtimeEnvSources)) {
|
|
658
|
+
if (source === 'auto' && PREDEFINED_RUNTIME_KEYS.includes(key)) api.runtimeEnvVars[key] = '';
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
let templateValues;
|
|
662
|
+
if (newProvider.modelEnvTemplate) {
|
|
663
|
+
templateValues = newProvider.modelEnvTemplate.getValues(api.model);
|
|
664
|
+
} else {
|
|
665
|
+
templateValues = {};
|
|
666
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = api.model;
|
|
667
|
+
templateValues.smallFastModel = api.model;
|
|
668
|
+
}
|
|
669
|
+
if (api._autoModelEnvVars) {
|
|
670
|
+
if (!api.modelEnvVars) api.modelEnvVars = {};
|
|
671
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
672
|
+
if (api.modelEnvVars[k] === api._autoModelEnvVars[k]) api.modelEnvVars[k] = templateValues[k] || '';
|
|
673
|
+
}
|
|
674
|
+
if (api.smallFastModel === api._autoModelEnvVars.smallFastModel) api.smallFastModel = templateValues.smallFastModel;
|
|
675
|
+
}
|
|
676
|
+
api._autoModelEnvVars = { ...templateValues };
|
|
677
|
+
if (detectProvider(api.baseUrl) !== newProviderId && warnings.some(w => w.code === 'MODEL_NOT_IN_PROVIDER')) {
|
|
678
|
+
warnings.push({ code: 'MIXED_PROVIDER_CONFIG', messageArgs: { providerId: newProviderId, baseUrl: api.baseUrl, model: api.model } });
|
|
679
|
+
}
|
|
680
|
+
api.provider = newProviderId;
|
|
681
|
+
this.saveConfig();
|
|
682
|
+
return { api, warnings };
|
|
683
|
+
}
|
|
684
|
+
|
|
340
685
|
/**
|
|
341
686
|
* Record a successful API launch
|
|
342
687
|
* @returns {Object|null} The updated API object or null
|
|
@@ -575,14 +920,16 @@ class ApiManager {
|
|
|
575
920
|
* Export configuration as plaintext JSON (already authenticated)
|
|
576
921
|
*/
|
|
577
922
|
exportConfigAuthenticated() {
|
|
578
|
-
// Create export data with plaintext API keys
|
|
579
923
|
const exportData = {
|
|
924
|
+
configVersion: 2,
|
|
580
925
|
version: this.config.version,
|
|
926
|
+
warning: 'This file contains plaintext API keys and custom environment variables. Handle with care.',
|
|
581
927
|
exportedAt: new Date().toISOString(),
|
|
582
928
|
apis: this.config.apis.map(api => {
|
|
583
929
|
const decrypted = decrypt(api.authToken);
|
|
930
|
+
const { _autoFilledModel, ...safe } = api;
|
|
584
931
|
return {
|
|
585
|
-
...
|
|
932
|
+
...safe,
|
|
586
933
|
authToken: decrypted.success ? decrypted.value : '***DECRYPTION_FAILED***'
|
|
587
934
|
};
|
|
588
935
|
}),
|
|
@@ -619,90 +966,203 @@ class ApiManager {
|
|
|
619
966
|
processImportData(configData) {
|
|
620
967
|
let imported = 0;
|
|
621
968
|
let skipped = 0;
|
|
969
|
+
const warnings = [];
|
|
970
|
+
const skippedItems = [];
|
|
622
971
|
|
|
623
972
|
if (!configData.apis || !Array.isArray(configData.apis)) {
|
|
624
973
|
throw new Error('Invalid configuration format - no APIs found');
|
|
625
974
|
}
|
|
626
975
|
|
|
976
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue, RESERVED_ENV_KEYS } = require('./validators');
|
|
977
|
+
|
|
627
978
|
configData.apis.forEach(importApi => {
|
|
628
979
|
if (this.config.apis.length >= 99) {
|
|
629
|
-
screen.debug('Import skipped: maximum 99 APIs reached');
|
|
630
980
|
skipped++;
|
|
981
|
+
skippedItems.push({ apiName: importApi.name || 'Unknown', reason: 'Maximum 99 APIs reached' });
|
|
631
982
|
return;
|
|
632
983
|
}
|
|
633
984
|
|
|
634
|
-
// Validate the API configuration before importing
|
|
635
985
|
try {
|
|
636
|
-
// Validate Base URL
|
|
637
986
|
const urlValidation = validateBaseUrl(importApi.baseUrl);
|
|
638
987
|
if (!urlValidation.valid) {
|
|
639
|
-
|
|
640
|
-
skipped++;
|
|
641
|
-
return;
|
|
988
|
+
skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: urlValidation.error }); return;
|
|
642
989
|
}
|
|
643
990
|
|
|
644
|
-
// Validate Auth Token (skip validation for placeholder tokens)
|
|
645
991
|
if (importApi.authToken !== '***REQUIRES_MANUAL_INPUT***') {
|
|
646
992
|
const tokenValidation = validateAuthToken(importApi.authToken);
|
|
647
993
|
if (!tokenValidation.valid) {
|
|
648
|
-
|
|
649
|
-
skipped++;
|
|
650
|
-
return;
|
|
994
|
+
skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: tokenValidation.error }); return;
|
|
651
995
|
}
|
|
652
996
|
}
|
|
653
997
|
|
|
654
|
-
// Validate Model
|
|
655
998
|
const modelValidation = validateModel(importApi.model);
|
|
656
999
|
if (!modelValidation.valid) {
|
|
657
|
-
|
|
658
|
-
skipped++;
|
|
659
|
-
return;
|
|
1000
|
+
skipped++; skippedItems.push({ apiName: importApi.name || 'Unknown', reason: modelValidation.error }); return;
|
|
660
1001
|
}
|
|
661
1002
|
|
|
662
|
-
// Check for duplicates using the same logic as addApi
|
|
663
1003
|
const importToken = importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
|
|
664
1004
|
const duplicate = this.checkDuplicate(importApi.baseUrl, importToken, importApi.model);
|
|
665
1005
|
|
|
666
1006
|
if (duplicate.isDuplicate) {
|
|
667
1007
|
skipped++;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1008
|
+
skippedItems.push({ apiName: importApi.name || 'Unknown', reason: 'Duplicate configuration' });
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Clean modelEnvVars: whitelist only
|
|
1013
|
+
const cleanedModelEnvVars = {};
|
|
1014
|
+
if (importApi.modelEnvVars) {
|
|
1015
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) {
|
|
1016
|
+
const v = importApi.modelEnvVars[k];
|
|
1017
|
+
cleanedModelEnvVars[k] = (typeof v === 'string') ? v : '';
|
|
1018
|
+
}
|
|
1019
|
+
for (const k of Object.keys(importApi.modelEnvVars)) {
|
|
1020
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(k)) {
|
|
1021
|
+
warnings.push({ code: 'UNKNOWN_MODEL_ENV_KEY', apiName: importApi.name || 'Unknown', key: k });
|
|
1022
|
+
}
|
|
675
1023
|
}
|
|
1024
|
+
}
|
|
676
1025
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1026
|
+
// Clean runtimeEnvVars: whitelist + validate
|
|
1027
|
+
const cleanedRuntimeEnvVars = {};
|
|
1028
|
+
const cleanedRuntimeEnvSources = {};
|
|
1029
|
+
if (importApi.runtimeEnvVars) {
|
|
1030
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) {
|
|
1031
|
+
let v = importApi.runtimeEnvVars[k];
|
|
1032
|
+
if (typeof v !== 'string') v = '';
|
|
1033
|
+
if (v !== '' && !validateRuntimeEnvValue(k, v).valid) {
|
|
1034
|
+
warnings.push({ code: 'INVALID_RUNTIME_ENV_VALUE', apiName: importApi.name || 'Unknown', key: k });
|
|
1035
|
+
v = '';
|
|
1036
|
+
}
|
|
1037
|
+
cleanedRuntimeEnvVars[k] = v;
|
|
1038
|
+
const src = (importApi._runtimeEnvSources || {})[k];
|
|
1039
|
+
cleanedRuntimeEnvSources[k] = (src === 'manual' && v !== '') ? 'manual' : 'auto';
|
|
1040
|
+
}
|
|
1041
|
+
for (const k of Object.keys(importApi.runtimeEnvVars)) {
|
|
1042
|
+
if (!PREDEFINED_RUNTIME_KEYS.includes(k)) {
|
|
1043
|
+
warnings.push({ code: 'UNKNOWN_RUNTIME_ENV_KEY', apiName: importApi.name || 'Unknown', key: k });
|
|
1044
|
+
}
|
|
695
1045
|
}
|
|
696
1046
|
}
|
|
1047
|
+
|
|
1048
|
+
// Clean customEnvVars: skip reserved/predefined
|
|
1049
|
+
const cleanedCustomEnvVars = {};
|
|
1050
|
+
if (importApi.customEnvVars) {
|
|
1051
|
+
const allP = new Set([...RESERVED_ENV_KEYS, ...PREDEFINED_RUNTIME_KEYS, ...PREDEFINED_MODEL_ENV_KEYS]);
|
|
1052
|
+
for (const [k, v] of Object.entries(importApi.customEnvVars)) {
|
|
1053
|
+
if (allP.has(k)) {
|
|
1054
|
+
warnings.push({ code: 'CUSTOM_ENV_KEY_RESERVED', apiName: importApi.name || 'Unknown', key: k });
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (typeof v === 'string') cleanedCustomEnvVars[k] = v;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const tokenToEncrypt =
|
|
1062
|
+
importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
|
|
1063
|
+
const encryptedResult = encrypt(tokenToEncrypt);
|
|
1064
|
+
if (!encryptedResult.success) {
|
|
1065
|
+
skipped++;
|
|
1066
|
+
skippedItems.push({
|
|
1067
|
+
apiName: importApi.name || 'Unknown',
|
|
1068
|
+
reason: `Failed to encrypt auth token: ${encryptedResult.error}`
|
|
1069
|
+
});
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const encryptedToken = encryptedResult.value;
|
|
1073
|
+
|
|
1074
|
+
const newApi = {
|
|
1075
|
+
id: Date.now() + Math.random(),
|
|
1076
|
+
name: importApi.name || `Imported API ${this.config.apis.length + 1}`,
|
|
1077
|
+
baseUrl: urlValidation.value,
|
|
1078
|
+
authToken: encryptedToken,
|
|
1079
|
+
model: modelValidation.value,
|
|
1080
|
+
provider: importApi.provider || 'custom',
|
|
1081
|
+
smallFastModel: importApi.smallFastModel || importApi.model,
|
|
1082
|
+
createdAt: new Date().toISOString(),
|
|
1083
|
+
lastUsed: null,
|
|
1084
|
+
usageCount: 0,
|
|
1085
|
+
modelEnvVars: cleanedModelEnvVars,
|
|
1086
|
+
runtimeEnvVars: cleanedRuntimeEnvVars,
|
|
1087
|
+
_runtimeEnvSources: cleanedRuntimeEnvSources,
|
|
1088
|
+
customEnvVars: cleanedCustomEnvVars,
|
|
1089
|
+
_autoFilledModel: importApi._autoFilledModel,
|
|
1090
|
+
};
|
|
1091
|
+
if (importApi._autoModelEnvVars) {
|
|
1092
|
+
newApi._autoModelEnvVars = importApi._autoModelEnvVars;
|
|
1093
|
+
}
|
|
1094
|
+
if (importApi.successCount !== undefined) newApi.successCount = importApi.successCount;
|
|
1095
|
+
if (importApi.failCount !== undefined) newApi.failCount = importApi.failCount;
|
|
1096
|
+
if (importApi.lastError !== undefined) newApi.lastError = importApi.lastError;
|
|
1097
|
+
|
|
1098
|
+
this._normalizeApiFields(newApi);
|
|
1099
|
+
this.config.apis.push(newApi);
|
|
1100
|
+
imported++;
|
|
1101
|
+
if (this.config.apis.length === 1) this.config.activeIndex = 0;
|
|
697
1102
|
} catch (error) {
|
|
698
|
-
screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Validation error: ${error.message}`);
|
|
699
1103
|
skipped++;
|
|
1104
|
+
skippedItems.push({ apiName: importApi.name || 'Unknown', reason: error.message });
|
|
700
1105
|
}
|
|
701
1106
|
});
|
|
702
1107
|
|
|
703
1108
|
this.saveConfig();
|
|
704
|
-
return { imported, skipped };
|
|
1109
|
+
return { imported, skipped, warnings, skippedItems };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// --- Draft methods (no persistence, for pre-create config editing) ---
|
|
1113
|
+
|
|
1114
|
+
static buildApiDraft(provider, baseUrl, authToken, model, name) {
|
|
1115
|
+
const { getProvider } = require('./presets/providers');
|
|
1116
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./validators');
|
|
1117
|
+
const providerConfig = getProvider(provider);
|
|
1118
|
+
let templateValues;
|
|
1119
|
+
if (providerConfig && providerConfig.modelEnvTemplate) {
|
|
1120
|
+
templateValues = providerConfig.modelEnvTemplate.getValues(model);
|
|
1121
|
+
} else {
|
|
1122
|
+
templateValues = {};
|
|
1123
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) templateValues[k] = model;
|
|
1124
|
+
templateValues.smallFastModel = model;
|
|
1125
|
+
}
|
|
1126
|
+
const modelEnvVars = {};
|
|
1127
|
+
for (const k of PREDEFINED_MODEL_ENV_KEYS) modelEnvVars[k] = templateValues[k] || '';
|
|
1128
|
+
const _autoModelEnvVars = { ...templateValues };
|
|
1129
|
+
const runtimeEnvVars = {};
|
|
1130
|
+
const _runtimeEnvSources = {};
|
|
1131
|
+
for (const k of PREDEFINED_RUNTIME_KEYS) { runtimeEnvVars[k] = ''; _runtimeEnvSources[k] = 'auto'; }
|
|
1132
|
+
return { provider, baseUrl, authToken, model, name,
|
|
1133
|
+
smallFastModel: templateValues.smallFastModel,
|
|
1134
|
+
modelEnvVars, _autoModelEnvVars,
|
|
1135
|
+
runtimeEnvVars, _runtimeEnvSources, customEnvVars: {} };
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
static applyDraftEnvChange(draft, section, key, value) {
|
|
1139
|
+
const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS, validateRuntimeEnvValue, validateEnvKey } = require('./validators');
|
|
1140
|
+
if (section === 'model') {
|
|
1141
|
+
if (!PREDEFINED_MODEL_ENV_KEYS.includes(key)) throw new Error(`Unknown model env key: ${key}`);
|
|
1142
|
+
if (typeof value !== 'string') throw new Error('model env value must be string');
|
|
1143
|
+
draft.modelEnvVars[key] = (value === '') ? (draft._autoModelEnvVars[key] || '') : value;
|
|
1144
|
+
} else if (section === 'runtime') {
|
|
1145
|
+
if (!PREDEFINED_RUNTIME_KEYS.includes(key)) throw new Error(`Unknown runtime env key: ${key}`);
|
|
1146
|
+
const v = validateRuntimeEnvValue(key, value);
|
|
1147
|
+
if (!v.valid) throw new Error(`Invalid: ${v.error}`);
|
|
1148
|
+
draft.runtimeEnvVars[key] = value;
|
|
1149
|
+
draft._runtimeEnvSources[key] = (value === '') ? 'auto' : 'manual';
|
|
1150
|
+
} else if (section === 'custom') {
|
|
1151
|
+
const kv = validateEnvKey(key);
|
|
1152
|
+
if (!kv.valid) throw new Error(`Invalid custom key: ${kv.error}`);
|
|
1153
|
+
if (typeof value !== 'string') throw new Error('custom env value must be string');
|
|
1154
|
+
draft.customEnvVars[key] = value;
|
|
1155
|
+
}
|
|
1156
|
+
return draft;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
static deleteDraftCustomEnvVar(draft, key) {
|
|
1160
|
+
delete draft.customEnvVars[key];
|
|
1161
|
+
return draft;
|
|
705
1162
|
}
|
|
706
1163
|
}
|
|
707
1164
|
|
|
708
|
-
module.exports = ApiManager;
|
|
1165
|
+
module.exports = ApiManager;
|
|
1166
|
+
module.exports.DuplicateApiError = DuplicateApiError;
|
|
1167
|
+
module.exports.MODEL_CONFIG_LABELS = MODEL_CONFIG_LABELS;
|
|
1168
|
+
module.exports.RUNTIME_CONFIG_LABELS = RUNTIME_CONFIG_LABELS;
|