mslxdff 0.1.229 → 0.1.230

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