mslxdff 0.1.199 → 0.1.203

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 (327) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/package.json +38 -38
  4. package/plugins/README.md +14 -14
  5. package/plugins/prefer-model.mjs.example +14 -14
  6. package/src/anthropic/count-tokens.js +61 -61
  7. package/src/anthropic/request-body.js +126 -126
  8. package/src/anthropic/shared.js +18 -18
  9. package/src/anthropic/sse-aggregate.js +56 -56
  10. package/src/anthropic/translate.js +287 -287
  11. package/src/anthropic/web-search.js +361 -361
  12. package/src/api-kind.js +31 -31
  13. package/src/autostart.js +288 -288
  14. package/src/bench/probe.js +1 -1
  15. package/src/bench/runner.js +1 -1
  16. package/src/bench/via-probe.js +1 -1
  17. package/src/bench/via.js +144 -144
  18. package/src/bench/workbuddy-bench.js +1 -1
  19. package/src/chat/config.js +11 -11
  20. package/src/chat/cooling.js +81 -81
  21. package/src/chat/direct.js +58 -58
  22. package/src/chat/index.js +6 -6
  23. package/src/chat/spinner.js +38 -38
  24. package/src/chat/sse.js +69 -69
  25. package/src/chat/stats.js +247 -247
  26. package/src/chat/store.js +48 -48
  27. package/src/chat/tool-handlers.js +67 -67
  28. package/src/chat/tools.js +1 -1
  29. package/src/chat/upstream.js +59 -59
  30. package/src/chat-pipeline/auto-race.js +158 -158
  31. package/src/chat-pipeline/empty-turn.js +147 -147
  32. package/src/chat-pipeline/engine.js +17 -17
  33. package/src/chat-pipeline/serial-trial.js +283 -283
  34. package/src/chat-pipeline/terminal-verdict.js +147 -147
  35. package/src/chooser.js +54 -54
  36. package/src/claude-desktop/apply.js +101 -101
  37. package/src/claude-desktop/context1m.js +38 -38
  38. package/src/claude-desktop/doctor.js +146 -146
  39. package/src/claude-desktop/slots.js +152 -152
  40. package/src/cli/bootstrap.js +1 -1
  41. package/src/cli/commands/claude-desktop.js +284 -284
  42. package/src/cli/commands/daemon.js +231 -231
  43. package/src/cli/commands/model/list-live.js +22 -22
  44. package/src/cli/commands/model/list-providers.js +76 -76
  45. package/src/cli/commands/model/list-render.js +77 -77
  46. package/src/cli/commands/model/list-sort.js +52 -52
  47. package/src/cli/commands/model/list-tty.js +76 -76
  48. package/src/cli/commands/model/list.js +166 -166
  49. package/src/cli/commands/model/live-models.js +24 -24
  50. package/src/cli/commands/model/stats.js +43 -43
  51. package/src/cli/commands/model/status.js +47 -47
  52. package/src/cli/commands/model.js +51 -51
  53. package/src/cli/commands/provider/add.js +108 -108
  54. package/src/cli/commands/provider/allowlist.js +99 -99
  55. package/src/cli/commands/provider/cline-free.js +99 -99
  56. package/src/cli/commands/provider/cline-login.js +218 -218
  57. package/src/cli/commands/provider/cline-quota.js +65 -65
  58. package/src/cli/commands/provider/codearts-login.js +45 -45
  59. package/src/cli/commands/provider/config.js +119 -119
  60. package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
  61. package/src/cli/commands/provider/keys-guard.js +50 -50
  62. package/src/cli/commands/provider/keys.js +161 -161
  63. package/src/cli/commands/provider/qoder-checkin.js +81 -81
  64. package/src/cli/commands/provider/qoder-login.js +102 -102
  65. package/src/cli/commands/provider/raccoon-login.js +180 -180
  66. package/src/cli/commands/provider/raccoon-password.js +162 -162
  67. package/src/cli/commands/provider/raccoon-quota.js +109 -109
  68. package/src/cli/commands/provider/traework-checkin.js +144 -144
  69. package/src/cli/commands/provider/traework-login.js +164 -164
  70. package/src/cli/commands/provider/workbuddy-login.js +153 -153
  71. package/src/cli/commands/provider/zcode-login.js +77 -77
  72. package/src/cli/commands/provider/zcode-quota.js +55 -55
  73. package/src/cli/commands/provider.js +1 -1
  74. package/src/cli/commands/stats.js +149 -149
  75. package/src/cli/commands/system.js +261 -261
  76. package/src/cli/commands/timezone.js +47 -47
  77. package/src/cli/commands/use-group.js +60 -60
  78. package/src/cli/commands/workbuddy.js +189 -189
  79. package/src/cli/format.js +141 -141
  80. package/src/cli/group-helpers.js +70 -70
  81. package/src/cli/help.js +80 -80
  82. package/src/cli/index.js +70 -70
  83. package/src/cli/interactive.js +121 -121
  84. package/src/cli/join-core.js +99 -99
  85. package/src/cli/join-wizard.js +175 -175
  86. package/src/cli/policy.js +136 -136
  87. package/src/cli/provider-row.js +126 -126
  88. package/src/cli/util.js +24 -24
  89. package/src/compat.js +53 -53
  90. package/src/events.js +38 -38
  91. package/src/free-lane.js +131 -131
  92. package/src/free-watcher.js +63 -63
  93. package/src/groups.js +262 -262
  94. package/src/logs.js +195 -195
  95. package/src/metrics.js +62 -62
  96. package/src/model-capabilities/context-lookup.js +65 -65
  97. package/src/model-capabilities/merge.js +142 -142
  98. package/src/model-trace.js +187 -187
  99. package/src/models.js +1 -1
  100. package/src/opencode-identity.js +72 -72
  101. package/src/peers.js +254 -251
  102. package/src/plugins.js +107 -107
  103. package/src/providers/base.js +164 -164
  104. package/src/providers/classify.js +22 -22
  105. package/src/providers/cline/allowlist-sync.js +93 -93
  106. package/src/providers/cline/free-catalog.js +71 -71
  107. package/src/providers/cline/headers.js +37 -37
  108. package/src/providers/cline/index.js +130 -130
  109. package/src/providers/cline/models.js +150 -150
  110. package/src/providers/cline/usage.js +274 -274
  111. package/src/providers/codearts/auth-pool.js +148 -148
  112. package/src/providers/codearts/concurrency.js +34 -34
  113. package/src/providers/codearts/const.js +65 -65
  114. package/src/providers/codearts/dpop.js +95 -95
  115. package/src/providers/codearts/index.js +175 -175
  116. package/src/providers/codearts/login.js +214 -214
  117. package/src/providers/codearts/models.js +182 -182
  118. package/src/providers/codearts/sign.js +96 -96
  119. package/src/providers/codearts/sse.js +204 -204
  120. package/src/providers/codearts/stream.js +209 -209
  121. package/src/providers/codearts/sts.js +124 -124
  122. package/src/providers/codearts.js +1 -1
  123. package/src/providers/dispatcher.js +146 -146
  124. package/src/providers/generic.js +156 -156
  125. package/src/providers/globalqwenwork/account-store.js +141 -141
  126. package/src/providers/globalqwenwork/constants.js +73 -73
  127. package/src/providers/globalqwenwork/crypto.js +2 -2
  128. package/src/providers/globalqwenwork/http.js +2 -2
  129. package/src/providers/globalqwenwork/index.js +331 -331
  130. package/src/providers/globalqwenwork/payload.js +147 -147
  131. package/src/providers/globalqwenwork/rsa.js +2 -2
  132. package/src/providers/globalqwenwork/sse.js +2 -2
  133. package/src/providers/globalqwenwork/stream.js +2 -2
  134. package/src/providers/globalqwenwork.js +1 -1
  135. package/src/providers/globalworkbuddy/account-store.js +198 -198
  136. package/src/providers/globalworkbuddy/auth.js +97 -97
  137. package/src/providers/globalworkbuddy/catalog.js +138 -138
  138. package/src/providers/globalworkbuddy/constants.js +73 -73
  139. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  140. package/src/providers/globalworkbuddy/errors.js +98 -98
  141. package/src/providers/globalworkbuddy/headers.js +89 -89
  142. package/src/providers/globalworkbuddy/models.js +96 -96
  143. package/src/providers/globalworkbuddy/payload.js +80 -80
  144. package/src/providers/index.js +5 -5
  145. package/src/providers/model-id.js +172 -172
  146. package/src/providers/opencode.js +19 -19
  147. package/src/providers/openrouter.js +31 -31
  148. package/src/providers/qoder/account-store.js +106 -106
  149. package/src/providers/qoder/aggregate.js +60 -60
  150. package/src/providers/qoder/baseprompt.json +591 -591
  151. package/src/providers/qoder/checkin.js +160 -160
  152. package/src/providers/qoder/constants.js +44 -44
  153. package/src/providers/qoder/encode.js +41 -41
  154. package/src/providers/qoder/fingerprint.js +38 -38
  155. package/src/providers/qoder/models.js +77 -77
  156. package/src/providers/qoder/oauth.js +84 -84
  157. package/src/providers/qoder/payload.js +162 -162
  158. package/src/providers/qoder/request.js +27 -27
  159. package/src/providers/qoder/session.js +134 -134
  160. package/src/providers/qoder/sticky.js +38 -38
  161. package/src/providers/qoder/stream.js +116 -116
  162. package/src/providers/qoder.js +1 -1
  163. package/src/providers/raccoon/account-store.js +216 -216
  164. package/src/providers/raccoon/auth.js +67 -67
  165. package/src/providers/raccoon/chat.js +228 -228
  166. package/src/providers/raccoon/const.js +180 -165
  167. package/src/providers/raccoon/credits.js +185 -175
  168. package/src/providers/raccoon/crypto.js +43 -0
  169. package/src/providers/raccoon/envelope.js +31 -31
  170. package/src/providers/raccoon/headers.js +45 -45
  171. package/src/providers/raccoon/index.js +216 -216
  172. package/src/providers/raccoon/login.js +279 -279
  173. package/src/providers/raccoon/models.js +160 -160
  174. package/src/providers/raccoon/password-login.js +102 -87
  175. package/src/providers/raccoon/qr-codewords.js +115 -115
  176. package/src/providers/raccoon/qr.js +236 -236
  177. package/src/providers/raccoon/sse.js +135 -135
  178. package/src/providers/responses-channel.js +120 -120
  179. package/src/providers/share-keys.js +53 -53
  180. package/src/providers/traework/account-store.js +234 -234
  181. package/src/providers/traework/chat.js +217 -217
  182. package/src/providers/traework/checkin.js +77 -77
  183. package/src/providers/traework/constants.js +41 -41
  184. package/src/providers/traework/errors.js +52 -52
  185. package/src/providers/traework/headers.js +79 -79
  186. package/src/providers/traework/index.js +132 -132
  187. package/src/providers/traework/market-identity.js +116 -116
  188. package/src/providers/traework/models.js +98 -98
  189. package/src/providers/traework/payload.js +86 -86
  190. package/src/providers/traework/sse.js +184 -184
  191. package/src/providers/traework/token.js +52 -52
  192. package/src/providers/traework.js +2 -2
  193. package/src/providers/workbuddy/account-store.js +186 -186
  194. package/src/providers/workbuddy/auth.js +158 -158
  195. package/src/providers/workbuddy/balance.js +86 -86
  196. package/src/providers/workbuddy/cat-travel.js +125 -125
  197. package/src/providers/workbuddy/checkin.js +75 -75
  198. package/src/providers/workbuddy/desktop-info.js +78 -78
  199. package/src/providers/workbuddy/growth-api.js +67 -67
  200. package/src/providers/workbuddy/growth-plans.js +63 -63
  201. package/src/providers/workbuddy/growth.js +181 -181
  202. package/src/providers/workbuddy/index.js +217 -217
  203. package/src/providers/workbuddy/models.js +126 -126
  204. package/src/providers/workbuddy/payload.js +141 -141
  205. package/src/providers/workbuddy/rotation-log.js +83 -83
  206. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  207. package/src/providers/workbuddy/sdk-chat.js +26 -26
  208. package/src/providers/workbuddy-balance.js +78 -78
  209. package/src/providers/zcode/account-store.js +129 -129
  210. package/src/providers/zcode/auth.js +28 -28
  211. package/src/providers/zcode/chat.js +179 -179
  212. package/src/providers/zcode/const.js +54 -54
  213. package/src/providers/zcode/context-shape.js +162 -162
  214. package/src/providers/zcode/headers.js +93 -93
  215. package/src/providers/zcode/index.js +124 -124
  216. package/src/providers/zcode/models.js +65 -65
  217. package/src/providers/zcode/oauth.js +120 -120
  218. package/src/providers/zcode/quota.js +176 -176
  219. package/src/providers/zcode/sse.js +196 -196
  220. package/src/readline-compat.js +25 -25
  221. package/src/responses/translate.js +311 -311
  222. package/src/routes/chat/broadband-handler.js +72 -72
  223. package/src/routes/chat/empty-turn-report.js +31 -31
  224. package/src/routes/chat/exhausted-handler.js +87 -87
  225. package/src/routes/chat/hedge-handler.js +130 -130
  226. package/src/routes/chat/index.js +1 -1
  227. package/src/routes/chat/local-handler.js +63 -63
  228. package/src/routes/chat/peer-handler.js +81 -81
  229. package/src/routes/chat.js +3 -3
  230. package/src/routes/fallback.js +88 -88
  231. package/src/routes/groups-relay.js +214 -214
  232. package/src/routes/groups.js +122 -122
  233. package/src/routes/helpers.js +83 -83
  234. package/src/routes/index.js +147 -147
  235. package/src/routes/messages-route.js +365 -365
  236. package/src/routes/peers.js +23 -1
  237. package/src/routes/relay-queue.js +181 -181
  238. package/src/routes/relay.js +60 -60
  239. package/src/routes/responses-route.js +213 -213
  240. package/src/routes/stream-body.js +84 -84
  241. package/src/routes/stream-hold.js +337 -337
  242. package/src/routes/stream-scan.js +366 -366
  243. package/src/routes.js +3 -3
  244. package/src/runtime/auto-update.js +113 -113
  245. package/src/runtime/bootstrap.js +8 -0
  246. package/src/runtime/broadband-stream.js +79 -79
  247. package/src/runtime/broadband.js +103 -103
  248. package/src/runtime/group-sync.js +81 -81
  249. package/src/runtime/keep-warm.js +120 -0
  250. package/src/runtime/lifecycle-forensics.js +116 -116
  251. package/src/runtime/lifecycle-log.js +87 -87
  252. package/src/runtime/log-stamp.js +73 -73
  253. package/src/runtime/proc-info.js +137 -137
  254. package/src/runtime/qoder-checkin.js +93 -93
  255. package/src/runtime/raccoon-checkin.js +126 -126
  256. package/src/runtime/server-lifecycle.js +189 -189
  257. package/src/runtime/traework-checkin.js +118 -118
  258. package/src/runtime/workbuddy-checkin.js +120 -120
  259. package/src/runtime/workbuddy-growth.js +133 -133
  260. package/src/server.js +7 -0
  261. package/src/state/facade.js +58 -58
  262. package/src/state/memory.js +162 -162
  263. package/src/state/merge.js +25 -25
  264. package/src/state/migrations/cline-unify.js +94 -94
  265. package/src/state/migrations.js +33 -33
  266. package/src/state/persist.js +42 -42
  267. package/src/state/provider-config.js +142 -142
  268. package/src/state/schemas/group.js +31 -31
  269. package/src/state/schemas/model.js +94 -94
  270. package/src/state/schemas/peer.js +31 -31
  271. package/src/state/schemas/port.js +10 -10
  272. package/src/state/schemas/provider.js +202 -202
  273. package/src/state/schemas/timezone.js +70 -70
  274. package/src/state/schemas/token.js +48 -48
  275. package/src/state/schemas/use-group.js +102 -102
  276. package/src/state/store.js +176 -176
  277. package/src/state.js +1 -1
  278. package/src/sync-claude-desktop.js +234 -234
  279. package/src/sync-claude.js +152 -152
  280. package/src/sync-codex.js +116 -116
  281. package/src/sync-workbuddy.js +158 -158
  282. package/src/talk-full-redact.js +129 -129
  283. package/src/talk-full.js +131 -131
  284. package/src/talk-log.js +257 -257
  285. package/src/text-tokens.js +52 -52
  286. package/src/thinking-effort.js +67 -67
  287. package/src/time.js +106 -106
  288. package/src/timeline.js +26 -26
  289. package/src/transport/index.js +1 -1
  290. package/src/upstream-engine/index.js +74 -72
  291. package/src/upstream-engine/mode.js +15 -15
  292. package/src/upstream-engine/sdk/attempt.js +221 -221
  293. package/src/upstream-engine/sdk/chat.js +28 -28
  294. package/src/upstream-engine/sdk/convert.js +192 -192
  295. package/src/upstream-engine/sdk/diagnose.js +62 -62
  296. package/src/upstream-engine/sdk/dispatch.js +55 -55
  297. package/src/upstream-engine/sdk/responses.js +176 -176
  298. package/src/upstream-engine/sdk/sse.js +166 -166
  299. package/src/upstream-responses.js +305 -305
  300. package/src/upstream.js +53 -1
  301. package/src/usage/record.js +106 -106
  302. package/src/usage/report.js +195 -195
  303. package/src/v6pool/claim.js +157 -157
  304. package/src/v6pool/config.js +65 -65
  305. package/src/v6pool/detect.js +130 -130
  306. package/src/v6pool/index.js +152 -152
  307. package/src/v6pool/prefix.js +140 -140
  308. package/src/v6pool/probe.js +40 -40
  309. package/src/v6pool/report.js +104 -104
  310. package/src/v6pool/rotate.js +263 -239
  311. package/src/warp-pool/config.js +74 -74
  312. package/src/warp-pool/health.js +106 -106
  313. package/src/warp-pool/identity.js +141 -141
  314. package/src/warp-pool/index.js +358 -358
  315. package/src/warp-pool/keys.js +29 -29
  316. package/src/warp-pool/nodes.js +95 -95
  317. package/src/warp-pool/ports.js +160 -160
  318. package/src/warp-pool/register.js +113 -113
  319. package/src/warp-pool/report.js +232 -232
  320. package/src/warp-pool/revive.js +168 -168
  321. package/src/warp-pool/rotate.js +214 -214
  322. package/src/warp-pool/strategy.js +43 -43
  323. package/src/warp-pool/verdict.js +69 -69
  324. package/src/warp-pool/wireproxy.js +162 -162
  325. package/docs/cli_help_mini.md +0 -146
  326. package/src/providers/AGENTS.md +0 -49
  327. package/src/routes/AGENTS.md +0 -37
@@ -1,365 +1,365 @@
1
- /**
2
- * POST /v1/messages + /v1/messages/count_tokens — 给 Claude Code 用的 Anthropic Messages 外壳(ADR-0047)。
3
- * 复用 ChatPipeline 全链路(auto/hedge/failover/tool_calls/空轮重试),只做形状翻译:
4
- * 非流式:收集 chat JSON → 转 Anthropic message(上游只会流式时先把 SSE 聚合成 chat JSON);流式:逐块实时翻成 Anthropic 具名事件。
5
- *
6
- * 出站格式与 /v1/responses **不兼容**,故不能复用 createLiveForwarder(它写死 `data:` + `[DONE]`):
7
- * Anthropic 用 `event: <type>` 具名帧、无 `[DONE]`,且需网关自发 `ping` 扛过客户端 idle watchdog。
8
- *
9
- * 三条收口纪律(第 1 轮评审 P0-2/P0-3 立下的,别改回去):
10
- * 1. 上游一有动静(含 pipeline 的 keepalive 注释帧)就发头 + `message_start` + 武装 ping —— 首块前的静默期正是客户端 watchdog 咬人的窗口;
11
- * 2. `headersSent`/`getHeader` 必须真反映已发头,否则 `helpers.js:32` 的「已 flush 走 in-band 错误帧」守卫永远判假,
12
- * 错误会被当成普通文本喂进 push() 后静默消失;
13
- * 3. 没等到 `finish_reason`(也没有 `[DONE]`)就收线 = 截断,发 `event: error`,**不伪装成功收场**。
14
- */
15
- import { json, readBody } from "./helpers.js";
16
- import { createChatPipeline } from "../chat-pipeline/index.js";
17
- import { withBody, createCollector, createEmitter } from "./responses-route.js";
18
- import {
19
- messagesToChatBody,
20
- chatJsonToAnthropic,
21
- createAnthropicChunkTranslator,
22
- anthropicError,
23
- } from "../anthropic/translate.js";
24
- import { estimateTokens } from "../anthropic/count-tokens.js";
25
- import { sseToChatJson } from "../anthropic/sse-aggregate.js";
26
- import {
27
- detectSearchRequest,
28
- runWebSearch,
29
- searchConfig,
30
- buildSearchMessage,
31
- searchEvents,
32
- } from "../anthropic/web-search.js";
33
-
34
- const ADEBUG = process.env.MSLXDFF_ANTHROPIC_DEBUG === "1";
35
- function alog(...a) {
36
- if (ADEBUG) console.log("[messages]", ...a);
37
- }
38
-
39
- // thinking 块外发:默认开,`MSLXDFF_ANTHROPIC_THINKING=0` 才关。外发的 thinking 块必须在封口前交一帧占位
40
- // signature_delta(translate.js 的 thinkingSignature),缺它 Claude Code 整块丢弃 — ADR-0051 外发 / ADR-0052 签名
41
- function emitThinking() { return process.env.MSLXDFF_ANTHROPIC_THINKING !== "0"; }
42
-
43
- // 空闲 ping 周期(ms):`0` 关闭;客户端长思考期靠它扛过 streaming idle timeout
44
- const PING_MS = (() => {
45
- const raw = Number(process.env.MSLXDFF_ANTHROPIC_PING_MS);
46
- return Number.isFinite(raw) && raw >= 0 ? raw : 15000;
47
- })();
48
-
49
- function frameOf(type, data) {
50
- return `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
51
- }
52
-
53
- // pipeline 在已 flush 的流里用 OpenAI 形状收口(`{error:{...}}` + `[DONE]`):
54
- // 检出后改发 Anthropic 的 `error` 帧,别把错误伪装成正常收场(grill-decisions Q2)
55
- function detectInBandError(text) {
56
- let msg = null;
57
- for (const line of String(text || "").split("\n")) {
58
- const t = line.trim();
59
- if (!t.startsWith("data:")) continue;
60
- const payload = t.slice(5).trim();
61
- if (!payload || payload === "[DONE]") continue;
62
- try {
63
- const j = JSON.parse(payload);
64
- const e = j?.error;
65
- if (e) msg = String(e?.message || e).slice(0, 500);
66
- } catch { /* 非 JSON 帧忽略 */ }
67
- }
68
- return msg;
69
- }
70
-
71
-
72
- // pipeline 未 flush 时用 json() 直写聚合错误体(无 `data:` 前缀):认出来只取 message,
73
- // 别把整坨 OpenAI JSON 当人话塞进 Anthropic error.message。
74
- function jsonErrorText(text) {
75
- const t = String(text || "").trim();
76
- if (!t || t.startsWith(":") || t.includes("data:")) return null;
77
- try {
78
- const j = JSON.parse(t);
79
- const e = j?.error;
80
- if (!e) return null;
81
- return String(e?.message || e).slice(0, 500);
82
- } catch {
83
- return null;
84
- }
85
- }
86
-
87
- /** 两种上游收场形状(in-band `data:` 帧 / 聚合 JSON 体)统一取错误文案。 */
88
- function errorText(text) {
89
- return detectInBandError(text) || jsonErrorText(text);
90
- }
91
- // 客户端要流式而上游给了聚合 JSON(muse-spark / workbuddy 聚合 / zcode 非流式聚合都是现网常态):
92
- // 那体没有 `data:` 前缀,会被 push() 当噪声整段丢掉 → 正文静默消失。认出来交给 fromChatJson 兜底。
93
- function tryAggregate(text) {
94
- const t = String(text || "").trim();
95
- if (!t || t.includes("data:") || t.startsWith(":")) return null;
96
- try {
97
- const j = JSON.parse(t);
98
- // 只认「非空 choices」:`{choices:[]}` 之类的空包什么也带不来,若当合法聚合包会跳过截断检测 → 零正文还谎报成功
99
- return j && Array.isArray(j.choices) && j.choices.length > 0 ? j : null;
100
- } catch {
101
- return null;
102
- }
103
- }
104
-
105
-
106
- /**
107
- * Anthropic 出站垫片:具名事件 + 无 [DONE] + 空闲自发 ping + 真 headersSent。
108
- * @param realRes 真 HTTP response
109
- * @param translator createAnthropicChunkTranslator 实例
110
- * @param opts { pingMs?: number } 测试注入口,缺省读 env
111
- */
112
- export function createAnthropicForwarder(realRes, translator, opts = {}) {
113
- const pingMs = opts.pingMs ?? PING_MS;
114
- let status = 200;
115
- let headSent = false;
116
- let ended = false;
117
- let timer = null;
118
- let pendingError = null;
119
- let aggregate = null;
120
- let rawAcc = ""; // 攒住无事件字节,供聚合回落识别
121
- const sent = () => headSent || realRes.headersSent === true; // headersSent 与 getHeader 的唯一口径
122
- // 上游按字节粒度喂 Buffer/Uint8Array:非流式解码会把多字节汉字切成 U+FFFD 并永久固化在行缓冲里
123
- const decoder = new TextDecoder("utf-8");
124
- const decode = (c, flush) => {
125
- if (c == null) return flush ? String(decoder.decode()) : "";
126
- if (typeof c === "string") return c;
127
- const bytes = (typeof Buffer !== "undefined" && Buffer.isBuffer(c)) || c instanceof Uint8Array ? c : null;
128
- if (bytes) return decoder.decode(bytes, { stream: !flush });
129
- return String(c);
130
- };
131
- const dead = () => ended || realRes.writableEnded === true || realRes.destroyed === true;
132
- const raw = (type, data) => { if (dead()) return; try { realRes.write(frameOf(type, data)); } catch { /* 下游已断 */ } };
133
- const stopPing = () => { if (timer) { clearTimeout(timer); timer = null; } };
134
- const armPing = () => {
135
- if (!pingMs || dead()) return;
136
- stopPing();
137
- timer = setTimeout(() => { if (dead()) return; raw("ping", { type: "ping" }); armPing(); }, pingMs);
138
- if (typeof timer.unref === "function") timer.unref();
139
- };
140
- const sendHead = () => {
141
- if (headSent) return;
142
- headSent = true;
143
- try { realRes.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }); } catch { /* 已发 */ }
144
- for (const e of translator.begin()) raw(e.type, e);
145
- armPing();
146
- };
147
- const finish = () => { if (ended) return; ended = true; stopPing(); try { realRes.end(); } catch { /* ignore */ } };
148
- // 错误收场:一帧 error 后关连接,不补 message_delta/message_stop(真 Anthropic API 即此行为)
149
- const closeError = (msg) => { sendHead(); raw("error", { type: "error", error: { type: "api_error", message: String(msg || "upstream error").slice(0, 500) } }); finish(); };
150
-
151
- // 整包聚合体可能被分多次 write:攒住「没产出任何事件」的字节再判一次(有事件即清空,防陈旧拼接)
152
- const RAW_ACC_CAP = 262144;
153
- let rawOverflow = false;
154
- const tryAccumulate = (text) => {
155
- if (aggregate || !text || rawOverflow) return;
156
- rawAcc += text;
157
- if (rawAcc.length > RAW_ACC_CAP) {
158
- // 超上限就停手并留标记:继续攒也拼不出完整 JSON,静默攒下去只会让错误文案说谎
159
- rawAcc = rawAcc.slice(0, RAW_ACC_CAP);
160
- rawOverflow = true;
161
- }
162
- const agg = tryAggregate(rawAcc);
163
- if (agg) aggregate = agg;
164
- };
165
- const res = {
166
- ...createEmitter(),
167
- set statusCode(v) { status = v; },
168
- get statusCode() { return status; },
169
- // helpers.js 的 json() 靠 headersSent + getHeader("content-type") 判「能不能回头」;
170
- // 两者必须同源说真话,否则会出现「判成已 flush、却又拿不到 event-stream」的全静默窗口(错误既不 in-band 也不 JSON)。
171
- get headersSent() { return sent(); },
172
- // json() 的幂等早退判据;补上它才不会在我们已收场后再被当「可写」的响应对待
173
- get writableEnded() { return ended || realRes.writableEnded === true; },
174
- setHeader() { /* Anthropic SSE 的头由 sendHead 统一发,逐键 set 无意义 */ },
175
- getHeader(name) {
176
- if (String(name).toLowerCase() === "content-type" && sent()) return "text/event-stream";
177
- return undefined;
178
- },
179
- write(c) {
180
- if (ended) { alog("late-write-ignored", JSON.stringify({ id: translator.id, bytes: String(c?.length ?? 0) })); return true; }
181
- const text = decode(c);
182
- const err = errorText(text);
183
- if (err) { pendingError = err; return true; } // 留给 end() 统一收口,不在这里半截发
184
- // 上游一有动静(连 pipeline 的 keepalive 注释帧都算)就落头 + message_start + 武装 ping:
185
- // 首块前的静默期正是客户端 idle watchdog 咬人的窗口。没收到任何字节时错误仍走干净 JSON(见 end())。
186
- sendHead();
187
- const evs = translator.push(text);
188
- for (const e of evs) raw(e.type, e);
189
- if (evs.length) rawAcc = ""; else tryAccumulate(text);
190
- armPing();
191
- return true;
192
- },
193
- end(c) {
194
- if (ended) return;
195
- const tail = decode(c, true);
196
- const err = pendingError || errorText(tail);
197
- if (status >= 400 && !headSent) {
198
- ended = true;
199
- stopPing();
200
- json(realRes, status, anthropicError("api_error", err || String(tail).slice(0, 500) || "upstream error"));
201
- return;
202
- }
203
- if (err) { closeError(err); return; }
204
- // 客户端要的是流:先落头 + message_start + 武装 ping,再处理残余
205
- sendHead();
206
- if (tail) {
207
- const evs = translator.push(tail);
208
- for (const e of evs) raw(e.type, e);
209
- if (evs.length) rawAcc = ""; else tryAccumulate(tail);
210
- }
211
- // 末帧常常没有收尾换行:补一个 `\n` 把残行逼出来,否则合法正文与 finish_reason 会一起烂在缓冲里(被误判成截断)
212
- for (const e of translator.push("\n")) raw(e.type, e);
213
- // 上游整包聚合(无 SSE 帧):喂同一套状态机兜底,别让正文静默消失
214
- if (aggregate && !translator.stats().blocks) { for (const e of translator.fromChatJson(aggregate)) raw(e.type, e); }
215
- const fin = translator.getFinal();
216
- if (!fin.finish && !aggregate) {
217
- alog("truncated", JSON.stringify({ id: translator.id, ...translator.stats() }));
218
- closeError(rawOverflow
219
- ? "upstream sent an oversized non-SSE body that could not be aggregated (no finish_reason)"
220
- : "upstream stream ended before the model finished its turn (no finish_reason)");
221
- return;
222
- }
223
- for (const e of translator.end({ finish: fin.finish, usage: fin.usage })) raw(e.type, e);
224
- finish();
225
- },
226
- };
227
- // 客户端提前断开:pipeline 会感知 close,但 ping 定时器归本垫片管,必须自己停
228
- try { realRes.on?.("close", () => { if (!ended) { ended = true; stopPing(); } }); } catch { /* ignore */ }
229
- return res;
230
- }
231
-
232
- /**
233
- * 代跑结果回给客户端:非流式整包 JSON,流式由同一份块生成的具名事件(无 `[DONE]`)。
234
- * 搜索在调用前就跑完了,这里一次写完即可,不需要 createAnthropicForwarder 的 ping 垫片。
235
- */
236
- function respondSearch(res, { message, stream, provider }, isDead) {
237
- if (isDead && isDead()) return undefined; // 搜索期间客户端断连:别再写任何字节
238
- if (!stream) {
239
- try { res.setHeader?.("x-mslxdff-web-search", provider); } catch { /* 已 flush */ }
240
- try { return json(res, 200, message); } catch { return undefined; /* 下游已断 */ }
241
- }
242
- try {
243
- res.writeHead(200, {
244
- "Content-Type": "text/event-stream",
245
- "Cache-Control": "no-cache",
246
- Connection: "keep-alive",
247
- "x-mslxdff-web-search": provider,
248
- });
249
- for (const e of searchEvents(message)) res.write(frameOf(e.type, e));
250
- res.end();
251
- } catch { /* 下游已断 */ }
252
- return undefined;
253
- }
254
-
255
- export async function messagesHandler(ctx) {
256
- const { req, res } = ctx;
257
- const t0 = Date.now();
258
- let body;
259
- try {
260
- body = await readBody(req);
261
- } catch {
262
- return json(res, 400, anthropicError("invalid_request_error", "Invalid JSON body"));
263
- }
264
- // —— 网关代跑 web_search(ADR-0049):旁路请求**一律由网关作答**——命中判据即不打模型。
265
- // 搜到就回结果块;关了/全挂/空查询就回一条明确的 400(把无工具的旁路丢给上游,只会让模型凭空
266
- // 编 URL 并让客户端按「0 次搜索」空转重试,那才是改前的老毛病)。主循环请求不受影响。
267
- // 整段包 try:搜索期最长 15s,其间客户端可能已断连(`isDead`),任何意外都不得逃逸成 5xx。
268
- const det = detectSearchRequest(body);
269
- if (det.hit) {
270
- const isDead = () => res.writableEnded === true || res.destroyed === true || res.headersSent === true;
271
- const wsCfg = searchConfig();
272
- let r;
273
- if (!wsCfg.enabled) r = { ok: false, error: "disabled by MSLXDFF_WEB_SEARCH=off" };
274
- else {
275
- try {
276
- r = det.query ? await runWebSearch(det.query, wsCfg) : { ok: false, error: "empty query" };
277
- } catch (e) {
278
- r = { ok: false, error: String(e?.message || e) };
279
- }
280
- }
281
- if (r.ok) {
282
- const readout = { provider: r.provider, ms: r.ms, results: r.results.length, queryChars: r.query.length, chain: r.chain || [] };
283
- alog("web-search", JSON.stringify(readout));
284
- try { ctx.logs?.appendEvent?.({ type: "web-search", ...readout }); } catch { /* 观测产物不参与判决 */ }
285
- return respondSearch(res, {
286
- message: buildSearchMessage({ model: String(body.model || ""), query: r.query, results: r.results }),
287
- stream: Boolean(body.stream),
288
- provider: r.provider,
289
- }, isDead);
290
- }
291
- const why = String(r.error || "unknown").slice(0, 200);
292
- alog("web-search-unavailable", why);
293
- try { ctx.logs?.appendEvent?.({ type: "web-search-unavailable", queryChars: (det.query || "").length, error: why, chain: r.chain || [] }); } catch { /* 同上 */ }
294
- if (isDead()) return undefined;
295
- try { return json(res, 400, anthropicError("invalid_request_error", `web search failed at the gateway: ${why}`)); } catch { return undefined; }
296
- }
297
- let chatBody;
298
- try {
299
- chatBody = messagesToChatBody(body);
300
- } catch (e) {
301
- alog("translate-req-400", String(e?.message || e));
302
- return json(res, 400, anthropicError("invalid_request_error", String(e?.message || e)));
303
- }
304
- alog("req", JSON.stringify({
305
- model: chatBody.model, stream: chatBody.stream,
306
- msgs: chatBody.messages.map((m) => `${m.role}:${(typeof m.content === "string" ? m.content : "[parts]").length}${m.tool_calls ? `+${m.tool_calls.length}tc` : ""}`),
307
- tools: Array.isArray(chatBody.tools) ? chatBody.tools.length : 0,
308
- }));
309
- const pipeline = createChatPipeline(ctx);
310
- const fakeReq = withBody(req, chatBody);
311
- // ADR-0055:请求侧 token 估算 thunk(路由层也 memoize,与决策 4「每请求至多算一次」对齐;
312
- // translator 内部还有一层 memoize,双保险)。
313
- let estCache;
314
- const estInput = () => (estCache ??= estimateTokens(body).input_tokens);
315
- const forwardClose = (shim) => { try { req.on?.("close", () => shim.emit("close")); } catch { /* ignore */ } };
316
- try {
317
- if (chatBody.stream) {
318
- const translator = createAnthropicChunkTranslator(chatBody.model, { thinking: emitThinking(), estInput });
319
- const live = createAnthropicForwarder(res, translator);
320
- forwardClose(live);
321
- await pipeline.execute({ req: fakeReq, res: live });
322
- alog("done-stream", JSON.stringify({ ms: Date.now() - t0, id: translator.id, ...translator.stats(), ...translator.getFinal() }));
323
- } else {
324
- const cap = createCollector();
325
- forwardClose(cap.res);
326
- await pipeline.execute({ req: fakeReq, res: cap.res });
327
- const { status, text } = cap.get();
328
- if (status >= 400) return json(res, status, anthropicError("api_error", errorText(text) || String(text).slice(0, 500)));
329
- let chatJson = null;
330
- try { chatJson = JSON.parse(text); } catch { /* 非 JSON */ }
331
- // 上游只会流式(workbuddy 写死 stream:true):非流式收到的 text 是 SSE → 聚成 chat JSON 再走同一翻译出口;
332
- // 空流/错误帧聚合不出 JSON 时仍落回下方原 502 路径。
333
- if (!chatJson) chatJson = sseToChatJson(text, chatBody.model);
334
- if (!chatJson || chatJson.error || chatJson.object === "error") {
335
- return json(res, status >= 400 ? status : 502, anthropicError("api_error", errorText(text) || String(text).slice(0, 500)));
336
- }
337
- // 上游偶发非 chat 形状(无 choices):包成单 text 块,绝不 500(照 responses-route.js 先例)
338
- if (!Array.isArray(chatJson.choices)) {
339
- const r = chatJsonToAnthropic({ choices: [{ message: { content: String(text).slice(0, 8000) } }] }, chatBody.model, { estInput });
340
- alog("done-nonchat", JSON.stringify({ ms: Date.now() - t0, bytes: text.length }));
341
- return json(res, 200, r);
342
- }
343
- const out = chatJsonToAnthropic(chatJson, chatBody.model, { estInput });
344
- alog("done-json", JSON.stringify({ ms: Date.now() - t0, status, blocks: out.content.length, stop: out.stop_reason, usage: out.usage }));
345
- return json(res, 200, out);
346
- }
347
- } catch (err) {
348
- alog("execute-throw", String(err?.message || err).slice(0, 300));
349
- if (!res.headersSent) return json(res, 502, anthropicError("api_error", String(err?.message || err)));
350
- try { res.end(); } catch { /* ignore */ }
351
- }
352
- }
353
-
354
- export async function countTokensHandler(ctx) {
355
- const { req, res } = ctx;
356
- let body;
357
- try {
358
- body = await readBody(req);
359
- } catch {
360
- return json(res, 400, anthropicError("invalid_request_error", "Invalid JSON body"));
361
- }
362
- const out = estimateTokens(body);
363
- alog("count-tokens", JSON.stringify({ model: body?.model, input_tokens: out.input_tokens }));
364
- return json(res, 200, out);
365
- }
1
+ /**
2
+ * POST /v1/messages + /v1/messages/count_tokens — 给 Claude Code 用的 Anthropic Messages 外壳(ADR-0047)。
3
+ * 复用 ChatPipeline 全链路(auto/hedge/failover/tool_calls/空轮重试),只做形状翻译:
4
+ * 非流式:收集 chat JSON → 转 Anthropic message(上游只会流式时先把 SSE 聚合成 chat JSON);流式:逐块实时翻成 Anthropic 具名事件。
5
+ *
6
+ * 出站格式与 /v1/responses **不兼容**,故不能复用 createLiveForwarder(它写死 `data:` + `[DONE]`):
7
+ * Anthropic 用 `event: <type>` 具名帧、无 `[DONE]`,且需网关自发 `ping` 扛过客户端 idle watchdog。
8
+ *
9
+ * 三条收口纪律(第 1 轮评审 P0-2/P0-3 立下的,别改回去):
10
+ * 1. 上游一有动静(含 pipeline 的 keepalive 注释帧)就发头 + `message_start` + 武装 ping —— 首块前的静默期正是客户端 watchdog 咬人的窗口;
11
+ * 2. `headersSent`/`getHeader` 必须真反映已发头,否则 `helpers.js:32` 的「已 flush 走 in-band 错误帧」守卫永远判假,
12
+ * 错误会被当成普通文本喂进 push() 后静默消失;
13
+ * 3. 没等到 `finish_reason`(也没有 `[DONE]`)就收线 = 截断,发 `event: error`,**不伪装成功收场**。
14
+ */
15
+ import { json, readBody } from "./helpers.js";
16
+ import { createChatPipeline } from "../chat-pipeline/index.js";
17
+ import { withBody, createCollector, createEmitter } from "./responses-route.js";
18
+ import {
19
+ messagesToChatBody,
20
+ chatJsonToAnthropic,
21
+ createAnthropicChunkTranslator,
22
+ anthropicError,
23
+ } from "../anthropic/translate.js";
24
+ import { estimateTokens } from "../anthropic/count-tokens.js";
25
+ import { sseToChatJson } from "../anthropic/sse-aggregate.js";
26
+ import {
27
+ detectSearchRequest,
28
+ runWebSearch,
29
+ searchConfig,
30
+ buildSearchMessage,
31
+ searchEvents,
32
+ } from "../anthropic/web-search.js";
33
+
34
+ const ADEBUG = process.env.MSLXDFF_ANTHROPIC_DEBUG === "1";
35
+ function alog(...a) {
36
+ if (ADEBUG) console.log("[messages]", ...a);
37
+ }
38
+
39
+ // thinking 块外发:默认开,`MSLXDFF_ANTHROPIC_THINKING=0` 才关。外发的 thinking 块必须在封口前交一帧占位
40
+ // signature_delta(translate.js 的 thinkingSignature),缺它 Claude Code 整块丢弃 — ADR-0051 外发 / ADR-0052 签名
41
+ function emitThinking() { return process.env.MSLXDFF_ANTHROPIC_THINKING !== "0"; }
42
+
43
+ // 空闲 ping 周期(ms):`0` 关闭;客户端长思考期靠它扛过 streaming idle timeout
44
+ const PING_MS = (() => {
45
+ const raw = Number(process.env.MSLXDFF_ANTHROPIC_PING_MS);
46
+ return Number.isFinite(raw) && raw >= 0 ? raw : 15000;
47
+ })();
48
+
49
+ function frameOf(type, data) {
50
+ return `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
51
+ }
52
+
53
+ // pipeline 在已 flush 的流里用 OpenAI 形状收口(`{error:{...}}` + `[DONE]`):
54
+ // 检出后改发 Anthropic 的 `error` 帧,别把错误伪装成正常收场(grill-decisions Q2)
55
+ function detectInBandError(text) {
56
+ let msg = null;
57
+ for (const line of String(text || "").split("\n")) {
58
+ const t = line.trim();
59
+ if (!t.startsWith("data:")) continue;
60
+ const payload = t.slice(5).trim();
61
+ if (!payload || payload === "[DONE]") continue;
62
+ try {
63
+ const j = JSON.parse(payload);
64
+ const e = j?.error;
65
+ if (e) msg = String(e?.message || e).slice(0, 500);
66
+ } catch { /* 非 JSON 帧忽略 */ }
67
+ }
68
+ return msg;
69
+ }
70
+
71
+
72
+ // pipeline 未 flush 时用 json() 直写聚合错误体(无 `data:` 前缀):认出来只取 message,
73
+ // 别把整坨 OpenAI JSON 当人话塞进 Anthropic error.message。
74
+ function jsonErrorText(text) {
75
+ const t = String(text || "").trim();
76
+ if (!t || t.startsWith(":") || t.includes("data:")) return null;
77
+ try {
78
+ const j = JSON.parse(t);
79
+ const e = j?.error;
80
+ if (!e) return null;
81
+ return String(e?.message || e).slice(0, 500);
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /** 两种上游收场形状(in-band `data:` 帧 / 聚合 JSON 体)统一取错误文案。 */
88
+ function errorText(text) {
89
+ return detectInBandError(text) || jsonErrorText(text);
90
+ }
91
+ // 客户端要流式而上游给了聚合 JSON(muse-spark / workbuddy 聚合 / zcode 非流式聚合都是现网常态):
92
+ // 那体没有 `data:` 前缀,会被 push() 当噪声整段丢掉 → 正文静默消失。认出来交给 fromChatJson 兜底。
93
+ function tryAggregate(text) {
94
+ const t = String(text || "").trim();
95
+ if (!t || t.includes("data:") || t.startsWith(":")) return null;
96
+ try {
97
+ const j = JSON.parse(t);
98
+ // 只认「非空 choices」:`{choices:[]}` 之类的空包什么也带不来,若当合法聚合包会跳过截断检测 → 零正文还谎报成功
99
+ return j && Array.isArray(j.choices) && j.choices.length > 0 ? j : null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+
106
+ /**
107
+ * Anthropic 出站垫片:具名事件 + 无 [DONE] + 空闲自发 ping + 真 headersSent。
108
+ * @param realRes 真 HTTP response
109
+ * @param translator createAnthropicChunkTranslator 实例
110
+ * @param opts { pingMs?: number } 测试注入口,缺省读 env
111
+ */
112
+ export function createAnthropicForwarder(realRes, translator, opts = {}) {
113
+ const pingMs = opts.pingMs ?? PING_MS;
114
+ let status = 200;
115
+ let headSent = false;
116
+ let ended = false;
117
+ let timer = null;
118
+ let pendingError = null;
119
+ let aggregate = null;
120
+ let rawAcc = ""; // 攒住无事件字节,供聚合回落识别
121
+ const sent = () => headSent || realRes.headersSent === true; // headersSent 与 getHeader 的唯一口径
122
+ // 上游按字节粒度喂 Buffer/Uint8Array:非流式解码会把多字节汉字切成 U+FFFD 并永久固化在行缓冲里
123
+ const decoder = new TextDecoder("utf-8");
124
+ const decode = (c, flush) => {
125
+ if (c == null) return flush ? String(decoder.decode()) : "";
126
+ if (typeof c === "string") return c;
127
+ const bytes = (typeof Buffer !== "undefined" && Buffer.isBuffer(c)) || c instanceof Uint8Array ? c : null;
128
+ if (bytes) return decoder.decode(bytes, { stream: !flush });
129
+ return String(c);
130
+ };
131
+ const dead = () => ended || realRes.writableEnded === true || realRes.destroyed === true;
132
+ const raw = (type, data) => { if (dead()) return; try { realRes.write(frameOf(type, data)); } catch { /* 下游已断 */ } };
133
+ const stopPing = () => { if (timer) { clearTimeout(timer); timer = null; } };
134
+ const armPing = () => {
135
+ if (!pingMs || dead()) return;
136
+ stopPing();
137
+ timer = setTimeout(() => { if (dead()) return; raw("ping", { type: "ping" }); armPing(); }, pingMs);
138
+ if (typeof timer.unref === "function") timer.unref();
139
+ };
140
+ const sendHead = () => {
141
+ if (headSent) return;
142
+ headSent = true;
143
+ try { realRes.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }); } catch { /* 已发 */ }
144
+ for (const e of translator.begin()) raw(e.type, e);
145
+ armPing();
146
+ };
147
+ const finish = () => { if (ended) return; ended = true; stopPing(); try { realRes.end(); } catch { /* ignore */ } };
148
+ // 错误收场:一帧 error 后关连接,不补 message_delta/message_stop(真 Anthropic API 即此行为)
149
+ const closeError = (msg) => { sendHead(); raw("error", { type: "error", error: { type: "api_error", message: String(msg || "upstream error").slice(0, 500) } }); finish(); };
150
+
151
+ // 整包聚合体可能被分多次 write:攒住「没产出任何事件」的字节再判一次(有事件即清空,防陈旧拼接)
152
+ const RAW_ACC_CAP = 262144;
153
+ let rawOverflow = false;
154
+ const tryAccumulate = (text) => {
155
+ if (aggregate || !text || rawOverflow) return;
156
+ rawAcc += text;
157
+ if (rawAcc.length > RAW_ACC_CAP) {
158
+ // 超上限就停手并留标记:继续攒也拼不出完整 JSON,静默攒下去只会让错误文案说谎
159
+ rawAcc = rawAcc.slice(0, RAW_ACC_CAP);
160
+ rawOverflow = true;
161
+ }
162
+ const agg = tryAggregate(rawAcc);
163
+ if (agg) aggregate = agg;
164
+ };
165
+ const res = {
166
+ ...createEmitter(),
167
+ set statusCode(v) { status = v; },
168
+ get statusCode() { return status; },
169
+ // helpers.js 的 json() 靠 headersSent + getHeader("content-type") 判「能不能回头」;
170
+ // 两者必须同源说真话,否则会出现「判成已 flush、却又拿不到 event-stream」的全静默窗口(错误既不 in-band 也不 JSON)。
171
+ get headersSent() { return sent(); },
172
+ // json() 的幂等早退判据;补上它才不会在我们已收场后再被当「可写」的响应对待
173
+ get writableEnded() { return ended || realRes.writableEnded === true; },
174
+ setHeader() { /* Anthropic SSE 的头由 sendHead 统一发,逐键 set 无意义 */ },
175
+ getHeader(name) {
176
+ if (String(name).toLowerCase() === "content-type" && sent()) return "text/event-stream";
177
+ return undefined;
178
+ },
179
+ write(c) {
180
+ if (ended) { alog("late-write-ignored", JSON.stringify({ id: translator.id, bytes: String(c?.length ?? 0) })); return true; }
181
+ const text = decode(c);
182
+ const err = errorText(text);
183
+ if (err) { pendingError = err; return true; } // 留给 end() 统一收口,不在这里半截发
184
+ // 上游一有动静(连 pipeline 的 keepalive 注释帧都算)就落头 + message_start + 武装 ping:
185
+ // 首块前的静默期正是客户端 idle watchdog 咬人的窗口。没收到任何字节时错误仍走干净 JSON(见 end())。
186
+ sendHead();
187
+ const evs = translator.push(text);
188
+ for (const e of evs) raw(e.type, e);
189
+ if (evs.length) rawAcc = ""; else tryAccumulate(text);
190
+ armPing();
191
+ return true;
192
+ },
193
+ end(c) {
194
+ if (ended) return;
195
+ const tail = decode(c, true);
196
+ const err = pendingError || errorText(tail);
197
+ if (status >= 400 && !headSent) {
198
+ ended = true;
199
+ stopPing();
200
+ json(realRes, status, anthropicError("api_error", err || String(tail).slice(0, 500) || "upstream error"));
201
+ return;
202
+ }
203
+ if (err) { closeError(err); return; }
204
+ // 客户端要的是流:先落头 + message_start + 武装 ping,再处理残余
205
+ sendHead();
206
+ if (tail) {
207
+ const evs = translator.push(tail);
208
+ for (const e of evs) raw(e.type, e);
209
+ if (evs.length) rawAcc = ""; else tryAccumulate(tail);
210
+ }
211
+ // 末帧常常没有收尾换行:补一个 `\n` 把残行逼出来,否则合法正文与 finish_reason 会一起烂在缓冲里(被误判成截断)
212
+ for (const e of translator.push("\n")) raw(e.type, e);
213
+ // 上游整包聚合(无 SSE 帧):喂同一套状态机兜底,别让正文静默消失
214
+ if (aggregate && !translator.stats().blocks) { for (const e of translator.fromChatJson(aggregate)) raw(e.type, e); }
215
+ const fin = translator.getFinal();
216
+ if (!fin.finish && !aggregate) {
217
+ alog("truncated", JSON.stringify({ id: translator.id, ...translator.stats() }));
218
+ closeError(rawOverflow
219
+ ? "upstream sent an oversized non-SSE body that could not be aggregated (no finish_reason)"
220
+ : "upstream stream ended before the model finished its turn (no finish_reason)");
221
+ return;
222
+ }
223
+ for (const e of translator.end({ finish: fin.finish, usage: fin.usage })) raw(e.type, e);
224
+ finish();
225
+ },
226
+ };
227
+ // 客户端提前断开:pipeline 会感知 close,但 ping 定时器归本垫片管,必须自己停
228
+ try { realRes.on?.("close", () => { if (!ended) { ended = true; stopPing(); } }); } catch { /* ignore */ }
229
+ return res;
230
+ }
231
+
232
+ /**
233
+ * 代跑结果回给客户端:非流式整包 JSON,流式由同一份块生成的具名事件(无 `[DONE]`)。
234
+ * 搜索在调用前就跑完了,这里一次写完即可,不需要 createAnthropicForwarder 的 ping 垫片。
235
+ */
236
+ function respondSearch(res, { message, stream, provider }, isDead) {
237
+ if (isDead && isDead()) return undefined; // 搜索期间客户端断连:别再写任何字节
238
+ if (!stream) {
239
+ try { res.setHeader?.("x-mslxdff-web-search", provider); } catch { /* 已 flush */ }
240
+ try { return json(res, 200, message); } catch { return undefined; /* 下游已断 */ }
241
+ }
242
+ try {
243
+ res.writeHead(200, {
244
+ "Content-Type": "text/event-stream",
245
+ "Cache-Control": "no-cache",
246
+ Connection: "keep-alive",
247
+ "x-mslxdff-web-search": provider,
248
+ });
249
+ for (const e of searchEvents(message)) res.write(frameOf(e.type, e));
250
+ res.end();
251
+ } catch { /* 下游已断 */ }
252
+ return undefined;
253
+ }
254
+
255
+ export async function messagesHandler(ctx) {
256
+ const { req, res } = ctx;
257
+ const t0 = Date.now();
258
+ let body;
259
+ try {
260
+ body = await readBody(req);
261
+ } catch {
262
+ return json(res, 400, anthropicError("invalid_request_error", "Invalid JSON body"));
263
+ }
264
+ // —— 网关代跑 web_search(ADR-0049):旁路请求**一律由网关作答**——命中判据即不打模型。
265
+ // 搜到就回结果块;关了/全挂/空查询就回一条明确的 400(把无工具的旁路丢给上游,只会让模型凭空
266
+ // 编 URL 并让客户端按「0 次搜索」空转重试,那才是改前的老毛病)。主循环请求不受影响。
267
+ // 整段包 try:搜索期最长 15s,其间客户端可能已断连(`isDead`),任何意外都不得逃逸成 5xx。
268
+ const det = detectSearchRequest(body);
269
+ if (det.hit) {
270
+ const isDead = () => res.writableEnded === true || res.destroyed === true || res.headersSent === true;
271
+ const wsCfg = searchConfig();
272
+ let r;
273
+ if (!wsCfg.enabled) r = { ok: false, error: "disabled by MSLXDFF_WEB_SEARCH=off" };
274
+ else {
275
+ try {
276
+ r = det.query ? await runWebSearch(det.query, wsCfg) : { ok: false, error: "empty query" };
277
+ } catch (e) {
278
+ r = { ok: false, error: String(e?.message || e) };
279
+ }
280
+ }
281
+ if (r.ok) {
282
+ const readout = { provider: r.provider, ms: r.ms, results: r.results.length, queryChars: r.query.length, chain: r.chain || [] };
283
+ alog("web-search", JSON.stringify(readout));
284
+ try { ctx.logs?.appendEvent?.({ type: "web-search", ...readout }); } catch { /* 观测产物不参与判决 */ }
285
+ return respondSearch(res, {
286
+ message: buildSearchMessage({ model: String(body.model || ""), query: r.query, results: r.results }),
287
+ stream: Boolean(body.stream),
288
+ provider: r.provider,
289
+ }, isDead);
290
+ }
291
+ const why = String(r.error || "unknown").slice(0, 200);
292
+ alog("web-search-unavailable", why);
293
+ try { ctx.logs?.appendEvent?.({ type: "web-search-unavailable", queryChars: (det.query || "").length, error: why, chain: r.chain || [] }); } catch { /* 同上 */ }
294
+ if (isDead()) return undefined;
295
+ try { return json(res, 400, anthropicError("invalid_request_error", `web search failed at the gateway: ${why}`)); } catch { return undefined; }
296
+ }
297
+ let chatBody;
298
+ try {
299
+ chatBody = messagesToChatBody(body);
300
+ } catch (e) {
301
+ alog("translate-req-400", String(e?.message || e));
302
+ return json(res, 400, anthropicError("invalid_request_error", String(e?.message || e)));
303
+ }
304
+ alog("req", JSON.stringify({
305
+ model: chatBody.model, stream: chatBody.stream,
306
+ msgs: chatBody.messages.map((m) => `${m.role}:${(typeof m.content === "string" ? m.content : "[parts]").length}${m.tool_calls ? `+${m.tool_calls.length}tc` : ""}`),
307
+ tools: Array.isArray(chatBody.tools) ? chatBody.tools.length : 0,
308
+ }));
309
+ const pipeline = createChatPipeline(ctx);
310
+ const fakeReq = withBody(req, chatBody);
311
+ // ADR-0055:请求侧 token 估算 thunk(路由层也 memoize,与决策 4「每请求至多算一次」对齐;
312
+ // translator 内部还有一层 memoize,双保险)。
313
+ let estCache;
314
+ const estInput = () => (estCache ??= estimateTokens(body).input_tokens);
315
+ const forwardClose = (shim) => { try { req.on?.("close", () => shim.emit("close")); } catch { /* ignore */ } };
316
+ try {
317
+ if (chatBody.stream) {
318
+ const translator = createAnthropicChunkTranslator(chatBody.model, { thinking: emitThinking(), estInput });
319
+ const live = createAnthropicForwarder(res, translator);
320
+ forwardClose(live);
321
+ await pipeline.execute({ req: fakeReq, res: live });
322
+ alog("done-stream", JSON.stringify({ ms: Date.now() - t0, id: translator.id, ...translator.stats(), ...translator.getFinal() }));
323
+ } else {
324
+ const cap = createCollector();
325
+ forwardClose(cap.res);
326
+ await pipeline.execute({ req: fakeReq, res: cap.res });
327
+ const { status, text } = cap.get();
328
+ if (status >= 400) return json(res, status, anthropicError("api_error", errorText(text) || String(text).slice(0, 500)));
329
+ let chatJson = null;
330
+ try { chatJson = JSON.parse(text); } catch { /* 非 JSON */ }
331
+ // 上游只会流式(workbuddy 写死 stream:true):非流式收到的 text 是 SSE → 聚成 chat JSON 再走同一翻译出口;
332
+ // 空流/错误帧聚合不出 JSON 时仍落回下方原 502 路径。
333
+ if (!chatJson) chatJson = sseToChatJson(text, chatBody.model);
334
+ if (!chatJson || chatJson.error || chatJson.object === "error") {
335
+ return json(res, status >= 400 ? status : 502, anthropicError("api_error", errorText(text) || String(text).slice(0, 500)));
336
+ }
337
+ // 上游偶发非 chat 形状(无 choices):包成单 text 块,绝不 500(照 responses-route.js 先例)
338
+ if (!Array.isArray(chatJson.choices)) {
339
+ const r = chatJsonToAnthropic({ choices: [{ message: { content: String(text).slice(0, 8000) } }] }, chatBody.model, { estInput });
340
+ alog("done-nonchat", JSON.stringify({ ms: Date.now() - t0, bytes: text.length }));
341
+ return json(res, 200, r);
342
+ }
343
+ const out = chatJsonToAnthropic(chatJson, chatBody.model, { estInput });
344
+ alog("done-json", JSON.stringify({ ms: Date.now() - t0, status, blocks: out.content.length, stop: out.stop_reason, usage: out.usage }));
345
+ return json(res, 200, out);
346
+ }
347
+ } catch (err) {
348
+ alog("execute-throw", String(err?.message || err).slice(0, 300));
349
+ if (!res.headersSent) return json(res, 502, anthropicError("api_error", String(err?.message || err)));
350
+ try { res.end(); } catch { /* ignore */ }
351
+ }
352
+ }
353
+
354
+ export async function countTokensHandler(ctx) {
355
+ const { req, res } = ctx;
356
+ let body;
357
+ try {
358
+ body = await readBody(req);
359
+ } catch {
360
+ return json(res, 400, anthropicError("invalid_request_error", "Invalid JSON body"));
361
+ }
362
+ const out = estimateTokens(body);
363
+ alog("count-tokens", JSON.stringify({ model: body?.model, input_tokens: out.input_tokens }));
364
+ return json(res, 200, out);
365
+ }