mslxdff 0.1.88 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 controller = new AbortController();
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 res = await fetchImpl(`${baseUrl}/api/v1/chat/completions`, {
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(`sess_bench_${Date.now()}`, accessToken), Accept: "text/event-stream" },
14
- body: JSON.stringify({ model, messages: [{ role: "user", content: prompt }], stream: true, max_tokens: maxTokens, session_id: `sess_bench_${Date.now()}`, reasoning_effort: "high" }),
15
- signal: controller.signal,
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: Math.round(performance.now() - t0), tps: null, charsPerSec: null, tokens: null };
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 reader = res.body.getReader();
25
- const decoder = new TextDecoder();
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
- const totalMs = Math.round(performance.now() - t0);
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: Math.round(performance.now() - t0), tps: null, charsPerSec: null, tokens: null };
53
- } finally { clearTimeout(timer); }
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
  }
@@ -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
- // workbuddy 异形已在 defaultModelsPath 注入,若仍未命中则 candidates 已含
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 controller = new AbortController();
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) {
@@ -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 controller = new AbortController();
60
- const timer = setTimeout(() => controller.abort(new Error(`timeout ${timeoutMs}ms`)), timeoutMs);
61
- const fetchStart = typeof performance !== "undefined" && performance.now ? performance.now() : clock();
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
  }
@@ -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
- // 纯中继模式:A targetUrl+headers+body 发给 B,B 原样 fetch 到上游(不查 B 本地 providerConfigs)
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 controller = new AbortController();
62
- const timer = setTimeout(() => controller.abort(new Error(`timeout ${timeoutMs}ms`)), timeoutMs);
63
- const fetchStart = typeof performance !== "undefined" && performance.now ? performance.now() : clock();
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 controller = new AbortController();
109
- const timer = setTimeout(() => controller.abort(new Error(`timeout ${timeoutMs}ms`)), timeoutMs);
110
- const fetchStart = typeof performance !== "undefined" && performance.now ? performance.now() : clock();
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 controller = new AbortController();
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 fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal: controller.signal });
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: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - t0), tps: null, charsPerSec: null, tokens: null };
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
- // SSE stream parsing 复用 cline 逻辑
39
- const reader = res.body.getReader();
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
- const totalMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - t0);
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
- } finally { clearTimeout(timer); }
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
+ }