mslxdff 0.1.236 → 0.1.237

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 (102) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +264 -264
  4. package/src/autostart.js +62 -15
  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 +155 -155
  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/index.js +139 -139
  26. package/src/cli/commands/provider/models.js +149 -149
  27. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  28. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  29. package/src/cli/commands/sync.js +310 -310
  30. package/src/cli/status.js +282 -282
  31. package/src/daemon.js +246 -246
  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 +293 -293
  36. package/src/peers.js +389 -389
  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/chat.js +292 -292
  53. package/src/providers/qoder/index.js +311 -311
  54. package/src/providers/qoder/sse.js +233 -233
  55. package/src/providers/qwenwork/account-store.js +133 -133
  56. package/src/providers/qwenwork/constants.js +67 -67
  57. package/src/providers/qwenwork/cosy.js +120 -120
  58. package/src/providers/qwenwork/crypto.js +218 -218
  59. package/src/providers/qwenwork/http.js +20 -20
  60. package/src/providers/qwenwork/index.js +337 -337
  61. package/src/providers/qwenwork/payload.js +144 -144
  62. package/src/providers/qwenwork/rsa.js +54 -54
  63. package/src/providers/qwenwork/sse.js +268 -268
  64. package/src/providers/qwenwork/stream.js +130 -130
  65. package/src/providers/qwenwork/upstream.js +120 -120
  66. package/src/providers/qwenwork.js +1 -1
  67. package/src/providers/raccoon/const.js +219 -219
  68. package/src/providers/raccoon/credits.js +285 -285
  69. package/src/providers/registry.js +89 -89
  70. package/src/providers/workbuddy/chat.js +298 -298
  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/broadband-handler.js +80 -80
  75. package/src/routes/chat/gateway.js +46 -46
  76. package/src/routes/chat/relay-pipeline.js +282 -282
  77. package/src/routes/chat/via-route-handler.js +158 -158
  78. package/src/routes/hedge.js +260 -260
  79. package/src/routes/models-route.js +167 -167
  80. package/src/routes/peers.js +373 -373
  81. package/src/routes/stream.js +364 -364
  82. package/src/runtime/bootstrap.js +75 -75
  83. package/src/runtime/keep-warm.js +135 -135
  84. package/src/runtime/provider-gate.js +75 -75
  85. package/src/runtime/providers-setup.js +187 -187
  86. package/src/runtime/raccoon-checkin.js +175 -175
  87. package/src/runtime/server-lifecycle.js +34 -10
  88. package/src/server.js +85 -85
  89. package/src/state/schemas/allowlist.js +92 -92
  90. package/src/sync-opencode.js +280 -280
  91. package/src/transport/index.js +244 -244
  92. package/src/transport/pool.js +56 -56
  93. package/src/transport/retry.js +24 -24
  94. package/src/transport/sse.js +93 -93
  95. package/src/upstream-probe/display.js +52 -52
  96. package/src/upstream-probe/probe.js +49 -49
  97. package/src/upstream-probe/rotate.js +110 -110
  98. package/src/upstream-probe/start.js +45 -45
  99. package/src/upstream.js +355 -355
  100. package/docs/cli_help_mini.md +0 -149
  101. package/src/providers/AGENTS.md +0 -49
  102. 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
+ }