@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.
- package/README.md +52 -9
- package/dist/adapters/claude.js +10 -10
- package/dist/adapters/codex.js +68 -16
- package/dist/adapters/opencode.js +45 -25
- package/dist/bridge/manager.js +8 -1
- package/dist/bridge/responses-to-chat-response.js +1 -1
- package/dist/bridge/runtime.js +40 -2
- package/dist/bridge/server.js +64 -20
- package/dist/bridge/state.js +59 -27
- package/dist/bridge/translate-response.js +98 -0
- package/dist/bridge/transport.js +39 -8
- package/dist/cli.js +18 -6
- package/dist/commands/bridge-cmd.js +15 -8
- package/dist/commands/gateway-cmd.js +112 -31
- package/dist/commands/home-cmd.js +20 -2
- package/dist/commands/launch-cmd.js +4 -0
- package/dist/commands/launch.js +36 -13
- package/dist/commands/prompts.js +96 -13
- package/dist/commands/tool.js +298 -134
- package/dist/gateway/keys.js +11 -2
- package/dist/gateway/rate-limit.js +8 -104
- package/dist/gateway/router.js +7 -1
- package/dist/gateway/server.js +1 -22
- package/dist/gateway/store.js +37 -4
- package/dist/gateway/usage.js +24 -4
- package/dist/store/profiles.js +54 -14
- package/dist/types.js +12 -0
- package/dist/utils/display.js +71 -0
- package/dist/utils/file-lock.js +149 -0
- package/dist/utils/fs.js +187 -9
- package/dist/utils/model-metadata.js +54 -1
- package/package.json +7 -2
package/dist/commands/tool.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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(
|
|
347
|
+
if (p.isCancel(sync)) {
|
|
253
348
|
p.cancel("已取消");
|
|
254
349
|
process.exit(0);
|
|
255
350
|
}
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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}
|
|
544
|
+
p.log.success(`已更新模型并写入工具配置:默认「${resolved.defaultModel}」${smallNote},共 ${resolved.modelList.length} 个`);
|
|
390
545
|
}
|
|
391
546
|
else {
|
|
392
|
-
p.log.success(`已更新模型:默认「${resolved.defaultModel}
|
|
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
|
-
|
|
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));
|
package/dist/gateway/keys.js
CHANGED
|
@@ -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:
|
|
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:
|
|
266
|
+
hint: keyHint(id, secret),
|
|
258
267
|
createdAt: new Date().toISOString(),
|
|
259
268
|
lastUsedAt: null,
|
|
260
269
|
};
|