mslxdff 0.1.231 → 0.1.232

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/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 +145 -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/models.js +225 -225
  35. package/src/peers.js +345 -345
  36. package/src/providers/agentrouter.js +238 -238
  37. package/src/providers/base.js +164 -164
  38. package/src/providers/cline/auth.js +228 -228
  39. package/src/providers/cline/chat.js +307 -307
  40. package/src/providers/cline.js +2 -2
  41. package/src/providers/codearts/chat.js +207 -207
  42. package/src/providers/dispatcher.js +147 -147
  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 +202 -202
  67. package/src/providers/raccoon/credits.js +285 -285
  68. package/src/providers/registry.js +89 -89
  69. package/src/providers/workbuddy/auth.js +16 -0
  70. package/src/providers/workbuddy/chat.js +298 -264
  71. package/src/providers/workbuddy/client-headers.js +148 -0
  72. package/src/providers/workbuddy/desktop-info.js +17 -1
  73. package/src/providers/workbuddy/models.js +8 -20
  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 +175 -175
  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 -148
  103. package/src/providers/AGENTS.md +0 -49
  104. 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
+ }