@nvae/llmswitch 0.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.
@@ -0,0 +1,380 @@
1
+ import * as p from "@clack/prompts";
2
+ import { applyProfile, deactivateProfile } from "../adapters/index.js";
3
+ import { formatLabel } from "../formats/compatibility.js";
4
+ import { deleteProfile, ensureDefaultProvider, getActiveProfile, getDefaultProfile, listProfiles, publicProfileView, requireProfile, saveProfile, setDefaultProfile, } from "../store/profiles.js";
5
+ import { formatProxySummary } from "../utils/proxy.js";
6
+ import { exitOnCancel, promptEditProfile, promptProfileDraft, resolveModelsInteractive, } from "./prompts.js";
7
+ export function registerToolCommand(program, tool) {
8
+ const cmd = program
9
+ .command(tool)
10
+ .description(`管理 ${tool} 的供应商与模型配置`);
11
+ cmd
12
+ .command("provider")
13
+ .description("管理模型供应商:添加 / 默认 / 启用禁用 / 查看 / 编辑 / 删除")
14
+ .option("--json", "以 JSON 列出全部供应商后退出")
15
+ .action(async (opts) => {
16
+ ensureDefaultProvider(tool);
17
+ if (opts.json) {
18
+ const active = getActiveProfile(tool)?.name ?? null;
19
+ const defaultName = getDefaultProfile(tool)?.name ?? null;
20
+ console.log(JSON.stringify(listProfiles(tool).map((profile) => ({
21
+ ...publicProfileView(profile),
22
+ active: profile.name === active,
23
+ default: profile.name === defaultName,
24
+ })), null, 2));
25
+ return;
26
+ }
27
+ await runProviderManager(tool);
28
+ });
29
+ cmd
30
+ .command("use")
31
+ .description("启用已有供应商(写入对应工具配置)")
32
+ .argument("[name]", "供应商名称;省略则交互选择")
33
+ .option("--json", "JSON 输出")
34
+ .action(async (name, opts) => {
35
+ ensureDefaultProvider(tool);
36
+ const profileName = await resolveProfileName(tool, name);
37
+ const profile = requireProfile(tool, profileName);
38
+ const result = await applyProfile(tool, profile);
39
+ if (opts?.json) {
40
+ console.log(JSON.stringify(result, null, 2));
41
+ return;
42
+ }
43
+ console.log(`已启用 ${tool}/${profile.name}`);
44
+ console.log(`配置文件:${result.configPath}`);
45
+ if (result.backupPath)
46
+ console.log(`备份:${result.backupPath}`);
47
+ console.log(result.restartHint);
48
+ });
49
+ cmd
50
+ .command("current")
51
+ .description("查看默认与当前启用的供应商")
52
+ .option("--json", "JSON 输出")
53
+ .action((opts) => {
54
+ ensureDefaultProvider(tool);
55
+ const active = getActiveProfile(tool);
56
+ const defaultProfile = getDefaultProfile(tool);
57
+ if (opts.json) {
58
+ console.log(JSON.stringify({
59
+ default: defaultProfile ? publicProfileView(defaultProfile) : null,
60
+ active: active ? publicProfileView(active) : null,
61
+ }, null, 2));
62
+ return;
63
+ }
64
+ if (!defaultProfile && !active) {
65
+ console.log(`当前没有 ${tool} 供应商`);
66
+ console.log(`请先:llms ${tool} provider`);
67
+ return;
68
+ }
69
+ if (defaultProfile) {
70
+ printProfileDetails(tool, defaultProfile, {
71
+ isActive: active?.name === defaultProfile.name,
72
+ isDefault: true,
73
+ title: "默认供应商",
74
+ });
75
+ }
76
+ if (active && active.name !== defaultProfile?.name) {
77
+ printProfileDetails(tool, active, {
78
+ isActive: true,
79
+ isDefault: false,
80
+ title: "当前启用",
81
+ });
82
+ }
83
+ else if (!active) {
84
+ console.log("当前没有已启用的供应商(可用 provider 菜单启用)");
85
+ }
86
+ });
87
+ registerModelCommands(cmd, tool);
88
+ }
89
+ async function runProviderManager(tool) {
90
+ p.intro(`${tool} 模型供应商`);
91
+ while (true) {
92
+ ensureDefaultProvider(tool);
93
+ const profiles = listProfiles(tool);
94
+ const active = getActiveProfile(tool)?.name ?? null;
95
+ const defaultName = getDefaultProfile(tool)?.name ?? null;
96
+ const selected = await p.select({
97
+ message: "选择供应商",
98
+ options: [
99
+ {
100
+ value: "__new__",
101
+ label: "添加新供应商",
102
+ hint: "自定义 / OpenAI / Anthropic",
103
+ },
104
+ ...profiles.map((profile) => ({
105
+ value: profile.name,
106
+ // hint 仅高亮时可见,状态标在 label 上便于扫一眼认出默认项
107
+ label: formatProviderListLabel(profile, {
108
+ defaultName,
109
+ activeName: active,
110
+ }),
111
+ hint: formatLabel(profile.apiFormat),
112
+ })),
113
+ {
114
+ value: "__exit__",
115
+ label: "退出",
116
+ },
117
+ ],
118
+ });
119
+ exitOnCancel(selected);
120
+ if (selected === "__exit__") {
121
+ p.outro("已退出供应商管理");
122
+ return;
123
+ }
124
+ if (selected === "__new__") {
125
+ await handleProviderAdd(tool);
126
+ continue;
127
+ }
128
+ await handleProviderActions(tool, selected);
129
+ }
130
+ }
131
+ async function handleProviderAdd(tool) {
132
+ const created = await promptProfileDraft(tool);
133
+ ensureDefaultProvider(tool);
134
+ const enable = await p.confirm({
135
+ message: `是否立即启用「${created.name}」?`,
136
+ initialValue: true,
137
+ });
138
+ if (p.isCancel(enable)) {
139
+ p.cancel("已取消");
140
+ process.exit(0);
141
+ }
142
+ if (enable) {
143
+ const result = await applyProfile(tool, created);
144
+ p.log.success(`已启用 ${tool}/${created.name}`);
145
+ p.log.info(`配置文件:${result.configPath}`);
146
+ p.log.info(result.restartHint);
147
+ }
148
+ }
149
+ async function handleProviderActions(tool, profileName) {
150
+ while (true) {
151
+ ensureDefaultProvider(tool);
152
+ let profile;
153
+ try {
154
+ profile = requireProfile(tool, profileName);
155
+ }
156
+ catch {
157
+ p.log.warn(`「${profileName}」已不存在`);
158
+ return;
159
+ }
160
+ const isActive = getActiveProfile(tool)?.name === profile.name;
161
+ const isDefault = getDefaultProfile(tool)?.name === profile.name;
162
+ const action = await p.select({
163
+ message: `${profile.displayName || profile.name}`,
164
+ options: [
165
+ {
166
+ value: "default",
167
+ label: "设置为默认供应商",
168
+ hint: isDefault ? "当前已是默认" : undefined,
169
+ },
170
+ {
171
+ value: "toggle",
172
+ label: isActive ? "禁用" : "启用",
173
+ hint: isActive ? "清除写入工具的配置" : "写入对应工具配置",
174
+ },
175
+ {
176
+ value: "view",
177
+ label: "查看配置",
178
+ },
179
+ {
180
+ value: "edit",
181
+ label: "编辑配置",
182
+ hint: "显示名 / 地址 / 密钥 / 代理 / 格式",
183
+ },
184
+ {
185
+ value: "delete",
186
+ label: "删除配置",
187
+ hint: isActive ? "将先禁用再删除" : undefined,
188
+ },
189
+ {
190
+ value: "back",
191
+ label: "返回列表",
192
+ },
193
+ ],
194
+ });
195
+ exitOnCancel(action);
196
+ if (action === "back")
197
+ return;
198
+ if (action === "default") {
199
+ setDefaultProfile(tool, profile.name);
200
+ p.log.success(`已将「${profile.name}」设为默认供应商`);
201
+ continue;
202
+ }
203
+ if (action === "toggle") {
204
+ if (isActive) {
205
+ const result = await deactivateProfile(tool, profile.name);
206
+ p.log.success(`已禁用「${profile.name}」`);
207
+ p.log.info(`配置文件:${result.configPath}`);
208
+ p.log.info(result.restartHint);
209
+ }
210
+ else {
211
+ const result = await applyProfile(tool, profile);
212
+ p.log.success(`已启用「${profile.name}」`);
213
+ p.log.info(`配置文件:${result.configPath}`);
214
+ if (result.backupPath)
215
+ p.log.info(`备份:${result.backupPath}`);
216
+ p.log.info(result.restartHint);
217
+ }
218
+ continue;
219
+ }
220
+ if (action === "view") {
221
+ printProfileDetails(tool, profile, { isActive, isDefault });
222
+ continue;
223
+ }
224
+ if (action === "edit") {
225
+ const updated = await promptEditProfile(tool, profile);
226
+ if (getActiveProfile(tool)?.name === updated.name) {
227
+ const sync = await p.confirm({
228
+ message: "该供应商当前已启用,是否立即写回工具配置?",
229
+ initialValue: true,
230
+ });
231
+ if (p.isCancel(sync)) {
232
+ p.cancel("已取消");
233
+ process.exit(0);
234
+ }
235
+ if (sync) {
236
+ const result = await applyProfile(tool, updated);
237
+ p.log.success("已同步写入工具配置");
238
+ p.log.info(result.restartHint);
239
+ }
240
+ }
241
+ continue;
242
+ }
243
+ if (action === "delete") {
244
+ const ok = await p.confirm({
245
+ message: `确认删除供应商「${profile.name}」?此操作不可恢复`,
246
+ initialValue: false,
247
+ });
248
+ if (p.isCancel(ok)) {
249
+ p.cancel("已取消");
250
+ process.exit(0);
251
+ }
252
+ if (!ok)
253
+ continue;
254
+ if (isActive) {
255
+ await deactivateProfile(tool, profile.name);
256
+ }
257
+ deleteProfile(tool, profile.name);
258
+ p.log.success(`已删除「${profile.name}」`);
259
+ return;
260
+ }
261
+ }
262
+ }
263
+ function printProfileDetails(tool, profile, flags) {
264
+ const view = publicProfileView(profile);
265
+ const status = [];
266
+ if (flags.isDefault)
267
+ status.push("默认");
268
+ if (flags.isActive)
269
+ status.push("已启用");
270
+ if (status.length === 0)
271
+ status.push("未启用");
272
+ p.note([
273
+ `标识:${view.name}`,
274
+ `显示名:${view.displayName}`,
275
+ `状态:${status.join(" · ")}`,
276
+ `格式:${formatLabel(view.apiFormat)}`,
277
+ `Base URL:${view.baseUrl}`,
278
+ `API Key:${view.apiKey}`,
279
+ `默认模型:${view.models.default}`,
280
+ `模型列表:${view.models.list.join(", ") || "(空)"}`,
281
+ `代理:${formatProxySummary(profile.proxy)}`,
282
+ profile.bridgeMode === "completions"
283
+ ? "上游接口:Completions"
284
+ : profile.bridgeMode === "chat"
285
+ ? "上游接口:Chat Completions"
286
+ : null,
287
+ `更新时间:${view.updatedAt}`,
288
+ ]
289
+ .filter(Boolean)
290
+ .join("\n"), flags.title || `${tool} / ${profile.name}`);
291
+ }
292
+ async function configureProfileModels(tool, profile) {
293
+ p.log.step(`配置 ${profile.displayName || profile.name} 的模型`);
294
+ const resolved = await resolveModelsInteractive({
295
+ apiFormat: profile.apiFormat,
296
+ baseUrl: profile.baseUrl,
297
+ apiKey: profile.apiKey,
298
+ proxy: profile.proxy,
299
+ preferredDefault: profile.models.default,
300
+ preferredList: profile.models.list,
301
+ });
302
+ profile.models.default = resolved.defaultModel;
303
+ profile.models.list = resolved.modelList;
304
+ if (resolved.resolvedBaseUrl &&
305
+ resolved.resolvedBaseUrl !== profile.baseUrl) {
306
+ p.log.info(`已根据可用接口将 Base URL 规范为 ${resolved.resolvedBaseUrl}(原:${profile.baseUrl})`);
307
+ profile.baseUrl = resolved.resolvedBaseUrl;
308
+ }
309
+ saveProfile(tool, profile);
310
+ const active = getActiveProfile(tool);
311
+ if (active?.name === profile.name) {
312
+ await applyProfile(tool, requireProfile(tool, profile.name));
313
+ p.log.success(`已更新模型并写入工具配置:默认「${resolved.defaultModel}」,共 ${resolved.modelList.length} 个`);
314
+ }
315
+ else {
316
+ p.log.success(`已更新模型:默认「${resolved.defaultModel}」,共 ${resolved.modelList.length} 个。启用:llms ${tool} use ${profile.name}`);
317
+ }
318
+ }
319
+ async function resolveProfileName(tool, name) {
320
+ if (name) {
321
+ requireProfile(tool, name);
322
+ return name;
323
+ }
324
+ ensureDefaultProvider(tool);
325
+ const profiles = listProfiles(tool);
326
+ if (profiles.length === 0) {
327
+ throw new Error(`暂无 ${tool} 供应商。请先:llms ${tool} provider`);
328
+ }
329
+ const active = getActiveProfile(tool)?.name;
330
+ const defaultName = getDefaultProfile(tool)?.name;
331
+ const selected = await p.select({
332
+ message: `选择 ${tool} 供应商`,
333
+ options: profiles.map((profile) => ({
334
+ value: profile.name,
335
+ label: formatProviderListLabel(profile, {
336
+ defaultName,
337
+ activeName: active,
338
+ }),
339
+ hint: formatLabel(profile.apiFormat),
340
+ })),
341
+ initialValue: defaultName || active || profiles[0].name,
342
+ });
343
+ exitOnCancel(selected);
344
+ return selected;
345
+ }
346
+ /** 列表项 label 始终可见;hint 仅在高亮行显示。 */
347
+ function formatProviderListLabel(profile, opts) {
348
+ const name = profile.displayName || profile.name;
349
+ const tags = [];
350
+ if (profile.name === opts.defaultName)
351
+ tags.push("默认");
352
+ if (profile.name === opts.activeName)
353
+ tags.push("已启用");
354
+ return tags.length ? `${name}(${tags.join(" · ")})` : name;
355
+ }
356
+ function registerModelCommands(parent, tool) {
357
+ parent
358
+ .command("model")
359
+ .description("先选供应商,再拉取并选择要启用的模型(空格多选,回车确认)")
360
+ .option("--profile <name>", "指定供应商,跳过列表选择")
361
+ .option("--json", "JSON 输出")
362
+ .action(async (opts) => {
363
+ p.intro(`配置 ${tool} 模型`);
364
+ const profile = opts.profile
365
+ ? requireProfile(tool, opts.profile)
366
+ : requireProfile(tool, await resolveProfileName(tool));
367
+ await configureProfileModels(tool, profile);
368
+ if (opts.json) {
369
+ const latest = requireProfile(tool, profile.name);
370
+ console.log(JSON.stringify({
371
+ profile: latest.name,
372
+ default: latest.models.default,
373
+ models: latest.models.list,
374
+ applied: getActiveProfile(tool)?.name === latest.name,
375
+ }, null, 2));
376
+ return;
377
+ }
378
+ p.outro("模型配置完成");
379
+ });
380
+ }
@@ -0,0 +1,33 @@
1
+ const SUPPORTED = {
2
+ // openai-chat 通过本地 bridge 转成 /v1/messages 供 Claude Code 使用
3
+ claude: ["anthropic", "openai-chat"],
4
+ // openai-chat 通过本地 bridge 转成 /v1/responses 供 Codex 使用
5
+ codex: ["openai-responses", "openai-chat"],
6
+ opencode: ["anthropic", "openai-chat", "openai-responses"],
7
+ };
8
+ export function supportedFormats(tool) {
9
+ return SUPPORTED[tool];
10
+ }
11
+ export function assertCompatible(tool, format) {
12
+ if (SUPPORTED[tool].includes(format))
13
+ return;
14
+ if (tool === "claude") {
15
+ throw new Error(`Claude Code 支持 anthropic(直连)或 openai-chat(经本地 bridge 转为 /v1/messages)。` +
16
+ `当前 profile 为 ${format}。`);
17
+ }
18
+ if (tool === "codex") {
19
+ throw new Error(`Codex 支持 openai-responses(原生)或 openai-chat(经本地 bridge 转为 /v1/responses)。` +
20
+ `当前 profile 为 ${format}。`);
21
+ }
22
+ throw new Error(`工具 ${tool} 不支持格式 ${format}`);
23
+ }
24
+ export function formatLabel(format) {
25
+ switch (format) {
26
+ case "anthropic":
27
+ return "Claude(Anthropic Messages)";
28
+ case "openai-chat":
29
+ return "OpenAI Chat Completions";
30
+ case "openai-responses":
31
+ return "OpenAI Responses";
32
+ }
33
+ }
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./cli.js";
3
+ await run();
@@ -0,0 +1,40 @@
1
+ import { supportedFormats } from "../formats/compatibility.js";
2
+ export const PRESET_IDS = ["custom", "openai", "anthropic"];
3
+ export const PRESETS = [
4
+ {
5
+ id: "custom",
6
+ displayName: "自定义(OpenAI 兼容)",
7
+ apiFormat: "openai-chat",
8
+ baseUrl: "",
9
+ defaultModel: "",
10
+ models: [],
11
+ tools: ["claude", "codex", "opencode"],
12
+ },
13
+ {
14
+ id: "openai",
15
+ displayName: "OpenAI",
16
+ apiFormat: "openai-responses",
17
+ baseUrl: "https://api.openai.com/v1",
18
+ defaultModel: "",
19
+ models: [],
20
+ tools: ["codex", "opencode"],
21
+ },
22
+ {
23
+ id: "anthropic",
24
+ displayName: "Anthropic",
25
+ apiFormat: "anthropic",
26
+ baseUrl: "https://api.anthropic.com",
27
+ defaultModel: "",
28
+ models: [],
29
+ tools: ["claude", "opencode"],
30
+ },
31
+ ];
32
+ export function isPresetId(value) {
33
+ return PRESET_IDS.includes(value);
34
+ }
35
+ export function presetsForTool(tool) {
36
+ return PRESETS.filter((p) => p.tools.includes(tool) && supportedFormats(tool).includes(p.apiFormat));
37
+ }
38
+ export function getPreset(id) {
39
+ return PRESETS.find((p) => p.id === id);
40
+ }
@@ -0,0 +1,202 @@
1
+ import { existsSync, readdirSync, unlinkSync } from "node:fs";
2
+ import { readFileSync } from "node:fs";
3
+ import { isApiFormat } from "../types.js";
4
+ import { normalizeBaseUrlForFormat } from "../utils/base-url.js";
5
+ import { atomicWriteFile, ensureDir, maskSecret } from "../utils/fs.js";
6
+ import { getProfilePath, getProfilesDir, getStatePath, getToolStoreDir, } from "../utils/paths.js";
7
+ const NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
8
+ export function assertValidProfileName(name) {
9
+ if (!NAME_RE.test(name)) {
10
+ throw new Error(`无效的 profile 名称「${name}」。仅允许字母、数字、下划线、连字符,且以字母或数字开头。`);
11
+ }
12
+ }
13
+ export function ensureToolStore(tool) {
14
+ ensureDir(getProfilesDir(tool));
15
+ }
16
+ export function readState(tool) {
17
+ const path = getStatePath(tool);
18
+ if (!existsSync(path))
19
+ return { active: null, default: null };
20
+ try {
21
+ const raw = JSON.parse(readFileSync(path, "utf8"));
22
+ return {
23
+ active: raw.active ?? null,
24
+ default: raw.default ?? null,
25
+ };
26
+ }
27
+ catch {
28
+ return { active: null, default: null };
29
+ }
30
+ }
31
+ export function writeState(tool, state) {
32
+ ensureToolStore(tool);
33
+ atomicWriteFile(getStatePath(tool), JSON.stringify({
34
+ active: state.active ?? null,
35
+ default: state.default ?? null,
36
+ }, null, 2) + "\n");
37
+ }
38
+ export function listProfiles(tool) {
39
+ ensureToolStore(tool);
40
+ const dir = getProfilesDir(tool);
41
+ if (!existsSync(dir))
42
+ return [];
43
+ return readdirSync(dir)
44
+ .filter((f) => f.endsWith(".json"))
45
+ .map((f) => readProfile(tool, f.replace(/\.json$/, "")))
46
+ .filter((p) => p !== null)
47
+ .sort((a, b) => a.name.localeCompare(b.name));
48
+ }
49
+ export function profileExists(tool, name) {
50
+ return existsSync(getProfilePath(tool, name));
51
+ }
52
+ export function readProfile(tool, name) {
53
+ const path = getProfilePath(tool, name);
54
+ if (!existsSync(path))
55
+ return null;
56
+ const raw = JSON.parse(readFileSync(path, "utf8"));
57
+ return normalizeProfile(raw, name);
58
+ }
59
+ export function requireProfile(tool, name) {
60
+ const profile = readProfile(tool, name);
61
+ if (!profile) {
62
+ throw new Error(`未找到 ${tool} 的 profile「${name}」`);
63
+ }
64
+ return profile;
65
+ }
66
+ export function saveProfile(tool, profile) {
67
+ assertValidProfileName(profile.name);
68
+ if (!isApiFormat(profile.apiFormat)) {
69
+ throw new Error(`无效的 apiFormat: ${profile.apiFormat}`);
70
+ }
71
+ if (!profile.baseUrl?.trim()) {
72
+ throw new Error("baseUrl 不能为空");
73
+ }
74
+ if (!profile.models?.default?.trim()) {
75
+ throw new Error("默认模型不能为空");
76
+ }
77
+ const list = Array.from(new Set([profile.models.default, profile.models.fast, ...(profile.models.list || [])]
78
+ .filter(Boolean)
79
+ .map((m) => m.trim())));
80
+ const next = {
81
+ ...profile,
82
+ displayName: profile.displayName || profile.name,
83
+ baseUrl: normalizeBaseUrlForFormat(profile.apiFormat, profile.baseUrl),
84
+ apiKey: profile.apiKey ?? "",
85
+ models: {
86
+ default: profile.models.default.trim(),
87
+ fast: profile.models.fast?.trim() || undefined,
88
+ list,
89
+ },
90
+ headers: profile.headers || {},
91
+ updatedAt: new Date().toISOString(),
92
+ };
93
+ ensureToolStore(tool);
94
+ atomicWriteFile(getProfilePath(tool, next.name), JSON.stringify(next, null, 2) + "\n");
95
+ }
96
+ export function deleteProfile(tool, name) {
97
+ const path = getProfilePath(tool, name);
98
+ if (!existsSync(path)) {
99
+ throw new Error(`未找到 ${tool} 的 profile「${name}」`);
100
+ }
101
+ unlinkSync(path);
102
+ const state = readState(tool);
103
+ writeState(tool, {
104
+ active: state.active === name ? null : state.active,
105
+ default: state.default === name ? null : state.default,
106
+ });
107
+ ensureDefaultProvider(tool);
108
+ }
109
+ export function getActiveProfile(tool) {
110
+ const { active } = readState(tool);
111
+ if (!active)
112
+ return null;
113
+ return readProfile(tool, active);
114
+ }
115
+ export function setActiveProfile(tool, name) {
116
+ requireProfile(tool, name);
117
+ const state = readState(tool);
118
+ const defaultName = state.default && profileExists(tool, state.default) ? state.default : name;
119
+ writeState(tool, { active: name, default: defaultName });
120
+ }
121
+ export function clearActiveProfile(tool) {
122
+ const state = readState(tool);
123
+ writeState(tool, { ...state, active: null });
124
+ }
125
+ /**
126
+ * Ensure a default provider exists whenever there is at least one profile.
127
+ * Missing/invalid default falls back to active (if valid), otherwise the first profile.
128
+ */
129
+ export function ensureDefaultProvider(tool) {
130
+ const profiles = listProfiles(tool);
131
+ const state = readState(tool);
132
+ if (profiles.length === 0) {
133
+ if (state.default !== null || state.active !== null) {
134
+ writeState(tool, { active: null, default: null });
135
+ }
136
+ return null;
137
+ }
138
+ if (state.default && profiles.some((p) => p.name === state.default)) {
139
+ return state.default;
140
+ }
141
+ if (state.active && profiles.some((p) => p.name === state.active)) {
142
+ writeState(tool, { ...state, default: state.active });
143
+ return state.active;
144
+ }
145
+ const first = profiles[0].name;
146
+ writeState(tool, { ...state, default: first });
147
+ return first;
148
+ }
149
+ export function getDefaultProfile(tool) {
150
+ const name = ensureDefaultProvider(tool);
151
+ if (!name)
152
+ return null;
153
+ return readProfile(tool, name);
154
+ }
155
+ export function setDefaultProfile(tool, name) {
156
+ requireProfile(tool, name);
157
+ const state = readState(tool);
158
+ writeState(tool, { ...state, default: name });
159
+ }
160
+ export function publicProfileView(profile) {
161
+ return {
162
+ name: profile.name,
163
+ displayName: profile.displayName,
164
+ apiFormat: profile.apiFormat,
165
+ baseUrl: profile.baseUrl,
166
+ apiKey: maskSecret(profile.apiKey),
167
+ models: profile.models,
168
+ proxy: profile.proxy || null,
169
+ updatedAt: profile.updatedAt,
170
+ };
171
+ }
172
+ function normalizeProfile(raw, fallbackName) {
173
+ const name = raw.name || fallbackName;
174
+ const list = Array.from(new Set([
175
+ raw.models?.default,
176
+ raw.models?.fast,
177
+ ...(raw.models?.list || []),
178
+ ]
179
+ .filter(Boolean)
180
+ .map((m) => String(m).trim())));
181
+ return {
182
+ name,
183
+ displayName: raw.displayName || name,
184
+ apiFormat: raw.apiFormat,
185
+ baseUrl: isApiFormat(raw.apiFormat)
186
+ ? normalizeBaseUrlForFormat(raw.apiFormat, String(raw.baseUrl || ""))
187
+ : String(raw.baseUrl || "").replace(/\/+$/, ""),
188
+ apiKey: raw.apiKey ?? "",
189
+ models: {
190
+ default: raw.models?.default || list[0] || "",
191
+ fast: raw.models?.fast || undefined,
192
+ list: list.length ? list : raw.models?.default ? [raw.models.default] : [],
193
+ },
194
+ proxy: raw.proxy,
195
+ bridgeMode: raw.bridgeMode,
196
+ headers: raw.headers || {},
197
+ updatedAt: raw.updatedAt || new Date(0).toISOString(),
198
+ };
199
+ }
200
+ export function storeRoot(tool) {
201
+ return getToolStoreDir(tool);
202
+ }
package/dist/types.js ADDED
@@ -0,0 +1,17 @@
1
+ export const TOOLS = ["claude", "codex", "opencode"];
2
+ export const API_FORMATS = [
3
+ "anthropic",
4
+ "openai-chat",
5
+ "openai-responses",
6
+ ];
7
+ export function isTool(value) {
8
+ return TOOLS.includes(value);
9
+ }
10
+ export function isApiFormat(value) {
11
+ return API_FORMATS.includes(value);
12
+ }
13
+ export function emptyProxy(proxy) {
14
+ if (!proxy)
15
+ return true;
16
+ return !proxy.http && !proxy.https && !proxy.all;
17
+ }