opencode-switch-cli 1.0.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/README.md +376 -0
- package/dist/bin.d.mts +2 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +1183 -0
- package/dist/bin.js.map +1 -0
- package/dist/bin.mjs +1171 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/index.d.mts +175 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +1192 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1161 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
package/dist/bin.mjs
ADDED
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path2 from "path";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
|
|
8
|
+
// src/config/json-file.ts
|
|
9
|
+
import { copyFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
function cloneValue(value) {
|
|
12
|
+
return JSON.parse(JSON.stringify(value));
|
|
13
|
+
}
|
|
14
|
+
async function fileExists(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
await readFile(filePath, "utf8");
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function createJsonFileStore() {
|
|
23
|
+
return {
|
|
24
|
+
async read({ filePath, defaultValue }) {
|
|
25
|
+
if (!await fileExists(filePath)) {
|
|
26
|
+
const initialValue = cloneValue(defaultValue);
|
|
27
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
28
|
+
await writeFile(filePath, `${JSON.stringify(initialValue, null, 2)}
|
|
29
|
+
`, "utf8");
|
|
30
|
+
return initialValue;
|
|
31
|
+
}
|
|
32
|
+
const content = await readFile(filePath, "utf8");
|
|
33
|
+
return JSON.parse(content);
|
|
34
|
+
},
|
|
35
|
+
async write({
|
|
36
|
+
filePath,
|
|
37
|
+
value,
|
|
38
|
+
backupExisting = false
|
|
39
|
+
}) {
|
|
40
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
41
|
+
if (backupExisting && await fileExists(filePath)) {
|
|
42
|
+
await copyFile(filePath, `${filePath}.bak`);
|
|
43
|
+
}
|
|
44
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}
|
|
45
|
+
`, "utf8");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/config/opencode-repository.ts
|
|
51
|
+
var DEFAULT_OPENCODE_CONFIG = {
|
|
52
|
+
provider: {},
|
|
53
|
+
model: null,
|
|
54
|
+
small_model: null
|
|
55
|
+
};
|
|
56
|
+
function normalizeOpenCodeConfig(value) {
|
|
57
|
+
if (!value) {
|
|
58
|
+
return { ...DEFAULT_OPENCODE_CONFIG };
|
|
59
|
+
}
|
|
60
|
+
const { provider, model, small_model, ...rest } = value;
|
|
61
|
+
return {
|
|
62
|
+
...rest,
|
|
63
|
+
provider: provider ?? {},
|
|
64
|
+
model: model ?? null,
|
|
65
|
+
small_model: small_model ?? null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function createOpencodeRepository(input) {
|
|
69
|
+
const { filePath, jsonStore } = input;
|
|
70
|
+
return {
|
|
71
|
+
async read() {
|
|
72
|
+
const config = await jsonStore.read({ filePath, defaultValue: DEFAULT_OPENCODE_CONFIG });
|
|
73
|
+
return normalizeOpenCodeConfig(config);
|
|
74
|
+
},
|
|
75
|
+
async write(config) {
|
|
76
|
+
await jsonStore.write({
|
|
77
|
+
filePath,
|
|
78
|
+
value: normalizeOpenCodeConfig(config),
|
|
79
|
+
backupExisting: true
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/config/switch-repository.ts
|
|
86
|
+
var DEFAULT_SWITCH_CONFIG = {
|
|
87
|
+
activeVendor: null,
|
|
88
|
+
managedProviders: []
|
|
89
|
+
};
|
|
90
|
+
function normalizeSwitchConfig(value) {
|
|
91
|
+
return {
|
|
92
|
+
activeVendor: value?.activeVendor ?? null,
|
|
93
|
+
managedProviders: (value?.managedProviders ?? []).map((provider) => ({
|
|
94
|
+
vendorName: provider.vendorName,
|
|
95
|
+
models: (provider.models ?? []).map((model) => ({
|
|
96
|
+
modelType: model.modelType,
|
|
97
|
+
realModel: model.realModel,
|
|
98
|
+
activeKeyAlias: model.activeKeyAlias ?? null,
|
|
99
|
+
keys: (model.keys ?? []).map((key) => ({
|
|
100
|
+
keyAlias: key.keyAlias,
|
|
101
|
+
apiKey: key.apiKey,
|
|
102
|
+
...key.baseURL ? { baseURL: key.baseURL } : {}
|
|
103
|
+
}))
|
|
104
|
+
}))
|
|
105
|
+
}))
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function createSwitchRepository(input) {
|
|
109
|
+
const { filePath, jsonStore } = input;
|
|
110
|
+
return {
|
|
111
|
+
async read() {
|
|
112
|
+
const config = await jsonStore.read({ filePath, defaultValue: DEFAULT_SWITCH_CONFIG });
|
|
113
|
+
return normalizeSwitchConfig(config);
|
|
114
|
+
},
|
|
115
|
+
async write(config) {
|
|
116
|
+
await jsonStore.write({
|
|
117
|
+
filePath,
|
|
118
|
+
value: normalizeSwitchConfig(config)
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/prompt-client.ts
|
|
125
|
+
import {
|
|
126
|
+
cancel,
|
|
127
|
+
confirm,
|
|
128
|
+
intro,
|
|
129
|
+
isCancel,
|
|
130
|
+
note,
|
|
131
|
+
outro,
|
|
132
|
+
password,
|
|
133
|
+
select,
|
|
134
|
+
spinner,
|
|
135
|
+
text
|
|
136
|
+
} from "@clack/prompts";
|
|
137
|
+
function unwrapCanceled(value) {
|
|
138
|
+
if (isCancel(value)) {
|
|
139
|
+
cancel("Canceled");
|
|
140
|
+
throw new Error("Canceled");
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
function createPromptClient() {
|
|
145
|
+
return {
|
|
146
|
+
intro(message) {
|
|
147
|
+
intro(message);
|
|
148
|
+
},
|
|
149
|
+
outro(message) {
|
|
150
|
+
outro(message);
|
|
151
|
+
},
|
|
152
|
+
note(message, title) {
|
|
153
|
+
note(message, title);
|
|
154
|
+
},
|
|
155
|
+
async select(input) {
|
|
156
|
+
return unwrapCanceled(
|
|
157
|
+
await select({
|
|
158
|
+
message: input.message,
|
|
159
|
+
options: input.options.map((option) => ({
|
|
160
|
+
value: option.value,
|
|
161
|
+
label: option.label,
|
|
162
|
+
...option.hint ? { hint: option.hint } : {}
|
|
163
|
+
}))
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
async text(input) {
|
|
168
|
+
return unwrapCanceled(
|
|
169
|
+
await text({
|
|
170
|
+
message: input.message,
|
|
171
|
+
...input.placeholder ? { placeholder: input.placeholder } : {},
|
|
172
|
+
...input.defaultValue ? { defaultValue: input.defaultValue } : {},
|
|
173
|
+
validate(value) {
|
|
174
|
+
return value?.trim().length ? void 0 : "Required";
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
async password(input) {
|
|
180
|
+
return unwrapCanceled(
|
|
181
|
+
await password({
|
|
182
|
+
message: input.message,
|
|
183
|
+
...input.placeholder ? { placeholder: input.placeholder } : {},
|
|
184
|
+
validate(value) {
|
|
185
|
+
return value?.trim().length ? void 0 : "Required";
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
async confirm(input) {
|
|
191
|
+
return unwrapCanceled(
|
|
192
|
+
await confirm({
|
|
193
|
+
message: input.message,
|
|
194
|
+
...input.initialValue !== void 0 ? { initialValue: input.initialValue } : {}
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
async withSpinner(message, action) {
|
|
199
|
+
const s = spinner();
|
|
200
|
+
s.start(message);
|
|
201
|
+
try {
|
|
202
|
+
const result = await action();
|
|
203
|
+
s.stop("Done");
|
|
204
|
+
return result;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
s.stop("Failed");
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/types.ts
|
|
214
|
+
var SUPPORTED_MODEL_TYPES = ["gpt-5.4", "gemini"];
|
|
215
|
+
|
|
216
|
+
// src/services/model-service.ts
|
|
217
|
+
function isSupportedModelType(modelType) {
|
|
218
|
+
return SUPPORTED_MODEL_TYPES.includes(modelType);
|
|
219
|
+
}
|
|
220
|
+
function requireSupportedModelType(modelType) {
|
|
221
|
+
if (!isSupportedModelType(modelType)) {
|
|
222
|
+
throw new Error("Unsupported model type");
|
|
223
|
+
}
|
|
224
|
+
return modelType;
|
|
225
|
+
}
|
|
226
|
+
function upsertManagedProvider(providers, nextProvider) {
|
|
227
|
+
const providerIndex = providers.findIndex((provider) => provider.vendorName === nextProvider.vendorName);
|
|
228
|
+
if (providerIndex === -1) {
|
|
229
|
+
return [...providers, nextProvider];
|
|
230
|
+
}
|
|
231
|
+
return providers.map((provider, index) => index === providerIndex ? nextProvider : provider);
|
|
232
|
+
}
|
|
233
|
+
function upsertManagedModel(providers, vendorName, nextModel) {
|
|
234
|
+
const existingProvider = providers.find((provider) => provider.vendorName === vendorName);
|
|
235
|
+
const existingModels = existingProvider?.models ?? [];
|
|
236
|
+
const otherModels = existingModels.filter((model) => model.modelType !== nextModel.modelType);
|
|
237
|
+
const nextProvider = {
|
|
238
|
+
vendorName,
|
|
239
|
+
models: [...otherModels, nextModel]
|
|
240
|
+
};
|
|
241
|
+
return upsertManagedProvider(providers, nextProvider);
|
|
242
|
+
}
|
|
243
|
+
function getManagedProvider(config, vendorName) {
|
|
244
|
+
return config.managedProviders.find((provider) => provider.vendorName === vendorName);
|
|
245
|
+
}
|
|
246
|
+
function getManagedModel(provider, modelType) {
|
|
247
|
+
return provider?.models.find((model) => model.modelType === modelType);
|
|
248
|
+
}
|
|
249
|
+
function requireManagedModel(config, vendorName, modelType) {
|
|
250
|
+
const managedModel = getManagedModel(getManagedProvider(config, vendorName), modelType);
|
|
251
|
+
if (!managedModel) {
|
|
252
|
+
throw new Error(`Unknown model: ${vendorName}/${modelType}`);
|
|
253
|
+
}
|
|
254
|
+
return managedModel;
|
|
255
|
+
}
|
|
256
|
+
function mergeProviderOptions(existingOptions, nextOptions) {
|
|
257
|
+
const mergedOptions = {
|
|
258
|
+
...existingOptions ?? {},
|
|
259
|
+
...nextOptions.baseURL ? { baseURL: nextOptions.baseURL } : {},
|
|
260
|
+
...nextOptions.apiKey ? { apiKey: nextOptions.apiKey } : {}
|
|
261
|
+
};
|
|
262
|
+
return Object.keys(mergedOptions).length > 0 ? mergedOptions : void 0;
|
|
263
|
+
}
|
|
264
|
+
function findTemplateModel(input) {
|
|
265
|
+
const currentProviderModel = input.opencodeConfig.provider[input.vendorName]?.models[input.modelType];
|
|
266
|
+
if (currentProviderModel) {
|
|
267
|
+
return currentProviderModel;
|
|
268
|
+
}
|
|
269
|
+
const duckTemplateModel = input.opencodeConfig.provider.duck?.models[input.modelType];
|
|
270
|
+
if (duckTemplateModel) {
|
|
271
|
+
return duckTemplateModel;
|
|
272
|
+
}
|
|
273
|
+
return Object.values(input.opencodeConfig.provider).map((provider) => provider.models[input.modelType]).find((model) => model !== void 0);
|
|
274
|
+
}
|
|
275
|
+
function applyActiveKeyToPrimaryModel(input) {
|
|
276
|
+
const { opencodeConfig, switchConfig, vendorName, modelType, key } = input;
|
|
277
|
+
const provider = opencodeConfig.provider[vendorName];
|
|
278
|
+
const primaryPointer = opencodeConfig.model;
|
|
279
|
+
if (!provider || switchConfig.activeVendor !== vendorName || primaryPointer !== `${vendorName}/${modelType}`) {
|
|
280
|
+
return opencodeConfig;
|
|
281
|
+
}
|
|
282
|
+
const mergedOptions = key ? mergeProviderOptions(provider.options, {
|
|
283
|
+
apiKey: key.apiKey,
|
|
284
|
+
...key.baseURL ? { baseURL: key.baseURL } : {}
|
|
285
|
+
}) : void 0;
|
|
286
|
+
const nextProvider = key ? {
|
|
287
|
+
...provider,
|
|
288
|
+
...mergedOptions ? { options: mergedOptions } : {}
|
|
289
|
+
} : provider;
|
|
290
|
+
return {
|
|
291
|
+
...opencodeConfig,
|
|
292
|
+
provider: {
|
|
293
|
+
...opencodeConfig.provider,
|
|
294
|
+
[vendorName]: nextProvider
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function createModelService(input) {
|
|
299
|
+
const { opencodeRepository, switchRepository } = input;
|
|
300
|
+
return {
|
|
301
|
+
async addModel(model) {
|
|
302
|
+
const supportedModelType = requireSupportedModelType(model.modelType);
|
|
303
|
+
const opencodeConfig = await opencodeRepository.read();
|
|
304
|
+
const provider = opencodeConfig.provider[model.vendorName];
|
|
305
|
+
if (!provider) {
|
|
306
|
+
throw new Error(`Unknown vendor: ${model.vendorName}`);
|
|
307
|
+
}
|
|
308
|
+
const nextOpencodeConfig = {
|
|
309
|
+
...opencodeConfig,
|
|
310
|
+
provider: {
|
|
311
|
+
...opencodeConfig.provider,
|
|
312
|
+
[model.vendorName]: {
|
|
313
|
+
...provider,
|
|
314
|
+
models: {
|
|
315
|
+
...provider.models,
|
|
316
|
+
[supportedModelType]: {
|
|
317
|
+
...findTemplateModel({
|
|
318
|
+
opencodeConfig,
|
|
319
|
+
vendorName: model.vendorName,
|
|
320
|
+
modelType: supportedModelType
|
|
321
|
+
}) ?? {},
|
|
322
|
+
name: model.realModel
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
await opencodeRepository.write(nextOpencodeConfig);
|
|
329
|
+
const switchConfig = await switchRepository.read();
|
|
330
|
+
const existingModel = getManagedModel(getManagedProvider(switchConfig, model.vendorName), supportedModelType);
|
|
331
|
+
const nextSwitchConfig = {
|
|
332
|
+
...switchConfig,
|
|
333
|
+
managedProviders: upsertManagedModel(switchConfig.managedProviders, model.vendorName, {
|
|
334
|
+
modelType: supportedModelType,
|
|
335
|
+
realModel: model.realModel,
|
|
336
|
+
activeKeyAlias: existingModel?.activeKeyAlias ?? null,
|
|
337
|
+
keys: existingModel?.keys ?? []
|
|
338
|
+
})
|
|
339
|
+
};
|
|
340
|
+
await switchRepository.write(nextSwitchConfig);
|
|
341
|
+
},
|
|
342
|
+
async addKey(input2) {
|
|
343
|
+
const modelType = requireSupportedModelType(input2.modelType);
|
|
344
|
+
const opencodeConfig = await opencodeRepository.read();
|
|
345
|
+
if (!opencodeConfig.provider[input2.vendorName]) {
|
|
346
|
+
throw new Error(`Unknown vendor: ${input2.vendorName}`);
|
|
347
|
+
}
|
|
348
|
+
const switchConfig = await switchRepository.read();
|
|
349
|
+
const managedModel = requireManagedModel(switchConfig, input2.vendorName, modelType);
|
|
350
|
+
const otherKeys = managedModel.keys.filter((key) => key.keyAlias !== input2.keyAlias);
|
|
351
|
+
const nextKey = {
|
|
352
|
+
keyAlias: input2.keyAlias,
|
|
353
|
+
apiKey: input2.apiKey,
|
|
354
|
+
...input2.baseURL ? { baseURL: input2.baseURL } : {}
|
|
355
|
+
};
|
|
356
|
+
const nextModel = {
|
|
357
|
+
...managedModel,
|
|
358
|
+
activeKeyAlias: managedModel.activeKeyAlias ?? input2.keyAlias,
|
|
359
|
+
keys: [...otherKeys, nextKey].sort((left, right) => left.keyAlias.localeCompare(right.keyAlias))
|
|
360
|
+
};
|
|
361
|
+
const nextSwitchConfig = {
|
|
362
|
+
...switchConfig,
|
|
363
|
+
managedProviders: upsertManagedModel(switchConfig.managedProviders, input2.vendorName, nextModel)
|
|
364
|
+
};
|
|
365
|
+
await switchRepository.write(nextSwitchConfig);
|
|
366
|
+
const activeKey = nextModel.keys.find((key) => key.keyAlias === nextModel.activeKeyAlias);
|
|
367
|
+
const nextOpencodeConfig = applyActiveKeyToPrimaryModel({
|
|
368
|
+
opencodeConfig,
|
|
369
|
+
switchConfig: nextSwitchConfig,
|
|
370
|
+
vendorName: input2.vendorName,
|
|
371
|
+
modelType,
|
|
372
|
+
key: activeKey
|
|
373
|
+
});
|
|
374
|
+
if (nextOpencodeConfig !== opencodeConfig) {
|
|
375
|
+
await opencodeRepository.write(nextOpencodeConfig);
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
async listModels(vendorName) {
|
|
379
|
+
const switchConfig = await switchRepository.read();
|
|
380
|
+
const managedProvider = getManagedProvider(switchConfig, vendorName);
|
|
381
|
+
return (managedProvider?.models ?? []).slice().sort((left, right) => left.modelType.localeCompare(right.modelType)).map((model) => ({
|
|
382
|
+
vendorName,
|
|
383
|
+
modelType: model.modelType,
|
|
384
|
+
realModel: model.realModel,
|
|
385
|
+
activeKeyAlias: model.activeKeyAlias,
|
|
386
|
+
keyCount: model.keys.length
|
|
387
|
+
}));
|
|
388
|
+
},
|
|
389
|
+
async listKeys(vendorName, modelType) {
|
|
390
|
+
const supportedModelType = requireSupportedModelType(modelType);
|
|
391
|
+
const switchConfig = await switchRepository.read();
|
|
392
|
+
const managedModel = requireManagedModel(switchConfig, vendorName, supportedModelType);
|
|
393
|
+
return managedModel.keys.slice().sort((left, right) => left.keyAlias.localeCompare(right.keyAlias)).map((key) => ({
|
|
394
|
+
keyAlias: key.keyAlias,
|
|
395
|
+
isActive: key.keyAlias === managedModel.activeKeyAlias
|
|
396
|
+
}));
|
|
397
|
+
},
|
|
398
|
+
async useKey(vendorName, modelType, keyAlias) {
|
|
399
|
+
const supportedModelType = requireSupportedModelType(modelType);
|
|
400
|
+
const switchConfig = await switchRepository.read();
|
|
401
|
+
const managedModel = requireManagedModel(switchConfig, vendorName, supportedModelType);
|
|
402
|
+
const activeKey = managedModel.keys.find((key) => key.keyAlias === keyAlias);
|
|
403
|
+
if (!activeKey) {
|
|
404
|
+
throw new Error(`Unknown key: ${vendorName}/${supportedModelType}/${keyAlias}`);
|
|
405
|
+
}
|
|
406
|
+
const nextModel = {
|
|
407
|
+
...managedModel,
|
|
408
|
+
activeKeyAlias: keyAlias
|
|
409
|
+
};
|
|
410
|
+
const nextSwitchConfig = {
|
|
411
|
+
...switchConfig,
|
|
412
|
+
managedProviders: upsertManagedModel(switchConfig.managedProviders, vendorName, nextModel)
|
|
413
|
+
};
|
|
414
|
+
await switchRepository.write(nextSwitchConfig);
|
|
415
|
+
const opencodeConfig = await opencodeRepository.read();
|
|
416
|
+
const nextOpencodeConfig = applyActiveKeyToPrimaryModel({
|
|
417
|
+
opencodeConfig,
|
|
418
|
+
switchConfig: nextSwitchConfig,
|
|
419
|
+
vendorName,
|
|
420
|
+
modelType: supportedModelType,
|
|
421
|
+
key: activeKey
|
|
422
|
+
});
|
|
423
|
+
if (nextOpencodeConfig !== opencodeConfig) {
|
|
424
|
+
await opencodeRepository.write(nextOpencodeConfig);
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
async removeModel(vendorName, modelType) {
|
|
428
|
+
const supportedModelType = requireSupportedModelType(modelType);
|
|
429
|
+
const opencodeConfig = await opencodeRepository.read();
|
|
430
|
+
const provider = opencodeConfig.provider[vendorName];
|
|
431
|
+
if (!provider) {
|
|
432
|
+
throw new Error(`Unknown vendor: ${vendorName}`);
|
|
433
|
+
}
|
|
434
|
+
const nextModels = { ...provider.models };
|
|
435
|
+
delete nextModels[supportedModelType];
|
|
436
|
+
const targetModelPointer = `${vendorName}/${supportedModelType}`;
|
|
437
|
+
await opencodeRepository.write({
|
|
438
|
+
...opencodeConfig,
|
|
439
|
+
provider: {
|
|
440
|
+
...opencodeConfig.provider,
|
|
441
|
+
[vendorName]: {
|
|
442
|
+
...provider,
|
|
443
|
+
models: nextModels
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
model: opencodeConfig.model === targetModelPointer ? null : opencodeConfig.model,
|
|
447
|
+
small_model: opencodeConfig.small_model === targetModelPointer ? null : opencodeConfig.small_model
|
|
448
|
+
});
|
|
449
|
+
const switchConfig = await switchRepository.read();
|
|
450
|
+
await switchRepository.write({
|
|
451
|
+
...switchConfig,
|
|
452
|
+
managedProviders: switchConfig.managedProviders.map((providerMetadata) => {
|
|
453
|
+
if (providerMetadata.vendorName !== vendorName) {
|
|
454
|
+
return providerMetadata;
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
...providerMetadata,
|
|
458
|
+
models: providerMetadata.models.filter(
|
|
459
|
+
(modelMetadata) => modelMetadata.modelType !== supportedModelType
|
|
460
|
+
)
|
|
461
|
+
};
|
|
462
|
+
})
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
async removeKey(vendorName, modelType, keyAlias) {
|
|
466
|
+
const supportedModelType = requireSupportedModelType(modelType);
|
|
467
|
+
const switchConfig = await switchRepository.read();
|
|
468
|
+
const managedModel = requireManagedModel(switchConfig, vendorName, supportedModelType);
|
|
469
|
+
if (!managedModel.keys.some((key) => key.keyAlias === keyAlias)) {
|
|
470
|
+
throw new Error(`Unknown key: ${vendorName}/${supportedModelType}/${keyAlias}`);
|
|
471
|
+
}
|
|
472
|
+
const nextKeys = managedModel.keys.filter((key) => key.keyAlias !== keyAlias);
|
|
473
|
+
const nextActiveKeyAlias = managedModel.activeKeyAlias === keyAlias ? nextKeys[0]?.keyAlias ?? null : managedModel.activeKeyAlias;
|
|
474
|
+
const nextModel = {
|
|
475
|
+
...managedModel,
|
|
476
|
+
activeKeyAlias: nextActiveKeyAlias,
|
|
477
|
+
keys: nextKeys
|
|
478
|
+
};
|
|
479
|
+
const nextSwitchConfig = {
|
|
480
|
+
...switchConfig,
|
|
481
|
+
managedProviders: upsertManagedModel(switchConfig.managedProviders, vendorName, nextModel)
|
|
482
|
+
};
|
|
483
|
+
await switchRepository.write(nextSwitchConfig);
|
|
484
|
+
const opencodeConfig = await opencodeRepository.read();
|
|
485
|
+
const activeKey = nextModel.keys.find((key) => key.keyAlias === nextActiveKeyAlias);
|
|
486
|
+
const nextOpencodeConfig = applyActiveKeyToPrimaryModel({
|
|
487
|
+
opencodeConfig,
|
|
488
|
+
switchConfig: nextSwitchConfig,
|
|
489
|
+
vendorName,
|
|
490
|
+
modelType: supportedModelType,
|
|
491
|
+
key: activeKey
|
|
492
|
+
});
|
|
493
|
+
if (nextOpencodeConfig !== opencodeConfig) {
|
|
494
|
+
await opencodeRepository.write(nextOpencodeConfig);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/services/vendor-service.ts
|
|
501
|
+
function upsertManagedProvider2(managedProviders, vendorName) {
|
|
502
|
+
const existingProvider = managedProviders.find((provider) => provider.vendorName === vendorName);
|
|
503
|
+
if (existingProvider) {
|
|
504
|
+
return managedProviders;
|
|
505
|
+
}
|
|
506
|
+
return [...managedProviders, { vendorName, models: [] }];
|
|
507
|
+
}
|
|
508
|
+
function createProviderOptions(input) {
|
|
509
|
+
const options = {
|
|
510
|
+
...input.templateOptions ?? {},
|
|
511
|
+
...input.existingOptions ?? {},
|
|
512
|
+
...input.baseURL ? { baseURL: input.baseURL } : {},
|
|
513
|
+
...input.apiKey ? { apiKey: input.apiKey } : {}
|
|
514
|
+
};
|
|
515
|
+
return Object.keys(options).length > 0 ? options : void 0;
|
|
516
|
+
}
|
|
517
|
+
function selectModelPointers(vendorName, provider) {
|
|
518
|
+
const availableModelTypes = Object.keys(provider.models).sort();
|
|
519
|
+
if (availableModelTypes.length === 0) {
|
|
520
|
+
return {
|
|
521
|
+
model: null,
|
|
522
|
+
smallModel: null
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const primaryModelType = provider.models["gpt-5.4"] ? "gpt-5.4" : availableModelTypes[0];
|
|
526
|
+
const smallModelType = provider.models.gemini ? "gemini" : primaryModelType;
|
|
527
|
+
return {
|
|
528
|
+
model: primaryModelType ? `${vendorName}/${primaryModelType}` : null,
|
|
529
|
+
smallModel: smallModelType ? `${vendorName}/${smallModelType}` : null
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function findActiveModelKey(input) {
|
|
533
|
+
const managedProvider = input.switchConfig.managedProviders.find(
|
|
534
|
+
(provider) => provider.vendorName === input.vendorName
|
|
535
|
+
);
|
|
536
|
+
const managedModel = managedProvider?.models.find((model) => model.modelType === input.modelType);
|
|
537
|
+
if (!managedModel?.activeKeyAlias) {
|
|
538
|
+
return void 0;
|
|
539
|
+
}
|
|
540
|
+
return managedModel.keys.find((key) => key.keyAlias === managedModel.activeKeyAlias);
|
|
541
|
+
}
|
|
542
|
+
function createVendorService(input) {
|
|
543
|
+
const { opencodeRepository, switchRepository } = input;
|
|
544
|
+
return {
|
|
545
|
+
async addVendor(vendor) {
|
|
546
|
+
const opencodeConfig = await opencodeRepository.read();
|
|
547
|
+
const existingProvider = opencodeConfig.provider[vendor.vendorName];
|
|
548
|
+
const providerOptions = createProviderOptions({
|
|
549
|
+
templateOptions: vendor.templateOptions,
|
|
550
|
+
existingOptions: existingProvider?.options,
|
|
551
|
+
...vendor.baseURL ? { baseURL: vendor.baseURL } : {},
|
|
552
|
+
...vendor.apiKey ? { apiKey: vendor.apiKey } : {}
|
|
553
|
+
}) ?? existingProvider?.options;
|
|
554
|
+
const providerConfig = {
|
|
555
|
+
...existingProvider ?? {},
|
|
556
|
+
name: vendor.vendorName,
|
|
557
|
+
npm: vendor.npm,
|
|
558
|
+
models: existingProvider?.models ?? {},
|
|
559
|
+
...providerOptions ? { options: providerOptions } : {}
|
|
560
|
+
};
|
|
561
|
+
const nextConfig = {
|
|
562
|
+
...opencodeConfig,
|
|
563
|
+
provider: {
|
|
564
|
+
...opencodeConfig.provider,
|
|
565
|
+
[vendor.vendorName]: providerConfig
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
await opencodeRepository.write(nextConfig);
|
|
569
|
+
const switchConfig = await switchRepository.read();
|
|
570
|
+
const nextSwitchConfig = {
|
|
571
|
+
...switchConfig,
|
|
572
|
+
managedProviders: upsertManagedProvider2(switchConfig.managedProviders, vendor.vendorName)
|
|
573
|
+
};
|
|
574
|
+
await switchRepository.write(nextSwitchConfig);
|
|
575
|
+
},
|
|
576
|
+
async listVendors() {
|
|
577
|
+
const opencodeConfig = await opencodeRepository.read();
|
|
578
|
+
const switchConfig = await switchRepository.read();
|
|
579
|
+
const managedProviders = new Map(
|
|
580
|
+
switchConfig.managedProviders.map((provider) => [provider.vendorName, provider])
|
|
581
|
+
);
|
|
582
|
+
return Object.keys(opencodeConfig.provider).sort().map((vendorName) => {
|
|
583
|
+
const managedProvider = managedProviders.get(vendorName);
|
|
584
|
+
const provider = opencodeConfig.provider[vendorName];
|
|
585
|
+
return {
|
|
586
|
+
vendorName,
|
|
587
|
+
managed: managedProvider !== void 0,
|
|
588
|
+
modelCount: managedProvider?.models.length ?? Object.keys(provider?.models ?? {}).length
|
|
589
|
+
};
|
|
590
|
+
});
|
|
591
|
+
},
|
|
592
|
+
async useVendor(vendorName) {
|
|
593
|
+
const opencodeConfig = await opencodeRepository.read();
|
|
594
|
+
const provider = opencodeConfig.provider[vendorName];
|
|
595
|
+
if (!provider) {
|
|
596
|
+
throw new Error(`Unknown vendor: ${vendorName}`);
|
|
597
|
+
}
|
|
598
|
+
const pointers = selectModelPointers(vendorName, provider);
|
|
599
|
+
const switchConfig = await switchRepository.read();
|
|
600
|
+
const primaryModelType = pointers.model?.split("/")[1] ?? null;
|
|
601
|
+
const activeKey = findActiveModelKey({
|
|
602
|
+
switchConfig,
|
|
603
|
+
vendorName,
|
|
604
|
+
modelType: primaryModelType
|
|
605
|
+
});
|
|
606
|
+
const providerOptions = activeKey ? {
|
|
607
|
+
...provider.options ?? {},
|
|
608
|
+
apiKey: activeKey.apiKey,
|
|
609
|
+
...activeKey.baseURL ? { baseURL: activeKey.baseURL } : {}
|
|
610
|
+
} : provider.options;
|
|
611
|
+
await opencodeRepository.write({
|
|
612
|
+
...opencodeConfig,
|
|
613
|
+
model: pointers.model,
|
|
614
|
+
small_model: pointers.smallModel,
|
|
615
|
+
provider: {
|
|
616
|
+
...opencodeConfig.provider,
|
|
617
|
+
[vendorName]: {
|
|
618
|
+
...provider,
|
|
619
|
+
...providerOptions ? { options: providerOptions } : {}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
await switchRepository.write({
|
|
624
|
+
...switchConfig,
|
|
625
|
+
activeVendor: vendorName
|
|
626
|
+
});
|
|
627
|
+
},
|
|
628
|
+
async removeVendor(vendorName) {
|
|
629
|
+
const opencodeConfig = await opencodeRepository.read();
|
|
630
|
+
if (!opencodeConfig.provider[vendorName]) {
|
|
631
|
+
throw new Error(`Unknown vendor: ${vendorName}`);
|
|
632
|
+
}
|
|
633
|
+
const nextProviders = { ...opencodeConfig.provider };
|
|
634
|
+
delete nextProviders[vendorName];
|
|
635
|
+
await opencodeRepository.write({
|
|
636
|
+
...opencodeConfig,
|
|
637
|
+
provider: nextProviders,
|
|
638
|
+
model: opencodeConfig.model?.startsWith(`${vendorName}/`) ? null : opencodeConfig.model,
|
|
639
|
+
small_model: opencodeConfig.small_model?.startsWith(`${vendorName}/`) ? null : opencodeConfig.small_model
|
|
640
|
+
});
|
|
641
|
+
const switchConfig = await switchRepository.read();
|
|
642
|
+
await switchRepository.write({
|
|
643
|
+
activeVendor: switchConfig.activeVendor === vendorName ? null : switchConfig.activeVendor,
|
|
644
|
+
managedProviders: switchConfig.managedProviders.filter(
|
|
645
|
+
(provider) => provider.vendorName !== vendorName
|
|
646
|
+
)
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/cli.ts
|
|
653
|
+
var DEFAULT_VENDOR_NPM = "@ai-sdk/openai";
|
|
654
|
+
function resolveConfigPaths(homeDir) {
|
|
655
|
+
const configDir = path2.join(homeDir, ".config", "opencode");
|
|
656
|
+
return {
|
|
657
|
+
opencodeConfigPath: path2.join(configDir, "opencode.json"),
|
|
658
|
+
switchConfigPath: path2.join(configDir, "opencode-switch.json")
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function parseFlags(args) {
|
|
662
|
+
const flags = {};
|
|
663
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
664
|
+
const key = args[index];
|
|
665
|
+
const value = args[index + 1];
|
|
666
|
+
if (!key?.startsWith("--") || value === void 0) {
|
|
667
|
+
throw new Error(`Invalid arguments: ${args.join(" ")}`);
|
|
668
|
+
}
|
|
669
|
+
flags[key.slice(2)] = value;
|
|
670
|
+
}
|
|
671
|
+
return flags;
|
|
672
|
+
}
|
|
673
|
+
function formatList(lines) {
|
|
674
|
+
return lines.length > 0 ? `${lines.join("\n")}
|
|
675
|
+
` : "";
|
|
676
|
+
}
|
|
677
|
+
function formatVendorDashboard(input) {
|
|
678
|
+
if (input.vendors.length === 0) {
|
|
679
|
+
return [pc.bold("OpenCode Switch"), "No vendors configured yet."].join("\n");
|
|
680
|
+
}
|
|
681
|
+
return [
|
|
682
|
+
pc.bold("OpenCode Switch"),
|
|
683
|
+
...input.vendors.map((vendor, index) => {
|
|
684
|
+
const marker = vendor.vendorName === input.activeVendor ? pc.cyan(">") : "-";
|
|
685
|
+
const activeLabel = vendor.vendorName === input.activeVendor ? ` ${pc.green("[active]")}` : "";
|
|
686
|
+
return `${marker} ${pc.bold(vendor.vendorName)}${activeLabel} models:${vendor.modelCount}`;
|
|
687
|
+
})
|
|
688
|
+
].join("\n");
|
|
689
|
+
}
|
|
690
|
+
function createCliContext(homeDir, promptClient) {
|
|
691
|
+
const { opencodeConfigPath, switchConfigPath } = resolveConfigPaths(homeDir);
|
|
692
|
+
const jsonStore = createJsonFileStore();
|
|
693
|
+
const opencodeRepository = createOpencodeRepository({ filePath: opencodeConfigPath, jsonStore });
|
|
694
|
+
const switchRepository = createSwitchRepository({ filePath: switchConfigPath, jsonStore });
|
|
695
|
+
return {
|
|
696
|
+
opencodeRepository,
|
|
697
|
+
promptClient,
|
|
698
|
+
switchRepository,
|
|
699
|
+
vendorService: createVendorService({ opencodeRepository, switchRepository }),
|
|
700
|
+
modelService: createModelService({ opencodeRepository, switchRepository })
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function stripSensitiveTemplateOptions(options) {
|
|
704
|
+
if (!options) {
|
|
705
|
+
return void 0;
|
|
706
|
+
}
|
|
707
|
+
const nextOptions = { ...options };
|
|
708
|
+
delete nextOptions.apiKey;
|
|
709
|
+
delete nextOptions.baseURL;
|
|
710
|
+
return nextOptions;
|
|
711
|
+
}
|
|
712
|
+
function asSupportedModelType(modelType) {
|
|
713
|
+
if (!SUPPORTED_MODEL_TYPES.includes(modelType)) {
|
|
714
|
+
throw new Error("Unsupported model type");
|
|
715
|
+
}
|
|
716
|
+
return modelType;
|
|
717
|
+
}
|
|
718
|
+
async function selectVendor(context, message) {
|
|
719
|
+
const vendors = await context.vendorService.listVendors();
|
|
720
|
+
if (vendors.length === 0) {
|
|
721
|
+
throw new Error("No vendors configured");
|
|
722
|
+
}
|
|
723
|
+
return context.promptClient.select({
|
|
724
|
+
message,
|
|
725
|
+
options: vendors.map((vendor) => ({
|
|
726
|
+
value: vendor.vendorName,
|
|
727
|
+
label: vendor.vendorName,
|
|
728
|
+
hint: `${vendor.modelCount} model(s)`
|
|
729
|
+
}))
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
async function selectModelForKeys(context, vendorName) {
|
|
733
|
+
while (true) {
|
|
734
|
+
const models = await context.modelService.listModels(vendorName);
|
|
735
|
+
const selected = await context.promptClient.select({
|
|
736
|
+
message: `Pick a model for ${vendorName}`,
|
|
737
|
+
options: [
|
|
738
|
+
...models.map((model) => ({
|
|
739
|
+
value: model.modelType,
|
|
740
|
+
label: model.modelType,
|
|
741
|
+
hint: `${model.realModel}${model.activeKeyAlias ? ` | active:${model.activeKeyAlias}` : ""}`
|
|
742
|
+
})),
|
|
743
|
+
{
|
|
744
|
+
value: "__add_new_model__",
|
|
745
|
+
label: "Add new model",
|
|
746
|
+
hint: "Create a model before choosing keys"
|
|
747
|
+
}
|
|
748
|
+
]
|
|
749
|
+
});
|
|
750
|
+
if (selected !== "__add_new_model__") {
|
|
751
|
+
return asSupportedModelType(selected);
|
|
752
|
+
}
|
|
753
|
+
const modelType = asSupportedModelType(
|
|
754
|
+
await context.promptClient.select({
|
|
755
|
+
message: `Choose a model type for ${vendorName}`,
|
|
756
|
+
options: SUPPORTED_MODEL_TYPES.map((supportedType) => ({
|
|
757
|
+
value: supportedType,
|
|
758
|
+
label: supportedType
|
|
759
|
+
}))
|
|
760
|
+
})
|
|
761
|
+
);
|
|
762
|
+
const realModel = await context.promptClient.text({
|
|
763
|
+
message: `Real model id for ${vendorName}/${modelType}`,
|
|
764
|
+
placeholder: modelType === "gpt-5.4" ? "duck/gpt-5.4" : "google/gemini-2.5-flash"
|
|
765
|
+
});
|
|
766
|
+
await context.promptClient.withSpinner(`Adding ${vendorName}/${modelType}`, async () => {
|
|
767
|
+
await context.modelService.addModel({ vendorName, modelType, realModel });
|
|
768
|
+
});
|
|
769
|
+
context.promptClient.note(`${vendorName}/${modelType} is ready.`, "Model added");
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async function runInteractiveRemoveVendor(context) {
|
|
773
|
+
context.promptClient.intro("Remove vendor");
|
|
774
|
+
const vendors = await context.vendorService.listVendors();
|
|
775
|
+
if (vendors.length === 0) {
|
|
776
|
+
return { exitCode: 1, stdout: "", stderr: "No vendors configured\n" };
|
|
777
|
+
}
|
|
778
|
+
const vendorName = await selectVendor(context, "Choose a vendor to remove");
|
|
779
|
+
const targetVendor = vendors.find((vendor) => vendor.vendorName === vendorName);
|
|
780
|
+
const confirmed = await context.promptClient.confirm({
|
|
781
|
+
message: `Delete ${vendorName} and its ${targetVendor?.modelCount ?? 0} model(s)?`,
|
|
782
|
+
initialValue: false
|
|
783
|
+
});
|
|
784
|
+
if (!confirmed) {
|
|
785
|
+
context.promptClient.outro("No changes made.");
|
|
786
|
+
return { exitCode: 0, stdout: "Canceled removal\n", stderr: "" };
|
|
787
|
+
}
|
|
788
|
+
await context.promptClient.withSpinner(`Removing ${vendorName}`, async () => {
|
|
789
|
+
await context.vendorService.removeVendor(vendorName);
|
|
790
|
+
});
|
|
791
|
+
context.promptClient.outro(`${vendorName} removed.`);
|
|
792
|
+
return {
|
|
793
|
+
exitCode: 0,
|
|
794
|
+
stdout: `Removed vendor ${vendorName}
|
|
795
|
+
`,
|
|
796
|
+
stderr: ""
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
async function runInteractiveAddKey(context) {
|
|
800
|
+
context.promptClient.intro("Add or switch key");
|
|
801
|
+
const vendorName = await selectVendor(context, "Choose a vendor");
|
|
802
|
+
const modelType = await selectModelForKeys(context, vendorName);
|
|
803
|
+
const keyAlias = await context.promptClient.text({
|
|
804
|
+
message: `Remark for ${vendorName}/${modelType}`,
|
|
805
|
+
placeholder: "project-a"
|
|
806
|
+
});
|
|
807
|
+
const apiKey = await context.promptClient.password({
|
|
808
|
+
message: `API key for ${vendorName}/${modelType}`,
|
|
809
|
+
placeholder: "sk-..."
|
|
810
|
+
});
|
|
811
|
+
await context.promptClient.withSpinner(`Saving key ${keyAlias}`, async () => {
|
|
812
|
+
await context.modelService.addKey({ vendorName, modelType, keyAlias, apiKey });
|
|
813
|
+
await context.modelService.useKey(vendorName, modelType, keyAlias);
|
|
814
|
+
});
|
|
815
|
+
context.promptClient.outro(`Active key for ${vendorName}/${modelType}: ${keyAlias}`);
|
|
816
|
+
return {
|
|
817
|
+
exitCode: 0,
|
|
818
|
+
stdout: `Added key ${keyAlias} for ${vendorName}/${modelType}
|
|
819
|
+
`,
|
|
820
|
+
stderr: ""
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
async function runInteractiveSwitchKey(context) {
|
|
824
|
+
context.promptClient.intro("Switch key");
|
|
825
|
+
const vendorName = await selectVendor(context, "Choose a vendor");
|
|
826
|
+
const models = await context.modelService.listModels(vendorName);
|
|
827
|
+
if (models.length === 0) {
|
|
828
|
+
return { exitCode: 1, stdout: "", stderr: `No models configured for ${vendorName}
|
|
829
|
+
` };
|
|
830
|
+
}
|
|
831
|
+
const modelType = asSupportedModelType(
|
|
832
|
+
await context.promptClient.select({
|
|
833
|
+
message: `Choose a model for ${vendorName}`,
|
|
834
|
+
options: models.map((model) => ({
|
|
835
|
+
value: model.modelType,
|
|
836
|
+
label: model.modelType,
|
|
837
|
+
hint: model.activeKeyAlias ? `active:${model.activeKeyAlias}` : `${model.keyCount} key(s)`
|
|
838
|
+
}))
|
|
839
|
+
})
|
|
840
|
+
);
|
|
841
|
+
const keys = await context.modelService.listKeys(vendorName, modelType);
|
|
842
|
+
if (keys.length === 0) {
|
|
843
|
+
return {
|
|
844
|
+
exitCode: 1,
|
|
845
|
+
stdout: "",
|
|
846
|
+
stderr: `No keys configured for ${vendorName}/${modelType}
|
|
847
|
+
`
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
const keyAlias = await context.promptClient.select({
|
|
851
|
+
message: `Choose the active key for ${vendorName}/${modelType}`,
|
|
852
|
+
options: keys.map((key) => ({
|
|
853
|
+
value: key.keyAlias,
|
|
854
|
+
label: key.keyAlias,
|
|
855
|
+
hint: key.isActive ? "active" : "saved key"
|
|
856
|
+
}))
|
|
857
|
+
});
|
|
858
|
+
await context.promptClient.withSpinner(`Switching to ${keyAlias}`, async () => {
|
|
859
|
+
await context.modelService.useKey(vendorName, modelType, keyAlias);
|
|
860
|
+
});
|
|
861
|
+
context.promptClient.outro(`Active key for ${vendorName}/${modelType}: ${keyAlias}`);
|
|
862
|
+
return {
|
|
863
|
+
exitCode: 0,
|
|
864
|
+
stdout: `Using key ${keyAlias} for ${vendorName}/${modelType}
|
|
865
|
+
`,
|
|
866
|
+
stderr: ""
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
async function runInteractiveRemoveKey(context) {
|
|
870
|
+
context.promptClient.intro("Remove key");
|
|
871
|
+
const vendorName = await selectVendor(context, "Choose a vendor");
|
|
872
|
+
const models = await context.modelService.listModels(vendorName);
|
|
873
|
+
if (models.length === 0) {
|
|
874
|
+
return { exitCode: 1, stdout: "", stderr: `No models configured for ${vendorName}
|
|
875
|
+
` };
|
|
876
|
+
}
|
|
877
|
+
const modelType = asSupportedModelType(
|
|
878
|
+
await context.promptClient.select({
|
|
879
|
+
message: `Choose a model for ${vendorName}`,
|
|
880
|
+
options: models.map((model) => ({
|
|
881
|
+
value: model.modelType,
|
|
882
|
+
label: model.modelType,
|
|
883
|
+
hint: `${model.keyCount} key(s)`
|
|
884
|
+
}))
|
|
885
|
+
})
|
|
886
|
+
);
|
|
887
|
+
const keys = await context.modelService.listKeys(vendorName, modelType);
|
|
888
|
+
if (keys.length === 0) {
|
|
889
|
+
return {
|
|
890
|
+
exitCode: 1,
|
|
891
|
+
stdout: "",
|
|
892
|
+
stderr: `No keys configured for ${vendorName}/${modelType}
|
|
893
|
+
`
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const keyAlias = await context.promptClient.select({
|
|
897
|
+
message: `Choose a key to remove from ${vendorName}/${modelType}`,
|
|
898
|
+
options: keys.map((key) => ({
|
|
899
|
+
value: key.keyAlias,
|
|
900
|
+
label: key.keyAlias,
|
|
901
|
+
hint: key.isActive ? "active" : "saved key"
|
|
902
|
+
}))
|
|
903
|
+
});
|
|
904
|
+
const confirmed = await context.promptClient.confirm({
|
|
905
|
+
message: `Remove key ${keyAlias} from ${vendorName}/${modelType}?`,
|
|
906
|
+
initialValue: false
|
|
907
|
+
});
|
|
908
|
+
if (!confirmed) {
|
|
909
|
+
context.promptClient.outro("No changes made.");
|
|
910
|
+
return { exitCode: 0, stdout: "Canceled removal\n", stderr: "" };
|
|
911
|
+
}
|
|
912
|
+
await context.promptClient.withSpinner(`Removing ${keyAlias}`, async () => {
|
|
913
|
+
await context.modelService.removeKey(vendorName, modelType, keyAlias);
|
|
914
|
+
});
|
|
915
|
+
context.promptClient.outro(`Removed ${keyAlias} from ${vendorName}/${modelType}.`);
|
|
916
|
+
return {
|
|
917
|
+
exitCode: 0,
|
|
918
|
+
stdout: `Removed key ${keyAlias} from ${vendorName}/${modelType}
|
|
919
|
+
`,
|
|
920
|
+
stderr: ""
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
async function executeCli(args, options = {}) {
|
|
924
|
+
const homeDir = options.homeDir ?? process.env.OPENCODE_SWITCH_HOME ?? os.homedir();
|
|
925
|
+
const context = createCliContext(homeDir, options.promptClient ?? createPromptClient());
|
|
926
|
+
try {
|
|
927
|
+
const [resource, action, ...rest] = args;
|
|
928
|
+
if (resource === "ls") {
|
|
929
|
+
const vendors = await context.vendorService.listVendors();
|
|
930
|
+
const switchConfig = await context.switchRepository.read();
|
|
931
|
+
return {
|
|
932
|
+
exitCode: 0,
|
|
933
|
+
stdout: `${formatVendorDashboard({ activeVendor: switchConfig.activeVendor, vendors })}
|
|
934
|
+
`,
|
|
935
|
+
stderr: ""
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
if (resource === "add") {
|
|
939
|
+
const name = action;
|
|
940
|
+
const url = rest[0];
|
|
941
|
+
if (!name || !url) {
|
|
942
|
+
throw new Error("Usage: add <name> <url>");
|
|
943
|
+
}
|
|
944
|
+
const opencodeConfig = await context.opencodeRepository.read();
|
|
945
|
+
const templateOptions = stripSensitiveTemplateOptions(opencodeConfig.provider.duck?.options);
|
|
946
|
+
await context.vendorService.addVendor({
|
|
947
|
+
vendorName: name,
|
|
948
|
+
npm: DEFAULT_VENDOR_NPM,
|
|
949
|
+
baseURL: url,
|
|
950
|
+
...templateOptions ? { templateOptions } : {}
|
|
951
|
+
});
|
|
952
|
+
return {
|
|
953
|
+
exitCode: 0,
|
|
954
|
+
stdout: `Added vendor ${name}
|
|
955
|
+
`,
|
|
956
|
+
stderr: ""
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
if (resource === "rm") {
|
|
960
|
+
return runInteractiveRemoveVendor(context);
|
|
961
|
+
}
|
|
962
|
+
if (resource === "addk") {
|
|
963
|
+
return runInteractiveAddKey(context);
|
|
964
|
+
}
|
|
965
|
+
if (resource === "switch") {
|
|
966
|
+
return runInteractiveSwitchKey(context);
|
|
967
|
+
}
|
|
968
|
+
if (resource === "rmk") {
|
|
969
|
+
return runInteractiveRemoveKey(context);
|
|
970
|
+
}
|
|
971
|
+
if (resource === "vendor" && action === "add") {
|
|
972
|
+
const [vendorName, ...flagArgs] = rest;
|
|
973
|
+
if (!vendorName) {
|
|
974
|
+
throw new Error("Missing vendor name");
|
|
975
|
+
}
|
|
976
|
+
const flags = parseFlags(flagArgs);
|
|
977
|
+
if (!flags.npm) {
|
|
978
|
+
throw new Error("Missing required flag: --npm");
|
|
979
|
+
}
|
|
980
|
+
await context.vendorService.addVendor({
|
|
981
|
+
vendorName,
|
|
982
|
+
npm: flags.npm,
|
|
983
|
+
...flags["base-url"] ? { baseURL: flags["base-url"] } : {},
|
|
984
|
+
...flags["api-key"] ? { apiKey: flags["api-key"] } : {}
|
|
985
|
+
});
|
|
986
|
+
return {
|
|
987
|
+
exitCode: 0,
|
|
988
|
+
stdout: `Added vendor ${vendorName}
|
|
989
|
+
`,
|
|
990
|
+
stderr: ""
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
if (resource === "vendor" && action === "list") {
|
|
994
|
+
const vendors = await context.vendorService.listVendors();
|
|
995
|
+
const lines = vendors.map((vendor) => `${vendor.vendorName} (${vendor.modelCount})`);
|
|
996
|
+
return {
|
|
997
|
+
exitCode: 0,
|
|
998
|
+
stdout: formatList(lines),
|
|
999
|
+
stderr: ""
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
if (resource === "vendor" && action === "use") {
|
|
1003
|
+
const [vendorName] = rest;
|
|
1004
|
+
if (!vendorName) {
|
|
1005
|
+
throw new Error("Missing vendor name");
|
|
1006
|
+
}
|
|
1007
|
+
await context.vendorService.useVendor(vendorName);
|
|
1008
|
+
return {
|
|
1009
|
+
exitCode: 0,
|
|
1010
|
+
stdout: `Using vendor ${vendorName}
|
|
1011
|
+
`,
|
|
1012
|
+
stderr: ""
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
if (resource === "vendor" && action === "remove") {
|
|
1016
|
+
const [vendorName] = rest;
|
|
1017
|
+
if (!vendorName) {
|
|
1018
|
+
throw new Error("Missing vendor name");
|
|
1019
|
+
}
|
|
1020
|
+
await context.vendorService.removeVendor(vendorName);
|
|
1021
|
+
return {
|
|
1022
|
+
exitCode: 0,
|
|
1023
|
+
stdout: `Removed vendor ${vendorName}
|
|
1024
|
+
`,
|
|
1025
|
+
stderr: ""
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
if (resource === "model" && action === "add") {
|
|
1029
|
+
const [vendorName, modelType, ...flagArgs] = rest;
|
|
1030
|
+
if (!vendorName || !modelType) {
|
|
1031
|
+
throw new Error("Missing vendor name or model type");
|
|
1032
|
+
}
|
|
1033
|
+
const flags = parseFlags(flagArgs);
|
|
1034
|
+
if (!flags.model) {
|
|
1035
|
+
throw new Error("Missing required flag: --model");
|
|
1036
|
+
}
|
|
1037
|
+
await context.modelService.addModel({
|
|
1038
|
+
vendorName,
|
|
1039
|
+
modelType: asSupportedModelType(modelType),
|
|
1040
|
+
realModel: flags.model,
|
|
1041
|
+
...flags["api-key"] ? { apiKey: flags["api-key"] } : {},
|
|
1042
|
+
...flags["base-url"] ? { baseURL: flags["base-url"] } : {}
|
|
1043
|
+
});
|
|
1044
|
+
return {
|
|
1045
|
+
exitCode: 0,
|
|
1046
|
+
stdout: `Added model ${modelType} for ${vendorName}
|
|
1047
|
+
`,
|
|
1048
|
+
stderr: ""
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
if (resource === "model" && action === "list") {
|
|
1052
|
+
const [vendorName] = rest;
|
|
1053
|
+
if (!vendorName) {
|
|
1054
|
+
throw new Error("Missing vendor name");
|
|
1055
|
+
}
|
|
1056
|
+
const models = await context.modelService.listModels(vendorName);
|
|
1057
|
+
const lines = models.map(
|
|
1058
|
+
(model) => model.activeKeyAlias ? `${model.modelType} -> ${model.realModel} (key: ${model.activeKeyAlias})` : `${model.modelType} -> ${model.realModel}`
|
|
1059
|
+
);
|
|
1060
|
+
return {
|
|
1061
|
+
exitCode: 0,
|
|
1062
|
+
stdout: formatList(lines),
|
|
1063
|
+
stderr: ""
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
if (resource === "model" && action === "remove") {
|
|
1067
|
+
const [vendorName, modelType] = rest;
|
|
1068
|
+
if (!vendorName || !modelType) {
|
|
1069
|
+
throw new Error("Missing vendor name or model type");
|
|
1070
|
+
}
|
|
1071
|
+
await context.modelService.removeModel(vendorName, asSupportedModelType(modelType));
|
|
1072
|
+
return {
|
|
1073
|
+
exitCode: 0,
|
|
1074
|
+
stdout: `Removed model ${modelType} from ${vendorName}
|
|
1075
|
+
`,
|
|
1076
|
+
stderr: ""
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
if (resource === "key" && action === "add") {
|
|
1080
|
+
const [vendorName, modelType, keyAlias, ...flagArgs] = rest;
|
|
1081
|
+
if (!vendorName || !modelType || !keyAlias) {
|
|
1082
|
+
throw new Error("Missing vendor name, model type, or key alias");
|
|
1083
|
+
}
|
|
1084
|
+
const flags = parseFlags(flagArgs);
|
|
1085
|
+
if (!flags["api-key"]) {
|
|
1086
|
+
throw new Error("Missing required flag: --api-key");
|
|
1087
|
+
}
|
|
1088
|
+
await context.modelService.addKey({
|
|
1089
|
+
vendorName,
|
|
1090
|
+
modelType: asSupportedModelType(modelType),
|
|
1091
|
+
keyAlias,
|
|
1092
|
+
apiKey: flags["api-key"],
|
|
1093
|
+
...flags["base-url"] ? { baseURL: flags["base-url"] } : {}
|
|
1094
|
+
});
|
|
1095
|
+
return {
|
|
1096
|
+
exitCode: 0,
|
|
1097
|
+
stdout: `Added key ${keyAlias} for ${vendorName}/${modelType}
|
|
1098
|
+
`,
|
|
1099
|
+
stderr: ""
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
if (resource === "key" && action === "list") {
|
|
1103
|
+
const [vendorName, modelType] = rest;
|
|
1104
|
+
if (!vendorName || !modelType) {
|
|
1105
|
+
throw new Error("Missing vendor name or model type");
|
|
1106
|
+
}
|
|
1107
|
+
const keys = await context.modelService.listKeys(vendorName, asSupportedModelType(modelType));
|
|
1108
|
+
const lines = keys.map((key) => key.isActive ? `${key.keyAlias} (active)` : key.keyAlias);
|
|
1109
|
+
return {
|
|
1110
|
+
exitCode: 0,
|
|
1111
|
+
stdout: formatList(lines),
|
|
1112
|
+
stderr: ""
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
if (resource === "key" && action === "use") {
|
|
1116
|
+
const [vendorName, modelType, keyAlias] = rest;
|
|
1117
|
+
if (!vendorName || !modelType || !keyAlias) {
|
|
1118
|
+
throw new Error("Missing vendor name, model type, or key alias");
|
|
1119
|
+
}
|
|
1120
|
+
await context.modelService.useKey(vendorName, asSupportedModelType(modelType), keyAlias);
|
|
1121
|
+
return {
|
|
1122
|
+
exitCode: 0,
|
|
1123
|
+
stdout: `Using key ${keyAlias} for ${vendorName}/${modelType}
|
|
1124
|
+
`,
|
|
1125
|
+
stderr: ""
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
if (resource === "key" && action === "remove") {
|
|
1129
|
+
const [vendorName, modelType, keyAlias] = rest;
|
|
1130
|
+
if (!vendorName || !modelType || !keyAlias) {
|
|
1131
|
+
throw new Error("Missing vendor name, model type, or key alias");
|
|
1132
|
+
}
|
|
1133
|
+
await context.modelService.removeKey(vendorName, asSupportedModelType(modelType), keyAlias);
|
|
1134
|
+
return {
|
|
1135
|
+
exitCode: 0,
|
|
1136
|
+
stdout: `Removed key ${keyAlias} from ${vendorName}/${modelType}
|
|
1137
|
+
`,
|
|
1138
|
+
stderr: ""
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
throw new Error("Unknown command");
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1144
|
+
return {
|
|
1145
|
+
exitCode: 1,
|
|
1146
|
+
stdout: "",
|
|
1147
|
+
stderr: `${message}
|
|
1148
|
+
`
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async function runCli(args, options = {}) {
|
|
1153
|
+
const result = await executeCli(args, options);
|
|
1154
|
+
const stdout = options.stdout ?? process.stdout;
|
|
1155
|
+
const stderr = options.stderr ?? process.stderr;
|
|
1156
|
+
if (result.stdout) {
|
|
1157
|
+
stdout.write(result.stdout);
|
|
1158
|
+
}
|
|
1159
|
+
if (result.stderr) {
|
|
1160
|
+
stderr.write(result.stderr);
|
|
1161
|
+
}
|
|
1162
|
+
return result.exitCode;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/bin.ts
|
|
1166
|
+
async function main() {
|
|
1167
|
+
const exitCode = await runCli(process.argv.slice(2));
|
|
1168
|
+
process.exitCode = exitCode;
|
|
1169
|
+
}
|
|
1170
|
+
void main();
|
|
1171
|
+
//# sourceMappingURL=bin.mjs.map
|