mslxdff 0.1.87 → 0.1.89
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-pipeline/engine.js +241 -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/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/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 +116 -34
- 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.js +113 -287
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,241 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { injectReasoningContent } from "../reasoning.js";
|
|
3
|
+
import { runHook } from "../plugins.js";
|
|
4
|
+
import { errMsg, json } from "../routes/helpers.js";
|
|
5
|
+
import { hedgeDelayMs, shouldHedge } from "../routes/hedge.js";
|
|
6
|
+
import { handleHedge } from "../routes/chat/hedge-handler.js";
|
|
7
|
+
import { handleLocalRelay } from "../routes/chat/local-handler.js";
|
|
8
|
+
import { handlePeerRelay } from "../routes/chat/peer-handler.js";
|
|
9
|
+
import { handleBroadbandRelay } from "../routes/chat/broadband-handler.js";
|
|
10
|
+
import { handleViaRoute } from "../routes/chat/via-route-handler.js";
|
|
11
|
+
import { handleExhaustedLocal, handleExhaustedAll } from "../routes/chat/exhausted-handler.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* ExecutionEngine — 统一执行:auto 并发择优 → via-route → 串行 trial → hedge/local/peer/broadband/exhausted
|
|
15
|
+
* 承接原 gateway 的 Executor 段(order/race/回退/对冲全在此),gateway 仅做薄适配。
|
|
16
|
+
*/
|
|
17
|
+
export function createEngine(_deps = {}) {
|
|
18
|
+
async function run(_plan, state) {
|
|
19
|
+
let order = state.order;
|
|
20
|
+
const {
|
|
21
|
+
reqId, startedAt, req, res, body, policy,
|
|
22
|
+
useAuto, lockModel, requested, hops,
|
|
23
|
+
canFallback, canForwardPeers,
|
|
24
|
+
perf0, stages, mark, evt, logCall, logError, done, handlerCtx,
|
|
25
|
+
auto, upstream, peers, groups, bus, token, plugins, logs,
|
|
26
|
+
} = state;
|
|
27
|
+
const { shareKeys, workbuddyUid } = policy;
|
|
28
|
+
|
|
29
|
+
// ===== auto 首次并发择优 =====
|
|
30
|
+
if (useAuto && order.length > 1 && auto && !lockModel) {
|
|
31
|
+
const statuses = auto.statuses?.() ?? {};
|
|
32
|
+
const hasPriorSuccess = Object.values(statuses).some((e) => e && typeof e === "object" && e.status === "normal");
|
|
33
|
+
const nonCoolingOrder = order.filter((m) => { try { return !auto.isCooling(m); } catch { return true; } });
|
|
34
|
+
if (!hasPriorSuccess && nonCoolingOrder.length > 1) {
|
|
35
|
+
const concLimit = (() => {
|
|
36
|
+
const v = Number(process.env.MSLXDFF_AUTO_CONCURRENT);
|
|
37
|
+
if (Number.isInteger(v) && v > 0) return Math.min(v, nonCoolingOrder.length);
|
|
38
|
+
return Math.min(nonCoolingOrder.length, 5);
|
|
39
|
+
})();
|
|
40
|
+
let raceModels = nonCoolingOrder.slice(0, concLimit);
|
|
41
|
+
if (plugins?.length) {
|
|
42
|
+
const k = [];
|
|
43
|
+
for (const m of raceModels) {
|
|
44
|
+
const b = await runHook(plugins, "model:beforeTry", { reqId, requested, model: m, hops });
|
|
45
|
+
if (b.value === false || b.value?.skip) continue;
|
|
46
|
+
k.push(m);
|
|
47
|
+
}
|
|
48
|
+
raceModels = k;
|
|
49
|
+
}
|
|
50
|
+
if (!raceModels.length) {
|
|
51
|
+
order = order.filter((m) => !new Set(nonCoolingOrder.slice(0, concLimit)).has(m));
|
|
52
|
+
if (!order.length) {
|
|
53
|
+
await handleExhaustedAll({ res, body, lastErr: { model: requested, status: 502, message: "all concurrent candidates skipped by plugin" }, order: nonCoolingOrder.slice(0, concLimit), requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
evt("auto-concurrent-race", { reqId, models: raceModels, skippedFaulty: order.length - nonCoolingOrder.length, limit: concLimit });
|
|
58
|
+
const raceStart = performance.now();
|
|
59
|
+
const attempts = raceModels.map(async (m) => {
|
|
60
|
+
let f = { ...injectReasoningContent(m, body), model: m };
|
|
61
|
+
if (plugins?.length) {
|
|
62
|
+
const u = await runHook(plugins, "upstream:request", { reqId, requested, model: m, payload: f, stream: Boolean(body.stream) });
|
|
63
|
+
if (u.changed && u.value?.payload) f = u.value.payload;
|
|
64
|
+
}
|
|
65
|
+
let r = null;
|
|
66
|
+
try {
|
|
67
|
+
const o = {};
|
|
68
|
+
if (Object.keys(shareKeys).length) o.shareKeys = shareKeys;
|
|
69
|
+
if (workbuddyUid) o.workbuddyUid = workbuddyUid;
|
|
70
|
+
r = await upstream.chat(f, Object.keys(o).length ? o : undefined);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (plugins?.length) runHook(plugins, "upstream:response", { reqId, requested, model: m, status: null, ok: false, error: errMsg(e), timing: e?._t ?? null }).catch(() => {});
|
|
73
|
+
return { model: m, ok: false, error: errMsg(e), status: 502, timing: e?._t ?? null };
|
|
74
|
+
}
|
|
75
|
+
if (plugins?.length) runHook(plugins, "upstream:response", { reqId, requested, model: m, status: r instanceof Error ? null : r?.status ?? null, ok: !(r instanceof Error) && r ? r.status < 400 : false, error: r instanceof Error ? errMsg(r) : null, timing: r?._t ?? null }).catch(() => {});
|
|
76
|
+
if (r && r.status >= 400) {
|
|
77
|
+
const a = r.status === 403 && r.headers?.get?.("x-mslxdff-allowlist") === "1";
|
|
78
|
+
if (a) return { model: m, ok: false, error: "allowlist", status: 403, allowlist: true };
|
|
79
|
+
return { model: m, ok: false, error: `upstream ${r.status}`, status: r.status, res: r, timing: r._t ?? null };
|
|
80
|
+
}
|
|
81
|
+
if (r instanceof Error) return { model: m, ok: false, error: errMsg(r), status: 502 };
|
|
82
|
+
return { model: m, ok: true, res: r, status: r.status, timing: r._t ?? null };
|
|
83
|
+
});
|
|
84
|
+
const results = await Promise.allSettled(attempts);
|
|
85
|
+
const okList = results.map((r, i) => ({ r, i, model: raceModels[i] }))
|
|
86
|
+
.filter(({ r }) => r.status === "fulfilled" && r.value?.ok)
|
|
87
|
+
.map(({ r, i, model }) => ({ model, idx: i, val: r.value, t: r.value.timing?.totalMs ?? r.value.timing?.ms ?? Number.MAX_SAFE_INTEGER }));
|
|
88
|
+
if (okList.length) {
|
|
89
|
+
okList.sort((a, b) => a.t - b.t);
|
|
90
|
+
const best = okList[0];
|
|
91
|
+
const winModel = best.model;
|
|
92
|
+
evt("auto-concurrent-win", { reqId, model: winModel, timing: best.val.timing, totalMs: Math.round(performance.now() - raceStart), tried: raceModels.length });
|
|
93
|
+
for (const { r, i } of results.map((r, i) => ({ r, i }))) {
|
|
94
|
+
const m = raceModels[i];
|
|
95
|
+
if (r.status === "fulfilled" && r.value?.ok) {
|
|
96
|
+
if (m === winModel) {
|
|
97
|
+
const latencyMs = r.value.timing?.totalMs ?? Math.round(performance.now() - raceStart);
|
|
98
|
+
await auto.recordOk(m, { latencyMs });
|
|
99
|
+
try { const { savePreferredModel } = await import("../state.js"); savePreferredModel(m); evt("auto-concurrent-preferred", { reqId, model: m }); } catch {}
|
|
100
|
+
}
|
|
101
|
+
} else if (r.status === "fulfilled" && !r.value?.ok && !r.value?.allowlist) await auto.recordError(m, { status: r.value.status || 502 });
|
|
102
|
+
else if (r.status === "rejected") await auto.recordError(m, { status: 502 });
|
|
103
|
+
}
|
|
104
|
+
handlerCtx.model = winModel;
|
|
105
|
+
const lr = await handleLocalRelay({ upRes: best.val.res, model: winModel, body, order: raceModels, idx: best.idx, lastErr: null, requested, useAuto, lockModel, auto, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res });
|
|
106
|
+
if (lr.handled) return;
|
|
107
|
+
if (!lr.lastErr) return;
|
|
108
|
+
} else {
|
|
109
|
+
evt("auto-concurrent-all-fail", { reqId, tried: raceModels.length, totalMs: Math.round(performance.now() - raceStart) });
|
|
110
|
+
for (const { r, i } of results.map((r, i) => ({ r, i }))) {
|
|
111
|
+
const m = raceModels[i];
|
|
112
|
+
if (r.status === "fulfilled" && !r.value?.ok && !r.value?.allowlist) await auto.recordError(m, { status: r.value.status || 502 });
|
|
113
|
+
else if (r.status === "rejected") await auto.recordError(m, { status: 502 });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const triedSet = new Set(raceModels);
|
|
117
|
+
order = order.filter((m) => !triedSet.has(m));
|
|
118
|
+
if (!order.length) {
|
|
119
|
+
const failedStatuses = results.map((r) => (r.status === "fulfilled" ? r.value?.status : null)).filter((s) => Number.isInteger(s));
|
|
120
|
+
const lastStatus = failedStatuses[failedStatuses.length - 1] || failedStatuses[0] || 502;
|
|
121
|
+
const last = { model: raceModels[0] || requested, status: lastStatus, message: "all concurrent candidates failed" };
|
|
122
|
+
await handleExhaustedAll({ res, body, lastErr: last, order: raceModels, requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ===== VIA-ROUTE 单路径择路(显式锁模型,不并发) =====
|
|
130
|
+
let viaRouteLastErr = null;
|
|
131
|
+
if (!useAuto && requested && requested.includes("/") && canForwardPeers && !lockModel && peers) {
|
|
132
|
+
try {
|
|
133
|
+
const vr = await handleViaRoute({ model: requested, body, peers, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res, requested, useAuto, lockModel, auto });
|
|
134
|
+
if (vr.handled) return;
|
|
135
|
+
if (vr.lastErr) viaRouteLastErr = vr.lastErr;
|
|
136
|
+
} catch (e) {
|
|
137
|
+
evt("via-route-exception", { reqId, model: requested, error: errMsg(e) });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ===== 串行 trial =====
|
|
142
|
+
let lastErr = viaRouteLastErr;
|
|
143
|
+
for (let idx = 0; idx < order.length; idx++) {
|
|
144
|
+
const model = order[idx];
|
|
145
|
+
handlerCtx.model = model;
|
|
146
|
+
evt("model-try", { reqId, model, idx, remaining: order.length - idx });
|
|
147
|
+
if (plugins?.length) {
|
|
148
|
+
const bt = await runHook(plugins, "model:beforeTry", { reqId, requested, model, idx, hops });
|
|
149
|
+
for (const e of bt.errors) evt("plugin-hook-error", { reqId, hook: "model:beforeTry", plugin: e.plugin, error: e.error });
|
|
150
|
+
if (bt.value === false || bt.value?.skip === true) { evt("plugin-hook", { reqId, hook: "model:beforeTry", applied: true, skipped: model }); continue; }
|
|
151
|
+
}
|
|
152
|
+
let upRes = null;
|
|
153
|
+
let forwarded = { ...injectReasoningContent(model, body), model };
|
|
154
|
+
if (plugins?.length) {
|
|
155
|
+
const ur = await runHook(plugins, "upstream:request", { reqId, requested, model, payload: forwarded, stream: Boolean(body.stream) });
|
|
156
|
+
for (const e of ur.errors) evt("plugin-hook-error", { reqId, hook: "upstream:request", plugin: e.plugin, error: e.error });
|
|
157
|
+
if (ur.changed && ur.value?.payload && typeof ur.value.payload === "object") { forwarded = ur.value.payload; evt("plugin-hook", { reqId, hook: "upstream:request", applied: true, model, rewrittenModel: forwarded.model ?? null }); }
|
|
158
|
+
}
|
|
159
|
+
const tUp = performance.now();
|
|
160
|
+
evt("upstream-try", { reqId, model, attempt: idx + 1 });
|
|
161
|
+
try {
|
|
162
|
+
const chatOpts = {};
|
|
163
|
+
if (Object.keys(shareKeys).length) chatOpts.shareKeys = shareKeys;
|
|
164
|
+
if (workbuddyUid) chatOpts.workbuddyUid = workbuddyUid;
|
|
165
|
+
upRes = await upstream.chat(forwarded, Object.keys(chatOpts).length ? chatOpts : undefined);
|
|
166
|
+
evt("upstream-done", { reqId, model, ok: !(upRes instanceof Error) && upRes.status < 400, status: upRes instanceof Error ? null : upRes.status, timing: upRes._t ?? null, error: null });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (auto) await auto.recordError(model, { message: errMsg(err) });
|
|
169
|
+
lastErr = { model, upstream: null, status: 502, message: errMsg(err) };
|
|
170
|
+
logError(model, 502, errMsg(err));
|
|
171
|
+
evt("upstream-error", { reqId, model, status: 502, message: errMsg(err), timing: err._t ?? { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - tUp) } });
|
|
172
|
+
}
|
|
173
|
+
if (plugins?.length) {
|
|
174
|
+
runHook(plugins, "upstream:response", {
|
|
175
|
+
reqId, requested, model,
|
|
176
|
+
status: upRes instanceof Error ? null : upRes instanceof Object ? (upRes.status ?? null) : null,
|
|
177
|
+
ok: !(upRes instanceof Error) && upRes ? upRes.status < 400 : false,
|
|
178
|
+
error: upRes instanceof Error ? errMsg(upRes) : null,
|
|
179
|
+
timing: upRes?._t ?? null,
|
|
180
|
+
}).catch(() => {});
|
|
181
|
+
}
|
|
182
|
+
mark(`up-${model}`);
|
|
183
|
+
if (upRes && upRes.status >= 400) {
|
|
184
|
+
const isAllowlistBlock = upRes.status === 403 && (upRes.headers?.get?.("x-mslxdff-allowlist") === "1");
|
|
185
|
+
if (isAllowlistBlock) {
|
|
186
|
+
let bodyText = null; try { bodyText = await upRes.clone().text(); } catch {}
|
|
187
|
+
let errBody = { error: `model not allowed for provider` };
|
|
188
|
+
try { errBody = bodyText ? JSON.parse(bodyText) : errBody; } catch { errBody = { error: bodyText || "model not allowed" }; }
|
|
189
|
+
if (useAuto) {
|
|
190
|
+
logError(model, 403, errBody.error || "model not allowed");
|
|
191
|
+
evt("upstream-error", { reqId, model, status: 403, message: errBody.error, timing: upRes._t ?? null, allowlist: true, skipped: true });
|
|
192
|
+
lastErr = { model, upstream: upRes, status: 403, message: errBody.error || "model not allowed" };
|
|
193
|
+
if (canFallback && idx < order.length - 1) { evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: `allowlist skip ${errBody.error || "blocked"}` }); continue; }
|
|
194
|
+
return json(res, 403, errBody);
|
|
195
|
+
}
|
|
196
|
+
logError(model, 403, errBody.error || "model not allowed");
|
|
197
|
+
evt("upstream-error", { reqId, model, status: 403, message: errBody.error, timing: upRes._t ?? null, allowlist: true });
|
|
198
|
+
return json(res, 403, errBody);
|
|
199
|
+
}
|
|
200
|
+
if (auto) await auto.recordError(model, { status: upRes.status });
|
|
201
|
+
lastErr = { model, upstream: upRes, status: upRes.status, message: null };
|
|
202
|
+
logError(model, upRes.status, `upstream ${upRes.status}`);
|
|
203
|
+
evt("upstream-error", { reqId, model, status: upRes.status, message: null, timing: upRes._t ?? null });
|
|
204
|
+
upRes = null;
|
|
205
|
+
}
|
|
206
|
+
if (upRes) {
|
|
207
|
+
const isStream = Boolean(body.stream);
|
|
208
|
+
const d = hedgeDelayMs();
|
|
209
|
+
const hasPeers = Boolean(peers) && peers.ordered().length > 0;
|
|
210
|
+
const doHedge = shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers }) && upRes.status === 200 && upRes.body;
|
|
211
|
+
if (doHedge) {
|
|
212
|
+
const hr = await handleHedge({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res, hedgeDelayMs: d });
|
|
213
|
+
if (hr.handled) return;
|
|
214
|
+
if (hr.lastErr) lastErr = hr.lastErr;
|
|
215
|
+
if (hr.upRes === null) upRes = null;
|
|
216
|
+
else if (hr.upRes) upRes = hr.upRes;
|
|
217
|
+
}
|
|
218
|
+
if (upRes) {
|
|
219
|
+
const lr = await handleLocalRelay({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res });
|
|
220
|
+
if (lr.handled) return;
|
|
221
|
+
if (lr.lastErr) { lastErr = lr.lastErr; continue; }
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (canForwardPeers) {
|
|
226
|
+
const pr = await handlePeerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, stages, startedAt, plugins, res });
|
|
227
|
+
if (pr.handled) return;
|
|
228
|
+
}
|
|
229
|
+
if (groups) {
|
|
230
|
+
const br = await handleBroadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, stages, res, startedAt, plugins });
|
|
231
|
+
if (br.handled) return;
|
|
232
|
+
}
|
|
233
|
+
if (canFallback) { evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: lastErr?.message || `upstream ${lastErr?.status ?? 502}` }); continue; }
|
|
234
|
+
await handleExhaustedLocal({ res, body, lastErr, order, handlerCtx: { ...handlerCtx, model, reqId }, evt, logCall, mark, perf0, stages, done, requested, useAuto });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
await handleExhaustedAll({ res, body, lastErr, order, requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { run };
|
|
241
|
+
}
|