mslxdff 0.1.231 → 0.1.233

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 (117) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +264 -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 +155 -145
  19. package/src/chat-pipeline/policy.js +84 -84
  20. package/src/cli/commands/group.js +266 -266
  21. package/src/cli/commands/model/picks.js +50 -50
  22. package/src/cli/commands/provider/bench-via.js +247 -247
  23. package/src/cli/commands/provider/bench.js +141 -141
  24. package/src/cli/commands/provider/index.js +139 -139
  25. package/src/cli/commands/provider/models.js +149 -149
  26. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  27. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  28. package/src/cli/commands/sync.js +310 -310
  29. package/src/cli/status.js +279 -279
  30. package/src/daemon.js +246 -246
  31. package/src/model-capabilities/enrich.js +86 -86
  32. package/src/model-capabilities/index.js +183 -183
  33. package/src/model-capabilities/parse.js +70 -70
  34. package/src/model-trace.js +10 -2
  35. package/src/models.js +292 -225
  36. package/src/peers.js +345 -345
  37. package/src/providers/agentrouter.js +238 -238
  38. package/src/providers/base.js +164 -164
  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 +207 -207
  43. package/src/providers/dispatcher.js +147 -147
  44. package/src/providers/globalqwenwork/cosy.js +123 -123
  45. package/src/providers/globalqwenwork/crypto.js +218 -218
  46. package/src/providers/globalqwenwork/http.js +20 -20
  47. package/src/providers/globalqwenwork/rsa.js +54 -54
  48. package/src/providers/globalqwenwork/sse.js +268 -268
  49. package/src/providers/globalqwenwork/stream.js +130 -130
  50. package/src/providers/globalqwenwork/upstream.js +122 -122
  51. package/src/providers/keyring.js +60 -60
  52. package/src/providers/qoder/account-store.js +18 -0
  53. package/src/providers/qoder/chat.js +292 -203
  54. package/src/providers/qoder/checkin.js +25 -12
  55. package/src/providers/qoder/index.js +311 -249
  56. package/src/providers/qoder/models.js +64 -2
  57. package/src/providers/qoder/oauth.js +42 -1
  58. package/src/providers/qoder/payload.js +116 -80
  59. package/src/providers/qoder/request.js +7 -2
  60. package/src/providers/qoder/session.js +26 -10
  61. package/src/providers/qoder/sse.js +233 -145
  62. package/src/providers/qoder/sticky.js +5 -2
  63. package/src/providers/qoder/wire-messages.js +242 -0
  64. package/src/providers/qwenwork/account-store.js +133 -133
  65. package/src/providers/qwenwork/constants.js +67 -67
  66. package/src/providers/qwenwork/cosy.js +120 -120
  67. package/src/providers/qwenwork/crypto.js +218 -218
  68. package/src/providers/qwenwork/http.js +20 -20
  69. package/src/providers/qwenwork/index.js +337 -337
  70. package/src/providers/qwenwork/payload.js +144 -144
  71. package/src/providers/qwenwork/rsa.js +54 -54
  72. package/src/providers/qwenwork/sse.js +268 -268
  73. package/src/providers/qwenwork/stream.js +130 -130
  74. package/src/providers/qwenwork/upstream.js +120 -120
  75. package/src/providers/qwenwork.js +1 -1
  76. package/src/providers/raccoon/chat.js +1 -1
  77. package/src/providers/raccoon/const.js +219 -202
  78. package/src/providers/raccoon/credits.js +285 -285
  79. package/src/providers/raccoon/index.js +15 -2
  80. package/src/providers/raccoon/models.js +68 -17
  81. package/src/providers/registry.js +89 -89
  82. package/src/providers/workbuddy/auth.js +16 -0
  83. package/src/providers/workbuddy/chat.js +298 -264
  84. package/src/providers/workbuddy/client-headers.js +148 -0
  85. package/src/providers/workbuddy/desktop-info.js +17 -1
  86. package/src/providers/workbuddy/models.js +8 -20
  87. package/src/providers/workbuddy/reshape.js +152 -152
  88. package/src/providers/workbuddy.js +2 -2
  89. package/src/reasoning.js +32 -32
  90. package/src/routes/chat/broadband-handler.js +80 -80
  91. package/src/routes/chat/gateway.js +46 -46
  92. package/src/routes/chat/relay-pipeline.js +282 -282
  93. package/src/routes/chat/via-route-handler.js +156 -156
  94. package/src/routes/hedge.js +260 -260
  95. package/src/routes/models-route.js +167 -167
  96. package/src/routes/peers.js +373 -373
  97. package/src/routes/stream.js +364 -347
  98. package/src/runtime/bootstrap.js +75 -75
  99. package/src/runtime/keep-warm.js +135 -135
  100. package/src/runtime/provider-gate.js +75 -75
  101. package/src/runtime/providers-setup.js +187 -187
  102. package/src/runtime/raccoon-checkin.js +175 -175
  103. package/src/server.js +85 -85
  104. package/src/state/schemas/allowlist.js +92 -92
  105. package/src/sync-opencode.js +280 -280
  106. package/src/transport/index.js +244 -244
  107. package/src/transport/pool.js +56 -56
  108. package/src/transport/retry.js +24 -24
  109. package/src/transport/sse.js +93 -93
  110. package/src/upstream-probe/display.js +52 -52
  111. package/src/upstream-probe/probe.js +49 -49
  112. package/src/upstream-probe/rotate.js +110 -110
  113. package/src/upstream-probe/start.js +45 -45
  114. package/src/upstream.js +355 -355
  115. package/docs/cli_help_mini.md +0 -148
  116. package/src/providers/AGENTS.md +0 -49
  117. package/src/routes/AGENTS.md +0 -37
@@ -1,238 +1,238 @@
1
- // AgentRouter 定制供应商:OpenAI 兼容聚合网关,有四道硬门槛(2026-10-21 现网 curl 实测):
2
- // ① 客户端身份白名单 —— UA 为 `mslxdff`/`curl`/浏览器一律 401 `unauthorized client detected`,
3
- // 只放行 `opencode/*` 与 `claude-cli/<ver> (external…)`;
4
- // ② 模型分端点 —— `deepseek-v4-flash` 吃 /v1/chat/completions,`gpt-6.1-sol`/`gpt-6-astra`
5
- // 对该端点回 400「请改用 /v1/responses」,只在 responses 通道可用;
6
- // ③ 内容闸 —— 正文含 `x-anthropic` 字样稳定 500 `sensitive words detected`(连 user 消息也拦,
7
- // 与 workbuddy 只扫 system/assistant 不同),见 stripAgentRouterSensitive;
8
- // ④ 传输层 —— 上游对 Node/undici 的**新建 TCP 连接**选择性丢包(现网对照:undici 0/4~0/6
9
- // 全 connect 超时,curl 同刻 100% 200,走系统代理亦不稳)⇒ 本家 fetchImpl 缺省换
10
- // curl-fetch 子进程通道(`MSLXDFF_AGENTROUTER_FETCH=undici` 可切回)。
11
- // 故不复用 generic(其 UA 写死 `mslxdff`、responses 判定只认 models.dev npm 索引),这里自组装,
12
- // 底层仍复用 base.js 的 runner 与 responses-channel 的协议互转。
13
- import { createKeyRing } from "./keyring.js";
14
- import { loadProviderBaseUrl, loadProviderKeys } from "../state.js";
15
- import { createChatRunner, createAgent, envInt, getUndici, joinUrl, collectApiKeysGeneric } from "./base.js";
16
- import { createResponsesChannel, resolveResponsesPath } from "./responses-channel.js";
17
- import { createCurlFetch } from "./curl-fetch.js";
18
- import { compatFetch } from "../compat.js";
19
- import { opencodeUa } from "../opencode-identity.js";
20
- import { joinModelId } from "./model-id.js";
21
-
22
- const { UndiciFetch } = getUndici();
23
-
24
- export const DEFAULT_AGENTROUTER_BASE = "https://agentrouter.org";
25
- export const MODELS_PATH = "/v1/models";
26
- export const CHAT_PATH = "/v1/chat/completions";
27
- export const RESPONSES_PATH = "/v1/responses";
28
-
29
- // 只吃 responses 的模型族:现网实测 gpt-6* 打 chat 端点回 400 并显式要求改 /v1/responses。
30
- const RESPONSES_PREFIXES = ["gpt-6"];
31
-
32
- export function isAgentRouterResponsesModel(model) {
33
- const m = String(model || "").trim().toLowerCase();
34
- if (!m) return false;
35
- return RESPONSES_PREFIXES.some((p) => m.startsWith(p));
36
- }
37
-
38
- // baseUrl 归一:缺省取 state/env,剥掉尾部 `/v1`(路径常量自带 `/v1`,避免 /v1/v1)
39
- export function resolveAgentRouterBase(baseUrl) {
40
- const raw = baseUrl || loadProviderBaseUrl("agentrouter") || DEFAULT_AGENTROUTER_BASE;
41
- return String(raw).trim().replace(/\/+$/, "").replace(/\/v1$/i, "");
42
- }
43
-
44
- // responses 通道内部两处 stream 判据不一致(响应通道用 `!== false` 决定读法,body 转换用
45
- // `=== true` 决定发什么):客户端不显式给 stream 时,会去把未转换的上游 JSON 当 chat 形状读。
46
- // 这里先把意图钉成布尔,两条路径就同源了。
47
- export function normalizeResponsesBody(body) {
48
- if (!body || typeof body !== "object") return body;
49
- return { ...body, stream: body.stream === true };
50
- }
51
-
52
- // AgentRouter 内容闸:稳定拦截正文里含 `x-anthropic` 字样的请求(2026-10-21 现网实测:
53
- // `500 sensitive words detected` / `400 content-blocked`,连 `x-anthropic` 一个词也拦;
54
- // `x-google`/`x-meta`/`x-openai`/`anthropic` 均放行——这是对 Anthropic 内部计费头/代理指纹的反代理风控)。
55
- // 与 workbuddy 的 CC_SENTENCES 剥离同类,但**必须覆盖所有 role**(workbuddy 只扫 system/assistant,
56
- // 而 agentrouter 连 user/tool 消息里出现该串也拦)。用零宽空格断开子串匹配、不动其余内容。
57
- // 返回新对象(messages 浅拷贝),不污染调用方 body(dispatcher 可能复用同一 body 做兜底重试)。
58
- const AR_SENSITIVE = /x-anthropic/gi;
59
-
60
- export function stripAgentRouterSensitive(obj) {
61
- if (!obj || typeof obj !== "object") return obj;
62
- if (!Array.isArray(obj.messages)) return obj;
63
- const mask = (text) => (typeof text === "string" && AR_SENSITIVE.test(text) ? text.replace(AR_SENSITIVE, "x​anthropic") : text);
64
- const out = { ...obj, messages: obj.messages.map((m) => {
65
- if (!m || typeof m !== "object") return m;
66
- const nm = { ...m };
67
- if (typeof m.content === "string") nm.content = mask(m.content);
68
- else if (Array.isArray(m.content)) nm.content = m.content.map((p) => (p && typeof p === "object" && typeof p.text === "string" ? { ...p, text: mask(p.text) } : p));
69
- if (typeof m.name === "string") nm.name = mask(m.name);
70
- return nm;
71
- }) };
72
- return out;
73
- }
74
-
75
- export function createAgentRouterProvider({
76
- id = "agentrouter",
77
- baseUrl,
78
- apiKeys,
79
- apiKey,
80
- connectTimeoutMs = envInt("MSLXDFF_AGENTROUTER_TIMEOUT_MS", 60_000),
81
- cooldownMs = envInt("MSLXDFF_AGENTROUTER_COOLDOWN_MS", 30_000),
82
- fetchImpl,
83
- file,
84
- } = {}) {
85
- if (id !== "agentrouter") throw new Error(`agentrouter provider id mismatch: ${id}`);
86
- const resolvedBase = resolveAgentRouterBase(baseUrl);
87
- // 传输层缺省走 curl 子进程(见文件头门槛④):上游对 undici 新建连接选择性丢包,
88
- // 实测 curl 同刻稳定。`MSLXDFF_AGENTROUTER_FETCH=undici` 切回 undici(配好上游修复后或对照排查用)。
89
- const useCurl = !fetchImpl && String(process.env.MSLXDFF_AGENTROUTER_FETCH || "curl").toLowerCase() !== "undici";
90
- if (!fetchImpl) fetchImpl = useCurl ? createCurlFetch() : (UndiciFetch || compatFetch);
91
-
92
- const ring = createKeyRing(
93
- collectApiKeysGeneric(id, apiKeys, apiKey, (pid) => loadProviderKeys(pid, file ? { file } : {})),
94
- { cooldownMs },
95
- );
96
-
97
- let agent = null; let dispatcher = null;
98
- {
99
- const a = createAgent({
100
- keepAliveTimeout: envInt("MSLXDFF_AGENTROUTER_KEEPALIVE_TIMEOUT", 30_000),
101
- keepAliveMaxTimeout: envInt("MSLXDFF_AGENTROUTER_KEEPALIVE_MAX_TIMEOUT", 60_000),
102
- connections: envInt("MSLXDFF_AGENTROUTER_KEEPALIVE_CONNECTIONS", 20),
103
- });
104
- agent = a.agent; dispatcher = a.dispatcher;
105
- }
106
-
107
- // 身份 UA:白名单只认 opencode / claude-cli,缺了恒 401。单独 env 可覆盖,不联动全局。
108
- function identityUa() {
109
- return process.env.MSLXDFF_AGENTROUTER_UA || opencodeUa();
110
- }
111
-
112
- function buildHeaders(body, key) {
113
- const isStream = body?.stream !== false;
114
- const h = {
115
- "Content-Type": "application/json",
116
- Accept: isStream ? "text/event-stream" : "*/*",
117
- "User-Agent": identityUa(),
118
- };
119
- if (key) h.Authorization = `Bearer ${key}`;
120
- return h;
121
- }
122
-
123
- function jsonHeaders(key) {
124
- const h = { Accept: "application/json", "User-Agent": identityUa() };
125
- if (key) h.Authorization = `Bearer ${key}`;
126
- return h;
127
- }
128
-
129
- // 网络重试:agentrouter 只有 1 把 key,任何一次瞬时连接抖动(现网实测 ECONNRESET 偶发,
130
- // 上游高频时会触发)若直接 onError,= 立刻冷却 30s、把后续请求全挡成
131
- // "all keys in cooldown"。补 attempts=2(对齐 generic/cline/workbuddy),让网络层重试先扛一下。
132
- const chatRetry = { network: { attempts: 2, delayMs: 300 }, 429: { attempts: 1, delayMs: 100 }, 502: { attempts: 1, delayMs: 100 }, 503: { attempts: 1, delayMs: 100 }, 504: { attempts: 1, delayMs: 100 } };
133
-
134
- const { runChat } = createChatRunner({
135
- id, ring, cooldownMs, retry: chatRetry, fetchImpl, dispatcher, buildHeaders,
136
- getUrl: () => joinUrl(resolvedBase, CHAT_PATH),
137
- connectTimeoutMs,
138
- // 非流式走原生(SDK 通道只服务 opencode zen 的形状),responses 另走专用通道
139
- sdkDispatch: { enabled: false },
140
- });
141
-
142
- const responses = createResponsesChannel({
143
- id,
144
- url: joinUrl(resolvedBase, resolveResponsesPath(id, RESPONSES_PATH)),
145
- connectTimeoutMs,
146
- cooldownMs,
147
- retry: chatRetry,
148
- // 关 SDK:其缺省会强塞 include:["reasoning.encrypted_content"] / store:false,agentrouter 未验证支持
149
- sdkEnabled: false,
150
- buildHeaders,
151
- fetchImpl,
152
- dispatcher,
153
- });
154
-
155
- async function chat(body, opts) {
156
- const sourceKey = `MSLXDFF_AGENTROUTER_KEY`;
157
- const clean = stripAgentRouterSensitive(body);
158
- if (isAgentRouterResponsesModel(clean?.model)) return responses.run(normalizeResponsesBody(clean), ring, sourceKey, opts);
159
- return runChat(clean, ring, sourceKey);
160
- }
161
-
162
- async function chatWithKeys(body, keys, opts) {
163
- const tmp = createKeyRing(keys, { cooldownMs });
164
- const clean = stripAgentRouterSensitive(body);
165
- if (isAgentRouterResponsesModel(clean?.model)) return responses.run(normalizeResponsesBody(clean), tmp, "shared provider keys", opts);
166
- const scoped = createChatRunner({
167
- id, ring: tmp, cooldownMs, retry: chatRetry, fetchImpl, dispatcher, buildHeaders,
168
- getUrl: () => joinUrl(resolvedBase, CHAT_PATH),
169
- connectTimeoutMs,
170
- sdkDispatch: { enabled: false },
171
- });
172
- return scoped.runChat(clean, tmp, "shared provider keys");
173
- }
174
-
175
- let modelsCache = { at: 0, list: [] };
176
- const MODELS_TTL_MS = 10 * 60 * 1000;
177
-
178
- async function fetchModels() {
179
- const controller = new AbortController();
180
- const timer = setTimeout(() => controller.abort(new Error(`${id} models timed out`)), 15_000);
181
- try {
182
- const key = ring.next() || ring.keys[0];
183
- const opts = { headers: jsonHeaders(key), signal: controller.signal };
184
- if (dispatcher) opts.dispatcher = dispatcher;
185
- const res = await fetchImpl(joinUrl(resolvedBase, MODELS_PATH), opts);
186
- if (!res.ok) throw new Error(`${id} models: http ${res.status}`);
187
- const json = await res.json().catch(() => ({}));
188
- return Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
189
- } finally {
190
- clearTimeout(timer);
191
- }
192
- }
193
-
194
- async function listModels() {
195
- if (modelsCache.list.length && Date.now() - modelsCache.at < MODELS_TTL_MS) return modelsCache.list;
196
- let raw = [];
197
- try {
198
- raw = await fetchModels();
199
- } catch {
200
- return modelsCache.list; // 拉取失败给上次缓存(首次为空),不阻断启动
201
- }
202
- const list = raw
203
- .filter((m) => m && typeof m.id === "string" && m.id.trim())
204
- .map((m) => ({
205
- id: joinModelId(id, m.id),
206
- object: "model",
207
- name: m.name || m.id,
208
- owned_by: m.owned_by || "agentrouter",
209
- ...(m.created ? { created: m.created } : {}),
210
- }));
211
- modelsCache = { at: Date.now(), list };
212
- return list;
213
- }
214
-
215
- async function preheat() {
216
- try {
217
- const m = await listModels();
218
- return { ok: m.length > 0, models: m.length };
219
- } catch (err) {
220
- return { ok: false, error: String(err?.message || err).slice(0, 120) };
221
- }
222
- }
223
-
224
- async function close() { if (agent?.close) try { await agent.close(); } catch {} }
225
-
226
- return {
227
- id,
228
- chat,
229
- chatWithKeys,
230
- listModels,
231
- preheat,
232
- close,
233
- agent,
234
- keyRing: ring,
235
- baseUrl: resolvedBase,
236
- responsesUrl: responses.url,
237
- };
238
- }
1
+ // AgentRouter 定制供应商:OpenAI 兼容聚合网关,有四道硬门槛(2026-10-21 现网 curl 实测):
2
+ // ① 客户端身份白名单 —— UA 为 `mslxdff`/`curl`/浏览器一律 401 `unauthorized client detected`,
3
+ // 只放行 `opencode/*` 与 `claude-cli/<ver> (external…)`;
4
+ // ② 模型分端点 —— `deepseek-v4-flash` 吃 /v1/chat/completions,`gpt-6.1-sol`/`gpt-6-astra`
5
+ // 对该端点回 400「请改用 /v1/responses」,只在 responses 通道可用;
6
+ // ③ 内容闸 —— 正文含 `x-anthropic` 字样稳定 500 `sensitive words detected`(连 user 消息也拦,
7
+ // 与 workbuddy 只扫 system/assistant 不同),见 stripAgentRouterSensitive;
8
+ // ④ 传输层 —— 上游对 Node/undici 的**新建 TCP 连接**选择性丢包(现网对照:undici 0/4~0/6
9
+ // 全 connect 超时,curl 同刻 100% 200,走系统代理亦不稳)⇒ 本家 fetchImpl 缺省换
10
+ // curl-fetch 子进程通道(`MSLXDFF_AGENTROUTER_FETCH=undici` 可切回)。
11
+ // 故不复用 generic(其 UA 写死 `mslxdff`、responses 判定只认 models.dev npm 索引),这里自组装,
12
+ // 底层仍复用 base.js 的 runner 与 responses-channel 的协议互转。
13
+ import { createKeyRing } from "./keyring.js";
14
+ import { loadProviderBaseUrl, loadProviderKeys } from "../state.js";
15
+ import { createChatRunner, createAgent, envInt, getUndici, joinUrl, collectApiKeysGeneric } from "./base.js";
16
+ import { createResponsesChannel, resolveResponsesPath } from "./responses-channel.js";
17
+ import { createCurlFetch } from "./curl-fetch.js";
18
+ import { compatFetch } from "../compat.js";
19
+ import { opencodeUa } from "../opencode-identity.js";
20
+ import { joinModelId } from "./model-id.js";
21
+
22
+ const { UndiciFetch } = getUndici();
23
+
24
+ export const DEFAULT_AGENTROUTER_BASE = "https://agentrouter.org";
25
+ export const MODELS_PATH = "/v1/models";
26
+ export const CHAT_PATH = "/v1/chat/completions";
27
+ export const RESPONSES_PATH = "/v1/responses";
28
+
29
+ // 只吃 responses 的模型族:现网实测 gpt-6* 打 chat 端点回 400 并显式要求改 /v1/responses。
30
+ const RESPONSES_PREFIXES = ["gpt-6"];
31
+
32
+ export function isAgentRouterResponsesModel(model) {
33
+ const m = String(model || "").trim().toLowerCase();
34
+ if (!m) return false;
35
+ return RESPONSES_PREFIXES.some((p) => m.startsWith(p));
36
+ }
37
+
38
+ // baseUrl 归一:缺省取 state/env,剥掉尾部 `/v1`(路径常量自带 `/v1`,避免 /v1/v1)
39
+ export function resolveAgentRouterBase(baseUrl) {
40
+ const raw = baseUrl || loadProviderBaseUrl("agentrouter") || DEFAULT_AGENTROUTER_BASE;
41
+ return String(raw).trim().replace(/\/+$/, "").replace(/\/v1$/i, "");
42
+ }
43
+
44
+ // responses 通道内部两处 stream 判据不一致(响应通道用 `!== false` 决定读法,body 转换用
45
+ // `=== true` 决定发什么):客户端不显式给 stream 时,会去把未转换的上游 JSON 当 chat 形状读。
46
+ // 这里先把意图钉成布尔,两条路径就同源了。
47
+ export function normalizeResponsesBody(body) {
48
+ if (!body || typeof body !== "object") return body;
49
+ return { ...body, stream: body.stream === true };
50
+ }
51
+
52
+ // AgentRouter 内容闸:稳定拦截正文里含 `x-anthropic` 字样的请求(2026-10-21 现网实测:
53
+ // `500 sensitive words detected` / `400 content-blocked`,连 `x-anthropic` 一个词也拦;
54
+ // `x-google`/`x-meta`/`x-openai`/`anthropic` 均放行——这是对 Anthropic 内部计费头/代理指纹的反代理风控)。
55
+ // 与 workbuddy 的 CC_SENTENCES 剥离同类,但**必须覆盖所有 role**(workbuddy 只扫 system/assistant,
56
+ // 而 agentrouter 连 user/tool 消息里出现该串也拦)。用零宽空格断开子串匹配、不动其余内容。
57
+ // 返回新对象(messages 浅拷贝),不污染调用方 body(dispatcher 可能复用同一 body 做兜底重试)。
58
+ const AR_SENSITIVE = /x-anthropic/gi;
59
+
60
+ export function stripAgentRouterSensitive(obj) {
61
+ if (!obj || typeof obj !== "object") return obj;
62
+ if (!Array.isArray(obj.messages)) return obj;
63
+ const mask = (text) => (typeof text === "string" && AR_SENSITIVE.test(text) ? text.replace(AR_SENSITIVE, "x​anthropic") : text);
64
+ const out = { ...obj, messages: obj.messages.map((m) => {
65
+ if (!m || typeof m !== "object") return m;
66
+ const nm = { ...m };
67
+ if (typeof m.content === "string") nm.content = mask(m.content);
68
+ else if (Array.isArray(m.content)) nm.content = m.content.map((p) => (p && typeof p === "object" && typeof p.text === "string" ? { ...p, text: mask(p.text) } : p));
69
+ if (typeof m.name === "string") nm.name = mask(m.name);
70
+ return nm;
71
+ }) };
72
+ return out;
73
+ }
74
+
75
+ export function createAgentRouterProvider({
76
+ id = "agentrouter",
77
+ baseUrl,
78
+ apiKeys,
79
+ apiKey,
80
+ connectTimeoutMs = envInt("MSLXDFF_AGENTROUTER_TIMEOUT_MS", 60_000),
81
+ cooldownMs = envInt("MSLXDFF_AGENTROUTER_COOLDOWN_MS", 30_000),
82
+ fetchImpl,
83
+ file,
84
+ } = {}) {
85
+ if (id !== "agentrouter") throw new Error(`agentrouter provider id mismatch: ${id}`);
86
+ const resolvedBase = resolveAgentRouterBase(baseUrl);
87
+ // 传输层缺省走 curl 子进程(见文件头门槛④):上游对 undici 新建连接选择性丢包,
88
+ // 实测 curl 同刻稳定。`MSLXDFF_AGENTROUTER_FETCH=undici` 切回 undici(配好上游修复后或对照排查用)。
89
+ const useCurl = !fetchImpl && String(process.env.MSLXDFF_AGENTROUTER_FETCH || "curl").toLowerCase() !== "undici";
90
+ if (!fetchImpl) fetchImpl = useCurl ? createCurlFetch() : (UndiciFetch || compatFetch);
91
+
92
+ const ring = createKeyRing(
93
+ collectApiKeysGeneric(id, apiKeys, apiKey, (pid) => loadProviderKeys(pid, file ? { file } : {})),
94
+ { cooldownMs },
95
+ );
96
+
97
+ let agent = null; let dispatcher = null;
98
+ {
99
+ const a = createAgent({
100
+ keepAliveTimeout: envInt("MSLXDFF_AGENTROUTER_KEEPALIVE_TIMEOUT", 30_000),
101
+ keepAliveMaxTimeout: envInt("MSLXDFF_AGENTROUTER_KEEPALIVE_MAX_TIMEOUT", 60_000),
102
+ connections: envInt("MSLXDFF_AGENTROUTER_KEEPALIVE_CONNECTIONS", 20),
103
+ });
104
+ agent = a.agent; dispatcher = a.dispatcher;
105
+ }
106
+
107
+ // 身份 UA:白名单只认 opencode / claude-cli,缺了恒 401。单独 env 可覆盖,不联动全局。
108
+ function identityUa() {
109
+ return process.env.MSLXDFF_AGENTROUTER_UA || opencodeUa();
110
+ }
111
+
112
+ function buildHeaders(body, key) {
113
+ const isStream = body?.stream !== false;
114
+ const h = {
115
+ "Content-Type": "application/json",
116
+ Accept: isStream ? "text/event-stream" : "*/*",
117
+ "User-Agent": identityUa(),
118
+ };
119
+ if (key) h.Authorization = `Bearer ${key}`;
120
+ return h;
121
+ }
122
+
123
+ function jsonHeaders(key) {
124
+ const h = { Accept: "application/json", "User-Agent": identityUa() };
125
+ if (key) h.Authorization = `Bearer ${key}`;
126
+ return h;
127
+ }
128
+
129
+ // 网络重试:agentrouter 只有 1 把 key,任何一次瞬时连接抖动(现网实测 ECONNRESET 偶发,
130
+ // 上游高频时会触发)若直接 onError,= 立刻冷却 30s、把后续请求全挡成
131
+ // "all keys in cooldown"。补 attempts=2(对齐 generic/cline/workbuddy),让网络层重试先扛一下。
132
+ const chatRetry = { network: { attempts: 2, delayMs: 300 }, 429: { attempts: 1, delayMs: 100 }, 502: { attempts: 1, delayMs: 100 }, 503: { attempts: 1, delayMs: 100 }, 504: { attempts: 1, delayMs: 100 } };
133
+
134
+ const { runChat } = createChatRunner({
135
+ id, ring, cooldownMs, retry: chatRetry, fetchImpl, dispatcher, buildHeaders,
136
+ getUrl: () => joinUrl(resolvedBase, CHAT_PATH),
137
+ connectTimeoutMs,
138
+ // 非流式走原生(SDK 通道只服务 opencode zen 的形状),responses 另走专用通道
139
+ sdkDispatch: { enabled: false },
140
+ });
141
+
142
+ const responses = createResponsesChannel({
143
+ id,
144
+ url: joinUrl(resolvedBase, resolveResponsesPath(id, RESPONSES_PATH)),
145
+ connectTimeoutMs,
146
+ cooldownMs,
147
+ retry: chatRetry,
148
+ // 关 SDK:其缺省会强塞 include:["reasoning.encrypted_content"] / store:false,agentrouter 未验证支持
149
+ sdkEnabled: false,
150
+ buildHeaders,
151
+ fetchImpl,
152
+ dispatcher,
153
+ });
154
+
155
+ async function chat(body, opts) {
156
+ const sourceKey = `MSLXDFF_AGENTROUTER_KEY`;
157
+ const clean = stripAgentRouterSensitive(body);
158
+ if (isAgentRouterResponsesModel(clean?.model)) return responses.run(normalizeResponsesBody(clean), ring, sourceKey, opts);
159
+ return runChat(clean, ring, sourceKey);
160
+ }
161
+
162
+ async function chatWithKeys(body, keys, opts) {
163
+ const tmp = createKeyRing(keys, { cooldownMs });
164
+ const clean = stripAgentRouterSensitive(body);
165
+ if (isAgentRouterResponsesModel(clean?.model)) return responses.run(normalizeResponsesBody(clean), tmp, "shared provider keys", opts);
166
+ const scoped = createChatRunner({
167
+ id, ring: tmp, cooldownMs, retry: chatRetry, fetchImpl, dispatcher, buildHeaders,
168
+ getUrl: () => joinUrl(resolvedBase, CHAT_PATH),
169
+ connectTimeoutMs,
170
+ sdkDispatch: { enabled: false },
171
+ });
172
+ return scoped.runChat(clean, tmp, "shared provider keys");
173
+ }
174
+
175
+ let modelsCache = { at: 0, list: [] };
176
+ const MODELS_TTL_MS = 10 * 60 * 1000;
177
+
178
+ async function fetchModels() {
179
+ const controller = new AbortController();
180
+ const timer = setTimeout(() => controller.abort(new Error(`${id} models timed out`)), 15_000);
181
+ try {
182
+ const key = ring.next() || ring.keys[0];
183
+ const opts = { headers: jsonHeaders(key), signal: controller.signal };
184
+ if (dispatcher) opts.dispatcher = dispatcher;
185
+ const res = await fetchImpl(joinUrl(resolvedBase, MODELS_PATH), opts);
186
+ if (!res.ok) throw new Error(`${id} models: http ${res.status}`);
187
+ const json = await res.json().catch(() => ({}));
188
+ return Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [];
189
+ } finally {
190
+ clearTimeout(timer);
191
+ }
192
+ }
193
+
194
+ async function listModels() {
195
+ if (modelsCache.list.length && Date.now() - modelsCache.at < MODELS_TTL_MS) return modelsCache.list;
196
+ let raw = [];
197
+ try {
198
+ raw = await fetchModels();
199
+ } catch {
200
+ return modelsCache.list; // 拉取失败给上次缓存(首次为空),不阻断启动
201
+ }
202
+ const list = raw
203
+ .filter((m) => m && typeof m.id === "string" && m.id.trim())
204
+ .map((m) => ({
205
+ id: joinModelId(id, m.id),
206
+ object: "model",
207
+ name: m.name || m.id,
208
+ owned_by: m.owned_by || "agentrouter",
209
+ ...(m.created ? { created: m.created } : {}),
210
+ }));
211
+ modelsCache = { at: Date.now(), list };
212
+ return list;
213
+ }
214
+
215
+ async function preheat() {
216
+ try {
217
+ const m = await listModels();
218
+ return { ok: m.length > 0, models: m.length };
219
+ } catch (err) {
220
+ return { ok: false, error: String(err?.message || err).slice(0, 120) };
221
+ }
222
+ }
223
+
224
+ async function close() { if (agent?.close) try { await agent.close(); } catch {} }
225
+
226
+ return {
227
+ id,
228
+ chat,
229
+ chatWithKeys,
230
+ listModels,
231
+ preheat,
232
+ close,
233
+ agent,
234
+ keyRing: ring,
235
+ baseUrl: resolvedBase,
236
+ responsesUrl: responses.url,
237
+ };
238
+ }