mslxdff 0.1.62 → 0.1.64
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/bin/mslxdff.js +396 -39
- package/package.json +1 -1
- package/src/auto.js +13 -0
- package/src/chat/config.js +2 -1
- package/src/chat/prompt.js +1 -1
- package/src/chat/repl.js +12 -49
- package/src/chat/stats.js +1 -1
- package/src/chat/upstream.js +183 -5
- package/src/providers/generic.js +17 -4
- package/src/providers/workbuddy.js +16 -5
- package/src/routes/index.js +10 -1
- package/src/routes/models-route.js +28 -0
- package/src/state.js +55 -10
package/bin/mslxdff.js
CHANGED
|
@@ -232,8 +232,25 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
232
232
|
for (const id of picks) console.log(` ${id}`);
|
|
233
233
|
process.exit(0);
|
|
234
234
|
}
|
|
235
|
+
// -model list supports optional provider filter: -model list --provider <id> | -model list <id> | --json
|
|
236
|
+
let modelListProvider = null;
|
|
237
|
+
let modelListJson = false;
|
|
238
|
+
if (sub === "list") {
|
|
239
|
+
const restArgs = args.slice(idx + 2);
|
|
240
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
241
|
+
const a = String(restArgs[i] || "");
|
|
242
|
+
if (a === "--json" || a === "-json") modelListJson = true;
|
|
243
|
+
else if (a === "--provider" || a === "-provider" || a === "--providerId") { modelListProvider = String(restArgs[i + 1] || "").trim() || null; i++; }
|
|
244
|
+
else if (!a.startsWith("-") && !modelListProvider) modelListProvider = a;
|
|
245
|
+
}
|
|
246
|
+
if (modelListProvider) {
|
|
247
|
+
const { normalizeProviderId } = await import("../src/providers/model-id.js");
|
|
248
|
+
const nid = normalizeProviderId(modelListProvider);
|
|
249
|
+
modelListProvider = nid || modelListProvider.toLowerCase();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
235
252
|
if (sub !== undefined && sub !== "list") {
|
|
236
|
-
console.error("usage: mslxdff -models (interactive multi-pick) | mslxdff -model list | mslxdff -model set <id> | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear | mslxdff -model picks | mslxdff -model status | mslxdff -model refresh");
|
|
253
|
+
console.error("usage: mslxdff -models (interactive multi-pick) | mslxdff -model list [--provider <id>] [--json] | mslxdff -model set <id> | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear | mslxdff -model picks | mslxdff -model status | mslxdff -model refresh");
|
|
237
254
|
process.exit(1);
|
|
238
255
|
}
|
|
239
256
|
const cacheFile = join(logDir(), "models.json");
|
|
@@ -275,16 +292,109 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
275
292
|
throw new Error("no cached models and refresh failed");
|
|
276
293
|
}
|
|
277
294
|
}
|
|
295
|
+
// provider filter: bare ids are opencode, prefixed are <provider>/...
|
|
296
|
+
if (modelListProvider) {
|
|
297
|
+
const prov = String(modelListProvider).toLowerCase();
|
|
298
|
+
const filtered = ids.filter((id) => {
|
|
299
|
+
const slash = String(id).indexOf("/");
|
|
300
|
+
const p = slash > 0 ? String(id).slice(0, slash).toLowerCase() : "opencode";
|
|
301
|
+
return p === prov;
|
|
302
|
+
});
|
|
303
|
+
// 非 opencode 供应商的模型不在 opencode 缓存中,改从 allowlist 展示(原名+别名)
|
|
304
|
+
if (prov !== "opencode" && filtered.length === 0) {
|
|
305
|
+
try {
|
|
306
|
+
const { loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderBaseUrl } = await import("../src/state.js");
|
|
307
|
+
const { loadModelAliases, getAliasForModel } = await import("../src/providers/model-id.js");
|
|
308
|
+
try { loadModelAliases(); } catch {}
|
|
309
|
+
const allowed = loadProviderAllowedModels(prov);
|
|
310
|
+
const allowAny = loadProviderAllowAnyModels(prov);
|
|
311
|
+
const baseUrl = loadProviderBaseUrl(prov);
|
|
312
|
+
if (modelListJson) {
|
|
313
|
+
const data = allowed.length
|
|
314
|
+
? allowed.map((raw) => ({ id: `${prov}/${raw}`, object: "model" }))
|
|
315
|
+
: [];
|
|
316
|
+
console.log(JSON.stringify({ object: "list", data }, null, 2));
|
|
317
|
+
process.exit(0);
|
|
318
|
+
}
|
|
319
|
+
if (!allowed.length) {
|
|
320
|
+
if (allowAny) {
|
|
321
|
+
console.log(`provider "${prov}" allowAny ON (allowlist 空=放行全部)${baseUrl ? ` baseUrl=${baseUrl}` : ""}`);
|
|
322
|
+
console.log(` (未设 allowlist,全部模型放行) 查看 live 列表: mslxdff -provider ${prov} models`);
|
|
323
|
+
} else {
|
|
324
|
+
console.log(`no models for provider "${prov}" — allowlist 空 + allowAny OFF = 阻塞`);
|
|
325
|
+
console.log(` 设白名单: mslxdff -provider ${prov} allowlist set <model1> <model2> 或 mslxdff -provider ${prov} allowAny on`);
|
|
326
|
+
console.log(` live 查看: mslxdff -provider ${prov} models`);
|
|
327
|
+
}
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
const at2 = cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)})` : "";
|
|
331
|
+
console.log(`${allowed.length} model(s) for ${prov}${at2} (allowlist,原名 + 别名):`);
|
|
332
|
+
const pickedIds2 = loadModelPicks();
|
|
333
|
+
for (const raw of allowed) {
|
|
334
|
+
const canonical = `${prov}/${raw}`;
|
|
335
|
+
let alias = null;
|
|
336
|
+
try { alias = getAliasForModel(canonical); } catch {}
|
|
337
|
+
if (!alias && String(canonical).includes("/")) alias = String(canonical).replace(/\//g, "-");
|
|
338
|
+
const aliasStr = alias && alias !== canonical ? ` (别名: ${alias})` : "";
|
|
339
|
+
const mark2 = pickedIds2.includes(canonical) || (alias && pickedIds2.includes(alias)) ? "*" : " ";
|
|
340
|
+
console.log(` ${mark2} ${canonical}${aliasStr}`);
|
|
341
|
+
}
|
|
342
|
+
process.exit(0);
|
|
343
|
+
} catch {}
|
|
344
|
+
}
|
|
345
|
+
ids = filtered;
|
|
346
|
+
if (modelListJson) {
|
|
347
|
+
console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
if (!ids.length) {
|
|
351
|
+
console.log(`no models for provider "${prov}" — try: mslxdff -provider ${prov} models or mslxdff -model refresh`);
|
|
352
|
+
process.exit(0);
|
|
353
|
+
}
|
|
354
|
+
} else if (modelListJson) {
|
|
355
|
+
console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
|
|
356
|
+
process.exit(0);
|
|
357
|
+
}
|
|
278
358
|
if (!ids.length) {
|
|
279
359
|
console.log("no models available — try: mslxdff -model refresh");
|
|
280
360
|
process.exit(0);
|
|
281
361
|
}
|
|
282
|
-
// TTY
|
|
283
|
-
|
|
362
|
+
// TTY 交互式:`mslxdff -models`(无 list)走交互;` -model list` 默认列表,但交互入口统一为 -models
|
|
363
|
+
// 为支持“opencode + 其他供应商 allowlist 原名/别名”一起勾选,交互池 = opencode 免费池 + 各供应商 allowlist + 已勾选的遗留 picks
|
|
364
|
+
if (sub === undefined && process.stdin.isTTY && process.stdout.isTTY) {
|
|
284
365
|
const statuses = loadModelErrors();
|
|
285
366
|
const current = getPreferredModel();
|
|
286
367
|
const pickedIds = loadModelPicks();
|
|
287
|
-
|
|
368
|
+
// 基础:opencode 免费池
|
|
369
|
+
const combinedIds = [...ids];
|
|
370
|
+
const seen = new Set(combinedIds);
|
|
371
|
+
try {
|
|
372
|
+
const { loadProviderConfigs, loadProviderAllowedModels } = await import("../src/state.js");
|
|
373
|
+
const configs = loadProviderConfigs();
|
|
374
|
+
for (const pid of Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode")) {
|
|
375
|
+
const allowed = loadProviderAllowedModels(pid);
|
|
376
|
+
for (const raw of allowed) {
|
|
377
|
+
const canonical = `${pid}/${raw}`;
|
|
378
|
+
if (!seen.has(canonical)) {
|
|
379
|
+
seen.add(canonical);
|
|
380
|
+
combinedIds.push(canonical);
|
|
381
|
+
}
|
|
382
|
+
// 别名(dash 版)若已被 picks 选中,也确保可见(便于取消)
|
|
383
|
+
const aliasDash = String(canonical).replace(/\//g, "-");
|
|
384
|
+
if (aliasDash !== canonical && pickedIds.includes(aliasDash) && !seen.has(aliasDash)) {
|
|
385
|
+
// 不直接加入 alias 作为独立选项,保留 canonical 即可(canonical 与 alias 视为同一模型,勾选 canonical)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch {}
|
|
390
|
+
// 已勾选但不在上述池中的(例如 workbuddy/* 当 allowAny ON 时无 allowlist,或历史 picks),补齐以便取消
|
|
391
|
+
for (const pid of pickedIds) {
|
|
392
|
+
if (!seen.has(pid)) {
|
|
393
|
+
seen.add(pid);
|
|
394
|
+
combinedIds.push(pid);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const items = combinedIds.map((id) => {
|
|
288
398
|
const e = statuses[id];
|
|
289
399
|
return {
|
|
290
400
|
id,
|
|
@@ -305,38 +415,158 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
305
415
|
const at = cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)})` : "";
|
|
306
416
|
const pickedIds = loadModelPicks();
|
|
307
417
|
const mark = (id) => (pickedIds.includes(id) ? "*" : " ");
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
418
|
+
// 分组展示:按供应商前缀分组,opencode 裸 id 归为 opencode
|
|
419
|
+
const groups = {};
|
|
420
|
+
for (const id of ids) {
|
|
421
|
+
const prov = String(id).includes("/") ? String(id).split("/")[0] : "opencode";
|
|
422
|
+
if (!groups[prov]) groups[prov] = [];
|
|
423
|
+
groups[prov].push(id);
|
|
424
|
+
}
|
|
425
|
+
const order = ["opencode", "workbuddy", "clinebot", "openrouter"];
|
|
426
|
+
const sortedProvs = Object.keys(groups).sort((a, b) => {
|
|
427
|
+
const ia = order.indexOf(a), ib = order.indexOf(b);
|
|
428
|
+
if (ia !== -1 || ib !== -1) {
|
|
429
|
+
if (ia === -1) return 1;
|
|
430
|
+
if (ib === -1) return -1;
|
|
431
|
+
return ia - ib;
|
|
318
432
|
}
|
|
319
|
-
|
|
433
|
+
return a.localeCompare(b);
|
|
434
|
+
});
|
|
435
|
+
if (modelListProvider) {
|
|
436
|
+
console.log(`${ids.length} model(s) for ${modelListProvider}${at} (${pickedIds.length} picked, * = picked):`);
|
|
437
|
+
// 加载别名映射,显示原始名+别名
|
|
438
|
+
let aliasMap = {};
|
|
320
439
|
try {
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
440
|
+
const { loadModelAliases, getAliasForModel } = await import("../src/providers/model-id.js");
|
|
441
|
+
loadModelAliases();
|
|
442
|
+
for (const id of ids) {
|
|
443
|
+
const alias = getAliasForModel(id);
|
|
444
|
+
if (alias) aliasMap[id] = alias;
|
|
445
|
+
else if (String(id).includes("/")) {
|
|
446
|
+
const dashAlias = String(id).replace(/\//g, "-");
|
|
447
|
+
if (dashAlias !== id) aliasMap[id] = dashAlias;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
324
450
|
} catch {}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (aliasEntries.length) {
|
|
334
|
-
console.log(`\nlocal aliases (${aliasEntries.length}):`);
|
|
335
|
-
for (const [alias, canonical] of aliasEntries) {
|
|
336
|
-
console.log(` ${canonical} => ${alias}`);
|
|
451
|
+
for (const prov of sortedProvs) {
|
|
452
|
+
const list = groups[prov];
|
|
453
|
+
console.log(`\n ── ${prov} (${list.length}) ──`);
|
|
454
|
+
for (const id of list) {
|
|
455
|
+
const alias = aliasMap[id];
|
|
456
|
+
const aliasStr = alias ? ` (别名: ${alias})` : "";
|
|
457
|
+
console.log(` ${mark(id)} ${id}${aliasStr}`);
|
|
458
|
+
}
|
|
337
459
|
}
|
|
460
|
+
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
461
|
+
} else {
|
|
462
|
+
console.log(`${ids.length} free model(s)${at} (${pickedIds.length} picked, * = picked):`);
|
|
463
|
+
// 加载别名映射,显示原始名+别名
|
|
464
|
+
let aliasMap = {};
|
|
465
|
+
let fullAliases = {};
|
|
466
|
+
try {
|
|
467
|
+
const { loadModelAliases, getAliasForModel } = await import("../src/providers/model-id.js");
|
|
468
|
+
loadModelAliases();
|
|
469
|
+
for (const id of ids) {
|
|
470
|
+
const alias = getAliasForModel(id);
|
|
471
|
+
if (alias) aliasMap[id] = alias;
|
|
472
|
+
else if (String(id).includes("/")) {
|
|
473
|
+
const dashAlias = String(id).replace(/\//g, "-");
|
|
474
|
+
if (dashAlias !== id) aliasMap[id] = dashAlias;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const aliasesFile = join(homedir(), ".config", "mslxdff", "model-aliases.json");
|
|
479
|
+
const raw = JSON.parse(readFileSync(aliasesFile, "utf8"));
|
|
480
|
+
if (raw && typeof raw === "object") fullAliases = raw;
|
|
481
|
+
} catch {}
|
|
482
|
+
} catch {}
|
|
483
|
+
for (const prov of sortedProvs) {
|
|
484
|
+
const list = groups[prov];
|
|
485
|
+
console.log(`\n ── ${prov} (${list.length}) ──`);
|
|
486
|
+
for (const id of list) {
|
|
487
|
+
const alias = aliasMap[id];
|
|
488
|
+
const aliasStr = alias ? ` (别名: ${alias})` : "";
|
|
489
|
+
console.log(` ${mark(id)} ${id}${aliasStr}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// ── 分隔 + 其他供应商 allowlist(原名 + 别名) ──
|
|
493
|
+
// 其余供应商的可用模型由 allowlist 决定(空 allowlist + allowAny OFF = 阻塞),此处直接展示 allowlist 条目与别名,不做网络拉取,速度快
|
|
494
|
+
try {
|
|
495
|
+
const { loadProviderConfigs, loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderBaseUrl } = await import("../src/state.js");
|
|
496
|
+
const { loadModelAliases: _la2, getAliasForModel: _gaf } = await import("../src/providers/model-id.js");
|
|
497
|
+
try { _la2(); } catch {}
|
|
498
|
+
const configs = loadProviderConfigs();
|
|
499
|
+
const otherIds = Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode");
|
|
500
|
+
// 仅展示已配置过的供应商(有 baseUrl/keys/allowlist 任一),按固定顺序 + 字典序
|
|
501
|
+
const order2 = ["workbuddy", "clinebot", "openrouter", "bai"];
|
|
502
|
+
otherIds.sort((a, b) => {
|
|
503
|
+
const ia = order2.indexOf(a), ib = order2.indexOf(b);
|
|
504
|
+
if (ia !== -1 || ib !== -1) {
|
|
505
|
+
if (ia === -1) return 1;
|
|
506
|
+
if (ib === -1) return -1;
|
|
507
|
+
return ia - ib;
|
|
508
|
+
}
|
|
509
|
+
return a.localeCompare(b);
|
|
510
|
+
});
|
|
511
|
+
if (otherIds.length) {
|
|
512
|
+
console.log(`\n────────────────────────────────────────`);
|
|
513
|
+
console.log(`其他供应商 (allowlist,原名 + 别名) (${otherIds.length} providers):`);
|
|
514
|
+
for (const pid of otherIds) {
|
|
515
|
+
const allowed = loadProviderAllowedModels(pid);
|
|
516
|
+
const allowAny = loadProviderAllowAnyModels(pid);
|
|
517
|
+
const baseUrl = loadProviderBaseUrl(pid) || configs[pid]?.baseUrl || "";
|
|
518
|
+
const header = allowAny
|
|
519
|
+
? (allowed.length ? `allowlist ${allowed.length} (allowAny ON)` : `allowAny ON (allowlist 空=放行全部)`)
|
|
520
|
+
: (allowed.length ? `allowlist ${allowed.length} (allowAny OFF)` : `allowlist 空 + allowAny OFF = 阻塞`);
|
|
521
|
+
console.log(`\n ── ${pid} (${header})${baseUrl ? ` baseUrl=${baseUrl}` : ""} ──`);
|
|
522
|
+
if (!allowed.length) {
|
|
523
|
+
if (allowAny) {
|
|
524
|
+
console.log(` (未设 allowlist,全部模型放行) 查看 live 列表: mslxdff -provider ${pid} models`);
|
|
525
|
+
console.log(` 限制可用模型: mslxdff -provider ${pid} allowlist set <model1> <model2>`);
|
|
526
|
+
} else {
|
|
527
|
+
console.log(` 阻塞中:无可用模型 — 设白名单: mslxdff -provider ${pid} allowlist set <model1> <model2>`);
|
|
528
|
+
console.log(` 或放行全部: mslxdff -provider ${pid} allowAny on`);
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
for (const raw of allowed) {
|
|
532
|
+
const canonical = `${pid}/${raw}`;
|
|
533
|
+
let alias = null;
|
|
534
|
+
try { alias = _gaf(canonical); } catch {}
|
|
535
|
+
if (!alias && String(canonical).includes("/")) alias = String(canonical).replace(/\//g, "-");
|
|
536
|
+
const aliasStr = alias && alias !== canonical ? ` (别名: ${alias})` : "";
|
|
537
|
+
const pickedMark = pickedIds.includes(canonical) || pickedIds.includes(alias || "") ? "*" : " ";
|
|
538
|
+
console.log(` ${pickedMark} ${canonical}${aliasStr}`);
|
|
539
|
+
}
|
|
540
|
+
console.log(` 管理: mslxdff -provider ${pid} allowlist [list|add|remove|clear] | allowAny on|off`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
console.log(`\n────────────────────────────────────────`);
|
|
545
|
+
console.log(`其他供应商 (allowlist,原名 + 别名): (none — 尚未配置)`);
|
|
546
|
+
console.log(` 添加示例: mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models`);
|
|
547
|
+
}
|
|
548
|
+
// 额外兜底:本地别名中不在任何 allowlist 里的,单独提示
|
|
549
|
+
const aliasEntries = Object.entries(fullAliases).filter(([alias, canonical]) => {
|
|
550
|
+
if (ids.includes(canonical)) return false;
|
|
551
|
+
// 已在 allowlist 中展示过的别名不重复
|
|
552
|
+
for (const pid of otherIds) {
|
|
553
|
+
const allowed = loadProviderAllowedModels(pid);
|
|
554
|
+
for (const raw of allowed) {
|
|
555
|
+
const can = `${pid}/${raw}`;
|
|
556
|
+
if (can === canonical) return false;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
});
|
|
561
|
+
if (aliasEntries.length) {
|
|
562
|
+
console.log(`\n 本地别名 (不在 allowlist 里的遗留映射 ${aliasEntries.length}):`);
|
|
563
|
+
for (const [alias, canonical] of aliasEntries) {
|
|
564
|
+
console.log(` ${canonical} => ${alias}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
} catch {}
|
|
568
|
+
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
338
569
|
}
|
|
339
|
-
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
340
570
|
} catch (err) {
|
|
341
571
|
console.error(`could not fetch models: ${String(err?.message || err)}`);
|
|
342
572
|
process.exit(1);
|
|
@@ -727,17 +957,21 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
727
957
|
const sub = args[idx + 2];
|
|
728
958
|
const rest = args.slice(idx + 2);
|
|
729
959
|
if (!id) {
|
|
730
|
-
console.error("usage: mslxdff -provider <id> [key...|add|remove|list|clear|share|set-url|allowlist|allowAny]");
|
|
960
|
+
console.error("usage: mslxdff -provider <id> [key...|add|remove|list|models|clear|share|set-url|allowlist|allowAny]");
|
|
731
961
|
console.error(" e.g. mslxdff -provider openrouter sk-1 sk-2 sk-3 set multiple keys (replaces all)");
|
|
732
962
|
console.error(" mslxdff -provider openrouter add sk-4 append one key");
|
|
733
963
|
console.error(" mslxdff -provider openrouter remove sk-1 remove a key by value");
|
|
734
964
|
console.error(" mslxdff -provider openrouter list list all keys (masked)");
|
|
965
|
+
console.error(" mslxdff -provider openrouter models [--json] list provider models (allowlist filtered)");
|
|
735
966
|
console.error(" mslxdff -provider openrouter share on|off share keys with peers on outgoing forward (ADR-0008)");
|
|
736
967
|
console.error(" mslxdff -provider openrouter set-url https://api.example.com/v1");
|
|
968
|
+
console.error(" mslxdff -provider openrouter set-models-path /v1/models");
|
|
969
|
+
console.error(" mslxdff -provider openrouter set-chat-path /v1/chat/completions");
|
|
737
970
|
console.error(" mslxdff -provider openrouter allowlist set gpt-4 gpt-3.5 manage allowed models (empty=block unless allowAny ON)");
|
|
738
971
|
console.error(" mslxdff -provider openrouter allowAny on|off empty allowlist = allow all or block all (default OFF, secure)");
|
|
739
972
|
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx add generic OpenAI-compatible provider");
|
|
740
973
|
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 add with allowlist");
|
|
974
|
+
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models --chat-path /v1/chat/completions");
|
|
741
975
|
console.error(" mslxdff -provider openrouter interactive hidden input (append)");
|
|
742
976
|
console.error(" mslxdff -provider openrouter clear remove all keys");
|
|
743
977
|
process.exit(1);
|
|
@@ -795,9 +1029,10 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
795
1029
|
const gBase = rest[1];
|
|
796
1030
|
const gKey = rest[2];
|
|
797
1031
|
if (!gid || !gBase || !gKey) {
|
|
798
|
-
console.error("usage: mslxdff -provider add <id> <baseUrl> <key> [allowedModel...]");
|
|
1032
|
+
console.error("usage: mslxdff -provider add <id> <baseUrl> <key> [allowedModel...] [--models-path <path>] [--chat-path <path>]");
|
|
799
1033
|
console.error(" e.g. mslxdff -provider add myapi https://api.example.com/v1 sk-xxx");
|
|
800
1034
|
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 gpt-3.5");
|
|
1035
|
+
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models --chat-path /v1/chat/completions");
|
|
801
1036
|
process.exit(1);
|
|
802
1037
|
}
|
|
803
1038
|
if (gid === "opencode" || gid === "oc" || gid === "openrouter") {
|
|
@@ -809,10 +1044,24 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
809
1044
|
const nid = normalizeProviderId(gid);
|
|
810
1045
|
if (!nid) { console.error(`invalid provider id: ${gid}`); process.exit(1); }
|
|
811
1046
|
if (!/^https?:\/\/.+/.test(String(gBase).trim())) { console.error(`invalid baseUrl: ${gBase} (must start with http:// or https://)`); process.exit(1); }
|
|
812
|
-
const cur = loadProviderConfig(nid) || { baseUrl: "", keys: [], allowedModels: [], auths: [] };
|
|
1047
|
+
const cur = loadProviderConfig(nid) || { baseUrl: "", keys: [], allowedModels: [], auths: [], modelsPath: "", chatPath: "" };
|
|
813
1048
|
let keys, auths, baseUrl;
|
|
814
1049
|
baseUrl = String(gBase).trim();
|
|
815
|
-
|
|
1050
|
+
// parse --models-path / --chat-path from tail
|
|
1051
|
+
let parsedModelsPath = null;
|
|
1052
|
+
let parsedChatPath = null;
|
|
1053
|
+
const extraTokens = [];
|
|
1054
|
+
for (let _i = 3; _i < rest.length; _i++) {
|
|
1055
|
+
const tok = String(rest[_i] || "");
|
|
1056
|
+
if (tok === "--models-path" || tok === "--modelsPath" || tok === "--models_path") { parsedModelsPath = String(rest[_i + 1] || "").trim() || null; _i++; }
|
|
1057
|
+
else if (tok.startsWith("--models-path=")) { parsedModelsPath = tok.slice("--models-path=".length).trim() || null; }
|
|
1058
|
+
else if (tok === "--chat-path" || tok === "--chatPath" || tok === "--chat_path") { parsedChatPath = String(rest[_i + 1] || "").trim() || null; _i++; }
|
|
1059
|
+
else if (tok.startsWith("--chat-path=")) { parsedChatPath = tok.slice("--chat-path=".length).trim() || null; }
|
|
1060
|
+
else extraTokens.push(tok);
|
|
1061
|
+
}
|
|
1062
|
+
if (parsedModelsPath && !String(parsedModelsPath).startsWith("/")) { console.error(`invalid --models-path: ${parsedModelsPath} (must start with /)`); process.exit(1); }
|
|
1063
|
+
if (parsedChatPath && !String(parsedChatPath).startsWith("/")) { console.error(`invalid --chat-path: ${parsedChatPath} (must start with /)`); process.exit(1); }
|
|
1064
|
+
const extraModels = extraTokens.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
|
|
816
1065
|
const allowedModels = extraModels.length ? [...new Set([...(cur.allowedModels || []), ...extraModels])] : (cur.allowedModels || []);
|
|
817
1066
|
if (nid === "workbuddy") {
|
|
818
1067
|
// workbuddy: keys/auths 一一对应,需解析 uid
|
|
@@ -838,7 +1087,12 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
838
1087
|
while (newKeys.length < newAuths.length) newKeys.push(token);
|
|
839
1088
|
}
|
|
840
1089
|
keys = newKeys; auths = newAuths;
|
|
841
|
-
|
|
1090
|
+
const cfgToSave = { baseUrl, keys, auths, allowedModels };
|
|
1091
|
+
if (parsedModelsPath) cfgToSave.modelsPath = parsedModelsPath;
|
|
1092
|
+
else if (cur.modelsPath) cfgToSave.modelsPath = cur.modelsPath;
|
|
1093
|
+
if (parsedChatPath) cfgToSave.chatPath = parsedChatPath;
|
|
1094
|
+
else if (cur.chatPath) cfgToSave.chatPath = cur.chatPath;
|
|
1095
|
+
saveProviderConfig(nid, cfgToSave);
|
|
842
1096
|
// 同步写 auths/workbuddy-<uid>.json 供 checkin 使用
|
|
843
1097
|
try {
|
|
844
1098
|
const { writeFileSync, mkdirSync } = await import("node:fs");
|
|
@@ -860,7 +1114,12 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
860
1114
|
}
|
|
861
1115
|
keys = [...new Set([...(cur.keys || []), trimmed].filter(Boolean))];
|
|
862
1116
|
auths = undefined;
|
|
863
|
-
|
|
1117
|
+
const cfgToSave2 = { baseUrl, keys, allowedModels };
|
|
1118
|
+
if (parsedModelsPath) cfgToSave2.modelsPath = parsedModelsPath;
|
|
1119
|
+
else if (cur.modelsPath) cfgToSave2.modelsPath = cur.modelsPath;
|
|
1120
|
+
if (parsedChatPath) cfgToSave2.chatPath = parsedChatPath;
|
|
1121
|
+
else if (cur.chatPath) cfgToSave2.chatPath = cur.chatPath;
|
|
1122
|
+
saveProviderConfig(nid, cfgToSave2);
|
|
864
1123
|
}
|
|
865
1124
|
console.log(`added generic provider: ${nid}`);
|
|
866
1125
|
console.log(` baseUrl: ${String(gBase).trim().replace(/\/+$/, "")}`);
|
|
@@ -880,6 +1139,40 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
880
1139
|
process.exit(0);
|
|
881
1140
|
}
|
|
882
1141
|
const { loadProviderKeys, saveProviderKeys, addProviderKey, removeProviderKeys, loadProviderShareKeys, saveProviderShareKeys, loadProviderConfig, saveProviderConfig, saveProviderBaseUrl, loadProviderConfigs } = await import("../src/state.js");
|
|
1142
|
+
if (sub === "set-models-path" || sub === "setModelsPath" || sub === "models-path") {
|
|
1143
|
+
const p = rest[1];
|
|
1144
|
+
if (!p) {
|
|
1145
|
+
console.error(`usage: mslxdff -provider ${id} set-models-path <path> (e.g. /v1/models)`);
|
|
1146
|
+
process.exit(1);
|
|
1147
|
+
}
|
|
1148
|
+
if (!String(p).trim().startsWith("/")) {
|
|
1149
|
+
console.error(`invalid modelsPath: ${p} (must start with /)`);
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
1153
|
+
const { normalizeProviderId } = await import("../src/providers/model-id.js");
|
|
1154
|
+
const nid = normalizeProviderId(id);
|
|
1155
|
+
saveProviderConfig(nid || id, { baseUrl: cur.baseUrl || "", keys: cur.keys || [], modelsPath: String(p).trim() });
|
|
1156
|
+
console.log(`set ${nid || id} modelsPath: ${String(p).trim()} — restart daemon to activate`);
|
|
1157
|
+
process.exit(0);
|
|
1158
|
+
}
|
|
1159
|
+
if (sub === "set-chat-path" || sub === "setChatPath" || sub === "chat-path") {
|
|
1160
|
+
const p = rest[1];
|
|
1161
|
+
if (!p) {
|
|
1162
|
+
console.error(`usage: mslxdff -provider ${id} set-chat-path <path> (e.g. /v1/chat/completions)`);
|
|
1163
|
+
process.exit(1);
|
|
1164
|
+
}
|
|
1165
|
+
if (!String(p).trim().startsWith("/")) {
|
|
1166
|
+
console.error(`invalid chatPath: ${p} (must start with /)`);
|
|
1167
|
+
process.exit(1);
|
|
1168
|
+
}
|
|
1169
|
+
const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
|
|
1170
|
+
const { normalizeProviderId } = await import("../src/providers/model-id.js");
|
|
1171
|
+
const nid = normalizeProviderId(id);
|
|
1172
|
+
saveProviderConfig(nid || id, { baseUrl: cur.baseUrl || "", keys: cur.keys || [], chatPath: String(p).trim() });
|
|
1173
|
+
console.log(`set ${nid || id} chatPath: ${String(p).trim()} — restart daemon to activate`);
|
|
1174
|
+
process.exit(0);
|
|
1175
|
+
}
|
|
883
1176
|
if (sub === "clear") {
|
|
884
1177
|
const configs = loadProviderConfigs();
|
|
885
1178
|
if (configs[id]) {
|
|
@@ -1020,6 +1313,69 @@ if (args.includes("-provider") || args.includes("--provider")) {
|
|
|
1020
1313
|
console.error(` mslxdff -provider ${id} allowAny on|off (empty allowlist = block or allow all)`);
|
|
1021
1314
|
process.exit(1);
|
|
1022
1315
|
}
|
|
1316
|
+
if (sub === "models" || sub === "list-models" || sub === "ls") {
|
|
1317
|
+
const wantsJson = args.includes("--json") || args.includes("-json");
|
|
1318
|
+
const cfg = loadProviderConfig(id);
|
|
1319
|
+
// opencode: use aggregated cache + provider-specific? For opencode, show bare ids from cache
|
|
1320
|
+
if (id === "opencode" || id === "oc") {
|
|
1321
|
+
try {
|
|
1322
|
+
const cacheFile = join(logDir(), "models.json");
|
|
1323
|
+
const { readFileSync } = await import("node:fs");
|
|
1324
|
+
const raw = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
1325
|
+
const ids = (raw.data || []).map((m) => m.id).filter((x) => !String(x).includes("/"));
|
|
1326
|
+
if (wantsJson) {
|
|
1327
|
+
console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
|
|
1328
|
+
} else {
|
|
1329
|
+
console.log(`opencode models (${ids.length}):`);
|
|
1330
|
+
for (const mid of ids) console.log(` ${mid}`);
|
|
1331
|
+
}
|
|
1332
|
+
} catch (e) {
|
|
1333
|
+
console.error(`could not read models cache: ${String(e?.message || e)}`);
|
|
1334
|
+
process.exit(1);
|
|
1335
|
+
}
|
|
1336
|
+
process.exit(0);
|
|
1337
|
+
}
|
|
1338
|
+
// generic/workbuddy: live fetch via provider listModels
|
|
1339
|
+
try {
|
|
1340
|
+
const { createGenericProvider } = await import("../src/providers/generic.js");
|
|
1341
|
+
const { createWorkbuddyProvider } = await import("../src/providers/workbuddy.js");
|
|
1342
|
+
const { isModelAllowed } = await import("../src/state.js");
|
|
1343
|
+
const baseUrl = cfg?.baseUrl || (id === "workbuddy" ? "https://copilot.tencent.com" : "");
|
|
1344
|
+
const keys = loadProviderKeys(id);
|
|
1345
|
+
const auths = cfg?.auths || [];
|
|
1346
|
+
let provider;
|
|
1347
|
+
if (id === "workbuddy") {
|
|
1348
|
+
provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
|
|
1349
|
+
} else {
|
|
1350
|
+
if (!baseUrl) {
|
|
1351
|
+
console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);
|
|
1352
|
+
process.exit(1);
|
|
1353
|
+
}
|
|
1354
|
+
provider = createGenericProvider({ id, baseUrl, apiKeys: keys, file: defaultStateFile() });
|
|
1355
|
+
}
|
|
1356
|
+
const all = await provider.listModels();
|
|
1357
|
+
// apply allowlist filtering mirroring dispatcher
|
|
1358
|
+
const filtered = all.filter((m) => {
|
|
1359
|
+
const raw = String(m.id || "").includes("/") ? String(m.id).split("/").slice(1).join("/") : String(m.id);
|
|
1360
|
+
// for workbuddy, raw is like hy3, for generic it's raw id without prefix? joinModelId adds prefix, so need to extract raw
|
|
1361
|
+
const checkRaw = m.id.startsWith(`${id}/`) ? m.id.slice(id.length + 1) : raw;
|
|
1362
|
+
return isModelAllowed(id, checkRaw);
|
|
1363
|
+
});
|
|
1364
|
+
if (wantsJson) {
|
|
1365
|
+
console.log(JSON.stringify({ object: "list", data: filtered }, null, 2));
|
|
1366
|
+
} else {
|
|
1367
|
+
console.log(`${id} models (${filtered.length}${filtered.length !== all.length ? `/${all.length}` : ""}):`);
|
|
1368
|
+
for (const m of filtered) console.log(` ${m.id}`);
|
|
1369
|
+
if (!filtered.length && all.length) console.log(` (all ${all.length} filtered by allowlist — use: mslxdff -provider ${id} allowlist list)`);
|
|
1370
|
+
if (!all.length) console.log(` (no models — check baseUrl/keys or try: curl ${baseUrl}/models)`);
|
|
1371
|
+
}
|
|
1372
|
+
try { await provider.close?.(); } catch {}
|
|
1373
|
+
} catch (e) {
|
|
1374
|
+
console.error(`could not list ${id} models: ${String(e?.message || e)}`);
|
|
1375
|
+
process.exit(1);
|
|
1376
|
+
}
|
|
1377
|
+
process.exit(0);
|
|
1378
|
+
}
|
|
1023
1379
|
if (sub === "list" || sub === "status") {
|
|
1024
1380
|
const keys = loadProviderKeys(id);
|
|
1025
1381
|
const cfg = loadProviderConfig(id);
|
|
@@ -1159,7 +1515,8 @@ async function pickInteractive(items, startCursor = 0) {
|
|
|
1159
1515
|
let cursor = Math.min(Math.max(startCursor, 0), items.length - 1);
|
|
1160
1516
|
const draw = () => {
|
|
1161
1517
|
const lines = [...renderChooser(items, cursor), ...renderChooserHelp()];
|
|
1162
|
-
|
|
1518
|
+
// PowerShell 兼容:\x1b[2J 清屏 + \x1b[H 回到左上,比 \x1b[G\x1b[J 更可靠
|
|
1519
|
+
process.stdout.write("\x1b[2J\x1b[H" + lines.join("\n"));
|
|
1163
1520
|
};
|
|
1164
1521
|
draw();
|
|
1165
1522
|
return new Promise((resolve) => {
|
|
@@ -1201,7 +1558,7 @@ async function pickInteractiveMulti(items, initialPicked = new Set(), startCurso
|
|
|
1201
1558
|
const draw = () => {
|
|
1202
1559
|
const rows = items.map((it, i) => ({ ...it, picked: picked.has(it.id) }));
|
|
1203
1560
|
const lines = [...renderChooser(rows, cursor, { multi: true }), ...renderChooserHelp(true)];
|
|
1204
|
-
process.stdout.write("\x1b[
|
|
1561
|
+
process.stdout.write("\x1b[2J\x1b[H" + lines.join("\n"));
|
|
1205
1562
|
};
|
|
1206
1563
|
draw();
|
|
1207
1564
|
return new Promise((resolve) => {
|
package/package.json
CHANGED
package/src/auto.js
CHANGED
|
@@ -85,6 +85,7 @@ function effectiveCooldown(entry, slowCooldownMs, cooldownMs) {
|
|
|
85
85
|
function inCooldown(id, errors, now, cooldownMs, slowCooldownMs) {
|
|
86
86
|
const e = normEntry(errors[id]);
|
|
87
87
|
if (!e || !(e.at > 0)) return false;
|
|
88
|
+
if (e.status === MODEL_STATUS.NORMAL) return false;
|
|
88
89
|
const cd = effectiveCooldown(e, slowCooldownMs, cooldownMs);
|
|
89
90
|
return cd > 0 && now - e.at < cd;
|
|
90
91
|
}
|
|
@@ -98,6 +99,16 @@ function normLatency(e) {
|
|
|
98
99
|
|
|
99
100
|
export function rankModels(ids, errors = {}, { now = Date.now(), cooldownMs = 0, slowCooldownMs = 0, latencies = {}, preferred } = {}) {
|
|
100
101
|
const pref = preferred ?? getPreferredModel();
|
|
102
|
+
// 最近一次成功的模型(NORMAL 且非慢且 at 最大),用于“上次成功优先”——慢模型即使刚成功也不应钉死
|
|
103
|
+
let lastSuccessId = null;
|
|
104
|
+
let lastSuccessAt = 0;
|
|
105
|
+
for (const [id, e] of Object.entries(errors)) {
|
|
106
|
+
const ne = normEntry(e);
|
|
107
|
+
if (ne && ne.status === MODEL_STATUS.NORMAL && !ne.slow && ne.at > lastSuccessAt) {
|
|
108
|
+
lastSuccessAt = ne.at;
|
|
109
|
+
lastSuccessId = id;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
101
112
|
return [...new Set(ids)]
|
|
102
113
|
.filter(Boolean)
|
|
103
114
|
.map((id) => ({
|
|
@@ -105,12 +116,14 @@ export function rankModels(ids, errors = {}, { now = Date.now(), cooldownMs = 0,
|
|
|
105
116
|
e: normEntry(errors[id]),
|
|
106
117
|
err: normEntry(errors[id])?.at ?? 0,
|
|
107
118
|
isPreferred: id === pref,
|
|
119
|
+
isLastSuccess: id === lastSuccessId,
|
|
108
120
|
cooling: inCooldown(id, errors, now, cooldownMs, slowCooldownMs),
|
|
109
121
|
latency: normLatency(latencies[id]) ?? Number.MAX_SAFE_INTEGER,
|
|
110
122
|
}))
|
|
111
123
|
.sort(
|
|
112
124
|
(a, b) =>
|
|
113
125
|
(a.cooling ? 1 : 0) - (b.cooling ? 1 : 0) ||
|
|
126
|
+
(b.isLastSuccess ? 1 : 0) - (a.isLastSuccess ? 1 : 0) ||
|
|
114
127
|
(b.isPreferred ? 1 : 0) - (a.isPreferred ? 1 : 0) ||
|
|
115
128
|
a.latency - b.latency ||
|
|
116
129
|
a.err - b.err
|
package/src/chat/config.js
CHANGED
|
@@ -6,5 +6,6 @@ export const CHAT_HISTORY_MAX_CHARS = 400000;
|
|
|
6
6
|
export const CHAT_KEEP_RECENT = 40;
|
|
7
7
|
export const CHAT_SUMMARY_TRIGGER = 400000;
|
|
8
8
|
export const CHAT_MAX_TOOL_LOOPS = 6;
|
|
9
|
-
export const CHAT_TIMEOUT_MS =
|
|
9
|
+
export const CHAT_TIMEOUT_MS = 15000;
|
|
10
|
+
export const CHAT_GATEWAY_TIMEOUT_MS = 25000;
|
|
10
11
|
export const FORBIDDEN = ["-uninstall", "--uninstall"];
|
package/src/chat/prompt.js
CHANGED
|
@@ -52,7 +52,7 @@ ${mini}
|
|
|
52
52
|
- 永远输出精确的命令与模型 id,大小写敏感。
|
|
53
53
|
- 需要执行命令时调用 run_command,需要看文件时调用 read_file,需要检查网络/服务可用性时调用 curl。
|
|
54
54
|
- curl 简写:upstream(=上游 https://opencode.ai/zen/v1/models)、local/health(=本机 /health)、local/models(=本机 /v1/models),也支持完整 http(s) URL;会自动补上游头、本机 token 与已配置供应商 key(直连 https://api.b.ai/v1/models 会自动带 bai 的 key,无需手动加头)。
|
|
55
|
-
-
|
|
55
|
+
- 查“某供应商有哪些模型”**优先用 CLI 直查**:若上方“可用模型”已能回答,直接前缀过滤回答(如 workbuddy/ 即 workbuddy);需实时拉取时调用 run_command: "-provider <id> models"(如 -provider workbuddy models)或 "-model list --provider <id>",按 allowlist 过滤,--json 供脚本。**禁止**调 -provider <id> list(这是查配置,不是查模型!)。**错误示例**:workbuddy有哪些模型 → 调 -provider workbuddy list → 错。**正确**:-provider workbuddy models。禁止为此调用 -showtoken。
|
|
56
56
|
- 严禁幻觉命令:mslxdff "hi" --model X / mslxdff --model X "hi" / mslxdff -chat --model X 都不存在,输出只会是 status 页。探活任意模型(含 clinebot/*、workbuddy/*、bai/*)必须用 curl POST http://localhost:8989/v1/chat/completions,body 为 {"model":"<前缀/模型>","messages":[{"role":"user","content":"hi"}],"stream":false},成功 200 + x-mslxdff-via:local 即通;401 代表本机 token 陈旧需提示 mslxdff -stop && mslxdff;403 + x-mslxdff-allowlist:1 代表白名单未放行需 allowlist add。
|
|
57
57
|
- **禁止重复调用(最高优先级)**:同一 run_command/curl/read_file 在本轮只执行一次,重复会被工具侧 SKIPPED_DUP 拦截;查询类(-showtoken/-status/-provider list/-providers list/-model list/-group list/-log 等)**调用一次即答案**,拿到 OK 结果后必须**立即用中文直接回答用户**,禁止再发起任何工具调用。收到 SKIPPED_DUP 或“请直接回答/禁止再调用”提示时,必须 0 工具直接回答。
|
|
58
58
|
- 禁止调用 -uninstall,包含即拒绝;-showtoken 仅在用户明确要求查看/调试本机 token 时才用,查模型/查供应商严禁调用。
|