mslxdff 0.1.158 → 0.1.160
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/bin/mslxdff.js +4 -4
- package/package.json +3 -3
- package/src/auto.js +254 -254
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/auto-race.js +1 -1
- package/src/chat-pipeline/index.js +123 -123
- package/src/chat-pipeline/policy.js +76 -76
- package/src/chat-pipeline/serial-trial.js +210 -210
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +124 -124
- package/src/cli/commands/provider/models.js +139 -139
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/stats.js +20 -5
- package/src/cli/commands/sync.js +232 -232
- package/src/cli/provider-row.js +2 -2
- package/src/cli/status.js +279 -279
- package/src/daemon.js +96 -96
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/models.js +225 -225
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +183 -183
- package/src/providers/qoder/index.js +230 -230
- package/src/providers/qoder/sse.js +103 -103
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +327 -327
- package/src/providers/qwenwork/payload.js +142 -142
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/registry.js +66 -66
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/chat.js +11 -10
- package/src/providers/zcode/const.js +1 -1
- package/src/providers/zcode/context-shape.js +162 -0
- package/src/providers/zcode/headers.js +38 -0
- package/src/reasoning.js +32 -32
- package/src/routes/chat/broadband-handler.js +3 -2
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/hedge-handler.js +3 -0
- package/src/routes/chat/local-handler.js +2 -0
- package/src/routes/chat/peer-handler.js +2 -0
- package/src/routes/chat/relay-pipeline.js +264 -250
- package/src/routes/chat/via-route-handler.js +146 -144
- package/src/routes/hedge.js +255 -255
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream-scan.js +86 -0
- package/src/routes/stream.js +393 -438
- package/src/runtime/bootstrap.js +45 -45
- package/src/runtime/provider-gate.js +33 -33
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +64 -64
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +289 -289
- package/src/usage/record.js +8 -1
- package/src/usage/report.js +39 -2
- package/docs/ARCHITECTURE.md +0 -426
- package/docs/FEATURE_TREE.md +0 -164
- package/docs/MOBILE.md +0 -82
- package/docs/adr/0001-reasoning-content-injection.md +0 -14
- package/docs/adr/0002-models-free-filter.md +0 -12
- package/docs/adr/0003-zero-state-no-auth.md +0 -10
- package/docs/adr/0004-bearer-token.md +0 -18
- package/docs/adr/0005-peer-mesh.md +0 -53
- package/docs/adr/0006-broadband-member.md +0 -103
- package/docs/adr/0007-multi-provider-prefix.md +0 -25
- package/docs/adr/0008-share-keys-to-peers.md +0 -52
- package/docs/adr/0009-chat-repl.md +0 -37
- package/docs/adr/0010-allowlist.md +0 -36
- package/docs/adr/0011-broadband-stream.md +0 -30
- package/docs/adr/0012-responses-endpoint-codex-sync.md +0 -48
- package/docs/adr/0013-node16-compat.md +0 -41
- package/docs/adr/0014-deepseek-provider.md +0 -52
- package/docs/adr/0015-upstream-probe-routing.md +0 -50
- package/docs/adr/0016-model-capabilities.md +0 -27
- package/docs/adr/0017-ai-sdk-upstream-engine.md +0 -59
- package/docs/adr/0018-zen-client-identity.md +0 -52
- package/docs/adr/0019-share-keys-always-lend.md +0 -59
- package/docs/adr/0020-zen-free-lane-agent-shape.md +0 -52
- package/docs/adr/0021-usage-report-jsonl.md +0 -47
- package/docs/adr/0022-models-capability-merge.md +0 -61
- package/docs/adr/0023-key-provider-default-direct.md +0 -58
- package/docs/adr/0024-node18-baseline.md +0 -63
- package/docs/adr/0025-workbuddy-authdir-follows-state.md +0 -71
- package/docs/adr/0026-cline-provider-id-unify.md +0 -67
- package/docs/adr/0027-codearts-provider.md +0 -48
- package/docs/adr/0028-traework-provider.md +0 -34
- package/docs/adr/0029-qoder-native-provider.md +0 -60
- package/docs/adr/0030-models-list-scoped-by-picks.md +0 -53
- package/docs/adr/0031-qoder-true-streaming.md +0 -50
- package/docs/adr/0032-generic-responses-channel.md +0 -72
- package/docs/adr/0033-cline-allowlist-auto-sync.md +0 -75
- package/docs/adr/0034-request-level-human-readable-observability.md +0 -60
- package/docs/adr/0035-sdk-channel-headers-timeout.md +0 -49
- package/docs/adr/0036-qoder-per-request-sticky-account.md +0 -82
- package/docs/adr/0037-qwenwork-independent-provider.md +0 -82
- package/docs/adr/0038-zcode-provider.md +0 -140
- package/docs/agents/domain.md +0 -51
- package/docs/agents/issue-tracker.md +0 -30
- package/docs/agents/triage-labels.md +0 -15
- package/docs/cli_help.md +0 -1391
- package/docs/cli_help_mini.md +0 -133
- package/docs/plans/bench-via-latency-2026-09-01.md +0 -215
- package/docs/plugins.md +0 -187
package/src/bench/via-probe.js
CHANGED
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
import { joinUrl } from "../providers/base.js";
|
|
2
|
-
import { computeMetrics, extractUsageFromJson } from "../metrics.js";
|
|
3
|
-
import { createTransport } from "../transport/index.js";
|
|
1
|
+
import { joinUrl } from "../providers/base.js";
|
|
2
|
+
import { computeMetrics, extractUsageFromJson } from "../metrics.js";
|
|
3
|
+
import { createTransport } from "../transport/index.js";
|
|
4
4
|
import { compatFetch } from "../compat.js";
|
|
5
|
-
|
|
6
|
-
function extractInnerMessage(bodyText) {
|
|
7
|
-
const t = String(bodyText || "");
|
|
8
|
-
try {
|
|
9
|
-
const j = JSON.parse(t);
|
|
10
|
-
const m = j?.error?.message || j?.error || j?.message || j?.data?.error || "";
|
|
11
|
-
if (typeof m === "string" && m.trim()) return m.trim().slice(0, 300);
|
|
12
|
-
if (typeof j?.error === "string") return j.error.slice(0, 300);
|
|
13
|
-
} catch {}
|
|
14
|
-
return t.slice(0, 300);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function classifyError(status, bodyText) {
|
|
18
|
-
const t = String(bodyText || "").slice(0, 500);
|
|
19
|
-
const low = t.toLowerCase();
|
|
20
|
-
if (status === 401) return { label: "鉴权失败", retryable: false };
|
|
21
|
-
if (status === 402 || /insufficient balance/i.test(t)) return { label: "余额不足", retryable: false };
|
|
22
|
-
if (low.includes("only available via cline")) return { label: "仅 Cline 客户端可用", retryable: false };
|
|
23
|
-
if (low.includes("invalid model format")) return { label: "模型格式错误", retryable: false };
|
|
24
|
-
if (status === 403) return { label: /insufficient/i.test(t) ? "余额不足" : "鉴权失败", retryable: false };
|
|
25
|
-
if (status === 429) return { label: "限流", retryable: true };
|
|
26
|
-
if (status >= 500) return { label: `上游错误 ${status}`, retryable: true };
|
|
27
|
-
if (status === 404) return { label: "模型不存在", retryable: false };
|
|
28
|
-
return { label: `HTTP ${status}`, retryable: false };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function viaProbe({
|
|
32
|
-
peerUrl,
|
|
33
|
-
token,
|
|
34
|
-
providerId,
|
|
35
|
-
model,
|
|
36
|
-
prompt = "hi",
|
|
37
|
-
maxTokens = 5,
|
|
38
|
-
timeoutMs = 30000,
|
|
39
|
-
fetchImpl = compatFetch,
|
|
40
|
-
clock = Date.now,
|
|
41
|
-
shareKeys,
|
|
42
|
-
shareKeysHeader,
|
|
43
|
-
relayTarget,
|
|
44
|
-
relayHeaders,
|
|
45
|
-
relayBody,
|
|
46
|
-
targetUrl,
|
|
47
|
-
} = {}) {
|
|
48
|
-
const base = String(peerUrl || "").replace(/\/+$/, "");
|
|
49
|
-
if (!base) return { ok: false, label: "配置错误", error: "missing peerUrl", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
50
|
-
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
51
|
-
const rt = String(relayTarget || targetUrl || "").trim();
|
|
52
|
-
if (rt) {
|
|
53
|
-
const rh = relayHeaders && typeof relayHeaders === "object" ? relayHeaders : {};
|
|
54
|
-
const rb = relayBody !== undefined ? relayBody : null;
|
|
55
|
-
const relayUrl = joinUrl(base, "/v1/relay");
|
|
56
|
-
const relayHeadersOut = { "Content-Type": "application/json", Accept: "application/json" };
|
|
57
|
-
if (token) relayHeadersOut.Authorization = `Bearer ${token}`;
|
|
58
|
-
const payload = { targetUrl: rt, method: "POST", headers: rh, body: rb };
|
|
59
|
-
try {
|
|
60
|
-
const res = await tr.request({ url: relayUrl, method: "POST", headers: relayHeadersOut, body: payload, stream: false });
|
|
61
|
-
const ttfbMs = res.ttfbMs;
|
|
62
|
-
const totalMs = res.totalMs;
|
|
63
|
-
if (!res.ok) {
|
|
64
|
-
let txt = ""; try { txt = await res.text(); } catch {}
|
|
65
|
-
const cls = classifyError(res.status, txt);
|
|
66
|
-
const msg = extractInnerMessage(txt) || `HTTP ${res.status}`;
|
|
67
|
-
return { ok: false, status: res.status, label: cls.label, error: msg, ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
68
|
-
}
|
|
69
|
-
let txt = ""; let j = {};
|
|
70
|
-
try { txt = await res.text(); j = JSON.parse(txt); } catch { j = {}; }
|
|
71
|
-
const relayStatus = res.headers.get("x-mslxdff-relay-status") ? Number(res.headers.get("x-mslxdff-relay-status")) : res.status;
|
|
72
|
-
if (relayStatus >= 400) {
|
|
73
|
-
const cls = classifyError(relayStatus, txt);
|
|
74
|
-
const msg = extractInnerMessage(txt) || `HTTP ${relayStatus}`;
|
|
75
|
-
return { ok: false, status: relayStatus, label: cls.label, error: msg, ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
76
|
-
}
|
|
77
|
-
const usage = extractUsageFromJson(j);
|
|
78
|
-
const content = j?.choices?.[0]?.message?.content || j?.choices?.[0]?.text || txt || "";
|
|
79
|
-
const chars = typeof content === "string" ? content.length : 0;
|
|
80
|
-
const pt = usage?.prompt_tokens ?? null;
|
|
81
|
-
const ct = usage?.completion_tokens ?? null;
|
|
82
|
-
const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens: pt, completionTokens: ct, chars });
|
|
83
|
-
const totalTokens = usage?.total_tokens ?? (pt !== null && ct !== null ? pt + ct : null);
|
|
84
|
-
return { ok: true, status: relayStatus, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: { prompt: pt, completion: ct, total: totalTokens }, chars };
|
|
85
|
-
} catch (e) {
|
|
86
|
-
const msg = e?.message || String(e);
|
|
87
|
-
const isTimeout = /timeout|abort/i.test(msg);
|
|
88
|
-
return { ok: false, label: isTimeout ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs: null, totalMs: null, tps: null, charsPerSec: null, tokens: null };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (!model) return { ok: false, label: "配置错误", error: "missing model", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
92
|
-
let rawModel = String(model).trim();
|
|
93
|
-
if (providerId && rawModel.startsWith(`${providerId}/`)) rawModel = rawModel.slice(providerId.length + 1);
|
|
94
|
-
const url = joinUrl(base, "/v1/chat/completions");
|
|
95
|
-
const body = { model: rawModel, stream: false, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens };
|
|
96
|
-
const headers = { "Content-Type": "application/json", Accept: "application/json" };
|
|
97
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
98
|
-
const sk = shareKeysHeader || shareKeys;
|
|
99
|
-
if (sk) headers["x-mslxdff-share-keys"] = String(sk);
|
|
100
|
-
try {
|
|
101
|
-
const res = await tr.request({ url, method: "POST", headers, body, stream: false });
|
|
102
|
-
const ttfbMs = res.ttfbMs;
|
|
103
|
-
const totalMs = res.totalMs;
|
|
104
|
-
if (!res.ok) {
|
|
105
|
-
let txt = ""; try { txt = await res.text(); } catch {}
|
|
106
|
-
const cls = classifyError(res.status, txt);
|
|
107
|
-
const msg = extractInnerMessage(txt) || `HTTP ${res.status}`;
|
|
108
|
-
return { ok: false, status: res.status, label: cls.label, error: msg, ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
109
|
-
}
|
|
110
|
-
let json = {}; let txt = "";
|
|
111
|
-
try { txt = await res.text(); json = JSON.parse(txt); } catch { json = {}; }
|
|
112
|
-
const usage = extractUsageFromJson(json);
|
|
113
|
-
const content = json?.choices?.[0]?.message?.content || json?.choices?.[0]?.text || txt || "";
|
|
114
|
-
const chars = typeof content === "string" ? content.length : 0;
|
|
115
|
-
const promptTokens = usage?.prompt_tokens ?? null;
|
|
116
|
-
const completionTokens = usage?.completion_tokens ?? null;
|
|
117
|
-
const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens, completionTokens, chars });
|
|
118
|
-
const totalTokens = usage?.total_tokens ?? (promptTokens !== null && completionTokens !== null ? promptTokens + completionTokens : null);
|
|
119
|
-
return { ok: true, status: res.status, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: { prompt: promptTokens, completion: completionTokens, total: totalTokens }, chars };
|
|
120
|
-
} catch (e) {
|
|
121
|
-
const msg = e?.message || String(e);
|
|
122
|
-
const isTimeout = /timeout|abort/i.test(msg);
|
|
123
|
-
return { ok: false, label: isTimeout ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs: null, totalMs: null, tps: null, charsPerSec: null, tokens: null };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
5
|
+
|
|
6
|
+
function extractInnerMessage(bodyText) {
|
|
7
|
+
const t = String(bodyText || "");
|
|
8
|
+
try {
|
|
9
|
+
const j = JSON.parse(t);
|
|
10
|
+
const m = j?.error?.message || j?.error || j?.message || j?.data?.error || "";
|
|
11
|
+
if (typeof m === "string" && m.trim()) return m.trim().slice(0, 300);
|
|
12
|
+
if (typeof j?.error === "string") return j.error.slice(0, 300);
|
|
13
|
+
} catch {}
|
|
14
|
+
return t.slice(0, 300);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function classifyError(status, bodyText) {
|
|
18
|
+
const t = String(bodyText || "").slice(0, 500);
|
|
19
|
+
const low = t.toLowerCase();
|
|
20
|
+
if (status === 401) return { label: "鉴权失败", retryable: false };
|
|
21
|
+
if (status === 402 || /insufficient balance/i.test(t)) return { label: "余额不足", retryable: false };
|
|
22
|
+
if (low.includes("only available via cline")) return { label: "仅 Cline 客户端可用", retryable: false };
|
|
23
|
+
if (low.includes("invalid model format")) return { label: "模型格式错误", retryable: false };
|
|
24
|
+
if (status === 403) return { label: /insufficient/i.test(t) ? "余额不足" : "鉴权失败", retryable: false };
|
|
25
|
+
if (status === 429) return { label: "限流", retryable: true };
|
|
26
|
+
if (status >= 500) return { label: `上游错误 ${status}`, retryable: true };
|
|
27
|
+
if (status === 404) return { label: "模型不存在", retryable: false };
|
|
28
|
+
return { label: `HTTP ${status}`, retryable: false };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function viaProbe({
|
|
32
|
+
peerUrl,
|
|
33
|
+
token,
|
|
34
|
+
providerId,
|
|
35
|
+
model,
|
|
36
|
+
prompt = "hi",
|
|
37
|
+
maxTokens = 5,
|
|
38
|
+
timeoutMs = 30000,
|
|
39
|
+
fetchImpl = compatFetch,
|
|
40
|
+
clock = Date.now,
|
|
41
|
+
shareKeys,
|
|
42
|
+
shareKeysHeader,
|
|
43
|
+
relayTarget,
|
|
44
|
+
relayHeaders,
|
|
45
|
+
relayBody,
|
|
46
|
+
targetUrl,
|
|
47
|
+
} = {}) {
|
|
48
|
+
const base = String(peerUrl || "").replace(/\/+$/, "");
|
|
49
|
+
if (!base) return { ok: false, label: "配置错误", error: "missing peerUrl", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
50
|
+
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
51
|
+
const rt = String(relayTarget || targetUrl || "").trim();
|
|
52
|
+
if (rt) {
|
|
53
|
+
const rh = relayHeaders && typeof relayHeaders === "object" ? relayHeaders : {};
|
|
54
|
+
const rb = relayBody !== undefined ? relayBody : null;
|
|
55
|
+
const relayUrl = joinUrl(base, "/v1/relay");
|
|
56
|
+
const relayHeadersOut = { "Content-Type": "application/json", Accept: "application/json" };
|
|
57
|
+
if (token) relayHeadersOut.Authorization = `Bearer ${token}`;
|
|
58
|
+
const payload = { targetUrl: rt, method: "POST", headers: rh, body: rb };
|
|
59
|
+
try {
|
|
60
|
+
const res = await tr.request({ url: relayUrl, method: "POST", headers: relayHeadersOut, body: payload, stream: false });
|
|
61
|
+
const ttfbMs = res.ttfbMs;
|
|
62
|
+
const totalMs = res.totalMs;
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
let txt = ""; try { txt = await res.text(); } catch {}
|
|
65
|
+
const cls = classifyError(res.status, txt);
|
|
66
|
+
const msg = extractInnerMessage(txt) || `HTTP ${res.status}`;
|
|
67
|
+
return { ok: false, status: res.status, label: cls.label, error: msg, ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
68
|
+
}
|
|
69
|
+
let txt = ""; let j = {};
|
|
70
|
+
try { txt = await res.text(); j = JSON.parse(txt); } catch { j = {}; }
|
|
71
|
+
const relayStatus = res.headers.get("x-mslxdff-relay-status") ? Number(res.headers.get("x-mslxdff-relay-status")) : res.status;
|
|
72
|
+
if (relayStatus >= 400) {
|
|
73
|
+
const cls = classifyError(relayStatus, txt);
|
|
74
|
+
const msg = extractInnerMessage(txt) || `HTTP ${relayStatus}`;
|
|
75
|
+
return { ok: false, status: relayStatus, label: cls.label, error: msg, ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
76
|
+
}
|
|
77
|
+
const usage = extractUsageFromJson(j);
|
|
78
|
+
const content = j?.choices?.[0]?.message?.content || j?.choices?.[0]?.text || txt || "";
|
|
79
|
+
const chars = typeof content === "string" ? content.length : 0;
|
|
80
|
+
const pt = usage?.prompt_tokens ?? null;
|
|
81
|
+
const ct = usage?.completion_tokens ?? null;
|
|
82
|
+
const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens: pt, completionTokens: ct, chars });
|
|
83
|
+
const totalTokens = usage?.total_tokens ?? (pt !== null && ct !== null ? pt + ct : null);
|
|
84
|
+
return { ok: true, status: relayStatus, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: { prompt: pt, completion: ct, total: totalTokens }, chars };
|
|
85
|
+
} catch (e) {
|
|
86
|
+
const msg = e?.message || String(e);
|
|
87
|
+
const isTimeout = /timeout|abort/i.test(msg);
|
|
88
|
+
return { ok: false, label: isTimeout ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs: null, totalMs: null, tps: null, charsPerSec: null, tokens: null };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!model) return { ok: false, label: "配置错误", error: "missing model", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
92
|
+
let rawModel = String(model).trim();
|
|
93
|
+
if (providerId && rawModel.startsWith(`${providerId}/`)) rawModel = rawModel.slice(providerId.length + 1);
|
|
94
|
+
const url = joinUrl(base, "/v1/chat/completions");
|
|
95
|
+
const body = { model: rawModel, stream: false, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens };
|
|
96
|
+
const headers = { "Content-Type": "application/json", Accept: "application/json" };
|
|
97
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
98
|
+
const sk = shareKeysHeader || shareKeys;
|
|
99
|
+
if (sk) headers["x-mslxdff-share-keys"] = String(sk);
|
|
100
|
+
try {
|
|
101
|
+
const res = await tr.request({ url, method: "POST", headers, body, stream: false });
|
|
102
|
+
const ttfbMs = res.ttfbMs;
|
|
103
|
+
const totalMs = res.totalMs;
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
let txt = ""; try { txt = await res.text(); } catch {}
|
|
106
|
+
const cls = classifyError(res.status, txt);
|
|
107
|
+
const msg = extractInnerMessage(txt) || `HTTP ${res.status}`;
|
|
108
|
+
return { ok: false, status: res.status, label: cls.label, error: msg, ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
109
|
+
}
|
|
110
|
+
let json = {}; let txt = "";
|
|
111
|
+
try { txt = await res.text(); json = JSON.parse(txt); } catch { json = {}; }
|
|
112
|
+
const usage = extractUsageFromJson(json);
|
|
113
|
+
const content = json?.choices?.[0]?.message?.content || json?.choices?.[0]?.text || txt || "";
|
|
114
|
+
const chars = typeof content === "string" ? content.length : 0;
|
|
115
|
+
const promptTokens = usage?.prompt_tokens ?? null;
|
|
116
|
+
const completionTokens = usage?.completion_tokens ?? null;
|
|
117
|
+
const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens, completionTokens, chars });
|
|
118
|
+
const totalTokens = usage?.total_tokens ?? (promptTokens !== null && completionTokens !== null ? promptTokens + completionTokens : null);
|
|
119
|
+
return { ok: true, status: res.status, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: { prompt: promptTokens, completion: completionTokens, total: totalTokens }, chars };
|
|
120
|
+
} catch (e) {
|
|
121
|
+
const msg = e?.message || String(e);
|
|
122
|
+
const isTimeout = /timeout|abort/i.test(msg);
|
|
123
|
+
return { ok: false, label: isTimeout ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs: null, totalMs: null, tps: null, charsPerSec: null, tokens: null };
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/bench/via-routes.js
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { join, dirname } from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { defaultStateFile } from "../state/store.js";
|
|
5
|
-
import { atomicWriteSync } from "../state/persist.js";
|
|
6
|
-
|
|
7
|
-
export function defaultViaRoutesFile() {
|
|
8
|
-
if (process.env.MSLXDFF_VIA_ROUTES_FILE) return String(process.env.MSLXDFF_VIA_ROUTES_FILE).trim();
|
|
9
|
-
const sf = defaultStateFile();
|
|
10
|
-
return join(dirname(sf), "via-routes.json");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function viaTtlMs() {
|
|
14
|
-
const raw = process.env.MSLXDFF_VIA_ROUTE_TTL_MS;
|
|
15
|
-
if (raw === undefined || raw === null || raw === "") return 0;
|
|
16
|
-
const s = String(raw).trim().toLowerCase();
|
|
17
|
-
if (s === "0" || s === "off" || s === "false" || s === "no" || s === "disable" || s === "disabled") return 0;
|
|
18
|
-
const n = Number(s);
|
|
19
|
-
if (Number.isInteger(n) && n >= 0) return n;
|
|
20
|
-
return 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function loadViaRoutes(file) {
|
|
24
|
-
const f = file || defaultViaRoutesFile();
|
|
25
|
-
try {
|
|
26
|
-
if (!existsSync(f)) return { version: 1, at: null, routes: {}, meta: {} };
|
|
27
|
-
const j = JSON.parse(readFileSync(f, "utf8"));
|
|
28
|
-
if (j && typeof j === "object" && j.routes && typeof j.routes === "object") return j;
|
|
29
|
-
if (j && typeof j === "object" && !j.routes) return { version: 1, at: j.at || null, routes: j, meta: {} };
|
|
30
|
-
return { version: 1, at: null, routes: {}, meta: {} };
|
|
31
|
-
} catch {
|
|
32
|
-
return { version: 1, at: null, routes: {}, meta: {} };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function getViaRoute(model, { file, ttlMs } = {}) {
|
|
37
|
-
const id = String(model || "").trim();
|
|
38
|
-
if (!id) return null;
|
|
39
|
-
const f = file || defaultViaRoutesFile();
|
|
40
|
-
const data = loadViaRoutes(f);
|
|
41
|
-
const entry = data.routes?.[id];
|
|
42
|
-
if (!entry) return null;
|
|
43
|
-
const t = ttlMs !== undefined ? ttlMs : viaTtlMs();
|
|
44
|
-
if (t > 0 && entry.at) {
|
|
45
|
-
const atMs = Date.parse(entry.at);
|
|
46
|
-
if (Number.isFinite(atMs) && Date.now() - atMs > t) return null;
|
|
47
|
-
}
|
|
48
|
-
return entry;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function saveViaRoutes(results, { file, meta } = {}) {
|
|
52
|
-
const f = file || defaultViaRoutesFile();
|
|
53
|
-
const now = new Date().toISOString();
|
|
54
|
-
const prev = loadViaRoutes(f);
|
|
55
|
-
const nextRoutes = { ...(prev.routes || {}) };
|
|
56
|
-
for (const r of results || []) {
|
|
57
|
-
const id = String(r.model || r.id || "").trim();
|
|
58
|
-
if (!id) continue;
|
|
59
|
-
const best = String(r.best || "direct").trim() || "direct";
|
|
60
|
-
const direct = r.direct ? { ok: Boolean(r.direct.ok), ttfbMs: r.direct.ttfbMs ?? r.direct.totalMs ?? null, totalMs: r.direct.totalMs ?? null, label: r.direct.label || null, error: r.direct.error ? String(r.direct.error).slice(0, 300) : null } : null;
|
|
61
|
-
const via = {};
|
|
62
|
-
for (const [k, v] of Object.entries(r.via || {})) {
|
|
63
|
-
via[k] = v?.ok ? { ok: true, ttfbMs: v.ttfbMs ?? v.totalMs ?? null, totalMs: v.totalMs ?? null } : { ok: false, ttfbMs: v?.ttfbMs ?? null, totalMs: v?.totalMs ?? null, label: v?.label || v?.error || "offline" };
|
|
64
|
-
}
|
|
65
|
-
nextRoutes[id] = {
|
|
66
|
-
best,
|
|
67
|
-
direct,
|
|
68
|
-
via,
|
|
69
|
-
deltaMs: r.deltaMs ?? null,
|
|
70
|
-
provider: r.provider || id.split("/")[0] || "",
|
|
71
|
-
at: now,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
const out = {
|
|
75
|
-
version: 1,
|
|
76
|
-
at: now,
|
|
77
|
-
routes: nextRoutes,
|
|
78
|
-
meta: meta || prev.meta || {},
|
|
79
|
-
};
|
|
80
|
-
atomicWriteSync(f, out);
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function clearViaRoutes(file) {
|
|
85
|
-
const f = file || defaultViaRoutesFile();
|
|
86
|
-
atomicWriteSync(f, { version: 1, at: new Date().toISOString(), routes: {}, meta: {} });
|
|
87
|
-
}
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { defaultStateFile } from "../state/store.js";
|
|
5
|
+
import { atomicWriteSync } from "../state/persist.js";
|
|
6
|
+
|
|
7
|
+
export function defaultViaRoutesFile() {
|
|
8
|
+
if (process.env.MSLXDFF_VIA_ROUTES_FILE) return String(process.env.MSLXDFF_VIA_ROUTES_FILE).trim();
|
|
9
|
+
const sf = defaultStateFile();
|
|
10
|
+
return join(dirname(sf), "via-routes.json");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function viaTtlMs() {
|
|
14
|
+
const raw = process.env.MSLXDFF_VIA_ROUTE_TTL_MS;
|
|
15
|
+
if (raw === undefined || raw === null || raw === "") return 0;
|
|
16
|
+
const s = String(raw).trim().toLowerCase();
|
|
17
|
+
if (s === "0" || s === "off" || s === "false" || s === "no" || s === "disable" || s === "disabled") return 0;
|
|
18
|
+
const n = Number(s);
|
|
19
|
+
if (Number.isInteger(n) && n >= 0) return n;
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadViaRoutes(file) {
|
|
24
|
+
const f = file || defaultViaRoutesFile();
|
|
25
|
+
try {
|
|
26
|
+
if (!existsSync(f)) return { version: 1, at: null, routes: {}, meta: {} };
|
|
27
|
+
const j = JSON.parse(readFileSync(f, "utf8"));
|
|
28
|
+
if (j && typeof j === "object" && j.routes && typeof j.routes === "object") return j;
|
|
29
|
+
if (j && typeof j === "object" && !j.routes) return { version: 1, at: j.at || null, routes: j, meta: {} };
|
|
30
|
+
return { version: 1, at: null, routes: {}, meta: {} };
|
|
31
|
+
} catch {
|
|
32
|
+
return { version: 1, at: null, routes: {}, meta: {} };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getViaRoute(model, { file, ttlMs } = {}) {
|
|
37
|
+
const id = String(model || "").trim();
|
|
38
|
+
if (!id) return null;
|
|
39
|
+
const f = file || defaultViaRoutesFile();
|
|
40
|
+
const data = loadViaRoutes(f);
|
|
41
|
+
const entry = data.routes?.[id];
|
|
42
|
+
if (!entry) return null;
|
|
43
|
+
const t = ttlMs !== undefined ? ttlMs : viaTtlMs();
|
|
44
|
+
if (t > 0 && entry.at) {
|
|
45
|
+
const atMs = Date.parse(entry.at);
|
|
46
|
+
if (Number.isFinite(atMs) && Date.now() - atMs > t) return null;
|
|
47
|
+
}
|
|
48
|
+
return entry;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function saveViaRoutes(results, { file, meta } = {}) {
|
|
52
|
+
const f = file || defaultViaRoutesFile();
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
const prev = loadViaRoutes(f);
|
|
55
|
+
const nextRoutes = { ...(prev.routes || {}) };
|
|
56
|
+
for (const r of results || []) {
|
|
57
|
+
const id = String(r.model || r.id || "").trim();
|
|
58
|
+
if (!id) continue;
|
|
59
|
+
const best = String(r.best || "direct").trim() || "direct";
|
|
60
|
+
const direct = r.direct ? { ok: Boolean(r.direct.ok), ttfbMs: r.direct.ttfbMs ?? r.direct.totalMs ?? null, totalMs: r.direct.totalMs ?? null, label: r.direct.label || null, error: r.direct.error ? String(r.direct.error).slice(0, 300) : null } : null;
|
|
61
|
+
const via = {};
|
|
62
|
+
for (const [k, v] of Object.entries(r.via || {})) {
|
|
63
|
+
via[k] = v?.ok ? { ok: true, ttfbMs: v.ttfbMs ?? v.totalMs ?? null, totalMs: v.totalMs ?? null } : { ok: false, ttfbMs: v?.ttfbMs ?? null, totalMs: v?.totalMs ?? null, label: v?.label || v?.error || "offline" };
|
|
64
|
+
}
|
|
65
|
+
nextRoutes[id] = {
|
|
66
|
+
best,
|
|
67
|
+
direct,
|
|
68
|
+
via,
|
|
69
|
+
deltaMs: r.deltaMs ?? null,
|
|
70
|
+
provider: r.provider || id.split("/")[0] || "",
|
|
71
|
+
at: now,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const out = {
|
|
75
|
+
version: 1,
|
|
76
|
+
at: now,
|
|
77
|
+
routes: nextRoutes,
|
|
78
|
+
meta: meta || prev.meta || {},
|
|
79
|
+
};
|
|
80
|
+
atomicWriteSync(f, out);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function clearViaRoutes(file) {
|
|
85
|
+
const f = file || defaultViaRoutesFile();
|
|
86
|
+
atomicWriteSync(f, { version: 1, at: new Date().toISOString(), routes: {}, meta: {} });
|
|
87
|
+
}
|
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
import { computeMetrics } from "../metrics.js";
|
|
2
|
-
import { createTransport } from "../transport/index.js";
|
|
1
|
+
import { computeMetrics } from "../metrics.js";
|
|
2
|
+
import { createTransport } from "../transport/index.js";
|
|
3
3
|
import { compatFetch } from "../compat.js";
|
|
4
|
-
|
|
5
|
-
function buildWorkbuddyHeaders(apiKey, auth) {
|
|
6
|
-
const h = {
|
|
7
|
-
"Content-Type": "application/json",
|
|
8
|
-
Accept: "text/event-stream",
|
|
9
|
-
"User-Agent": "CLI/2.115.0 WorkBuddy/2.115.0",
|
|
10
|
-
Origin: "https://www.codebuddy.cn",
|
|
11
|
-
Referer: "https://www.codebuddy.cn/",
|
|
12
|
-
"X-Product": "SaaS",
|
|
13
|
-
};
|
|
14
|
-
if (apiKey) h["Authorization"] = `Bearer ${apiKey}`;
|
|
15
|
-
if (auth?.uid) h["X-User-Id"] = auth.uid;
|
|
16
|
-
h["X-Domain"] = auth?.domain || "www.codebuddy.cn";
|
|
17
|
-
if (auth?.enterpriseId) { h["X-Enterprise-Id"] = auth.enterpriseId; h["X-Tenant-Id"] = auth.enterpriseId; }
|
|
18
|
-
return h;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function sseContent(obj) {
|
|
22
|
-
const c = obj?.choices?.[0]?.delta?.content || obj?.choices?.[0]?.message?.content || "";
|
|
23
|
-
return typeof c === "string" ? c : "";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function workbuddyBenchOne({ baseUrl, chatPath = "/v2/chat/completions", model, apiKey, auth, prompt = "hi", maxTokens = 5, timeoutMs = 30000, fetchImpl = compatFetch }) {
|
|
27
|
-
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
28
|
-
let ttfbMs = null;
|
|
29
|
-
let totalMs = null;
|
|
30
|
-
let content = "";
|
|
31
|
-
try {
|
|
32
|
-
const url = String(baseUrl).replace(/\/+$/, "") + String(chatPath || "/v2/chat/completions");
|
|
33
|
-
const headers = buildWorkbuddyHeaders(apiKey, auth);
|
|
34
|
-
const rawModel = String(model || "").trim();
|
|
35
|
-
const body = { model: rawModel, stream: true, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens };
|
|
36
|
-
const res = await tr.request({ url, method: "POST", headers, body, stream: true });
|
|
37
|
-
if (!res.ok) {
|
|
38
|
-
let txt = "";
|
|
39
|
-
try { txt = await res.text(); } catch {}
|
|
40
|
-
const label = res.status === 401 ? "鉴权失败" : res.status === 402 ? "余额不足" : res.status === 429 ? "限流" : res.status >= 500 ? `上游错误 ${res.status}` : `HTTP ${res.status}`;
|
|
41
|
-
return { id: model, ok: false, status: res.status, label, error: txt.slice(0, 300), ttfbMs, totalMs: res.totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
42
|
-
}
|
|
43
|
-
for await (const ev of res.stream()) {
|
|
44
|
-
try { content += sseContent(JSON.parse(ev)); } catch {}
|
|
45
|
-
}
|
|
46
|
-
ttfbMs = res.ttfbMs;
|
|
47
|
-
totalMs = res.totalMs;
|
|
48
|
-
const chars = content.length;
|
|
49
|
-
const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens: null, completionTokens: null, chars });
|
|
50
|
-
return { id: model, ok: true, status: 200, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: null, chars };
|
|
51
|
-
} catch (e) {
|
|
52
|
-
const msg = e?.message || String(e);
|
|
53
|
-
return { id: model, ok: false, label: /timeout|abort/i.test(msg) ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
4
|
+
|
|
5
|
+
function buildWorkbuddyHeaders(apiKey, auth) {
|
|
6
|
+
const h = {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
Accept: "text/event-stream",
|
|
9
|
+
"User-Agent": "CLI/2.115.0 WorkBuddy/2.115.0",
|
|
10
|
+
Origin: "https://www.codebuddy.cn",
|
|
11
|
+
Referer: "https://www.codebuddy.cn/",
|
|
12
|
+
"X-Product": "SaaS",
|
|
13
|
+
};
|
|
14
|
+
if (apiKey) h["Authorization"] = `Bearer ${apiKey}`;
|
|
15
|
+
if (auth?.uid) h["X-User-Id"] = auth.uid;
|
|
16
|
+
h["X-Domain"] = auth?.domain || "www.codebuddy.cn";
|
|
17
|
+
if (auth?.enterpriseId) { h["X-Enterprise-Id"] = auth.enterpriseId; h["X-Tenant-Id"] = auth.enterpriseId; }
|
|
18
|
+
return h;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sseContent(obj) {
|
|
22
|
+
const c = obj?.choices?.[0]?.delta?.content || obj?.choices?.[0]?.message?.content || "";
|
|
23
|
+
return typeof c === "string" ? c : "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function workbuddyBenchOne({ baseUrl, chatPath = "/v2/chat/completions", model, apiKey, auth, prompt = "hi", maxTokens = 5, timeoutMs = 30000, fetchImpl = compatFetch }) {
|
|
27
|
+
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
28
|
+
let ttfbMs = null;
|
|
29
|
+
let totalMs = null;
|
|
30
|
+
let content = "";
|
|
31
|
+
try {
|
|
32
|
+
const url = String(baseUrl).replace(/\/+$/, "") + String(chatPath || "/v2/chat/completions");
|
|
33
|
+
const headers = buildWorkbuddyHeaders(apiKey, auth);
|
|
34
|
+
const rawModel = String(model || "").trim();
|
|
35
|
+
const body = { model: rawModel, stream: true, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens };
|
|
36
|
+
const res = await tr.request({ url, method: "POST", headers, body, stream: true });
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
let txt = "";
|
|
39
|
+
try { txt = await res.text(); } catch {}
|
|
40
|
+
const label = res.status === 401 ? "鉴权失败" : res.status === 402 ? "余额不足" : res.status === 429 ? "限流" : res.status >= 500 ? `上游错误 ${res.status}` : `HTTP ${res.status}`;
|
|
41
|
+
return { id: model, ok: false, status: res.status, label, error: txt.slice(0, 300), ttfbMs, totalMs: res.totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
42
|
+
}
|
|
43
|
+
for await (const ev of res.stream()) {
|
|
44
|
+
try { content += sseContent(JSON.parse(ev)); } catch {}
|
|
45
|
+
}
|
|
46
|
+
ttfbMs = res.ttfbMs;
|
|
47
|
+
totalMs = res.totalMs;
|
|
48
|
+
const chars = content.length;
|
|
49
|
+
const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens: null, completionTokens: null, chars });
|
|
50
|
+
return { id: model, ok: true, status: 200, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: null, chars };
|
|
51
|
+
} catch (e) {
|
|
52
|
+
const msg = e?.message || String(e);
|
|
53
|
+
return { id: model, ok: false, label: /timeout|abort/i.test(msg) ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
54
|
+
}
|
|
55
|
+
}
|