mslxdff 0.1.197 → 0.1.199

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.
Files changed (95) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/docs/cli_help_mini.md +146 -0
  3. package/package.json +1 -1
  4. package/src/auto.js +254 -254
  5. package/src/bench/cline-bench.js +42 -42
  6. package/src/bench/probe.js +70 -70
  7. package/src/bench/report.js +162 -162
  8. package/src/bench/runner.js +77 -77
  9. package/src/bench/via-probe.js +124 -124
  10. package/src/bench/via-routes.js +87 -87
  11. package/src/bench/workbuddy-bench.js +54 -54
  12. package/src/chat/engine.js +160 -160
  13. package/src/chat/gateway.js +163 -163
  14. package/src/chat/orchestrator.js +234 -234
  15. package/src/chat/prompt.js +70 -70
  16. package/src/chat/repl.js +88 -88
  17. package/src/chat/terminal.js +135 -135
  18. package/src/chat/tools.js +306 -306
  19. package/src/chat-pipeline/index.js +173 -173
  20. package/src/chat-pipeline/policy.js +84 -84
  21. package/src/chat-pipeline/serial-trial.js +8 -6
  22. package/src/cli/commands/group.js +249 -249
  23. package/src/cli/commands/model/picks.js +50 -50
  24. package/src/cli/commands/provider/bench-via.js +247 -247
  25. package/src/cli/commands/provider/bench.js +141 -141
  26. package/src/cli/commands/provider/index.js +137 -136
  27. package/src/cli/commands/provider/models.js +145 -145
  28. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  29. package/src/cli/commands/provider/raccoon-login.js +29 -1
  30. package/src/cli/commands/provider/raccoon-password.js +162 -0
  31. package/src/cli/commands/sync.js +310 -310
  32. package/src/cli/status.js +279 -279
  33. package/src/daemon.js +232 -232
  34. package/src/model-capabilities/enrich.js +86 -86
  35. package/src/model-capabilities/index.js +183 -183
  36. package/src/model-capabilities/parse.js +70 -70
  37. package/src/models.js +225 -225
  38. package/src/providers/AGENTS.md +49 -0
  39. package/src/providers/cline/auth.js +228 -228
  40. package/src/providers/cline/chat.js +307 -307
  41. package/src/providers/cline.js +2 -2
  42. package/src/providers/codearts/chat.js +182 -182
  43. package/src/providers/globalqwenwork/cosy.js +123 -123
  44. package/src/providers/globalqwenwork/crypto.js +218 -218
  45. package/src/providers/globalqwenwork/http.js +20 -20
  46. package/src/providers/globalqwenwork/rsa.js +54 -54
  47. package/src/providers/globalqwenwork/sse.js +268 -268
  48. package/src/providers/globalqwenwork/stream.js +130 -130
  49. package/src/providers/globalqwenwork/upstream.js +122 -122
  50. package/src/providers/keyring.js +60 -60
  51. package/src/providers/qoder/chat.js +203 -203
  52. package/src/providers/qoder/index.js +249 -249
  53. package/src/providers/qoder/sse.js +145 -145
  54. package/src/providers/qwenwork/account-store.js +133 -133
  55. package/src/providers/qwenwork/constants.js +67 -67
  56. package/src/providers/qwenwork/cosy.js +120 -120
  57. package/src/providers/qwenwork/crypto.js +218 -218
  58. package/src/providers/qwenwork/http.js +20 -20
  59. package/src/providers/qwenwork/index.js +337 -337
  60. package/src/providers/qwenwork/payload.js +144 -144
  61. package/src/providers/qwenwork/rsa.js +54 -54
  62. package/src/providers/qwenwork/sse.js +268 -268
  63. package/src/providers/qwenwork/stream.js +130 -130
  64. package/src/providers/qwenwork/upstream.js +120 -120
  65. package/src/providers/qwenwork.js +1 -1
  66. package/src/providers/raccoon/const.js +25 -0
  67. package/src/providers/raccoon/password-login.js +87 -0
  68. package/src/providers/registry.js +79 -79
  69. package/src/providers/workbuddy/chat.js +264 -264
  70. package/src/providers/workbuddy/reshape.js +152 -152
  71. package/src/providers/workbuddy.js +2 -2
  72. package/src/reasoning.js +32 -32
  73. package/src/routes/AGENTS.md +37 -0
  74. package/src/routes/chat/gateway.js +46 -46
  75. package/src/routes/chat/relay-pipeline.js +282 -282
  76. package/src/routes/chat/via-route-handler.js +146 -146
  77. package/src/routes/hedge.js +260 -260
  78. package/src/routes/models-route.js +167 -167
  79. package/src/routes/peers.js +335 -335
  80. package/src/routes/stream.js +347 -347
  81. package/src/runtime/bootstrap.js +67 -67
  82. package/src/runtime/provider-gate.js +36 -36
  83. package/src/runtime/providers-setup.js +168 -168
  84. package/src/server.js +78 -78
  85. package/src/state/schemas/allowlist.js +92 -92
  86. package/src/sync-opencode.js +280 -280
  87. package/src/transport/index.js +244 -244
  88. package/src/transport/pool.js +56 -56
  89. package/src/transport/retry.js +24 -24
  90. package/src/transport/sse.js +93 -93
  91. package/src/upstream-probe/display.js +52 -52
  92. package/src/upstream-probe/probe.js +49 -49
  93. package/src/upstream-probe/rotate.js +110 -110
  94. package/src/upstream-probe/start.js +45 -45
  95. package/src/upstream.js +293 -293
@@ -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
+ }
@@ -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
+ }