mslxdff 0.1.88 → 0.1.90
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/bench/cline-bench.js +21 -33
- package/src/bench/probe.js +4 -11
- package/src/bench/runner.js +8 -23
- package/src/bench/via-probe.js +12 -30
- package/src/bench/workbuddy-bench.js +15 -31
- 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 +17 -0
- package/src/chat-pipeline/index.js +86 -0
- package/src/chat-pipeline/planner.js +31 -0
- package/src/chat-pipeline/policy.js +73 -0
- 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 +26 -53
- package/src/providers/workbuddy/chat.js +109 -221
- package/src/routes/chat/gateway.js +23 -286
- package/src/routes/chat/relay-pipeline.js +26 -0
- package/src/routes/groups-relay.js +85 -1
- package/src/routes/index.js +7 -1
- package/src/routes/relay-queue.js +53 -0
- package/src/runtime/bootstrap.js +14 -387
- 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/transport/index.js +248 -0
- package/src/transport/pool.js +60 -0
- package/src/transport/retry.js +24 -0
- package/src/transport/sse.js +93 -0
- package/src/upstream-responses.js +64 -0
- package/src/upstream.js +106 -334
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
2
1
|
import { join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
2
|
import { createModelsService } from "../../models.js";
|
|
5
3
|
import { createUpstreamClient } from "../../upstream.js";
|
|
6
4
|
import { logDir } from "../../logs.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { pickInteractiveMulti } from "../interactive.js";
|
|
5
|
+
import { handleModelStatus } from "./model/status.js";
|
|
6
|
+
import { handleModelStats } from "./model/stats.js";
|
|
7
|
+
import { handlePicksCommand } from "./model/picks.js";
|
|
8
|
+
import { handleModelList } from "./model/list.js";
|
|
12
9
|
|
|
10
|
+
/**
|
|
11
|
+
* `-model` 门面 — 仅分发:refresh 直连,status/stats/picks/list 下沉子模块。
|
|
12
|
+
* 对外接缝 `handleModel(args)` 不变(src/cli/index.js 动态 import)。
|
|
13
|
+
*/
|
|
13
14
|
export async function handleModel(args) {
|
|
14
15
|
if (!(args.includes("-model") || args.includes("-models"))) return false;
|
|
15
16
|
const idx = args.findIndex((x) => x === "-model" || x === "-models");
|
|
@@ -33,373 +34,18 @@ export async function handleModel(args) {
|
|
|
33
34
|
process.exit(0);
|
|
34
35
|
}
|
|
35
36
|
if (sub === "status") {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const cached = readModelsCache(cacheFile);
|
|
39
|
-
const ids = new Set([
|
|
40
|
-
...(cached?.data || []).map((m) => m.id),
|
|
41
|
-
...Object.keys(statuses),
|
|
42
|
-
]);
|
|
43
|
-
for (const id of ids) {
|
|
44
|
-
const e = statuses[id];
|
|
45
|
-
const st = typeof e === "number" ? "error" : e?.status || "normal";
|
|
46
|
-
const at = typeof e === "number" ? e : e?.at;
|
|
47
|
-
const when = at ? ` (${fmtShanghaiYMDHM ? fmtShanghaiYMDHM(at) : at})` : "";
|
|
48
|
-
const extra = e?.code ? ` HTTP ${e.code}` : "";
|
|
49
|
-
console.log(` ${id} ${st}${when}${extra}`);
|
|
50
|
-
}
|
|
51
|
-
process.exit(0);
|
|
52
|
-
}
|
|
53
|
-
if (sub === "set" && args[idx + 2]) {
|
|
54
|
-
const id = args[idx + 2];
|
|
55
|
-
savePreferredModel(id);
|
|
56
|
-
const picks = [...new Set([...loadModelPicks(), id])];
|
|
57
|
-
saveModelPicks(picks);
|
|
58
|
-
console.log(`default model set to: ${id} (daemon hot-reloads on next request)`);
|
|
59
|
-
console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
|
|
60
|
-
process.exit(0);
|
|
61
|
-
}
|
|
62
|
-
if (sub === "pick" && args[idx + 2] && args[idx + 2] !== "clear") {
|
|
63
|
-
const picks = [...new Set([...loadModelPicks(), args[idx + 2]])];
|
|
64
|
-
saveModelPicks(picks);
|
|
65
|
-
console.log(`picked: ${picks.join(", ") || "(none)"} (auto will pick within these)`);
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
if (sub === "pick" && args[idx + 2] === "clear") {
|
|
69
|
-
saveModelPicks([]);
|
|
70
|
-
console.log("picks cleared — auto uses the full model list again");
|
|
71
|
-
process.exit(0);
|
|
37
|
+
await handleModelStatus(args);
|
|
38
|
+
return true;
|
|
72
39
|
}
|
|
73
|
-
if (sub === "
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
console.log(`picked: ${picks.join(", ") || "(none)"}${picks.length === 0 ? " (auto uses full list)" : ""}`);
|
|
77
|
-
process.exit(0);
|
|
78
|
-
}
|
|
79
|
-
if (sub === "picks") {
|
|
80
|
-
const picks = loadModelPicks();
|
|
81
|
-
if (!picks.length) {
|
|
82
|
-
console.log("no picks — auto uses the full model list");
|
|
83
|
-
} else {
|
|
84
|
-
console.log(`${picks.length} picked model(s), auto only selects within these:`);
|
|
85
|
-
}
|
|
86
|
-
for (const id of picks) console.log(` ${id}`);
|
|
87
|
-
process.exit(0);
|
|
88
|
-
}
|
|
89
|
-
let modelListProvider = null;
|
|
90
|
-
let modelListJson = false;
|
|
91
|
-
if (sub === "list") {
|
|
92
|
-
const restArgs = args.slice(idx + 2);
|
|
93
|
-
for (let i = 0; i < restArgs.length; i++) {
|
|
94
|
-
const a = String(restArgs[i] || "");
|
|
95
|
-
if (a === "--json" || a === "-json") modelListJson = true;
|
|
96
|
-
else if (a === "--provider" || a === "-provider" || a === "--providerId") { modelListProvider = String(restArgs[i + 1] || "").trim() || null; i++; }
|
|
97
|
-
else if (!a.startsWith("-") && !modelListProvider) modelListProvider = a;
|
|
98
|
-
}
|
|
99
|
-
if (modelListProvider) {
|
|
100
|
-
const { normalizeProviderId } = await import("../../providers/model-id.js");
|
|
101
|
-
const nid = normalizeProviderId(modelListProvider);
|
|
102
|
-
modelListProvider = nid || modelListProvider.toLowerCase();
|
|
103
|
-
}
|
|
40
|
+
if (sub === "stats") {
|
|
41
|
+
await handleModelStats(args);
|
|
42
|
+
return true;
|
|
104
43
|
}
|
|
44
|
+
if (await handlePicksCommand(args, idx, sub)) return true;
|
|
105
45
|
if (sub !== undefined && sub !== "list") {
|
|
106
|
-
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");
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
const cacheFile = join(logDir(), "models.json");
|
|
110
|
-
async function tryRefreshModels() {
|
|
111
|
-
try {
|
|
112
|
-
const models = createModelsService({
|
|
113
|
-
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
114
|
-
headers: createUpstreamClient({}).headers,
|
|
115
|
-
refreshMs: 0,
|
|
116
|
-
cacheFile,
|
|
117
|
-
});
|
|
118
|
-
const list = await Promise.race([
|
|
119
|
-
models.get(),
|
|
120
|
-
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
121
|
-
]);
|
|
122
|
-
return list;
|
|
123
|
-
} catch {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
try {
|
|
128
|
-
let ids = [];
|
|
129
|
-
let cachedAt = null;
|
|
130
|
-
let refreshed = null;
|
|
131
|
-
refreshed = await tryRefreshModels();
|
|
132
|
-
if (refreshed?.data) {
|
|
133
|
-
ids = (refreshed.data || []).map((m) => m.id).filter(Boolean);
|
|
134
|
-
cachedAt = refreshed.cachedAt || Date.now();
|
|
135
|
-
} else {
|
|
136
|
-
const cached = readModelsCache(cacheFile);
|
|
137
|
-
if (cached) {
|
|
138
|
-
ids = (cached.data || []).map((m) => m.id).filter(Boolean);
|
|
139
|
-
cachedAt = cached.cachedAt || null;
|
|
140
|
-
} else {
|
|
141
|
-
throw new Error("no cached models and refresh failed");
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (modelListProvider) {
|
|
145
|
-
const prov = String(modelListProvider).toLowerCase();
|
|
146
|
-
const filtered = ids.filter((id) => {
|
|
147
|
-
const slash = String(id).indexOf("/");
|
|
148
|
-
const p = slash > 0 ? String(id).slice(0, slash).toLowerCase() : "opencode";
|
|
149
|
-
return p === prov;
|
|
150
|
-
});
|
|
151
|
-
if (prov !== "opencode" && filtered.length === 0) {
|
|
152
|
-
try {
|
|
153
|
-
const { loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderBaseUrl } = await import("../../state.js");
|
|
154
|
-
const { loadModelAliases, getAliasForModel } = await import("../../providers/model-id.js");
|
|
155
|
-
try { loadModelAliases(); } catch {}
|
|
156
|
-
const allowed = loadProviderAllowedModels(prov);
|
|
157
|
-
const allowAny = loadProviderAllowAnyModels(prov);
|
|
158
|
-
const baseUrl = loadProviderBaseUrl(prov);
|
|
159
|
-
if (modelListJson) {
|
|
160
|
-
const data = allowed.length
|
|
161
|
-
? allowed.map((raw) => ({ id: `${prov}/${raw}`, object: "model" }))
|
|
162
|
-
: [];
|
|
163
|
-
console.log(JSON.stringify({ object: "list", data }, null, 2));
|
|
164
|
-
process.exit(0);
|
|
165
|
-
}
|
|
166
|
-
if (!allowed.length) {
|
|
167
|
-
if (allowAny) {
|
|
168
|
-
console.log(`provider "${prov}" allowAny ON (allowlist 空=放行全部)${baseUrl ? ` baseUrl=${baseUrl}` : ""}`);
|
|
169
|
-
console.log(` (未设 allowlist,全部模型放行) 查看 live 列表: mslxdff -provider ${prov} models`);
|
|
170
|
-
} else {
|
|
171
|
-
console.log(`no models for provider "${prov}" — allowlist 空 + allowAny OFF = 阻塞`);
|
|
172
|
-
console.log(` 设白名单: mslxdff -provider ${prov} allowlist set <model1> <model2> 或 mslxdff -provider ${prov} allowAny on`);
|
|
173
|
-
console.log(` live 查看: mslxdff -provider ${prov} models`);
|
|
174
|
-
}
|
|
175
|
-
process.exit(0);
|
|
176
|
-
}
|
|
177
|
-
const at2 = cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)})` : "";
|
|
178
|
-
console.log(`${allowed.length} model(s) for ${prov}${at2} (allowlist,原名 + 别名):`);
|
|
179
|
-
const pickedIds2 = loadModelPicks();
|
|
180
|
-
for (const raw of allowed) {
|
|
181
|
-
const canonical = `${prov}/${raw}`;
|
|
182
|
-
let alias = null;
|
|
183
|
-
try { alias = getAliasForModel(canonical); } catch {}
|
|
184
|
-
if (!alias && String(canonical).includes("/")) alias = String(canonical).replace(/\//g, "-");
|
|
185
|
-
const aliasStr = alias && alias !== canonical ? ` (别名: ${alias})` : "";
|
|
186
|
-
const mark2 = pickedIds2.includes(canonical) || (alias && pickedIds2.includes(alias)) ? "*" : " ";
|
|
187
|
-
console.log(` ${mark2} ${canonical}${aliasStr}`);
|
|
188
|
-
}
|
|
189
|
-
process.exit(0);
|
|
190
|
-
} catch {}
|
|
191
|
-
}
|
|
192
|
-
ids = filtered;
|
|
193
|
-
if (modelListJson) {
|
|
194
|
-
console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
|
|
195
|
-
process.exit(0);
|
|
196
|
-
}
|
|
197
|
-
if (!ids.length) {
|
|
198
|
-
console.log(`no models for provider "${prov}" — try: mslxdff -provider ${prov} models or mslxdff -model refresh`);
|
|
199
|
-
process.exit(0);
|
|
200
|
-
}
|
|
201
|
-
} else if (modelListJson) {
|
|
202
|
-
console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
|
|
203
|
-
process.exit(0);
|
|
204
|
-
}
|
|
205
|
-
if (!ids.length) {
|
|
206
|
-
console.log("no models available — try: mslxdff -model refresh");
|
|
207
|
-
process.exit(0);
|
|
208
|
-
}
|
|
209
|
-
if (sub === undefined && process.stdin.isTTY && process.stdout.isTTY) {
|
|
210
|
-
const statuses = loadModelErrors();
|
|
211
|
-
const current = getPreferredModel();
|
|
212
|
-
const pickedIds = loadModelPicks();
|
|
213
|
-
const combinedIds = [...ids];
|
|
214
|
-
const seen = new Set(combinedIds);
|
|
215
|
-
try {
|
|
216
|
-
const { loadProviderConfigs, loadProviderAllowedModels } = await import("../../state.js");
|
|
217
|
-
const configs = loadProviderConfigs();
|
|
218
|
-
for (const pid of Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode")) {
|
|
219
|
-
const allowed = loadProviderAllowedModels(pid);
|
|
220
|
-
for (const raw of allowed) {
|
|
221
|
-
const canonical = `${pid}/${raw}`;
|
|
222
|
-
if (!seen.has(canonical)) {
|
|
223
|
-
seen.add(canonical);
|
|
224
|
-
combinedIds.push(canonical);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} catch {}
|
|
229
|
-
for (const pid of pickedIds) {
|
|
230
|
-
if (!seen.has(pid)) {
|
|
231
|
-
seen.add(pid);
|
|
232
|
-
combinedIds.push(pid);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
const items = combinedIds.map((id) => {
|
|
236
|
-
const e = statuses[id];
|
|
237
|
-
return {
|
|
238
|
-
id,
|
|
239
|
-
status: typeof e === "number" ? "error" : e?.status || "normal",
|
|
240
|
-
current: id === current,
|
|
241
|
-
picked: pickedIds.includes(id),
|
|
242
|
-
};
|
|
243
|
-
});
|
|
244
|
-
const result = await pickInteractiveMulti(items, new Set(pickedIds), Math.max(0, items.findIndex((x) => x.current)));
|
|
245
|
-
if (!result) {
|
|
246
|
-
console.log("cancelled — picks unchanged");
|
|
247
|
-
process.exit(0);
|
|
248
|
-
}
|
|
249
|
-
saveModelPicks([...result]);
|
|
250
|
-
console.log(`saved ${result.size} picked model(s): ${[...result].join(", ") || "(none — auto uses full list)"}`);
|
|
251
|
-
process.exit(0);
|
|
252
|
-
}
|
|
253
|
-
const at = cachedAt ? ` (cached ${fmtShanghaiYMDHM(cachedAt)})` : "";
|
|
254
|
-
const pickedIds = loadModelPicks();
|
|
255
|
-
const mark = (id) => (pickedIds.includes(id) ? "*" : " ");
|
|
256
|
-
const groups = {};
|
|
257
|
-
for (const id of ids) {
|
|
258
|
-
const prov = String(id).includes("/") ? String(id).split("/")[0] : "opencode";
|
|
259
|
-
if (!groups[prov]) groups[prov] = [];
|
|
260
|
-
groups[prov].push(id);
|
|
261
|
-
}
|
|
262
|
-
const order = ["opencode", "workbuddy", "clinebot", "openrouter"];
|
|
263
|
-
const sortedProvs = Object.keys(groups).sort((a, b) => {
|
|
264
|
-
const ia = order.indexOf(a), ib = order.indexOf(b);
|
|
265
|
-
if (ia !== -1 || ib !== -1) {
|
|
266
|
-
if (ia === -1) return 1;
|
|
267
|
-
if (ib === -1) return -1;
|
|
268
|
-
return ia - ib;
|
|
269
|
-
}
|
|
270
|
-
return a.localeCompare(b);
|
|
271
|
-
});
|
|
272
|
-
if (modelListProvider) {
|
|
273
|
-
console.log(`${ids.length} model(s) for ${modelListProvider}${at} (${pickedIds.length} picked, * = picked):`);
|
|
274
|
-
let aliasMap = {};
|
|
275
|
-
try {
|
|
276
|
-
const { loadModelAliases, getAliasForModel } = await import("../../providers/model-id.js");
|
|
277
|
-
loadModelAliases();
|
|
278
|
-
for (const id of ids) {
|
|
279
|
-
const alias = getAliasForModel(id);
|
|
280
|
-
if (alias) aliasMap[id] = alias;
|
|
281
|
-
else if (String(id).includes("/")) {
|
|
282
|
-
const dashAlias = String(id).replace(/\//g, "-");
|
|
283
|
-
if (dashAlias !== id) aliasMap[id] = dashAlias;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
} catch {}
|
|
287
|
-
for (const prov of sortedProvs) {
|
|
288
|
-
const list = groups[prov];
|
|
289
|
-
console.log(`\n ── ${prov} (${list.length}) ──`);
|
|
290
|
-
for (const id of list) {
|
|
291
|
-
const alias = aliasMap[id];
|
|
292
|
-
const aliasStr = alias ? ` (别名: ${alias})` : "";
|
|
293
|
-
console.log(` ${mark(id)} ${id}${aliasStr}`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
297
|
-
} else {
|
|
298
|
-
console.log(`${ids.length} free model(s)${at} (${pickedIds.length} picked, * = picked):`);
|
|
299
|
-
let aliasMap = {};
|
|
300
|
-
let fullAliases = {};
|
|
301
|
-
try {
|
|
302
|
-
const { loadModelAliases, getAliasForModel } = await import("../../providers/model-id.js");
|
|
303
|
-
loadModelAliases();
|
|
304
|
-
for (const id of ids) {
|
|
305
|
-
const alias = getAliasForModel(id);
|
|
306
|
-
if (alias) aliasMap[id] = alias;
|
|
307
|
-
else if (String(id).includes("/")) {
|
|
308
|
-
const dashAlias = String(id).replace(/\//g, "-");
|
|
309
|
-
if (dashAlias !== id) aliasMap[id] = dashAlias;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
const aliasesFile = join(homedir(), ".config", "mslxdff", "model-aliases.json");
|
|
314
|
-
const raw = JSON.parse(readFileSync(aliasesFile, "utf8"));
|
|
315
|
-
if (raw && typeof raw === "object") fullAliases = raw;
|
|
316
|
-
} catch {}
|
|
317
|
-
} catch {}
|
|
318
|
-
for (const prov of sortedProvs) {
|
|
319
|
-
const list = groups[prov];
|
|
320
|
-
console.log(`\n ── ${prov} (${list.length}) ──`);
|
|
321
|
-
for (const id of list) {
|
|
322
|
-
const alias = aliasMap[id];
|
|
323
|
-
const aliasStr = alias ? ` (别名: ${alias})` : "";
|
|
324
|
-
console.log(` ${mark(id)} ${id}${aliasStr}`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
try {
|
|
328
|
-
const { loadProviderConfigs, loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderBaseUrl } = await import("../../state.js");
|
|
329
|
-
const { loadModelAliases: _la2, getAliasForModel: _gaf } = await import("../../providers/model-id.js");
|
|
330
|
-
try { _la2(); } catch {}
|
|
331
|
-
const configs = loadProviderConfigs();
|
|
332
|
-
const otherIds = Object.keys(configs).filter((k) => String(k).toLowerCase() !== "opencode");
|
|
333
|
-
const order2 = ["workbuddy", "clinebot", "openrouter", "bai"];
|
|
334
|
-
otherIds.sort((a, b) => {
|
|
335
|
-
const ia = order2.indexOf(a), ib = order2.indexOf(b);
|
|
336
|
-
if (ia !== -1 || ib !== -1) {
|
|
337
|
-
if (ia === -1) return 1;
|
|
338
|
-
if (ib === -1) return -1;
|
|
339
|
-
return ia - ib;
|
|
340
|
-
}
|
|
341
|
-
return a.localeCompare(b);
|
|
342
|
-
});
|
|
343
|
-
if (otherIds.length) {
|
|
344
|
-
console.log(`\n────────────────────────────────────────`);
|
|
345
|
-
console.log(`其他供应商 (allowlist,原名 + 别名) (${otherIds.length} providers):`);
|
|
346
|
-
for (const pid of otherIds) {
|
|
347
|
-
const allowed = loadProviderAllowedModels(pid);
|
|
348
|
-
const allowAny = loadProviderAllowAnyModels(pid);
|
|
349
|
-
const baseUrl = loadProviderBaseUrl(pid) || configs[pid]?.baseUrl || "";
|
|
350
|
-
const header = allowAny
|
|
351
|
-
? (allowed.length ? `allowlist ${allowed.length} (allowAny ON)` : `allowAny ON (allowlist 空=放行全部)`)
|
|
352
|
-
: (allowed.length ? `allowlist ${allowed.length} (allowAny OFF)` : `allowlist 空 + allowAny OFF = 阻塞`);
|
|
353
|
-
console.log(`\n ── ${pid} (${header})${baseUrl ? ` baseUrl=${baseUrl}` : ""} ──`);
|
|
354
|
-
if (!allowed.length) {
|
|
355
|
-
if (allowAny) {
|
|
356
|
-
console.log(` (未设 allowlist,全部模型放行) 查看 live 列表: mslxdff -provider ${pid} models`);
|
|
357
|
-
console.log(` 限制可用模型: mslxdff -provider ${pid} allowlist set <model1> <model2>`);
|
|
358
|
-
} else {
|
|
359
|
-
console.log(` 阻塞中:无可用模型 — 设白名单: mslxdff -provider ${pid} allowlist set <model1> <model2>`);
|
|
360
|
-
console.log(` 或放行全部: mslxdff -provider ${pid} allowAny on`);
|
|
361
|
-
}
|
|
362
|
-
} else {
|
|
363
|
-
for (const raw of allowed) {
|
|
364
|
-
const canonical = `${pid}/${raw}`;
|
|
365
|
-
let alias = null;
|
|
366
|
-
try { alias = _gaf(canonical); } catch {}
|
|
367
|
-
if (!alias && String(canonical).includes("/")) alias = String(canonical).replace(/\//g, "-");
|
|
368
|
-
const aliasStr = alias && alias !== canonical ? ` (别名: ${alias})` : "";
|
|
369
|
-
const pickedMark = pickedIds.includes(canonical) || pickedIds.includes(alias || "") ? "*" : " ";
|
|
370
|
-
console.log(` ${pickedMark} ${canonical}${aliasStr}`);
|
|
371
|
-
}
|
|
372
|
-
console.log(` 管理: mslxdff -provider ${pid} allowlist [list|add|remove|clear] | allowAny on|off`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
} else {
|
|
376
|
-
console.log(`\n────────────────────────────────────────`);
|
|
377
|
-
console.log(`其他供应商 (allowlist,原名 + 别名): (none — 尚未配置)`);
|
|
378
|
-
console.log(` 添加示例: mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models`);
|
|
379
|
-
}
|
|
380
|
-
const aliasEntries = Object.entries(fullAliases).filter(([alias, canonical]) => {
|
|
381
|
-
if (ids.includes(canonical)) return false;
|
|
382
|
-
for (const pid of otherIds) {
|
|
383
|
-
const allowed = loadProviderAllowedModels(pid);
|
|
384
|
-
for (const raw of allowed) {
|
|
385
|
-
const can = `${pid}/${raw}`;
|
|
386
|
-
if (can === canonical) return false;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return true;
|
|
390
|
-
});
|
|
391
|
-
if (aliasEntries.length) {
|
|
392
|
-
console.log(`\n 本地别名 (不在 allowlist 里的遗留映射 ${aliasEntries.length}):`);
|
|
393
|
-
for (const [alias, canonical] of aliasEntries) {
|
|
394
|
-
console.log(` ${canonical} => ${alias}`);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
} catch {}
|
|
398
|
-
console.log(`\npicked only constrains auto; manage with: mslxdff -models (TTY) | mslxdff -model pick <id> | mslxdff -model unpick <id> | mslxdff -model pick clear`);
|
|
399
|
-
}
|
|
400
|
-
} catch (err) {
|
|
401
|
-
console.error(`could not fetch models: ${String(err?.message || err)}`);
|
|
46
|
+
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 [--all] | mslxdff -model stats [--all] | mslxdff -model refresh");
|
|
402
47
|
process.exit(1);
|
|
403
48
|
}
|
|
404
|
-
|
|
49
|
+
await handleModelList(args, idx, sub);
|
|
50
|
+
return true;
|
|
405
51
|
}
|
package/src/cli/help.js
CHANGED
|
@@ -9,7 +9,8 @@ Usage:
|
|
|
9
9
|
mslxdff -models interactive picker: ↑/↓ select a model, Enter sets it as the default (non-TTY: plain list)
|
|
10
10
|
mslxdff -model list list the free models this proxy serves (cached)
|
|
11
11
|
mslxdff -model set <id> set the default (preferred) model without the interactive picker
|
|
12
|
-
mslxdff -model status
|
|
12
|
+
mslxdff -model status [--all] show per-model health status (normal/limit/error, 默认隐藏孤儿 --all 看全部)
|
|
13
|
+
mslxdff -model stats [--all] 监控:每模型 请求/首字/总耗时/速度 样本(--all 含孤儿,精简版 -status)
|
|
13
14
|
mslxdff -model refresh force-refresh the model cache from the upstream
|
|
14
15
|
mslxdff -debug live-follow the daemon event stream (requests, errors, peer forwards)
|
|
15
16
|
mslxdff -stop stop the running daemon
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { joinUrl, sleep } from "../base.js";
|
|
2
2
|
import { clineHeaders } from "./headers.js";
|
|
3
|
+
import { createTransport } from "../../transport/index.js";
|
|
3
4
|
|
|
4
5
|
function genSessionId() { return `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; }
|
|
5
6
|
|
|
@@ -12,40 +13,27 @@ function unwrapData(obj) {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
async function streamToNonStream(upstream) {
|
|
15
|
-
const reader = upstream.body.getReader();
|
|
16
|
-
const decoder = new TextDecoder();
|
|
17
|
-
let buf = "";
|
|
18
16
|
let content = "";
|
|
19
17
|
let reasoning = "";
|
|
20
18
|
let finishReason = null;
|
|
21
19
|
let model = "";
|
|
22
20
|
let id = "";
|
|
23
21
|
let usage = null;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!choice) continue;
|
|
40
|
-
const delta = choice.delta || {};
|
|
41
|
-
if (delta.content) content += delta.content;
|
|
42
|
-
if (delta.reasoning) reasoning += delta.reasoning;
|
|
43
|
-
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
44
|
-
if (normalized.id) id = normalized.id;
|
|
45
|
-
if (normalized.model) model = normalized.model;
|
|
46
|
-
if (normalized.usage) usage = normalized.usage;
|
|
47
|
-
} catch {}
|
|
48
|
-
}
|
|
22
|
+
for await (const ev of upstream.stream()) {
|
|
23
|
+
if (!ev || ev === "[DONE]") continue;
|
|
24
|
+
try {
|
|
25
|
+
const obj = JSON.parse(ev);
|
|
26
|
+
const normalized = unwrapData(obj);
|
|
27
|
+
const choice = normalized?.choices?.[0];
|
|
28
|
+
if (!choice) continue;
|
|
29
|
+
const delta = choice.delta || {};
|
|
30
|
+
if (delta.content) content += delta.content;
|
|
31
|
+
if (delta.reasoning) reasoning += delta.reasoning;
|
|
32
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
33
|
+
if (normalized.id) id = normalized.id;
|
|
34
|
+
if (normalized.model) model = normalized.model;
|
|
35
|
+
if (normalized.usage) usage = normalized.usage;
|
|
36
|
+
} catch {}
|
|
49
37
|
}
|
|
50
38
|
const msg = { role: "assistant", content };
|
|
51
39
|
if (reasoning) msg.reasoning = reasoning;
|
|
@@ -68,42 +56,34 @@ export function createChatService({
|
|
|
68
56
|
dispatcher,
|
|
69
57
|
authPool,
|
|
70
58
|
connectTimeoutMs = 30_000,
|
|
71
|
-
retry = { network: { attempts: 2, delayMs: 300 } },
|
|
72
59
|
} = {}) {
|
|
73
60
|
const resolvedBase = String(baseUrl).trim().replace(/\/+$/, "");
|
|
74
|
-
// chat 固定落在 /api/v1/chat/completions:base 已含 /api/v1 则只拼 /chat/completions,
|
|
75
|
-
// 否则拼 /api/v1/chat/completions(避免双 /api/v1 或 /v1 错路径导致 401/empty response)
|
|
76
61
|
const resolvedChat = chatPath || (String(resolvedBase).includes("/api/v1") ? "/chat/completions" : "/api/v1/chat/completions");
|
|
62
|
+
const transport = createTransport({ fetchImpl, dispatcher, keepAlive: !!dispatcher, timeoutMs: connectTimeoutMs, retry: {} });
|
|
77
63
|
|
|
78
64
|
async function clineFetch(body, sessionId) {
|
|
79
65
|
const token = await authPool.getAccessToken();
|
|
80
66
|
const headers = clineHeaders(sessionId, token);
|
|
81
67
|
const finalUrl = joinUrl(resolvedBase, resolvedChat);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const controller = new AbortController();
|
|
85
|
-
const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
|
|
86
|
-
opts.signal = controller.signal;
|
|
87
|
-
try { return await fetchImpl(finalUrl, opts); } finally { clearTimeout(timer); }
|
|
68
|
+
const isStream = body?.stream === true;
|
|
69
|
+
return transport.request({ url: finalUrl, headers, body, stream: isStream, timeoutMs: connectTimeoutMs });
|
|
88
70
|
}
|
|
89
71
|
|
|
90
|
-
|
|
91
|
-
function isLimitHit(status, bodyText, isStream) {
|
|
72
|
+
function isLimitHit(status, bodyText) {
|
|
92
73
|
if (status === 429) return true;
|
|
93
74
|
if (status >= 500 && String(bodyText).includes("empty response content")) return true;
|
|
94
|
-
if (status === 200 && !isStream && String(bodyText).includes("empty response content")) return true;
|
|
95
75
|
return false;
|
|
96
76
|
}
|
|
97
77
|
|
|
98
|
-
async function clineFetchWithRetry(body, sessionId
|
|
78
|
+
async function clineFetchWithRetry(body, sessionId) {
|
|
99
79
|
const maxRetries = 4;
|
|
100
80
|
let lastResp = null;
|
|
101
81
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
102
82
|
const resp = await authPool.enqueue(() => clineFetch(body, sessionId));
|
|
103
83
|
lastResp = resp;
|
|
104
84
|
let bodyText = "";
|
|
105
|
-
try { bodyText = await resp.
|
|
106
|
-
const hit = isLimitHit(resp.status, bodyText
|
|
85
|
+
if (resp.status !== 200) { try { bodyText = await resp.text(); } catch {} }
|
|
86
|
+
const hit = isLimitHit(resp.status, bodyText);
|
|
107
87
|
if (hit) {
|
|
108
88
|
const { parseCooldown } = await import("./auth.js");
|
|
109
89
|
const cooldownMs = parseCooldown(bodyText, resp.status);
|
|
@@ -126,7 +106,7 @@ export function createChatService({
|
|
|
126
106
|
let lastData = null;
|
|
127
107
|
let resp = firstResp;
|
|
128
108
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
129
|
-
if (!resp) resp = await clineFetchWithRetry(body, sessionId
|
|
109
|
+
if (!resp) resp = await clineFetchWithRetry(body, sessionId);
|
|
130
110
|
if (!resp.ok) {
|
|
131
111
|
const errText = await resp.text().catch(() => "");
|
|
132
112
|
const hdrs = new Headers(resp.headers);
|
|
@@ -165,7 +145,6 @@ export function createChatService({
|
|
|
165
145
|
const sessionId = genSessionId();
|
|
166
146
|
const isStream = body?.stream === true;
|
|
167
147
|
const upstreamModel = String(model).split("/").pop().includes(":") ? model : model;
|
|
168
|
-
// 构造上游 body:保留外部 model 名,Cline 上游用同名
|
|
169
148
|
const upstreamBody = {
|
|
170
149
|
model: upstreamModel,
|
|
171
150
|
max_tokens: body?.max_tokens || body?.max_completion_tokens || 4096,
|
|
@@ -178,16 +157,11 @@ export function createChatService({
|
|
|
178
157
|
for (const k of ["temperature", "top_p", "tools", "tool_choice", "stop", "presence_penalty", "frequency_penalty", "response_format", "user", "n", "seed"]) {
|
|
179
158
|
if (body[k] !== undefined) upstreamBody[k] = body[k];
|
|
180
159
|
}
|
|
181
|
-
|
|
182
|
-
// 网络层重试
|
|
183
160
|
for (let netAttempt = 0; netAttempt < 3; netAttempt++) {
|
|
184
161
|
try {
|
|
185
|
-
const resp = await clineFetchWithRetry(upstreamBody, sessionId
|
|
162
|
+
const resp = await clineFetchWithRetry(upstreamBody, sessionId);
|
|
186
163
|
if (!resp) throw new Error("empty response");
|
|
187
|
-
if (!resp.ok)
|
|
188
|
-
// 直通错误(403/400 等)
|
|
189
|
-
return resp;
|
|
190
|
-
}
|
|
164
|
+
if (!resp.ok) return resp;
|
|
191
165
|
if (isStream) return resp;
|
|
192
166
|
if (forceStream) {
|
|
193
167
|
const ret = await nonStreamWithContentCheck(upstreamBody, sessionId, resp);
|
|
@@ -196,7 +170,6 @@ export function createChatService({
|
|
|
196
170
|
const hdrs = new Headers({ "Content-Type": "application/json" });
|
|
197
171
|
return new Response(JSON.stringify(ret.data), { status: 200, headers: hdrs });
|
|
198
172
|
}
|
|
199
|
-
// 普通非流式(非 deepseek)
|
|
200
173
|
const raw = await resp.json().catch(() => null);
|
|
201
174
|
if (!raw) return resp;
|
|
202
175
|
const normalized = unwrapData(raw);
|