@nvae/llmswitch 1.2.0 → 1.3.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.
@@ -1,35 +1,145 @@
1
1
  import * as p from "@clack/prompts";
2
+ import { API_FORMATS, isApiFormat, supportsSmallModel } from "../types.js";
2
3
  import { applyProfile, deactivateProfile } from "../adapters/index.js";
3
4
  import { formatLabel } from "../formats/compatibility.js";
5
+ import { PRESET_IDS } from "../presets/index.js";
4
6
  import { deleteProfile, ensureDefaultProvider, getActiveProfile, getDefaultProfile, listProfiles, publicProfileView, requireProfile, resolveProfileOrThrow, saveProfile, setDefaultProfile, } from "../store/profiles.js";
5
7
  import { formatProxySummary } from "../utils/proxy.js";
6
8
  import { runToolFlow } from "./setup-cmd.js";
7
9
  import { launchTool } from "./launch.js";
8
- import { exitOnCancel, formatProfileListLabel, promptEditProfile, promptProfileDraft, resolveModelsInteractive, } from "./prompts.js";
10
+ import { exitOnCancel, formatProfileListLabel, promptEditProfile, promptProfileDraft, requireInteractive, resolveModelsInteractive, } from "./prompts.js";
9
11
  export function registerToolCommand(program, tool) {
10
12
  const cmd = program
11
13
  .command(tool)
12
14
  .description(`管理 ${tool} 的供应商与模型配置`);
13
15
  // llms <tool>(无子命令):连贯启动,未配置时自动引导
14
16
  cmd.action(() => runToolFlow(tool));
15
- cmd
17
+ const provider = cmd
16
18
  .command("provider")
17
19
  .description("管理模型供应商:添加 / 默认 / 启用禁用 / 查看 / 编辑 / 删除")
18
20
  .option("--json", "以 JSON 列出全部供应商后退出")
19
21
  .action(async (opts) => {
20
22
  ensureDefaultProvider(tool);
21
23
  if (opts.json) {
22
- const active = getActiveProfile(tool)?.name ?? null;
23
- const defaultName = getDefaultProfile(tool)?.name ?? null;
24
- console.log(JSON.stringify(listProfiles(tool).map((profile) => ({
25
- ...publicProfileView(profile),
26
- active: profile.name === active,
27
- default: profile.name === defaultName,
28
- })), null, 2));
24
+ printProviderList(tool);
29
25
  return;
30
26
  }
31
27
  await runProviderManager(tool);
32
28
  });
29
+ // 非交互入口:脚本 / CI 里也要能增删查,不必走菜单。
30
+ provider
31
+ .command("list")
32
+ .description("列出全部供应商")
33
+ .option("--json", "JSON 输出")
34
+ .action((opts) => {
35
+ ensureDefaultProvider(tool);
36
+ if (opts.json) {
37
+ printProviderList(tool);
38
+ return;
39
+ }
40
+ const profiles = listProfiles(tool);
41
+ if (profiles.length === 0) {
42
+ console.log(`当前没有 ${tool} 供应商。添加:llms ${tool} provider add --help`);
43
+ return;
44
+ }
45
+ const active = getActiveProfile(tool)?.name ?? null;
46
+ const defaultName = getDefaultProfile(tool)?.name ?? null;
47
+ for (const profile of profiles) {
48
+ console.log(formatProfileListLabel(profile, { defaultName, activeName: active }) +
49
+ ` ${formatLabel(profile.apiFormat)} ${profile.models.default}`);
50
+ }
51
+ });
52
+ provider
53
+ .command("add")
54
+ .description("非交互添加供应商(参数齐全时不会进入任何提示)")
55
+ .option("--preset <id>", `预设:${PRESET_IDS.join(" | ")}`, "custom")
56
+ .option("--base-url <url>", "API Base URL")
57
+ .option("--api-key <key>", "API Key(可留空)")
58
+ .option("--name <name>", "供应商标识(省略则自动生成 5 位随机名)")
59
+ .option("--display-name <name>", "显示名称")
60
+ .option("--format <format>", `接口格式:${API_FORMATS.join(" | ")}(省略则自动探测)`)
61
+ .option("--model <id>", "默认模型 ID")
62
+ .option("--models <list>", "启用模型列表,逗号分隔(省略则取上游全部)")
63
+ .option("--small <model>", "轻量小模型(仅 claude / opencode 支持)")
64
+ .option("--proxy <url>", "上游代理地址")
65
+ .option("--no-enable", "只保存,不写入工具配置")
66
+ .option("--json", "JSON 输出")
67
+ .action(async (opts) => {
68
+ if (opts.format && !isApiFormat(opts.format)) {
69
+ throw new Error(`无效的 --format「${opts.format}」。可选:${API_FORMATS.join("、")}`);
70
+ }
71
+ if (!process.stdin.isTTY) {
72
+ if (!opts.baseUrl) {
73
+ throw new Error("非交互模式下必须提供 --base-url");
74
+ }
75
+ if (!opts.model) {
76
+ throw new Error("非交互模式下必须提供 --model");
77
+ }
78
+ }
79
+ if (opts.small && !supportsSmallModel(tool)) {
80
+ throw new Error(`${tool} 不支持小模型配置`);
81
+ }
82
+ const created = await promptProfileDraft(tool, {
83
+ preset: opts.preset,
84
+ baseUrl: opts.baseUrl,
85
+ apiKey: opts.apiKey ?? (process.stdin.isTTY ? undefined : ""),
86
+ name: opts.name,
87
+ displayName: opts.displayName,
88
+ apiFormat: opts.format && isApiFormat(opts.format) ? opts.format : undefined,
89
+ model: opts.model,
90
+ models: opts.models ? splitCommaList(opts.models) : undefined,
91
+ smallModel: opts.small ?? (process.stdin.isTTY ? undefined : null),
92
+ proxy: opts.proxy ?? (process.stdin.isTTY ? undefined : ""),
93
+ });
94
+ ensureDefaultProvider(tool);
95
+ let configPath;
96
+ if (opts.enable !== false) {
97
+ const result = await applyProfile(tool, created);
98
+ configPath = result.configPath;
99
+ }
100
+ if (opts.json) {
101
+ console.log(JSON.stringify({
102
+ ...publicProfileView(created),
103
+ enabled: opts.enable !== false,
104
+ configPath: configPath ?? null,
105
+ }, null, 2));
106
+ return;
107
+ }
108
+ console.log(`已添加 ${tool}/${created.name}(${created.displayName})`);
109
+ if (configPath)
110
+ console.log(`已启用,配置文件:${configPath}`);
111
+ else
112
+ console.log(`未启用。启用:llms ${tool} use ${created.name}`);
113
+ });
114
+ provider
115
+ .command("rm")
116
+ .alias("remove")
117
+ .description("删除供应商(若已启用会先禁用)")
118
+ .argument("<name>", "供应商名称或显示名称")
119
+ .option("--yes", "跳过确认")
120
+ .action(async (name, opts) => {
121
+ const profile = resolveProfileOrThrow(tool, name);
122
+ if (!opts.yes) {
123
+ requireInteractive("删除确认", `确认无误可加 --yes:llms ${tool} provider rm ${profile.name} --yes`);
124
+ const ok = await p.confirm({
125
+ message: `确认删除供应商「${profile.name}」?此操作不可恢复`,
126
+ initialValue: false,
127
+ });
128
+ if (p.isCancel(ok)) {
129
+ p.cancel("已取消");
130
+ process.exit(0);
131
+ }
132
+ if (!ok) {
133
+ console.log("已取消");
134
+ return;
135
+ }
136
+ }
137
+ if (getActiveProfile(tool)?.name === profile.name) {
138
+ await deactivateProfile(tool, profile.name);
139
+ }
140
+ deleteProfile(tool, profile.name);
141
+ console.log(`已删除 ${tool}/${profile.name}`);
142
+ });
33
143
  cmd
34
144
  .command("use")
35
145
  .description("启用已有供应商(写入对应工具配置)")
@@ -98,6 +208,8 @@ export function registerToolCommand(program, tool) {
98
208
  registerModelCommands(cmd, tool);
99
209
  }
100
210
  async function runProviderManager(tool) {
211
+ requireInteractive(`llms ${tool} provider 的管理菜单`, `请在终端中运行,或改用:llms ${tool} provider --json 列出、` +
212
+ `llms ${tool} provider add --base-url … 添加、llms ${tool} use <name> 启用。`);
101
213
  p.intro(`${tool} 模型供应商`);
102
214
  while (true) {
103
215
  ensureDefaultProvider(tool);
@@ -138,131 +250,161 @@ async function runProviderManager(tool) {
138
250
  p.outro("添加完成");
139
251
  return;
140
252
  }
141
- await handleProviderActions(tool, selected);
253
+ // 任一增删改查动作执行完即结束;仅「返回列表」会回到上面的列表
254
+ const outro = await handleProviderActions(tool, selected);
255
+ if (outro !== BACK_TO_LIST) {
256
+ p.outro(outro);
257
+ return;
258
+ }
142
259
  }
143
260
  }
144
- async function handleProviderAdd(tool) {
145
- const created = await promptProfileDraft(tool);
146
- ensureDefaultProvider(tool);
147
- // 静默启用:不再询问,用户可在 provider 菜单中关闭
148
- const result = await applyProfile(tool, created);
149
- p.log.success(`已启用 ${tool}/${created.name}(${created.displayName || created.name})`);
150
- p.log.info(`配置文件:${result.configPath}`);
151
- p.log.info(result.restartHint);
152
- }
261
+ /** handleProviderActions 的哨兵返回值:回到供应商列表而不是退出。 */
262
+ const BACK_TO_LIST = Symbol("back-to-list");
263
+ /**
264
+ * 单个供应商的动作菜单。执行完一个动作即返回 outro 文案由调用方结束流程;
265
+ * 只有显式「返回列表」或供应商已不存在时才返回 BACK_TO_LIST。
266
+ */
153
267
  async function handleProviderActions(tool, profileName) {
154
- while (true) {
155
- ensureDefaultProvider(tool);
156
- let profile;
157
- try {
158
- profile = requireProfile(tool, profileName);
159
- }
160
- catch {
161
- p.log.warn(`「${profileName}」已不存在`);
162
- return;
163
- }
164
- const isActive = getActiveProfile(tool)?.name === profile.name;
165
- const isDefault = getDefaultProfile(tool)?.name === profile.name;
166
- const action = await p.select({
167
- message: `${profile.displayName || profile.name}`,
168
- options: [
169
- {
170
- value: "default",
171
- label: "设置为默认供应商",
172
- hint: isDefault ? "当前已是默认" : undefined,
173
- },
174
- {
175
- value: "toggle",
176
- label: isActive ? "禁用" : "启用",
177
- hint: isActive ? "清除写入工具的配置" : "写入对应工具配置",
178
- },
179
- {
180
- value: "view",
181
- label: "查看配置",
182
- },
183
- {
184
- value: "edit",
185
- label: "编辑配置",
186
- hint: "显示名 / 地址 / 密钥 / 代理 / 格式",
187
- },
188
- {
189
- value: "delete",
190
- label: "删除配置",
191
- hint: isActive ? "将先禁用再删除" : undefined,
192
- },
193
- {
194
- value: "back",
195
- label: "返回列表",
196
- },
197
- ],
198
- });
199
- exitOnCancel(action);
200
- if (action === "back")
201
- return;
202
- if (action === "default") {
203
- setDefaultProfile(tool, profile.name);
204
- p.log.success(`已将「${profile.name}」设为默认供应商`);
205
- continue;
206
- }
207
- if (action === "toggle") {
208
- if (isActive) {
209
- const result = await deactivateProfile(tool, profile.name);
210
- p.log.success(`已禁用「${profile.name}」`);
211
- p.log.info(`配置文件:${result.configPath}`);
212
- p.log.info(result.restartHint);
213
- }
214
- else {
215
- const result = await applyProfile(tool, profile);
216
- p.log.success(`已启用「${profile.name}」`);
217
- p.log.info(`配置文件:${result.configPath}`);
218
- if (result.backupPath)
219
- p.log.info(`备份:${result.backupPath}`);
220
- p.log.info(result.restartHint);
221
- }
222
- continue;
223
- }
224
- if (action === "view") {
225
- printProfileDetails(tool, profile, { isActive, isDefault });
226
- continue;
227
- }
228
- if (action === "edit") {
229
- const updated = await promptEditProfile(tool, profile);
230
- if (getActiveProfile(tool)?.name === updated.name) {
231
- const sync = await p.confirm({
232
- message: "该供应商当前已启用,是否立即写回工具配置?",
233
- initialValue: true,
234
- });
235
- if (p.isCancel(sync)) {
236
- p.cancel("已取消");
237
- process.exit(0);
238
- }
239
- if (sync) {
240
- const result = await applyProfile(tool, updated);
241
- p.log.success("已同步写入工具配置");
242
- p.log.info(result.restartHint);
243
- }
244
- }
245
- continue;
268
+ ensureDefaultProvider(tool);
269
+ let profile;
270
+ try {
271
+ profile = requireProfile(tool, profileName);
272
+ }
273
+ catch {
274
+ p.log.warn(`「${profileName}」已不存在`);
275
+ return BACK_TO_LIST;
276
+ }
277
+ const isActive = getActiveProfile(tool)?.name === profile.name;
278
+ const isDefault = getDefaultProfile(tool)?.name === profile.name;
279
+ const action = await p.select({
280
+ message: `${profile.displayName || profile.name}`,
281
+ options: [
282
+ {
283
+ value: "default",
284
+ label: "设置为默认供应商",
285
+ hint: isDefault ? "当前已是默认" : undefined,
286
+ },
287
+ {
288
+ value: "toggle",
289
+ label: isActive ? "禁用" : "启用",
290
+ hint: isActive ? "清除写入工具的配置" : "写入对应工具配置",
291
+ },
292
+ {
293
+ value: "view",
294
+ label: "查看配置",
295
+ },
296
+ {
297
+ value: "edit",
298
+ label: "编辑配置",
299
+ hint: "显示名 / 地址 / 密钥 / 代理 / 格式",
300
+ },
301
+ {
302
+ value: "delete",
303
+ label: "删除配置",
304
+ hint: isActive ? "将先禁用再删除" : undefined,
305
+ },
306
+ {
307
+ value: "back",
308
+ label: "返回列表",
309
+ },
310
+ ],
311
+ });
312
+ exitOnCancel(action);
313
+ if (action === "back")
314
+ return BACK_TO_LIST;
315
+ if (action === "default") {
316
+ setDefaultProfile(tool, profile.name);
317
+ p.log.success(`已将「${profile.name}」设为默认供应商`);
318
+ return "已设置默认供应商";
319
+ }
320
+ if (action === "toggle") {
321
+ if (isActive) {
322
+ const result = await deactivateProfile(tool, profile.name);
323
+ p.log.success(`已禁用「${profile.name}」`);
324
+ p.log.info(`配置文件:${result.configPath}`);
325
+ p.log.info(result.restartHint);
326
+ return "已禁用供应商";
246
327
  }
247
- if (action === "delete") {
248
- const ok = await p.confirm({
249
- message: `确认删除供应商「${profile.name}」?此操作不可恢复`,
250
- initialValue: false,
328
+ const result = await applyProfile(tool, profile);
329
+ p.log.success(`已启用「${profile.name}」`);
330
+ p.log.info(`配置文件:${result.configPath}`);
331
+ if (result.backupPath)
332
+ p.log.info(`备份:${result.backupPath}`);
333
+ p.log.info(result.restartHint);
334
+ return "已启用供应商";
335
+ }
336
+ if (action === "view") {
337
+ printProfileDetails(tool, profile, { isActive, isDefault });
338
+ return "已退出供应商管理";
339
+ }
340
+ if (action === "edit") {
341
+ const updated = await promptEditProfile(tool, profile);
342
+ if (getActiveProfile(tool)?.name === updated.name) {
343
+ const sync = await p.confirm({
344
+ message: "该供应商当前已启用,是否立即写回工具配置?",
345
+ initialValue: true,
251
346
  });
252
- if (p.isCancel(ok)) {
347
+ if (p.isCancel(sync)) {
253
348
  p.cancel("已取消");
254
349
  process.exit(0);
255
350
  }
256
- if (!ok)
257
- continue;
258
- if (isActive) {
259
- await deactivateProfile(tool, profile.name);
351
+ if (sync) {
352
+ const result = await applyProfile(tool, updated);
353
+ p.log.success("已同步写入工具配置");
354
+ p.log.info(result.restartHint);
260
355
  }
261
- deleteProfile(tool, profile.name);
262
- p.log.success(`已删除「${profile.name}」`);
263
- return;
264
356
  }
357
+ return "编辑完成";
358
+ }
359
+ if (action === "delete") {
360
+ const ok = await p.confirm({
361
+ message: `确认删除供应商「${profile.name}」?此操作不可恢复`,
362
+ initialValue: false,
363
+ });
364
+ if (p.isCancel(ok)) {
365
+ p.cancel("已取消");
366
+ process.exit(0);
367
+ }
368
+ // 放弃删除:回到该供应商的动作菜单,而不是直接退出
369
+ if (!ok)
370
+ return handleProviderActions(tool, profileName);
371
+ if (isActive) {
372
+ await deactivateProfile(tool, profile.name);
373
+ }
374
+ deleteProfile(tool, profile.name);
375
+ p.log.success(`已删除「${profile.name}」`);
376
+ return "已删除供应商";
265
377
  }
378
+ return BACK_TO_LIST;
379
+ }
380
+ async function handleProviderAdd(tool) {
381
+ const previous = getActiveProfile(tool)?.name ?? null;
382
+ const created = await promptProfileDraft(tool);
383
+ ensureDefaultProvider(tool);
384
+ // 静默启用:不再询问,用户可在 provider 菜单中关闭
385
+ const result = await applyProfile(tool, created);
386
+ p.log.success(`已启用 ${tool}/${created.name}(${created.displayName || created.name})`);
387
+ // 添加即启用会覆盖工具当前生效的供应商,必须说清楚被换掉的是哪个。
388
+ if (previous && previous !== created.name) {
389
+ p.log.warn(`${tool} 原先启用的是「${previous}」,已被替换。恢复:llms ${tool} use ${previous}`);
390
+ }
391
+ p.log.info(`配置文件:${result.configPath}`);
392
+ p.log.info(result.restartHint);
393
+ }
394
+ function splitCommaList(value) {
395
+ return value
396
+ .split(",")
397
+ .map((s) => s.trim())
398
+ .filter(Boolean);
399
+ }
400
+ function printProviderList(tool) {
401
+ const active = getActiveProfile(tool)?.name ?? null;
402
+ const defaultName = getDefaultProfile(tool)?.name ?? null;
403
+ console.log(JSON.stringify(listProfiles(tool).map((profile) => ({
404
+ ...publicProfileView(profile),
405
+ active: profile.name === active,
406
+ default: profile.name === defaultName,
407
+ })), null, 2));
266
408
  }
267
409
  function printProfileDetails(tool, profile, flags) {
268
410
  const view = publicProfileView(profile);
@@ -273,7 +415,9 @@ function printProfileDetails(tool, profile, flags) {
273
415
  status.push("已启用");
274
416
  if (status.length === 0)
275
417
  status.push("未启用");
276
- p.note([
418
+ // 纯文本而不是 clack 的方框:一是这里的输出经常被管道/重定向消费,
419
+ // 二是方框宽度按码点算,中文行会把右边框顶歪。
420
+ const lines = [
277
421
  `标识:${view.name}`,
278
422
  `显示名:${view.displayName}`,
279
423
  `状态:${status.join(" · ")}`,
@@ -281,6 +425,9 @@ function printProfileDetails(tool, profile, flags) {
281
425
  `Base URL:${view.baseUrl}`,
282
426
  `API Key:${view.apiKey}`,
283
427
  `默认模型:${view.models.default}`,
428
+ supportsSmallModel(tool)
429
+ ? `小模型:${view.models.smallModel || "(未设置,沿用默认模型)"}`
430
+ : null,
284
431
  `模型列表:${view.models.list.join(", ") || "(空)"}`,
285
432
  `代理:${formatProxySummary(profile.proxy)}`,
286
433
  profile.bridgeMode === "completions"
@@ -289,9 +436,10 @@ function printProfileDetails(tool, profile, flags) {
289
436
  ? "上游接口:Chat Completions"
290
437
  : null,
291
438
  `更新时间:${view.updatedAt}`,
292
- ]
293
- .filter(Boolean)
294
- .join("\n"), flags.title || `${tool} / ${profile.name}`);
439
+ ].filter((line) => Boolean(line));
440
+ console.log(`[${flags.title || `${tool} / ${profile.name}`}]`);
441
+ for (const line of lines)
442
+ console.log(` ${line}`);
295
443
  }
296
444
  /**
297
445
  * 启用供应商前:若模型列表内有多个候选,交互选择默认模型;
@@ -364,7 +512,7 @@ async function maybeLaunchNow(tool, profile) {
364
512
  p.log.warn(`未能启动。稍后可用:llms launch ${tool},或设置 ${tool.toUpperCase()}_BIN 指定可执行文件。`);
365
513
  }
366
514
  }
367
- async function configureProfileModels(tool, profile) {
515
+ async function configureProfileModels(tool, profile, opts = {}) {
368
516
  p.log.step(`配置 ${profile.displayName || profile.name} 的模型`);
369
517
  const resolved = await resolveModelsInteractive({
370
518
  apiFormat: profile.apiFormat,
@@ -373,8 +521,12 @@ async function configureProfileModels(tool, profile) {
373
521
  proxy: profile.proxy,
374
522
  preferredDefault: profile.models.default,
375
523
  preferredList: profile.models.list,
524
+ supportsSmall: supportsSmallModel(tool),
525
+ preferredSmall: profile.models.smallModel,
526
+ fixedSmall: opts.fixedSmall,
376
527
  });
377
528
  profile.models.default = resolved.defaultModel;
529
+ profile.models.smallModel = resolved.smallModel;
378
530
  profile.models.list = resolved.modelList;
379
531
  profile.models.meta = resolved.modelMeta;
380
532
  if (resolved.resolvedBaseUrl &&
@@ -383,13 +535,16 @@ async function configureProfileModels(tool, profile) {
383
535
  profile.baseUrl = resolved.resolvedBaseUrl;
384
536
  }
385
537
  saveProfile(tool, profile);
538
+ const smallNote = resolved.smallModel
539
+ ? `,小模型「${resolved.smallModel}」`
540
+ : "";
386
541
  const active = getActiveProfile(tool);
387
542
  if (active?.name === profile.name) {
388
543
  await applyProfile(tool, requireProfile(tool, profile.name));
389
- p.log.success(`已更新模型并写入工具配置:默认「${resolved.defaultModel}」,共 ${resolved.modelList.length} 个`);
544
+ p.log.success(`已更新模型并写入工具配置:默认「${resolved.defaultModel}」${smallNote},共 ${resolved.modelList.length} 个`);
390
545
  }
391
546
  else {
392
- p.log.success(`已更新模型:默认「${resolved.defaultModel}」,共 ${resolved.modelList.length} 个。启用:llms ${tool} use ${profile.name}`);
547
+ p.log.success(`已更新模型:默认「${resolved.defaultModel}」${smallNote},共 ${resolved.modelList.length} 个。启用:llms ${tool} use ${profile.name}`);
393
548
  }
394
549
  }
395
550
  async function resolveProfileName(tool, name) {
@@ -401,6 +556,7 @@ async function resolveProfileName(tool, name) {
401
556
  if (profiles.length === 0) {
402
557
  throw new Error(`暂无 ${tool} 供应商。请先:llms ${tool} provider`);
403
558
  }
559
+ requireInteractive("选择供应商", `请改用 llms ${tool} use <name>(现有:${profiles.map((x) => x.name).join(", ")})。`);
404
560
  const active = getActiveProfile(tool)?.name;
405
561
  const defaultName = getDefaultProfile(tool)?.name;
406
562
  const selected = await p.select({
@@ -419,22 +575,30 @@ async function resolveProfileName(tool, name) {
419
575
  return selected;
420
576
  }
421
577
  function registerModelCommands(parent, tool) {
422
- parent
578
+ const cmd = parent
423
579
  .command("model")
424
580
  .description("先选供应商,再拉取并选择要启用的模型(空格多选,回车确认)")
425
581
  .option("--profile <name>", "指定供应商,跳过列表选择")
426
- .option("--json", "JSON 输出")
427
- .action(async (opts) => {
582
+ .option("--json", "JSON 输出");
583
+ if (supportsSmallModel(tool)) {
584
+ cmd.option("--small <model>", "轻量小模型(标题生成等低成本任务);跳过交互选择,传空字符串则清除");
585
+ }
586
+ cmd.action(async (opts) => {
587
+ requireInteractive(`llms ${tool} model 的模型选择`, `请在终端中运行,或用 llms ${tool} provider add --model <id> 直接指定。`);
428
588
  p.intro(`配置 ${tool} 模型`);
429
589
  const profile = opts.profile
430
590
  ? resolveProfileOrThrow(tool, opts.profile)
431
591
  : requireProfile(tool, await resolveProfileName(tool));
432
- await configureProfileModels(tool, profile);
592
+ await configureProfileModels(tool, profile, {
593
+ // undefined → 交互询问;"" → 清除;其他 → 直接采用
594
+ fixedSmall: opts.small === undefined ? undefined : opts.small || null,
595
+ });
433
596
  if (opts.json) {
434
597
  const latest = requireProfile(tool, profile.name);
435
598
  console.log(JSON.stringify({
436
599
  profile: latest.name,
437
600
  default: latest.models.default,
601
+ small: latest.models.smallModel ?? null,
438
602
  models: latest.models.list,
439
603
  applied: getActiveProfile(tool)?.name === latest.name,
440
604
  }, null, 2));
@@ -150,6 +150,15 @@ function writeGatewayKeys(keys) {
150
150
  }
151
151
  atomicWriteFile(getGatewayKeysPath(), JSON.stringify({ version: 1, keys }, null, 2) + "\n");
152
152
  }
153
+ /**
154
+ * Display hint for a key. The id alone already identifies the key, so only the
155
+ * last 4 characters of the secret are shown — the previous form leaked both the
156
+ * first and last 4 characters into `key list`, `status --json` and the keys file,
157
+ * which sits badly with the "only the hash is stored" promise.
158
+ */
159
+ function keyHint(id, secret) {
160
+ return `${KEY_PREFIX}-${id}-…${secret.slice(-4)}`;
161
+ }
153
162
  export function createGatewayKey(options = {}) {
154
163
  assertValidFormats(options.formats);
155
164
  const id = randomBytes(KEY_ID_BYTES).toString("hex");
@@ -168,7 +177,7 @@ export function createGatewayKey(options = {}) {
168
177
  name: options.name?.trim() || `key-${id.slice(0, 4)}`,
169
178
  hash: hashSecret(secret, salt),
170
179
  salt,
171
- hint: `${KEY_PREFIX}-${id}-${secret.slice(0, 4)}…${secret.slice(-4)}`,
180
+ hint: keyHint(id, secret),
172
181
  createdAt: new Date().toISOString(),
173
182
  expiresAt,
174
183
  revokedAt: null,
@@ -254,7 +263,7 @@ export function rotateGatewayKey(idOrName) {
254
263
  id,
255
264
  hash: hashSecret(secret, salt),
256
265
  salt,
257
- hint: `${KEY_PREFIX}-${id}-${secret.slice(0, 4)}…${secret.slice(-4)}`,
266
+ hint: keyHint(id, secret),
258
267
  createdAt: new Date().toISOString(),
259
268
  lastUsedAt: null,
260
269
  };