mslxdff 0.1.228 → 0.1.230

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 (104) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/anthropic/request-shape.js +13 -0
  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 +145 -145
  20. package/src/chat-pipeline/policy.js +84 -84
  21. package/src/cli/commands/group.js +266 -266
  22. package/src/cli/commands/model/picks.js +50 -50
  23. package/src/cli/commands/provider/bench-via.js +247 -247
  24. package/src/cli/commands/provider/bench.js +141 -141
  25. package/src/cli/commands/provider/cline-free.js +19 -9
  26. package/src/cli/commands/provider/index.js +139 -139
  27. package/src/cli/commands/provider/models.js +149 -149
  28. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  29. package/src/cli/commands/provider/raccoon-quota.js +57 -10
  30. package/src/cli/commands/sync.js +310 -310
  31. package/src/cli/status.js +279 -279
  32. package/src/daemon.js +246 -246
  33. package/src/model-capabilities/enrich.js +86 -86
  34. package/src/model-capabilities/index.js +183 -183
  35. package/src/model-capabilities/parse.js +70 -70
  36. package/src/models.js +225 -225
  37. package/src/peers.js +345 -345
  38. package/src/providers/agentrouter.js +231 -0
  39. package/src/providers/cline/allowlist-sync.js +76 -38
  40. package/src/providers/cline/auth.js +228 -228
  41. package/src/providers/cline/chat.js +307 -307
  42. package/src/providers/cline/free-catalog.js +9 -6
  43. package/src/providers/cline.js +2 -2
  44. package/src/providers/codearts/chat.js +207 -207
  45. package/src/providers/dispatcher.js +1 -1
  46. package/src/providers/globalqwenwork/cosy.js +123 -123
  47. package/src/providers/globalqwenwork/crypto.js +218 -218
  48. package/src/providers/globalqwenwork/http.js +20 -20
  49. package/src/providers/globalqwenwork/rsa.js +54 -54
  50. package/src/providers/globalqwenwork/sse.js +268 -268
  51. package/src/providers/globalqwenwork/stream.js +130 -130
  52. package/src/providers/globalqwenwork/upstream.js +122 -122
  53. package/src/providers/keyring.js +60 -60
  54. package/src/providers/qoder/chat.js +203 -203
  55. package/src/providers/qoder/index.js +249 -249
  56. package/src/providers/qoder/sse.js +145 -145
  57. package/src/providers/qwenwork/account-store.js +133 -133
  58. package/src/providers/qwenwork/constants.js +67 -67
  59. package/src/providers/qwenwork/cosy.js +120 -120
  60. package/src/providers/qwenwork/crypto.js +218 -218
  61. package/src/providers/qwenwork/http.js +20 -20
  62. package/src/providers/qwenwork/index.js +337 -337
  63. package/src/providers/qwenwork/payload.js +144 -144
  64. package/src/providers/qwenwork/rsa.js +54 -54
  65. package/src/providers/qwenwork/sse.js +268 -268
  66. package/src/providers/qwenwork/stream.js +130 -130
  67. package/src/providers/qwenwork/upstream.js +120 -120
  68. package/src/providers/qwenwork.js +1 -1
  69. package/src/providers/raccoon/const.js +22 -0
  70. package/src/providers/raccoon/credits.js +105 -5
  71. package/src/providers/raccoon/sse.js +1 -1
  72. package/src/providers/registry.js +93 -86
  73. package/src/providers/workbuddy/chat.js +264 -264
  74. package/src/providers/workbuddy/reshape.js +152 -152
  75. package/src/providers/workbuddy.js +2 -2
  76. package/src/reasoning.js +32 -32
  77. package/src/routes/chat/broadband-handler.js +80 -80
  78. package/src/routes/chat/gateway.js +46 -46
  79. package/src/routes/chat/relay-pipeline.js +282 -282
  80. package/src/routes/chat/via-route-handler.js +156 -156
  81. package/src/routes/hedge.js +260 -260
  82. package/src/routes/models-route.js +167 -167
  83. package/src/routes/peers.js +373 -373
  84. package/src/routes/stream.js +347 -347
  85. package/src/runtime/bootstrap.js +75 -75
  86. package/src/runtime/keep-warm.js +135 -135
  87. package/src/runtime/provider-gate.js +75 -75
  88. package/src/runtime/providers-setup.js +187 -187
  89. package/src/runtime/raccoon-checkin.js +72 -23
  90. package/src/server.js +85 -85
  91. package/src/state/schemas/allowlist.js +92 -92
  92. package/src/sync-opencode.js +280 -280
  93. package/src/transport/index.js +244 -244
  94. package/src/transport/pool.js +56 -56
  95. package/src/transport/retry.js +24 -24
  96. package/src/transport/sse.js +93 -93
  97. package/src/upstream-probe/display.js +52 -52
  98. package/src/upstream-probe/probe.js +49 -49
  99. package/src/upstream-probe/rotate.js +110 -110
  100. package/src/upstream-probe/start.js +45 -45
  101. package/src/upstream.js +355 -355
  102. package/docs/cli_help_mini.md +0 -151
  103. package/src/providers/AGENTS.md +0 -49
  104. package/src/routes/AGENTS.md +0 -37
@@ -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
+ }