mslxdff 0.1.194 → 0.1.197

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 (98) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +254 -254
  4. package/src/bench/cline-bench.js +42 -42
  5. package/src/bench/probe.js +70 -70
  6. package/src/bench/report.js +162 -162
  7. package/src/bench/runner.js +77 -77
  8. package/src/bench/via-probe.js +124 -124
  9. package/src/bench/via-routes.js +87 -87
  10. package/src/bench/workbuddy-bench.js +54 -54
  11. package/src/chat/engine.js +160 -160
  12. package/src/chat/gateway.js +163 -163
  13. package/src/chat/orchestrator.js +234 -234
  14. package/src/chat/prompt.js +70 -70
  15. package/src/chat/repl.js +88 -88
  16. package/src/chat/terminal.js +135 -135
  17. package/src/chat/tools.js +306 -306
  18. package/src/chat-pipeline/index.js +173 -173
  19. package/src/chat-pipeline/policy.js +84 -84
  20. package/src/chat-pipeline/terminal-verdict.js +15 -6
  21. package/src/cli/commands/group.js +249 -249
  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/index.js +136 -136
  26. package/src/cli/commands/provider/models.js +145 -145
  27. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  28. package/src/cli/commands/provider/raccoon-login.js +6 -0
  29. package/src/cli/commands/sync.js +310 -310
  30. package/src/cli/status.js +279 -279
  31. package/src/daemon.js +232 -232
  32. package/src/model-capabilities/enrich.js +86 -86
  33. package/src/model-capabilities/index.js +183 -183
  34. package/src/model-capabilities/parse.js +70 -70
  35. package/src/models.js +225 -225
  36. package/src/providers/cline/auth.js +228 -228
  37. package/src/providers/cline/chat.js +307 -307
  38. package/src/providers/cline.js +2 -2
  39. package/src/providers/codearts/chat.js +182 -182
  40. package/src/providers/globalqwenwork/cosy.js +123 -123
  41. package/src/providers/globalqwenwork/crypto.js +218 -218
  42. package/src/providers/globalqwenwork/http.js +20 -20
  43. package/src/providers/globalqwenwork/rsa.js +54 -54
  44. package/src/providers/globalqwenwork/sse.js +268 -268
  45. package/src/providers/globalqwenwork/stream.js +130 -130
  46. package/src/providers/globalqwenwork/upstream.js +122 -122
  47. package/src/providers/keyring.js +60 -60
  48. package/src/providers/qoder/chat.js +203 -203
  49. package/src/providers/qoder/index.js +249 -249
  50. package/src/providers/qoder/sse.js +145 -145
  51. package/src/providers/qwenwork/account-store.js +133 -133
  52. package/src/providers/qwenwork/constants.js +67 -67
  53. package/src/providers/qwenwork/cosy.js +120 -120
  54. package/src/providers/qwenwork/crypto.js +218 -218
  55. package/src/providers/qwenwork/http.js +20 -20
  56. package/src/providers/qwenwork/index.js +337 -337
  57. package/src/providers/qwenwork/payload.js +144 -144
  58. package/src/providers/qwenwork/rsa.js +54 -54
  59. package/src/providers/qwenwork/sse.js +268 -268
  60. package/src/providers/qwenwork/stream.js +130 -130
  61. package/src/providers/qwenwork/upstream.js +120 -120
  62. package/src/providers/qwenwork.js +1 -1
  63. package/src/providers/raccoon/chat.js +28 -12
  64. package/src/providers/raccoon/const.js +8 -0
  65. package/src/providers/raccoon/credits.js +26 -12
  66. package/src/providers/raccoon/login.js +30 -0
  67. package/src/providers/registry.js +79 -79
  68. package/src/providers/workbuddy/auth.js +26 -1
  69. package/src/providers/workbuddy/chat.js +264 -248
  70. package/src/providers/workbuddy/index.js +1 -1
  71. package/src/providers/workbuddy/reshape.js +152 -152
  72. package/src/providers/workbuddy.js +2 -2
  73. package/src/reasoning.js +32 -32
  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
  96. package/docs/cli_help_mini.md +0 -145
  97. package/src/providers/AGENTS.md +0 -49
  98. 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
+ }