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
package/package.json
CHANGED
package/src/bench/cline-bench.js
CHANGED
|
@@ -1,54 +1,42 @@
|
|
|
1
1
|
import { clineHeaders } from "../providers/cline/headers.js";
|
|
2
2
|
import { computeMetrics } from "../metrics.js";
|
|
3
|
+
import { createTransport } from "../transport/index.js";
|
|
4
|
+
|
|
5
|
+
function sseContent(obj) {
|
|
6
|
+
const c = obj?.choices?.[0]?.delta?.content || obj?.choices?.[0]?.message?.content || "";
|
|
7
|
+
return typeof c === "string" ? c : "";
|
|
8
|
+
}
|
|
3
9
|
|
|
4
10
|
export async function clineBenchOne({ baseUrl, model, accessToken, prompt, maxTokens, timeoutMs, fetchImpl }) {
|
|
5
|
-
const
|
|
6
|
-
const timer = setTimeout(() => controller.abort(new Error(`timeout ${timeoutMs}ms`)), timeoutMs);
|
|
7
|
-
const t0 = performance.now();
|
|
11
|
+
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
8
12
|
let ttfbMs = null;
|
|
13
|
+
let totalMs = null;
|
|
9
14
|
let content = "";
|
|
10
15
|
try {
|
|
11
|
-
const
|
|
16
|
+
const sid = `sess_bench_${Date.now()}`;
|
|
17
|
+
const res = await tr.request({
|
|
18
|
+
url: `${baseUrl}/api/v1/chat/completions`,
|
|
12
19
|
method: "POST",
|
|
13
|
-
headers: { ...clineHeaders(
|
|
14
|
-
body:
|
|
15
|
-
|
|
20
|
+
headers: { ...clineHeaders(sid, accessToken), Accept: "text/event-stream" },
|
|
21
|
+
body: { model, messages: [{ role: "user", content: prompt }], stream: true, max_tokens: maxTokens, session_id: sid, reasoning_effort: "high" },
|
|
22
|
+
stream: true,
|
|
16
23
|
});
|
|
17
|
-
if (res instanceof Error) throw res;
|
|
18
24
|
if (!res.ok) {
|
|
19
25
|
let txt = "";
|
|
20
26
|
try { txt = await res.text(); } catch {}
|
|
21
27
|
const label = res.status === 401 ? "鉴权失败" : res.status === 429 ? "限流" : res.status >= 500 ? `上游错误 ${res.status}` : `HTTP ${res.status}`;
|
|
22
|
-
return { id: model, ok: false, status: res.status, label, error: txt.slice(0, 300), ttfbMs, totalMs:
|
|
28
|
+
return { id: model, ok: false, status: res.status, label, error: txt.slice(0, 300), ttfbMs, totalMs: res.totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
23
29
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
let buf = "";
|
|
27
|
-
for (;;) {
|
|
28
|
-
const { done, value } = await reader.read();
|
|
29
|
-
if (done) break;
|
|
30
|
-
if (ttfbMs === null) ttfbMs = Math.round(performance.now() - t0);
|
|
31
|
-
buf += decoder.decode(value, { stream: true });
|
|
32
|
-
let idx;
|
|
33
|
-
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
34
|
-
const line = buf.slice(0, idx);
|
|
35
|
-
buf = buf.slice(idx + 1);
|
|
36
|
-
if (!line.startsWith("data:")) continue;
|
|
37
|
-
const payload = line.slice(5).trim();
|
|
38
|
-
if (!payload || payload === "[DONE]") continue;
|
|
39
|
-
try {
|
|
40
|
-
const j = JSON.parse(payload);
|
|
41
|
-
const c = j?.choices?.[0]?.delta?.content || j?.choices?.[0]?.message?.content || "";
|
|
42
|
-
if (typeof c === "string") content += c;
|
|
43
|
-
} catch {}
|
|
44
|
-
}
|
|
30
|
+
for await (const ev of res.stream()) {
|
|
31
|
+
try { content += sseContent(JSON.parse(ev)); } catch {}
|
|
45
32
|
}
|
|
46
|
-
|
|
33
|
+
ttfbMs = res.ttfbMs;
|
|
34
|
+
totalMs = res.totalMs;
|
|
47
35
|
const chars = content.length;
|
|
48
36
|
const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens: null, completionTokens: null, chars });
|
|
49
37
|
return { id: model, ok: true, status: 200, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: null, chars };
|
|
50
38
|
} catch (e) {
|
|
51
39
|
const msg = e?.message || String(e);
|
|
52
|
-
return { id: model, ok: false, label: /timeout|abort/i.test(msg) ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs, totalMs
|
|
53
|
-
}
|
|
40
|
+
return { id: model, ok: false, label: /timeout|abort/i.test(msg) ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
41
|
+
}
|
|
54
42
|
}
|
package/src/bench/probe.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { joinUrl } from "../providers/base.js";
|
|
2
2
|
import { getCustomNormalizer } from "../providers/registry.js";
|
|
3
|
+
import { createTransport } from "../transport/index.js";
|
|
3
4
|
|
|
4
5
|
function normalizeModelsPayload(json, baseUrl = "") {
|
|
5
6
|
if (!json) return [];
|
|
6
|
-
// 可扩展:优先走注册表的定制化解析(如 cline.bot 仅 free)
|
|
7
7
|
try {
|
|
8
8
|
const custom = getCustomNormalizer(baseUrl);
|
|
9
9
|
if (custom) {
|
|
@@ -46,27 +46,20 @@ export async function probeModels({
|
|
|
46
46
|
if (!seen.has(norm)) { seen.add(norm); candidates.push(norm); }
|
|
47
47
|
};
|
|
48
48
|
if (modelsPath) push(modelsPath);
|
|
49
|
-
// 通用回退:/v1/models 与 /models
|
|
50
49
|
push("/v1/models");
|
|
51
50
|
push("/models");
|
|
52
|
-
|
|
51
|
+
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
53
52
|
const tried = [];
|
|
54
53
|
let lastError = "";
|
|
55
54
|
for (const p of candidates) {
|
|
56
55
|
const url = joinUrl(base, p);
|
|
57
56
|
tried.push(url);
|
|
58
57
|
try {
|
|
59
|
-
const
|
|
60
|
-
const t = setTimeout(() => controller.abort(new Error(`probe timeout ${timeoutMs}ms`)), timeoutMs);
|
|
61
|
-
let res;
|
|
62
|
-
try {
|
|
63
|
-
res = await fetchImpl(url, { headers, signal: controller.signal });
|
|
64
|
-
} finally { clearTimeout(t); }
|
|
65
|
-
if (res instanceof Error) { lastError = res.message || String(res); continue; }
|
|
58
|
+
const res = await tr.request({ url, method: "GET", headers, stream: false });
|
|
66
59
|
if (!res.ok) { lastError = `HTTP ${res.status}`; continue; }
|
|
67
60
|
let json = {};
|
|
68
61
|
try { json = await res.json(); } catch { json = {}; }
|
|
69
|
-
const raw = normalizeModelsPayload(json);
|
|
62
|
+
const raw = normalizeModelsPayload(json, baseUrl);
|
|
70
63
|
const data = raw.map((m) => ({ id: toModelId(m), raw: m })).filter((x) => x.id).map((x) => ({ id: x.id, ...x.raw }));
|
|
71
64
|
return { ok: true, data, tried, url, rawCount: raw.length };
|
|
72
65
|
} catch (e) {
|
package/src/bench/runner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { joinUrl } from "../providers/base.js";
|
|
2
2
|
import { computeMetrics, extractUsageFromJson } from "../metrics.js";
|
|
3
|
+
import { createTransport } from "../transport/index.js";
|
|
3
4
|
|
|
4
5
|
function extractInnerMessage(bodyText) {
|
|
5
6
|
const t = String(bodyText || "");
|
|
@@ -37,43 +38,28 @@ export async function runOne({
|
|
|
37
38
|
maxTokens = 32,
|
|
38
39
|
timeoutMs = 30000,
|
|
39
40
|
fetchImpl = globalThis.fetch,
|
|
40
|
-
clock = Date.now,
|
|
41
41
|
} = {}) {
|
|
42
|
-
const started = clock();
|
|
43
|
-
const t0 = typeof performance !== "undefined" && performance.now ? performance.now() : started;
|
|
44
42
|
if (!baseUrl) return { id: model, ok: false, error: "missing baseUrl", label: "配置错误", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
45
43
|
if (!model) return { id: model, ok: false, error: "missing model", label: "配置错误", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
46
44
|
if (!apiKey) return { id: model, ok: false, error: "missing apiKey", label: "未配置 Key", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
47
45
|
const url = joinUrl(String(baseUrl).replace(/\/+$/, ""), chatPath);
|
|
48
46
|
let rawModel = String(model || "").trim();
|
|
49
|
-
// allowlist 已是 raw(如 z-ai/glm-5.3-flash),仅当带供应商前缀时才剥
|
|
50
47
|
if (providerId && rawModel.startsWith(`${providerId}/`)) rawModel = rawModel.slice(providerId.length + 1);
|
|
51
48
|
const body = { model: rawModel, stream: false, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens };
|
|
52
|
-
// workbuddy 需额外 workbuddy 透传头由调用方 headers 注入;此处直接透传
|
|
53
49
|
const finalHeaders = { "Content-Type": "application/json", Accept: "application/json", ...headers };
|
|
54
50
|
if (apiKey && !finalHeaders.Authorization) finalHeaders.Authorization = `Bearer ${apiKey}`;
|
|
55
|
-
|
|
56
|
-
let ttfbMs = null;
|
|
57
|
-
let res;
|
|
51
|
+
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
58
52
|
try {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
try {
|
|
63
|
-
res = await fetchImpl(url, { method: "POST", headers: finalHeaders, body: JSON.stringify(body), signal: controller.signal });
|
|
64
|
-
} finally { clearTimeout(timer); }
|
|
65
|
-
ttfbMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - fetchStart);
|
|
66
|
-
if (res instanceof Error) throw res;
|
|
67
|
-
const totalMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0);
|
|
53
|
+
const res = await tr.request({ url, method: "POST", headers: finalHeaders, body, stream: false });
|
|
54
|
+
const ttfbMs = res.ttfbMs;
|
|
55
|
+
const totalMs = res.totalMs;
|
|
68
56
|
if (!res.ok) {
|
|
69
|
-
let txt = "";
|
|
70
|
-
try { txt = await res.text(); } catch {}
|
|
57
|
+
let txt = ""; try { txt = await res.text(); } catch {}
|
|
71
58
|
const cls = classifyError(res.status, txt);
|
|
72
59
|
const msg = extractInnerMessage(txt) || `HTTP ${res.status}`;
|
|
73
60
|
return { id: model, providerId, ok: false, status: res.status, error: msg, label: cls.label, ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
74
61
|
}
|
|
75
|
-
let json = {};
|
|
76
|
-
let txt = "";
|
|
62
|
+
let json = {}; let txt = "";
|
|
77
63
|
try { txt = await res.text(); json = JSON.parse(txt); } catch { json = {}; }
|
|
78
64
|
const usage = extractUsageFromJson(json);
|
|
79
65
|
const content = json?.choices?.[0]?.message?.content || json?.choices?.[0]?.text || txt || "";
|
|
@@ -84,9 +70,8 @@ export async function runOne({
|
|
|
84
70
|
const totalTokens = usage?.total_tokens ?? (promptTokens !== null && completionTokens !== null ? promptTokens + completionTokens : null);
|
|
85
71
|
return { id: model, providerId, ok: true, status: res.status, ttfbMs, totalMs, tps, charsPerSec, tokens: { prompt: promptTokens, completion: completionTokens, total: totalTokens }, chars, label: "成功", raw: json };
|
|
86
72
|
} catch (e) {
|
|
87
|
-
const totalMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0);
|
|
88
73
|
const msg = e?.message || String(e);
|
|
89
74
|
const isTimeout = /timeout|abort/i.test(msg);
|
|
90
|
-
return { id: model, providerId, ok: false, error: msg.slice(0, 300), label: isTimeout ? "超时" : "网络错误", ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
75
|
+
return { id: model, providerId, ok: false, error: msg.slice(0, 300), label: isTimeout ? "超时" : "网络错误", ttfbMs: null, totalMs: null, tps: null, charsPerSec: null, tokens: null };
|
|
91
76
|
}
|
|
92
77
|
}
|
package/src/bench/via-probe.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { joinUrl } from "../providers/base.js";
|
|
2
2
|
import { computeMetrics, extractUsageFromJson } from "../metrics.js";
|
|
3
|
+
import { createTransport } from "../transport/index.js";
|
|
3
4
|
|
|
4
5
|
function extractInnerMessage(bodyText) {
|
|
5
6
|
const t = String(bodyText || "");
|
|
@@ -43,11 +44,9 @@ export async function viaProbe({
|
|
|
43
44
|
relayBody,
|
|
44
45
|
targetUrl,
|
|
45
46
|
} = {}) {
|
|
46
|
-
const started = clock();
|
|
47
|
-
const t0 = typeof performance !== "undefined" && performance.now ? performance.now() : started;
|
|
48
47
|
const base = String(peerUrl || "").replace(/\/+$/, "");
|
|
49
48
|
if (!base) return { ok: false, label: "配置错误", error: "missing peerUrl", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
50
|
-
|
|
49
|
+
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
51
50
|
const rt = String(relayTarget || targetUrl || "").trim();
|
|
52
51
|
if (rt) {
|
|
53
52
|
const rh = relayHeaders && typeof relayHeaders === "object" ? relayHeaders : {};
|
|
@@ -56,15 +55,10 @@ export async function viaProbe({
|
|
|
56
55
|
const relayHeadersOut = { "Content-Type": "application/json", Accept: "application/json" };
|
|
57
56
|
if (token) relayHeadersOut.Authorization = `Bearer ${token}`;
|
|
58
57
|
const payload = { targetUrl: rt, method: "POST", headers: rh, body: rb };
|
|
59
|
-
let ttfbMs = null;
|
|
60
58
|
try {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
let res;
|
|
65
|
-
try { res = await fetchImpl(relayUrl, { method: "POST", headers: relayHeadersOut, body: JSON.stringify(payload), signal: controller.signal }); } finally { clearTimeout(timer); }
|
|
66
|
-
ttfbMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - fetchStart);
|
|
67
|
-
const totalMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0);
|
|
59
|
+
const res = await tr.request({ url: relayUrl, method: "POST", headers: relayHeadersOut, body: payload, stream: false });
|
|
60
|
+
const ttfbMs = res.ttfbMs;
|
|
61
|
+
const totalMs = res.totalMs;
|
|
68
62
|
if (!res.ok) {
|
|
69
63
|
let txt = ""; try { txt = await res.text(); } catch {}
|
|
70
64
|
const cls = classifyError(res.status, txt);
|
|
@@ -88,10 +82,9 @@ export async function viaProbe({
|
|
|
88
82
|
const totalTokens = usage?.total_tokens ?? (pt !== null && ct !== null ? pt + ct : null);
|
|
89
83
|
return { ok: true, status: relayStatus, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: { prompt: pt, completion: ct, total: totalTokens }, chars };
|
|
90
84
|
} catch (e) {
|
|
91
|
-
const totalMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0);
|
|
92
85
|
const msg = e?.message || String(e);
|
|
93
86
|
const isTimeout = /timeout|abort/i.test(msg);
|
|
94
|
-
return { ok: false, label: isTimeout ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
87
|
+
return { ok: false, label: isTimeout ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs: null, totalMs: null, tps: null, charsPerSec: null, tokens: null };
|
|
95
88
|
}
|
|
96
89
|
}
|
|
97
90
|
if (!model) return { ok: false, label: "配置错误", error: "missing model", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
@@ -103,27 +96,17 @@ export async function viaProbe({
|
|
|
103
96
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
104
97
|
const sk = shareKeysHeader || shareKeys;
|
|
105
98
|
if (sk) headers["x-mslxdff-share-keys"] = String(sk);
|
|
106
|
-
let ttfbMs = null;
|
|
107
99
|
try {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
let res;
|
|
112
|
-
try {
|
|
113
|
-
res = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal: controller.signal });
|
|
114
|
-
} finally { clearTimeout(timer); }
|
|
115
|
-
ttfbMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - fetchStart);
|
|
116
|
-
if (res instanceof Error) throw res;
|
|
117
|
-
const totalMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0);
|
|
100
|
+
const res = await tr.request({ url, method: "POST", headers, body, stream: false });
|
|
101
|
+
const ttfbMs = res.ttfbMs;
|
|
102
|
+
const totalMs = res.totalMs;
|
|
118
103
|
if (!res.ok) {
|
|
119
|
-
let txt = "";
|
|
120
|
-
try { txt = await res.text(); } catch {}
|
|
104
|
+
let txt = ""; try { txt = await res.text(); } catch {}
|
|
121
105
|
const cls = classifyError(res.status, txt);
|
|
122
106
|
const msg = extractInnerMessage(txt) || `HTTP ${res.status}`;
|
|
123
107
|
return { ok: false, status: res.status, label: cls.label, error: msg, ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
124
108
|
}
|
|
125
|
-
let json = {};
|
|
126
|
-
let txt = "";
|
|
109
|
+
let json = {}; let txt = "";
|
|
127
110
|
try { txt = await res.text(); json = JSON.parse(txt); } catch { json = {}; }
|
|
128
111
|
const usage = extractUsageFromJson(json);
|
|
129
112
|
const content = json?.choices?.[0]?.message?.content || json?.choices?.[0]?.text || txt || "";
|
|
@@ -134,9 +117,8 @@ export async function viaProbe({
|
|
|
134
117
|
const totalTokens = usage?.total_tokens ?? (promptTokens !== null && completionTokens !== null ? promptTokens + completionTokens : null);
|
|
135
118
|
return { ok: true, status: res.status, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: { prompt: promptTokens, completion: completionTokens, total: totalTokens }, chars };
|
|
136
119
|
} catch (e) {
|
|
137
|
-
const totalMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0);
|
|
138
120
|
const msg = e?.message || String(e);
|
|
139
121
|
const isTimeout = /timeout|abort/i.test(msg);
|
|
140
|
-
return { ok: false, label: isTimeout ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
122
|
+
return { ok: false, label: isTimeout ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs: null, totalMs: null, tps: null, charsPerSec: null, tokens: null };
|
|
141
123
|
}
|
|
142
124
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computeMetrics } from "../metrics.js";
|
|
2
|
+
import { createTransport } from "../transport/index.js";
|
|
2
3
|
|
|
3
4
|
function buildWorkbuddyHeaders(apiKey, auth) {
|
|
4
5
|
const h = {
|
|
@@ -16,55 +17,38 @@ function buildWorkbuddyHeaders(apiKey, auth) {
|
|
|
16
17
|
return h;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
function sseContent(obj) {
|
|
21
|
+
const c = obj?.choices?.[0]?.delta?.content || obj?.choices?.[0]?.message?.content || "";
|
|
22
|
+
return typeof c === "string" ? c : "";
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
export async function workbuddyBenchOne({ baseUrl, chatPath = "/v2/chat/completions", model, apiKey, auth, prompt = "hi", maxTokens = 5, timeoutMs = 30000, fetchImpl = globalThis.fetch }) {
|
|
20
|
-
const
|
|
21
|
-
const timer = setTimeout(() => controller.abort(new Error(`timeout ${timeoutMs}ms`)), timeoutMs);
|
|
22
|
-
const t0 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
26
|
+
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
23
27
|
let ttfbMs = null;
|
|
28
|
+
let totalMs = null;
|
|
24
29
|
let content = "";
|
|
25
30
|
try {
|
|
26
31
|
const url = String(baseUrl).replace(/\/+$/, "") + String(chatPath || "/v2/chat/completions");
|
|
27
32
|
const headers = buildWorkbuddyHeaders(apiKey, auth);
|
|
28
33
|
const rawModel = String(model || "").trim();
|
|
29
34
|
const body = { model: rawModel, stream: true, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens };
|
|
30
|
-
const res = await
|
|
31
|
-
if (res instanceof Error) throw res;
|
|
35
|
+
const res = await tr.request({ url, method: "POST", headers, body, stream: true });
|
|
32
36
|
if (!res.ok) {
|
|
33
37
|
let txt = "";
|
|
34
38
|
try { txt = await res.text(); } catch {}
|
|
35
39
|
const label = res.status === 401 ? "鉴权失败" : res.status === 402 ? "余额不足" : res.status === 429 ? "限流" : res.status >= 500 ? `上游错误 ${res.status}` : `HTTP ${res.status}`;
|
|
36
|
-
return { id: model, ok: false, status: res.status, label, error: txt.slice(0, 300), ttfbMs, totalMs:
|
|
40
|
+
return { id: model, ok: false, status: res.status, label, error: txt.slice(0, 300), ttfbMs, totalMs: res.totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
37
41
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const decoder = new TextDecoder();
|
|
41
|
-
let buf = "";
|
|
42
|
-
for (;;) {
|
|
43
|
-
const { done, value } = await reader.read();
|
|
44
|
-
if (done) break;
|
|
45
|
-
if (ttfbMs === null) ttfbMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - t0);
|
|
46
|
-
buf += decoder.decode(value, { stream: true });
|
|
47
|
-
let idx;
|
|
48
|
-
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
49
|
-
const line = buf.slice(0, idx);
|
|
50
|
-
buf = buf.slice(idx + 1);
|
|
51
|
-
if (!line.startsWith("data:")) continue;
|
|
52
|
-
const payload = line.slice(5).trim();
|
|
53
|
-
if (!payload || payload === "[DONE]") continue;
|
|
54
|
-
try {
|
|
55
|
-
const j = JSON.parse(payload);
|
|
56
|
-
const c = j?.choices?.[0]?.delta?.content || j?.choices?.[0]?.message?.content || "";
|
|
57
|
-
if (typeof c === "string") content += c;
|
|
58
|
-
} catch {}
|
|
59
|
-
}
|
|
42
|
+
for await (const ev of res.stream()) {
|
|
43
|
+
try { content += sseContent(JSON.parse(ev)); } catch {}
|
|
60
44
|
}
|
|
61
|
-
|
|
45
|
+
ttfbMs = res.ttfbMs;
|
|
46
|
+
totalMs = res.totalMs;
|
|
62
47
|
const chars = content.length;
|
|
63
48
|
const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens: null, completionTokens: null, chars });
|
|
64
49
|
return { id: model, ok: true, status: 200, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: null, chars };
|
|
65
50
|
} catch (e) {
|
|
66
51
|
const msg = e?.message || String(e);
|
|
67
|
-
const totalMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - t0);
|
|
68
52
|
return { id: model, ok: false, label: /timeout|abort/i.test(msg) ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
69
|
-
}
|
|
53
|
+
}
|
|
70
54
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { estimateChars, needsCompress } from "./store.js";
|
|
3
|
+
import { CHAT_KEEP_RECENT, CHAT_MAX_TOOL_LOOPS } from "./config.js";
|
|
4
|
+
import { buildDedupKey, runTool } from "./tool-handlers.js";
|
|
5
|
+
|
|
6
|
+
// 纯引擎:无 readline/ANSI/spinner,单一 runTurn 可测
|
|
7
|
+
export function createEngine({
|
|
8
|
+
chatWithFallback,
|
|
9
|
+
summarizeHistory,
|
|
10
|
+
getToolDefs = () => [],
|
|
11
|
+
execCommand,
|
|
12
|
+
readFileTool,
|
|
13
|
+
curlTool,
|
|
14
|
+
onTrace = () => {},
|
|
15
|
+
config = {},
|
|
16
|
+
} = {}) {
|
|
17
|
+
const CWF = chatWithFallback || (async () => ({ ok: false, error: "no chat" }));
|
|
18
|
+
const SUM = summarizeHistory || (async () => null);
|
|
19
|
+
const GTOOLS = getToolDefs;
|
|
20
|
+
const EXEC = execCommand || (async () => ({ ok: false, output: "no exec" }));
|
|
21
|
+
const READ = readFileTool || (async () => ({ ok: false, output: "no read" }));
|
|
22
|
+
const CURL = curlTool || (async () => ({ ok: false, output: "no curl" }));
|
|
23
|
+
|
|
24
|
+
async function maybeCompress(messages) {
|
|
25
|
+
if (!needsCompress(messages)) return [...messages];
|
|
26
|
+
const sys = messages[0];
|
|
27
|
+
const rest = messages.slice(1);
|
|
28
|
+
if (rest.length <= CHAT_KEEP_RECENT + 2) return [...messages];
|
|
29
|
+
const t0 = performance.now();
|
|
30
|
+
const toSummarize = rest.slice(0, -CHAT_KEEP_RECENT);
|
|
31
|
+
const keep = rest.slice(-CHAT_KEEP_RECENT);
|
|
32
|
+
const chars = estimateChars(messages);
|
|
33
|
+
onTrace(`[压缩] 触发 ${chars}字 > 400000阈值 · 待压 ${toSummarize.length}条 保留 ${keep.length}条`);
|
|
34
|
+
const summary = await SUM(toSummarize);
|
|
35
|
+
const dt = Math.round(performance.now() - t0);
|
|
36
|
+
if (!summary) {
|
|
37
|
+
onTrace(`[压缩] 失败/空 · ${dt}ms → 截断`);
|
|
38
|
+
return [sys, ...keep];
|
|
39
|
+
}
|
|
40
|
+
const summaryMsg = { role: "system", content: summary };
|
|
41
|
+
const next = [sys, summaryMsg, ...keep];
|
|
42
|
+
onTrace(`[压缩] 完成 ${toSummarize.length}条→${summary.length}字 · ${dt}ms · 新总量约 ${estimateChars(next)}字`);
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function runTurn(userText, messages) {
|
|
47
|
+
const tools = GTOOLS();
|
|
48
|
+
messages.push({ role: "user", content: userText });
|
|
49
|
+
let loops = 0;
|
|
50
|
+
let lastModel = null;
|
|
51
|
+
let lastProvider = null;
|
|
52
|
+
let lastUsage = null;
|
|
53
|
+
let lastFallback = false;
|
|
54
|
+
let lastFallbackGateway = false;
|
|
55
|
+
let lastLatency = 0;
|
|
56
|
+
const t0 = performance.now();
|
|
57
|
+
const turnStart = performance.now();
|
|
58
|
+
const seenCalls = new Map();
|
|
59
|
+
let duplicateStrikes = 0;
|
|
60
|
+
let forceNoTools = false;
|
|
61
|
+
onTrace(`[turn] 开始 "${userText.slice(0, 60)}${userText.length > 60 ? "…" : ""}" · 历史 ${messages.length}条 约 ${estimateChars(messages)}字`);
|
|
62
|
+
while (loops < CHAT_MAX_TOOL_LOOPS) {
|
|
63
|
+
const tLoop = performance.now();
|
|
64
|
+
const tComp = performance.now();
|
|
65
|
+
const cur = await maybeCompress(messages);
|
|
66
|
+
const compressMs = Math.round(performance.now() - tComp);
|
|
67
|
+
if (compressMs > 50) onTrace(`[loop ${loops}] 压缩耗时 ${compressMs}ms`);
|
|
68
|
+
messages.length = 0;
|
|
69
|
+
for (const m of cur) messages.push(m);
|
|
70
|
+
const tCall = performance.now();
|
|
71
|
+
let res;
|
|
72
|
+
const activeTools = forceNoTools ? [] : tools;
|
|
73
|
+
res = await CWF({ messages, tools: activeTools });
|
|
74
|
+
if (forceNoTools && res.ok && res.message?.tool_calls?.length) {
|
|
75
|
+
onTrace(`[guard] 禁工具模式下仍收到 tool_calls,已拦截`);
|
|
76
|
+
res.message.tool_calls = [];
|
|
77
|
+
if (!res.message.content) res.message.content = "(已拦截违规工具调用,请基于已有结果直接回答)";
|
|
78
|
+
}
|
|
79
|
+
const llmMs = Math.round(performance.now() - tCall);
|
|
80
|
+
onTrace(`[loop ${loops}] LLM ${llmMs}ms${compressMs > 50 ? ` (含压缩 ${compressMs}ms)` : ""} · ${estimateChars(messages)}字上下文`);
|
|
81
|
+
lastLatency = llmMs;
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const err = `大模型暂不可用:${res.error}`;
|
|
84
|
+
messages.push({ role: "assistant", content: err });
|
|
85
|
+
return { text: err, model: null, latency: lastLatency, usage: null, fallback: false, ok: false };
|
|
86
|
+
}
|
|
87
|
+
lastModel = res.model;
|
|
88
|
+
lastProvider = res.provider || null;
|
|
89
|
+
lastUsage = res.usage || null;
|
|
90
|
+
lastFallback = !!res.fallback;
|
|
91
|
+
lastFallbackGateway = !!res.fallbackGateway || !!res.viaGateway;
|
|
92
|
+
const msg = res.message;
|
|
93
|
+
const toolCalls = msg.tool_calls || [];
|
|
94
|
+
let fallbackCmd = null;
|
|
95
|
+
if (!toolCalls.length && msg.content) {
|
|
96
|
+
const m = String(msg.content).match(/\{[^}]*"command"\s*:\s*"([^"]+)"[^}]*\}/);
|
|
97
|
+
if (m) fallbackCmd = m[1];
|
|
98
|
+
}
|
|
99
|
+
if (!toolCalls.length && !fallbackCmd) {
|
|
100
|
+
const text = String(msg.content || "").trim() || "(空回复)";
|
|
101
|
+
messages.push({ role: "assistant", content: text });
|
|
102
|
+
const totalMs = Math.round(performance.now() - t0);
|
|
103
|
+
let note = "";
|
|
104
|
+
if (lastFallbackGateway) note = "\n[注:mimo/big-pickle 均不可用,已自动切本地网关 auto(:8989)]";
|
|
105
|
+
else if (lastFallback) note = "\n[注:mimo 不可用,已用 big-pickle]";
|
|
106
|
+
onTrace(`[turn] 完成 总计 ${totalMs}ms · LLM ${lastLatency}ms · 0 工具${lastFallbackGateway ? " · gateway-fallback" : ""}`);
|
|
107
|
+
return { text: text + note, model: lastModel, provider: lastProvider, latency: lastLatency, usage: lastUsage, fallback: lastFallback, fallbackGateway: lastFallbackGateway, ok: true, totalMs };
|
|
108
|
+
}
|
|
109
|
+
const calls = toolCalls.length ? toolCalls : [{ id: "fallback-1", function: { name: "run_command", arguments: JSON.stringify({ command: fallbackCmd }) } }];
|
|
110
|
+
messages.push({ role: "assistant", content: msg.content || "", tool_calls: calls.map((c) => ({ id: c.id, type: "function", function: c.function })) });
|
|
111
|
+
onTrace(`[tools] 本轮 ${calls.length} 个调用 ${calls.map((c) => c.function?.name).join(",")} · 顺序执行`);
|
|
112
|
+
const tTools = performance.now();
|
|
113
|
+
const toolResults = [];
|
|
114
|
+
const toolDeps = { execCommand: EXEC, readFileTool: READ, curlTool: CURL, onTrace };
|
|
115
|
+
for (const c of calls) {
|
|
116
|
+
const name = c.function?.name;
|
|
117
|
+
let args = {};
|
|
118
|
+
try { args = JSON.parse(c.function?.arguments || "{}"); } catch {}
|
|
119
|
+
const t1 = performance.now();
|
|
120
|
+
const dedupKey = buildDedupKey(name, args);
|
|
121
|
+
const seen = seenCalls.get(dedupKey);
|
|
122
|
+
if (seen) {
|
|
123
|
+
const dt = Math.round(performance.now() - t1);
|
|
124
|
+
onTrace(`[tool] ${name} 重复调用已跳过 · ${dt}ms · 之前 ${seen.count} 次`);
|
|
125
|
+
toolResults.push({
|
|
126
|
+
id: c.id,
|
|
127
|
+
content: `SKIPPED_DUP: 此工具调用在本轮已执行过 ${seen.count} 次,结果相同请直接基于已有信息回答用户,不要再重复调用。\n--- 首次结果复用 ---\n${seen.firstResult.slice(0, 6000)}`,
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const result = await runTool({ name, args, userText, ...toolDeps });
|
|
132
|
+
if (!seenCalls.has(dedupKey)) seenCalls.set(dedupKey, { count: 1, firstResult: result });
|
|
133
|
+
else seenCalls.get(dedupKey).count++;
|
|
134
|
+
toolResults.push({ id: c.id, content: result });
|
|
135
|
+
}
|
|
136
|
+
for (const tr of toolResults) messages.push({ role: "tool", tool_call_id: tr.id, content: tr.content });
|
|
137
|
+
if (toolResults.some((tr) => String(tr.content).startsWith("SKIPPED_DUP"))) {
|
|
138
|
+
duplicateStrikes++;
|
|
139
|
+
forceNoTools = true;
|
|
140
|
+
messages.push({ role: "system", content: "系统提示:你已重复调用相同工具,工具侧已复用首次结果并跳过执行。你已被禁止再调用任何工具,必须立即基于以上工具结果用中文直接回答用户,0 工具调用。" });
|
|
141
|
+
onTrace(`[dup] 检测到重复调用 ${duplicateStrikes} 次,已禁用后续工具调用`);
|
|
142
|
+
if (duplicateStrikes >= 2) {
|
|
143
|
+
const seen = [...seenCalls.values()].map((v) => v.firstResult).join("\n---\n").slice(0, 6000);
|
|
144
|
+
const synth = `检测到重复调用已达 ${duplicateStrikes} 次,为避免空转,直接基于已有结果回答:\n\n${seen}`;
|
|
145
|
+
messages.push({ role: "assistant", content: synth });
|
|
146
|
+
const totalMs = Math.round(performance.now() - t0);
|
|
147
|
+
onTrace(`[turn] 提前结束(重复阈值) 总计 ${totalMs}ms`);
|
|
148
|
+
return { text: synth, model: lastModel || "local", provider: lastProvider, latency: lastLatency, usage: lastUsage, fallback: lastFallback, ok: true, totalMs };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const toolsMs = Math.round(performance.now() - tTools);
|
|
152
|
+
const loopMs = Math.round(performance.now() - tLoop);
|
|
153
|
+
onTrace(`[loop ${loops}] 工具 ${toolsMs}ms · 本轮总 ${loopMs}ms · 累计 ${Math.round(performance.now() - turnStart)}ms`);
|
|
154
|
+
loops++;
|
|
155
|
+
}
|
|
156
|
+
return { text: "(工具调用次数已达上限,已停止)", model: lastModel, latency: lastLatency, usage: lastUsage, fallback: lastFallback, fallbackGateway: lastFallbackGateway, ok: false };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { runTurn, maybeCompress };
|
|
160
|
+
}
|