mslxdff 0.1.89 → 0.1.91
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/package.json +1 -1
- package/src/chat/engine.js +160 -0
- package/src/chat/repl.js +32 -299
- package/src/chat/terminal.js +95 -0
- package/src/chat/tool-handlers.js +67 -0
- package/src/chat/upstream.js +1 -4
- package/src/chat-pipeline/auto-race.js +124 -0
- package/src/chat-pipeline/engine.js +10 -234
- package/src/chat-pipeline/serial-trial.js +144 -0
- package/src/cli/commands/model/list-providers.js +75 -0
- package/src/cli/commands/model/list-render.js +79 -0
- package/src/cli/commands/model/list.js +208 -0
- package/src/cli/commands/model/picks.js +45 -0
- package/src/cli/commands/model/stats.js +43 -0
- package/src/cli/commands/model/status.js +47 -0
- package/src/cli/commands/model.js +17 -371
- package/src/cli/help.js +2 -1
- package/src/providers/cline/chat.js +15 -2
- package/src/routes/chat/relay-pipeline.js +26 -0
- package/src/runtime/bootstrap.js +14 -469
- package/src/runtime/broadband-stream.js +76 -0
- package/src/runtime/broadband.js +97 -0
- package/src/runtime/group-sync.js +28 -0
- package/src/runtime/providers-setup.js +147 -0
- package/src/runtime/server-lifecycle.js +156 -0
- package/src/state/facade.js +1 -0
- package/src/state/schemas/model.js +41 -0
- package/src/upstream-responses.js +64 -0
- package/src/upstream.js +5 -59
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 其他供应商 allowlist 段 + 遗留别名映射展示。
|
|
3
|
+
* 输入均为已算好的数据;state/model-id 走动态 import(与原内联一致,避免循环依赖)。
|
|
4
|
+
*/
|
|
5
|
+
export async function renderOtherProviders({ pickedIds, ids, fullAliases }) {
|
|
6
|
+
const { loadProviderConfigs, loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderBaseUrl } = await import("../../../state.js");
|
|
7
|
+
const { loadModelAliases: _la2, getAliasForModel: _gaf } = await import("../../../providers/model-id.js");
|
|
8
|
+
try { _la2(); } catch {}
|
|
9
|
+
const configs = loadProviderConfigs();
|
|
10
|
+
const otherIds = Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode");
|
|
11
|
+
const order2 = ["workbuddy", "clinebot", "openrouter", "bai"];
|
|
12
|
+
otherIds.sort((a, b) => {
|
|
13
|
+
const ia = order2.indexOf(a), ib = order2.indexOf(b);
|
|
14
|
+
if (ia !== -1 || ib !== -1) {
|
|
15
|
+
if (ia === -1) return 1;
|
|
16
|
+
if (ib === -1) return -1;
|
|
17
|
+
return ia - ib;
|
|
18
|
+
}
|
|
19
|
+
return a.localeCompare(b);
|
|
20
|
+
});
|
|
21
|
+
if (otherIds.length) {
|
|
22
|
+
console.log(`\n────────────────────────────────────────`);
|
|
23
|
+
console.log(`其他供应商 (allowlist,原名 + 别名) (${otherIds.length} providers):`);
|
|
24
|
+
for (const pid of otherIds) {
|
|
25
|
+
const allowed = loadProviderAllowedModels(pid);
|
|
26
|
+
const allowAny = loadProviderAllowAnyModels(pid);
|
|
27
|
+
const baseUrl = loadProviderBaseUrl(pid) || configs[pid]?.baseUrl || "";
|
|
28
|
+
const header = allowAny
|
|
29
|
+
? (allowed.length ? `allowlist ${allowed.length} (allowAny ON)` : `allowAny ON (allowlist 空=放行全部)`)
|
|
30
|
+
: (allowed.length ? `allowlist ${allowed.length} (allowAny OFF)` : `allowlist 空 + allowAny OFF = 阻塞`);
|
|
31
|
+
console.log(`\n ── ${pid} (${header})${baseUrl ? ` baseUrl=${baseUrl}` : ""} ──`);
|
|
32
|
+
if (!allowed.length) {
|
|
33
|
+
if (allowAny) {
|
|
34
|
+
console.log(` (未设 allowlist,全部模型放行) 查看 live 列表: mslxdff -provider ${pid} models`);
|
|
35
|
+
console.log(` 限制可用模型: mslxdff -provider ${pid} allowlist set <model1> <model2>`);
|
|
36
|
+
} else {
|
|
37
|
+
console.log(` 阻塞中:无可用模型 — 设白名单: mslxdff -provider ${pid} allowlist set <model1> <model2>`);
|
|
38
|
+
console.log(` 或放行全部: mslxdff -provider ${pid} allowAny on`);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
for (const raw of allowed) {
|
|
42
|
+
const canonical = `${pid}/${raw}`;
|
|
43
|
+
let alias = null;
|
|
44
|
+
try { alias = _gaf(canonical); } catch {}
|
|
45
|
+
if (!alias && String(canonical).includes("/")) alias = String(canonical).replace(/\//g, "-");
|
|
46
|
+
const aliasStr = alias && alias !== canonical ? ` (别名: ${alias})` : "";
|
|
47
|
+
const pickedMark = pickedIds.includes(canonical) || pickedIds.includes(alias || "") ? "*" : " ";
|
|
48
|
+
console.log(` ${pickedMark} ${canonical}${aliasStr}`);
|
|
49
|
+
}
|
|
50
|
+
console.log(` 管理: mslxdff -provider ${pid} allowlist [list|add|remove|clear] | allowAny on|off`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
console.log(`\n────────────────────────────────────────`);
|
|
55
|
+
console.log(`其他供应商 (allowlist,原名 + 别名): (none — 尚未配置)`);
|
|
56
|
+
console.log(` 添加示例: mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models`);
|
|
57
|
+
}
|
|
58
|
+
const aliasEntries = Object.entries(fullAliases).filter(([alias, canonical]) => {
|
|
59
|
+
if (ids.includes(canonical)) return false;
|
|
60
|
+
for (const pid of otherIds) {
|
|
61
|
+
const allowed = loadProviderAllowedModels(pid);
|
|
62
|
+
for (const raw of allowed) {
|
|
63
|
+
const can = `${pid}/${raw}`;
|
|
64
|
+
if (can === canonical) return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
if (aliasEntries.length) {
|
|
70
|
+
console.log(`\n 本地别名 (不在 allowlist 里的遗留映射 ${aliasEntries.length}):`);
|
|
71
|
+
for (const [alias, canonical] of aliasEntries) {
|
|
72
|
+
console.log(` ${canonical} => ${alias}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
/** 构建 id → 别名映射(原 list 两处重复逻辑收敛) */
|
|
6
|
+
export function buildAliasMap(ids, getAliasForModel) {
|
|
7
|
+
const aliasMap = {};
|
|
8
|
+
for (const id of ids) {
|
|
9
|
+
const alias = getAliasForModel(id);
|
|
10
|
+
if (alias) aliasMap[id] = alias;
|
|
11
|
+
else if (String(id).includes("/")) {
|
|
12
|
+
const dashAlias = String(id).replace(/\//g, "-");
|
|
13
|
+
if (dashAlias !== id) aliasMap[id] = dashAlias;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return aliasMap;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 读本地别名全表(遗留映射展示用) */
|
|
20
|
+
export function readFullAliases() {
|
|
21
|
+
try {
|
|
22
|
+
const aliasesFile = join(homedir(), ".config", "mslxdff", "model-aliases.json");
|
|
23
|
+
const raw = JSON.parse(readFileSync(aliasesFile, "utf8"));
|
|
24
|
+
if (raw && typeof raw === "object") return raw;
|
|
25
|
+
} catch {}
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `--provider <id>` 分支的分组渲染 */
|
|
30
|
+
export function renderProviderList({ ids, at, pickedIds, modelListProvider, sortedProvs, groups, aliasMap }) {
|
|
31
|
+
console.log(`${ids.length} model(s) for ${modelListProvider}${at} (${pickedIds.length} picked, * = picked):`);
|
|
32
|
+
const mark = (id) => (pickedIds.includes(id) ? "*" : " ");
|
|
33
|
+
for (const prov of sortedProvs) {
|
|
34
|
+
const list = groups[prov];
|
|
35
|
+
console.log(`\n ── ${prov} (${list.length}) ──`);
|
|
36
|
+
for (const id of list) {
|
|
37
|
+
const alias = aliasMap[id];
|
|
38
|
+
const aliasStr = alias ? ` (别名: ${alias})` : "";
|
|
39
|
+
console.log(` ${mark(id)} ${id}${aliasStr}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 全量 free 列表的分组渲染 */
|
|
46
|
+
export function renderFreeList({ ids, at, pickedIds, sortedProvs, groups, aliasMap }) {
|
|
47
|
+
console.log(`${ids.length} free model(s)${at} (${pickedIds.length} picked, * = picked):`);
|
|
48
|
+
const mark = (id) => (pickedIds.includes(id) ? "*" : " ");
|
|
49
|
+
for (const prov of sortedProvs) {
|
|
50
|
+
const list = groups[prov];
|
|
51
|
+
console.log(`\n ── ${prov} (${list.length}) ──`);
|
|
52
|
+
for (const id of list) {
|
|
53
|
+
const alias = aliasMap[id];
|
|
54
|
+
const aliasStr = alias ? ` (别名: ${alias})` : "";
|
|
55
|
+
console.log(` ${mark(id)} ${id}${aliasStr}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 按供应商分组 + 排序(opencode/workbuddy/clinebot/openrouter 优先) */
|
|
61
|
+
export function groupByProvider(ids) {
|
|
62
|
+
const groups = {};
|
|
63
|
+
for (const id of ids) {
|
|
64
|
+
const prov = String(id).includes("/") ? String(id).split("/")[0] : "opencode";
|
|
65
|
+
if (!groups[prov]) groups[prov] = [];
|
|
66
|
+
groups[prov].push(id);
|
|
67
|
+
}
|
|
68
|
+
const order = ["opencode", "workbuddy", "clinebot", "openrouter"];
|
|
69
|
+
const sortedProvs = Object.keys(groups).sort((a, b) => {
|
|
70
|
+
const ia = order.indexOf(a), ib = order.indexOf(b);
|
|
71
|
+
if (ia !== -1 || ib !== -1) {
|
|
72
|
+
if (ia === -1) return 1;
|
|
73
|
+
if (ib === -1) return -1;
|
|
74
|
+
return ia - ib;
|
|
75
|
+
}
|
|
76
|
+
return a.localeCompare(b);
|
|
77
|
+
});
|
|
78
|
+
return { groups, sortedProvs };
|
|
79
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createModelsService } from "../../../models.js";
|
|
3
|
+
import { createUpstreamClient } from "../../../upstream.js";
|
|
4
|
+
import { logDir } from "../../../logs.js";
|
|
5
|
+
import { loadModelErrors, loadModelPicks, saveModelPicks } from "../../../state.js";
|
|
6
|
+
import { getPreferredModel } from "../../../auto.js";
|
|
7
|
+
import { fmtShanghaiYMDHM } from "../../../time.js";
|
|
8
|
+
import { readModelsCache } from "../../util.js";
|
|
9
|
+
import { pickInteractiveMulti } from "../../interactive.js";
|
|
10
|
+
import { buildAliasMap, readFullAliases, renderProviderList, renderFreeList, groupByProvider } from "./list-render.js";
|
|
11
|
+
import { renderOtherProviders } from "./list-providers.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `-model list` 全流程:参数解析 → 刷新/回退 → provider 分支 → TTY 交互 → 分组渲染。
|
|
15
|
+
* 渲染下沉 list-render / list-providers,本文件只留流程编排。
|
|
16
|
+
*/
|
|
17
|
+
export async function handleModelList(args, idx, sub) {
|
|
18
|
+
let modelListProvider = null;
|
|
19
|
+
let modelListJson = false;
|
|
20
|
+
if (sub === "list") {
|
|
21
|
+
const restArgs = args.slice(idx + 2);
|
|
22
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
23
|
+
const a = String(restArgs[i] || "");
|
|
24
|
+
if (a === "--json" || a === "-json") modelListJson = true;
|
|
25
|
+
else if (a === "--provider" || a === "-provider" || a === "--providerId") { modelListProvider = String(restArgs[i + 1] || "").trim() || null; i++; }
|
|
26
|
+
else if (!a.startsWith("-") && !modelListProvider) modelListProvider = a;
|
|
27
|
+
}
|
|
28
|
+
if (modelListProvider) {
|
|
29
|
+
const { normalizeProviderId } = await import("../../../providers/model-id.js");
|
|
30
|
+
const nid = normalizeProviderId(modelListProvider);
|
|
31
|
+
modelListProvider = nid || modelListProvider.toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const cacheFile = join(logDir(), "models.json");
|
|
35
|
+
async function tryRefreshModels() {
|
|
36
|
+
try {
|
|
37
|
+
const models = createModelsService({
|
|
38
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
39
|
+
headers: createUpstreamClient({}).headers,
|
|
40
|
+
refreshMs: 0,
|
|
41
|
+
cacheFile,
|
|
42
|
+
});
|
|
43
|
+
const list = await Promise.race([
|
|
44
|
+
models.get(),
|
|
45
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
46
|
+
]);
|
|
47
|
+
return list;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
let ids = [];
|
|
54
|
+
let cachedAt = null;
|
|
55
|
+
let refreshed = null;
|
|
56
|
+
refreshed = await tryRefreshModels();
|
|
57
|
+
if (refreshed?.data) {
|
|
58
|
+
ids = (refreshed.data || []).map((m) => m.id).filter(Boolean);
|
|
59
|
+
cachedAt = refreshed.cachedAt || Date.now();
|
|
60
|
+
} else {
|
|
61
|
+
const cached = readModelsCache(cacheFile);
|
|
62
|
+
if (cached) {
|
|
63
|
+
ids = (cached.data || []).map((m) => m.id).filter(Boolean);
|
|
64
|
+
cachedAt = cached.cachedAt || null;
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error("no cached models and refresh failed");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (modelListProvider) {
|
|
70
|
+
const prov = String(modelListProvider).toLowerCase();
|
|
71
|
+
const filtered = ids.filter((id) => {
|
|
72
|
+
const slash = String(id).indexOf("/");
|
|
73
|
+
const p = slash > 0 ? String(id).slice(0, slash).toLowerCase() : "opencode";
|
|
74
|
+
return p === prov;
|
|
75
|
+
});
|
|
76
|
+
if (prov !== "opencode" && filtered.length === 0) {
|
|
77
|
+
try {
|
|
78
|
+
const { loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderBaseUrl } = await import("../../../state.js");
|
|
79
|
+
const { loadModelAliases, getAliasForModel } = await import("../../../providers/model-id.js");
|
|
80
|
+
try { loadModelAliases(); } catch {}
|
|
81
|
+
const allowed = loadProviderAllowedModels(prov);
|
|
82
|
+
const allowAny = loadProviderAllowAnyModels(prov);
|
|
83
|
+
const baseUrl = loadProviderBaseUrl(prov);
|
|
84
|
+
if (modelListJson) {
|
|
85
|
+
const data = allowed.length
|
|
86
|
+
? allowed.map((raw) => ({ id: `${prov}/${raw}`, object: "model" }))
|
|
87
|
+
: [];
|
|
88
|
+
console.log(JSON.stringify({ object: "list", data }, null, 2));
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
if (!allowed.length) {
|
|
92
|
+
if (allowAny) {
|
|
93
|
+
console.log(`provider "${prov}" allowAny ON (allowlist 空=放行全部)${baseUrl ? ` baseUrl=${baseUrl}` : ""}`);
|
|
94
|
+
console.log(` (未设 allowlist,全部模型放行) 查看 live 列表: mslxdff -provider ${prov} models`);
|
|
95
|
+
} else {
|
|
96
|
+
console.log(`no models for provider "${prov}" — allowlist 空 + allowAny OFF = 阻塞`);
|
|
97
|
+
console.log(` 设白名单: mslxdff -provider ${prov} allowlist set <model1> <model2> 或 mslxdff -provider ${prov} allowAny on`);
|
|
98
|
+
console.log(` live 查看: mslxdff -provider ${prov} models`);
|
|
99
|
+
}
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
const at2 = cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)})` : "";
|
|
103
|
+
console.log(`${allowed.length} model(s) for ${prov}${at2} (allowlist,原名 + 别名):`);
|
|
104
|
+
const pickedIds2 = loadModelPicks();
|
|
105
|
+
for (const raw of allowed) {
|
|
106
|
+
const canonical = `${prov}/${raw}`;
|
|
107
|
+
let alias = null;
|
|
108
|
+
try { alias = getAliasForModel(canonical); } catch {}
|
|
109
|
+
if (!alias && String(canonical).includes("/")) alias = String(canonical).replace(/\//g, "-");
|
|
110
|
+
const aliasStr = alias && alias !== canonical ? ` (别名: ${alias})` : "";
|
|
111
|
+
const mark2 = pickedIds2.includes(canonical) || (alias && pickedIds2.includes(alias)) ? "*" : " ";
|
|
112
|
+
console.log(` ${mark2} ${canonical}${aliasStr}`);
|
|
113
|
+
}
|
|
114
|
+
process.exit(0);
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
ids = filtered;
|
|
118
|
+
if (modelListJson) {
|
|
119
|
+
console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
if (!ids.length) {
|
|
123
|
+
console.log(`no models for provider "${prov}" — try: mslxdff -provider ${prov} models or mslxdff -model refresh`);
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
} else if (modelListJson) {
|
|
127
|
+
console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
if (!ids.length) {
|
|
131
|
+
console.log("no models available — try: mslxdff -model refresh");
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
if (sub === undefined && process.stdin.isTTY && process.stdout.isTTY) {
|
|
135
|
+
const statuses = loadModelErrors();
|
|
136
|
+
const current = getPreferredModel();
|
|
137
|
+
const pickedIds = loadModelPicks();
|
|
138
|
+
const combinedIds = [...ids];
|
|
139
|
+
const seen = new Set(combinedIds);
|
|
140
|
+
try {
|
|
141
|
+
const { loadProviderConfigs, loadProviderAllowedModels } = await import("../../../state.js");
|
|
142
|
+
const configs = loadProviderConfigs();
|
|
143
|
+
for (const pid of Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode")) {
|
|
144
|
+
const allowed = loadProviderAllowedModels(pid);
|
|
145
|
+
for (const raw of allowed) {
|
|
146
|
+
const canonical = `${pid}/${raw}`;
|
|
147
|
+
if (!seen.has(canonical)) {
|
|
148
|
+
seen.add(canonical);
|
|
149
|
+
combinedIds.push(canonical);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {}
|
|
154
|
+
for (const pid of pickedIds) {
|
|
155
|
+
if (!seen.has(pid)) {
|
|
156
|
+
seen.add(pid);
|
|
157
|
+
combinedIds.push(pid);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const items = combinedIds.map((id) => {
|
|
161
|
+
const e = statuses[id];
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
status: typeof e === "number" ? "error" : e?.status || "normal",
|
|
165
|
+
current: id === current,
|
|
166
|
+
picked: pickedIds.includes(id),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
const result = await pickInteractiveMulti(items, new Set(pickedIds), Math.max(0, items.findIndex((x) => x.current)));
|
|
170
|
+
if (!result) {
|
|
171
|
+
console.log("cancelled — picks unchanged");
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
saveModelPicks([...result]);
|
|
175
|
+
console.log(`saved ${result.size} picked model(s): ${[...result].join(", ") || "(none — auto uses full list)"}`);
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
const at = cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)})` : "";
|
|
179
|
+
const pickedIds = loadModelPicks();
|
|
180
|
+
const { groups, sortedProvs } = groupByProvider(ids);
|
|
181
|
+
const { loadModelAliases, getAliasForModel } = await import("../../../providers/model-id.js");
|
|
182
|
+
if (modelListProvider) {
|
|
183
|
+
let aliasMap = {};
|
|
184
|
+
try {
|
|
185
|
+
loadModelAliases();
|
|
186
|
+
aliasMap = buildAliasMap(ids, getAliasForModel);
|
|
187
|
+
} catch {}
|
|
188
|
+
renderProviderList({ ids, at, pickedIds, modelListProvider, sortedProvs, groups, aliasMap });
|
|
189
|
+
} else {
|
|
190
|
+
let aliasMap = {};
|
|
191
|
+
let fullAliases = {};
|
|
192
|
+
try {
|
|
193
|
+
loadModelAliases();
|
|
194
|
+
aliasMap = buildAliasMap(ids, getAliasForModel);
|
|
195
|
+
fullAliases = readFullAliases();
|
|
196
|
+
} catch {}
|
|
197
|
+
renderFreeList({ ids, at, pickedIds, sortedProvs, groups, aliasMap });
|
|
198
|
+
try {
|
|
199
|
+
await renderOtherProviders({ pickedIds, ids, fullAliases });
|
|
200
|
+
} catch {}
|
|
201
|
+
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error(`could not fetch models: ${String(err?.message || err)}`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { savePreferredModel, loadModelPicks, saveModelPicks } from "../../../state.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 勾选集分支:set / pick / unpick / picks / pick clear。
|
|
5
|
+
* 命中返回 true(分支内 process.exit,不会实际返回);未命中返回 false。
|
|
6
|
+
*/
|
|
7
|
+
export async function handlePicksCommand(args, idx, sub) {
|
|
8
|
+
if (sub === "set" && args[idx + 2]) {
|
|
9
|
+
const id = args[idx + 2];
|
|
10
|
+
savePreferredModel(id);
|
|
11
|
+
const picks = [...new Set([...loadModelPicks(), id])];
|
|
12
|
+
saveModelPicks(picks);
|
|
13
|
+
console.log(`default model set to: ${id} (daemon hot-reloads on next request)`);
|
|
14
|
+
console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
if (sub === "pick" && args[idx + 2] && args[idx + 2] !== "clear") {
|
|
18
|
+
const picks = [...new Set([...loadModelPicks(), args[idx + 2]])];
|
|
19
|
+
saveModelPicks(picks);
|
|
20
|
+
console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
if (sub === "pick" && args[idx + 2] === "clear") {
|
|
24
|
+
saveModelPicks([]);
|
|
25
|
+
console.log("picks cleared — auto uses the full model list again");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
if (sub === "unpick" && args[idx + 2]) {
|
|
29
|
+
const picks = loadModelPicks().filter((x) => x !== args[idx + 2]);
|
|
30
|
+
saveModelPicks(picks);
|
|
31
|
+
console.log(`picked: ${picks.join(", ") || "(none)"}${picks.length === 0 ? " (auto uses full list)" : ""}`);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
if (sub === "picks") {
|
|
35
|
+
const picks = loadModelPicks();
|
|
36
|
+
if (!picks.length) {
|
|
37
|
+
console.log("no picks — auto uses the full model list");
|
|
38
|
+
} else {
|
|
39
|
+
console.log(`${picks.length} picked model(s), auto only selects within these:`);
|
|
40
|
+
}
|
|
41
|
+
for (const id of picks) console.log(` ${id}`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { logDir } from "../../../logs.js";
|
|
3
|
+
|
|
4
|
+
/** `-model stats` 监控表(精简版 -status,只看有样本模型) */
|
|
5
|
+
export async function handleModelStats(args) {
|
|
6
|
+
const stats = (await import("../../../state.js")).loadModelStats();
|
|
7
|
+
const errors = (await import("../../../state.js")).loadModelErrors();
|
|
8
|
+
const picks = (await import("../../../state.js")).loadModelPicks();
|
|
9
|
+
const ids = Object.keys(stats);
|
|
10
|
+
if (!ids.length) {
|
|
11
|
+
console.log("暂无样本 — 先经 8989 发请求(mslxdff -chat hi 或 curl auto),100次后均值更稳");
|
|
12
|
+
console.log("提示:mslxdff -status 看全量体检,mslxdff -provider <id> bench 看主动测速");
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
const showAll = args.includes("--all");
|
|
16
|
+
let list = ids.map((id) => ({ id, s: stats[id] }));
|
|
17
|
+
if (!showAll) {
|
|
18
|
+
// 默认只看正在用:free 或 picks 里的
|
|
19
|
+
try {
|
|
20
|
+
const cached = (await import("../../util.js")).readModelsCache(join(logDir(), "models.json"));
|
|
21
|
+
const freeSet = new Set((cached?.data || []).map((m) => m.id));
|
|
22
|
+
const pickSet = new Set(picks);
|
|
23
|
+
const filtered = list.filter(({ id }) => freeSet.has(id) || freeSet.has(id.replace(/^opencode\//, "")) || pickSet.has(id));
|
|
24
|
+
if (filtered.length) list = filtered;
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
list.sort((a, b) => (b.s.count - a.s.count) || (b.s.lastAt - a.s.lastAt));
|
|
28
|
+
const fmtMs = (v) => v == null || !Number.isFinite(v) ? "—" : v < 1000 ? `${v}ms` : `${(v / 1000).toFixed(1)}s`;
|
|
29
|
+
const fmtTps = (v) => v == null || !Number.isFinite(v) ? "—" : `${v} tok/s`;
|
|
30
|
+
console.log(`模型监控(${list.length} 个,样本>0 按次数) — mslxdff -model stats --all 看全部`);
|
|
31
|
+
console.log(` ${"模型".padEnd(30)} ${"请求".padEnd(6)} ${"成功".padEnd(6)} ${"首字".padEnd(8)} ${"总耗时".padEnd(8)} ${"速度".padEnd(12)} 状态`);
|
|
32
|
+
for (const { id, s } of list.slice(0, 20)) {
|
|
33
|
+
const e = errors[id] || errors[id.replace(/^opencode\//, "")];
|
|
34
|
+
const status = e ? (typeof e === "number" ? "error" : e.status || "error") : "normal";
|
|
35
|
+
const ttfb = fmtMs(s.avgTtfbMs ?? s.emaTtfbMs);
|
|
36
|
+
const total = fmtMs(s.avgTotalMs ?? s.emaTotalMs);
|
|
37
|
+
const tps = fmtTps(s.avgTps ?? s.emaTps);
|
|
38
|
+
console.log(` ${id.padEnd(30)} ${String(s.count).padEnd(6)} ${String(s.count).padEnd(6)} ${ttfb.padEnd(8)} ${total.padEnd(8)} ${tps.padEnd(12)} ${status}`);
|
|
39
|
+
}
|
|
40
|
+
if (list.length > 20) console.log(` … 还有 ${list.length - 20} 个`);
|
|
41
|
+
console.log(`\n提示:失败次数看 mslxdff -model status;实时单条看 mslxdff -log 20`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { logDir } from "../../../logs.js";
|
|
3
|
+
import { loadModelErrors, loadModelPicks } from "../../../state.js";
|
|
4
|
+
import { fmtShanghaiYMDHM } from "../../../time.js";
|
|
5
|
+
import { readModelsCache } from "../../util.js";
|
|
6
|
+
|
|
7
|
+
/** `-model status` 健康表 + 孤儿隐藏(--all 看全部) */
|
|
8
|
+
export async function handleModelStatus(args) {
|
|
9
|
+
const statuses = loadModelErrors();
|
|
10
|
+
const cacheFile = join(logDir(), "models.json");
|
|
11
|
+
const cached = readModelsCache(cacheFile);
|
|
12
|
+
const showAll = args.includes("--all") || args.includes("-all") || args.includes("--orphans");
|
|
13
|
+
const freeIds = new Set((cached?.data || []).map((m) => m.id));
|
|
14
|
+
const picks = new Set(loadModelPicks());
|
|
15
|
+
const ids = new Set([...freeIds]);
|
|
16
|
+
if (showAll) {
|
|
17
|
+
for (const k of Object.keys(statuses)) ids.add(k);
|
|
18
|
+
for (const k of picks) ids.add(k);
|
|
19
|
+
} else {
|
|
20
|
+
// 默认只看正在用的:free 列表内,且(被 picks 钉住 或 近期有错误/在用)
|
|
21
|
+
// 不在 free 里的孤儿直接隐藏,需 --all 才看
|
|
22
|
+
for (const k of Object.keys(statuses)) if (freeIds.has(k)) ids.add(k);
|
|
23
|
+
for (const k of picks) if (freeIds.has(k)) ids.add(k);
|
|
24
|
+
if (!ids.size && freeIds.size === 0) {
|
|
25
|
+
// 无缓存时回退显示有状态的,避免空屏
|
|
26
|
+
for (const k of Object.keys(statuses)) ids.add(k);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!ids.size) {
|
|
30
|
+
console.log(showAll ? "no models (free + orphans) — try: mslxdff -model refresh" : "no free models — try: mslxdff -model refresh (use --all to see orphans)");
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
for (const id of ids) {
|
|
34
|
+
const e = statuses[id];
|
|
35
|
+
const st = typeof e === "number" ? "error" : e?.status || "normal";
|
|
36
|
+
const at = typeof e === "number" ? e : e?.at;
|
|
37
|
+
const when = at ? ` (${fmtShanghaiYMDHM ? fmtShanghaiYMDHM(at) : at})` : "";
|
|
38
|
+
const extra = e?.code ? ` HTTP ${e.code}` : "";
|
|
39
|
+
const orphanMark = !freeIds.has(id) ? " [orphan]" : "";
|
|
40
|
+
console.log(` ${id} ${st}${when}${extra}${orphanMark}`);
|
|
41
|
+
}
|
|
42
|
+
if (!showAll) {
|
|
43
|
+
const orphans = [...new Set([...Object.keys(statuses), ...picks])].filter((x) => !freeIds.has(x));
|
|
44
|
+
if (orphans.length) console.log(`\n(已隐藏 ${orphans.length} 个不在用/已下线模型,需查看: mslxdff -model status --all)`);
|
|
45
|
+
}
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|