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,287 +1,287 @@
1
- /**
2
- * Anthropic Messages 出站翻译层:chat → Anthropic message / SSE 具名事件(/v1/messages 外壳,ADR-0047)。
3
- * 纯函数,无网络、无副作用;出站形状依据 code.claude.com/docs/en/llm-gateway-protocol
4
- * 与参考实现 lamdt1/ms-copilot365-2api/app/formatters/anthropic_sse.py。
5
- * 分档(ADR-0055,为守本文件 ≤20KB 体积硬门;对外 import 路径全走本文件、调用方零改动):
6
- * · 请求侧 Anthropic → chat 在 ./request-body.js;
7
- * · 消息/工具 id 与协议错误体在 ./shared.js;
8
- * · token 估算的单一来源在 ../text-tokens.js(上游未报 usage 时由它兜底)。
9
- *
10
- * 与 src/responses/translate.js 的三点关键差异:
11
- * 1) Anthropic 的 content block **只封口不复用**(Responses 侧 ensureTextItem 会复用已开项);
12
- * 2) 事件是具名的(`event: <type>` + `data:`),且**没有 `data: [DONE]`**,收场只有
13
- * `message_delta`+`message_stop`(成功)或一帧 `error`(失败)——见 grill-decisions.md Q2;
14
- * 3) 请求侧**保留思考档位**、**必须主动丢** output_config/context_management 与消息块内 thinking
15
- * (块内 thinking 我们无法签发 signature,回传必被上游拒)——细则见 ./request-body.js 头(ADR-0047)。
16
- */
17
- import { chunkToString } from "../responses/translate.js";
18
- import { createTokenCounter } from "../text-tokens.js";
19
- import { newMessageId, newToolUseId } from "./shared.js";
20
-
21
- // re-export:拆档不改对外接口,调用方与测试仍从本文件取这些符号
22
- export { messagesToChatBody, toolResultTextOf } from "./request-body.js";
23
- export { newMessageId, newToolUseId, anthropicError } from "./shared.js";
24
-
25
- // ---------- usage / stop_reason ----------
26
-
27
- /**
28
- * chat 口径 usage → Anthropic 口径。
29
- * Anthropic 的 `input_tokens` **不含**缓存读命中的部分(两者分开计费),而上游
30
- * `prompt_tokens` 是含缓存的总输入 —— 故命中数要从 input 里扣掉,否则客户端把同一段
31
- * 前缀算两遍(读数虚高)。
32
- * `fallback()` 只在**上游整条未报**(缺字段,或 prompt/output/cache_read/cache_creation 四项全为 0)时才用:
33
- * 真实模型对非空 prompt 不可能报 0,照抄 0 会让客户端的上下文计度停在原地、永不压实(ADR-0055 决策 3)。
34
- */
35
- export function toAnthropicUsage(u = {}, fallback = null) {
36
- const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
37
- const src = u && typeof u === "object" ? u : {};
38
- const prompt = num(src.prompt_tokens ?? src.input_tokens);
39
- const output = num(src.completion_tokens ?? src.output_tokens);
40
- const cacheRead = num(src.cache_read_input_tokens ?? src.prompt_tokens_details?.cached_tokens ?? src.input_tokens_details?.cached_tokens);
41
- const cacheWrite = num(src.cache_creation_input_tokens ?? src.prompt_tokens_details?.cache_creation_input_tokens);
42
- const usage = {
43
- input_tokens: Math.max(0, prompt - cacheRead - cacheWrite),
44
- output_tokens: output,
45
- cache_read_input_tokens: cacheRead,
46
- cache_creation_input_tokens: cacheWrite,
47
- };
48
- // cache 非零也算「报了」:SDK 通道(upstream-engine/sdk/sse.js 的 usageToOpenAI)会输出
49
- // `{prompt_tokens:0, …, prompt_tokens_details:{cached_tokens:N}}` 这种形状,若当未报整条覆盖,
50
- // 客户端「input + cache_read」会把同一段前缀算两遍(正是上面扣减要防的事,评审 P2)。
51
- if (fallback && !(prompt > 0 || output > 0 || cacheRead > 0 || cacheWrite > 0)) return { ...usage, ...(fallback() || {}) };
52
- return usage;
53
- }
54
-
55
- function toStopReason(finish, hasToolUse) {
56
- if (hasToolUse || finish === "tool_calls" || finish === "function_call") return "tool_use";
57
- if (finish === "length") return "max_tokens";
58
- return "end_turn";
59
- }
60
-
61
- // ---------- 非流式响应:chat JSON → Anthropic message ----------
62
-
63
- function parseToolInput(args) {
64
- const raw = String(args || "");
65
- if (!raw.trim()) return {};
66
- try {
67
- const v = JSON.parse(raw);
68
- return v && typeof v === "object" && !Array.isArray(v) ? v : { value: v };
69
- } catch {
70
- return { raw }; // 上游吐出发散 JSON:退化成 raw,不整轮失败
71
- }
72
- }
73
-
74
- /**
75
- * 上游 chat 的 `message.content` 既可能是字符串,也可能是部件数组(`[{type:"text",text:…}, {type:"image_url",…}]`,
76
- * 多模态上游与部分聚合网关都这么回)。统一抽成正文文本;图片部件在本外壳里无法映射成 Anthropic 的 image 块,只能弃(见 ADR-0047 边界 11)。
77
- */
78
- export function textFromChatContent(c) {
79
- if (typeof c === "string") return c;
80
- if (!Array.isArray(c)) return "";
81
- let t = "";
82
- for (const p of c) {
83
- if (p && typeof p === "object" && typeof p.text === "string") t += p.text;
84
- }
85
- return t;
86
- }
87
-
88
- export function chatJsonToAnthropic(chatJson = {}, model = "", opts = {}) {
89
- const choice = chatJson.choices?.[0] || {};
90
- const message = choice.message || {};
91
- const content = [];
92
- const text = textFromChatContent(message.content);
93
- if (text) content.push({ type: "text", text });
94
- for (const tc of message.tool_calls || []) {
95
- content.push({ type: "tool_use", id: String(tc.id || newToolUseId()), name: String(tc.function?.name || ""), input: parseToolInput(tc.function?.arguments) });
96
- }
97
- return {
98
- id: typeof chatJson.id === "string" && chatJson.id.startsWith("msg_") ? chatJson.id : newMessageId(),
99
- type: "message",
100
- role: "assistant",
101
- model: model || chatJson.model || "",
102
- content: content.length ? content : [{ type: "text", text: "" }],
103
- stop_reason: toStopReason(choice.finish_reason, content.some((b) => b.type === "tool_use")),
104
- stop_sequence: null,
105
- usage: toAnthropicUsage(chatJson.usage, () => fallbackUsage(opts, content)),
106
- };
107
- }
108
-
109
- // 上游整条未报 usage 时的兜底(ADR-0055 决策 3):请求侧用注入的 estInput,生成侧按已产出的块估。
110
- function fallbackUsage(opts = {}, content = []) {
111
- const counter = createTokenCounter();
112
- for (const b of content) {
113
- if (b?.type === "text") counter.add(b.text || "");
114
- else if (b?.type === "tool_use") counter.add(JSON.stringify(b.input ?? {})); // 只计参数:与流式 pushTool 同口径(name 从不进 gen)
115
- }
116
- const n = typeof opts.estInput === "function" ? Number(opts.estInput()) : 0;
117
- return { input_tokens: Number.isFinite(n) && n > 0 ? Math.floor(n) : 0, output_tokens: counter.tokens() };
118
- }
119
-
120
- // ---------- 流式响应:chat SSE chunk → Anthropic 具名事件 ----------
121
-
122
- // thinking 块必须在 content_block_stop **之前**交一帧 signature_delta,缺它 Claude Code 把整块静默丢弃
123
- // (routatic/proxy#51 真机复现、PR#133 修)。下面是**身份占位**:不含思考原文、不冒充密码学签名,理由与风险见 ADR-0052。
124
- const SIG_PREFIX = "mslxdff:thinking:v1:";
125
- function thinkingSignature(messageId, index) {
126
- return Buffer.from(`${SIG_PREFIX}${messageId}:${index}`, "utf8").toString("base64url");
127
- }
128
-
129
- /**
130
- * @param model 回显给客户端的模型名
131
- * @param opts { id?, thinking?: boolean } thinking=true 才把 reasoning_content 发成 thinking 块
132
- */
133
- export function createAnthropicChunkTranslator(model = "", opts = {}) {
134
- const id = opts.id || newMessageId();
135
- const emitThinking = opts.thinking === true;
136
- const created = Math.floor(Date.now() / 1000);
137
- const ev = (type, extra = {}) => ({ type, ...extra });
138
-
139
- // 请求侧 token 估算(ADR-0055 决策 3/5):路由注入 `opts.estInput()`,memoize —— `message_start` 必须在
140
- // 首块前就带 input_tokens,全文只能扫一遍;上游后报实测 usage 时估算只留在首帧,不进 message_delta。
141
- let estCached = null;
142
- const estInput = () => {
143
- if (estCached == null) {
144
- const n = typeof opts.estInput === "function" ? Number(opts.estInput()) : 0;
145
- estCached = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
146
- }
147
- return estCached;
148
- };
149
- const gen = createTokenCounter(); // 生成侧累计(text + thinking + tool 参数增量)
150
-
151
- let buf = "";
152
- let nextIndex = 0;
153
- let open = null; // 当前未封口块:{ index, kind: "text"|"thinking"|"tool_use", text? }
154
- const tools = new Map(); // 上游 tc.index → { id, name, args, announced, index }
155
- let sawToolUse = false;
156
- let lastFinish = null;
157
- let lastUsage = null;
158
- const dbg = { chunks: 0, textChars: 0, toolDeltas: 0, thinkingChars: 0, skippedLines: 0, jsonFails: 0, lateToolDeltas: 0, blocks: 0 };
159
-
160
- function closeOpen(out) {
161
- if (!open) return;
162
- if (open.kind === "thinking") out.push(ev("content_block_delta", { index: open.index, delta: { type: "signature_delta", signature: thinkingSignature(id, open.index) } }));
163
- out.push(ev("content_block_stop", { index: open.index }));
164
- open = null;
165
- }
166
- function openBlock(kind, contentBlock, out) {
167
- closeOpen(out);
168
- open = { index: nextIndex++, kind, text: "" };
169
- dbg.blocks = open.index + 1;
170
- out.push(ev("content_block_start", { index: open.index, content_block: contentBlock }));
171
- return open;
172
- }
173
- function pushText(delta) {
174
- const out = [];
175
- if (!open || open.kind !== "text") openBlock("text", { type: "text", text: "" }, out);
176
- open.text += delta;
177
- gen.add(delta); // 生成侧只累计字数、末帧一次套公式:逐帧套会把参照实现的 +1 下限乘上帧数
178
- out.push(ev("content_block_delta", { index: open.index, delta: { type: "text_delta", text: delta } }));
179
- return out;
180
- }
181
- function pushThinking(delta) {
182
- const out = [];
183
- if (!open || open.kind !== "thinking") openBlock("thinking", { type: "thinking", thinking: "", signature: "" }, out);
184
- open.text += delta;
185
- gen.add(delta); // thinking 也按 output 计费(ADR-0055 grill Q6)
186
- out.push(ev("content_block_delta", { index: open.index, delta: { type: "thinking_delta", thinking: delta } }));
187
- return out;
188
- }
189
- function announceTool(t, out) {
190
- t.announced = true;
191
- sawToolUse = true;
192
- // 统一走 openBlock:自动封掉当前 text/thinking/上一个工具块,本块成为新的待封 open
193
- openBlock("tool_use", { type: "tool_use", id: t.id, name: t.name, input: {} }, out);
194
- t.index = open.index;
195
- // 增量早于 id/name 到达时先攒着,announce 后补发(事件顺序仍合规)
196
- if (t.args) out.push(ev("content_block_delta", { index: t.index, delta: { type: "input_json_delta", partial_json: t.args } }));
197
- }
198
- function pushTool(tc) {
199
- const slot = Number(tc?.index ?? 0);
200
- const out = [];
201
- let t = tools.get(slot);
202
- if (!t) { t = { id: "", name: "", args: "", announced: false, index: null }; tools.set(slot, t); }
203
- if (tc?.id) t.id = String(tc.id);
204
- if (tc?.function?.name) t.name += String(tc.function.name);
205
- const frag = tc?.function?.arguments ? String(tc.function.arguments) : "";
206
- if (frag) { dbg.toolDeltas++; t.args += frag; gen.add(frag); }
207
- if (!t.announced && t.id && t.name) announceTool(t, out);
208
- else if (t.announced && frag && open && open.index === t.index) out.push(ev("content_block_delta", { index: t.index, delta: { type: "input_json_delta", partial_json: frag } }));
209
- else if (t.announced && frag) dbg.lateToolDeltas++; // 块已封口后才来的参数增量:发出即违规帧,丢弃但上面已计入 gen(output_tokens 取计费口径:模型确实生成了这些字节)
210
- return out;
211
- }
212
-
213
- function begin() {
214
- return [ev("message_start", { message: { id, type: "message", role: "assistant", model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: estInput(), output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 } } })];
215
- }
216
-
217
- function push(data) {
218
- buf += chunkToString(data);
219
- const out = [];
220
- let nl;
221
- while ((nl = buf.indexOf("\n")) >= 0) {
222
- const line = buf.slice(0, nl).trim();
223
- buf = buf.slice(nl + 1);
224
- // 注释帧/keepalive(`:` 开头)与上游 `[DONE]` 都不进 Anthropic 事件序列(grill Q6)
225
- if (!line || line.startsWith(":")) continue;
226
- if (!line.startsWith("data:")) { dbg.skippedLines++; continue; }
227
- const payload = line.slice(5).trim();
228
- if (!payload || payload === "[DONE]") continue;
229
- let chunk;
230
- try { chunk = JSON.parse(payload); } catch { dbg.jsonFails++; continue; }
231
- dbg.chunks++;
232
- const choice = chunk.choices?.[0] || {};
233
- const delta = choice.delta || {};
234
- if (choice.finish_reason) lastFinish = choice.finish_reason;
235
- if (chunk.usage) lastUsage = chunk.usage;
236
- if (typeof delta.content === "string" && delta.content) { dbg.textChars += delta.content.length; out.push(...pushText(delta.content)); }
237
- for (const tc of delta.tool_calls || []) out.push(...pushTool(tc));
238
- const rc = typeof delta.reasoning_content === "string" ? delta.reasoning_content : "";
239
- if (rc) {
240
- dbg.thinkingChars += rc.length;
241
- if (emitThinking) out.push(...pushThinking(rc)); // 只发 thinking_delta,无 signature 可发
242
- else gen.add(rc); // `MSLXDFF_ANTHROPIC_THINKING=0` 时思考字节仍是真实生成,兜底 output 照计(在此加、不在 pushThinking 内加,避免双计)
243
- }
244
- }
245
- return out;
246
- }
247
-
248
- function end({ finish = null, usage = null } = {}) {
249
- const out = [];
250
- const fr = finish || lastFinish;
251
- for (const t of tools.values()) {
252
- if (!t.announced && (t.id || t.name || t.args)) announceTool(t, out); // 只有 arguments 无 name 的退化收尾
253
- }
254
- closeOpen(out);
255
- out.push(ev("message_delta", { delta: { stop_reason: toStopReason(fr, sawToolUse), stop_sequence: null }, usage: toAnthropicUsage(usage || lastUsage, () => ({ input_tokens: estInput(), output_tokens: gen.tokens() })) }));
256
- out.push(ev("message_stop"));
257
- return out;
258
- }
259
-
260
- // 上游给聚合 JSON(非 SSE,muse-spark / workbuddy 聚合 / zcode 非流式都是这种)时,
261
- // 把整包喂进同一套 pushText/pushTool 状态机 —— 与流式共用封口规则,不出第二套形状。
262
- function fromChatJson(chatJson = {}) {
263
- const out = [];
264
- const choice = chatJson.choices?.[0] || {};
265
- const m = choice.message || {};
266
- const ct = textFromChatContent(m.content); // 字符串或部件数组都吃,别把数组正文整段丢掉
267
- // 聚合上游把思考放在 message.reasoning_content(muse-spark / workbuddy 聚合 / zcode 非流式):thinking 块须排 text 前。
268
- const rt = typeof m.reasoning_content === "string" ? m.reasoning_content : "";
269
- if (rt && emitThinking) out.push(...pushThinking(rt));
270
- if (ct) out.push(...pushText(ct));
271
- (Array.isArray(m.tool_calls) ? m.tool_calls : []).forEach((tc, i) => {
272
- out.push(...pushTool({ index: i, id: tc?.id, function: { name: tc?.function?.name, arguments: tc?.function?.arguments } }));
273
- });
274
- if (choice.finish_reason) lastFinish = choice.finish_reason;
275
- if (chatJson.usage) lastUsage = chatJson.usage;
276
- return out;
277
- }
278
-
279
- function getFinal() {
280
- return { finish: lastFinish, usage: lastUsage, sawToolUse };
281
- }
282
- function stats() {
283
- return { ...dbg, openKind: open?.kind || null, toolSlots: tools.size };
284
- }
285
-
286
- return { id, created, begin, push, end, fromChatJson, getFinal, stats };
287
- }
1
+ /**
2
+ * Anthropic Messages 出站翻译层:chat → Anthropic message / SSE 具名事件(/v1/messages 外壳,ADR-0047)。
3
+ * 纯函数,无网络、无副作用;出站形状依据 code.claude.com/docs/en/llm-gateway-protocol
4
+ * 与参考实现 lamdt1/ms-copilot365-2api/app/formatters/anthropic_sse.py。
5
+ * 分档(ADR-0055,为守本文件 ≤20KB 体积硬门;对外 import 路径全走本文件、调用方零改动):
6
+ * · 请求侧 Anthropic → chat 在 ./request-body.js;
7
+ * · 消息/工具 id 与协议错误体在 ./shared.js;
8
+ * · token 估算的单一来源在 ../text-tokens.js(上游未报 usage 时由它兜底)。
9
+ *
10
+ * 与 src/responses/translate.js 的三点关键差异:
11
+ * 1) Anthropic 的 content block **只封口不复用**(Responses 侧 ensureTextItem 会复用已开项);
12
+ * 2) 事件是具名的(`event: <type>` + `data:`),且**没有 `data: [DONE]`**,收场只有
13
+ * `message_delta`+`message_stop`(成功)或一帧 `error`(失败)——见 grill-decisions.md Q2;
14
+ * 3) 请求侧**保留思考档位**、**必须主动丢** output_config/context_management 与消息块内 thinking
15
+ * (块内 thinking 我们无法签发 signature,回传必被上游拒)——细则见 ./request-body.js 头(ADR-0047)。
16
+ */
17
+ import { chunkToString } from "../responses/translate.js";
18
+ import { createTokenCounter } from "../text-tokens.js";
19
+ import { newMessageId, newToolUseId } from "./shared.js";
20
+
21
+ // re-export:拆档不改对外接口,调用方与测试仍从本文件取这些符号
22
+ export { messagesToChatBody, toolResultTextOf } from "./request-body.js";
23
+ export { newMessageId, newToolUseId, anthropicError } from "./shared.js";
24
+
25
+ // ---------- usage / stop_reason ----------
26
+
27
+ /**
28
+ * chat 口径 usage → Anthropic 口径。
29
+ * Anthropic 的 `input_tokens` **不含**缓存读命中的部分(两者分开计费),而上游
30
+ * `prompt_tokens` 是含缓存的总输入 —— 故命中数要从 input 里扣掉,否则客户端把同一段
31
+ * 前缀算两遍(读数虚高)。
32
+ * `fallback()` 只在**上游整条未报**(缺字段,或 prompt/output/cache_read/cache_creation 四项全为 0)时才用:
33
+ * 真实模型对非空 prompt 不可能报 0,照抄 0 会让客户端的上下文计度停在原地、永不压实(ADR-0055 决策 3)。
34
+ */
35
+ export function toAnthropicUsage(u = {}, fallback = null) {
36
+ const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
37
+ const src = u && typeof u === "object" ? u : {};
38
+ const prompt = num(src.prompt_tokens ?? src.input_tokens);
39
+ const output = num(src.completion_tokens ?? src.output_tokens);
40
+ const cacheRead = num(src.cache_read_input_tokens ?? src.prompt_tokens_details?.cached_tokens ?? src.input_tokens_details?.cached_tokens);
41
+ const cacheWrite = num(src.cache_creation_input_tokens ?? src.prompt_tokens_details?.cache_creation_input_tokens);
42
+ const usage = {
43
+ input_tokens: Math.max(0, prompt - cacheRead - cacheWrite),
44
+ output_tokens: output,
45
+ cache_read_input_tokens: cacheRead,
46
+ cache_creation_input_tokens: cacheWrite,
47
+ };
48
+ // cache 非零也算「报了」:SDK 通道(upstream-engine/sdk/sse.js 的 usageToOpenAI)会输出
49
+ // `{prompt_tokens:0, …, prompt_tokens_details:{cached_tokens:N}}` 这种形状,若当未报整条覆盖,
50
+ // 客户端「input + cache_read」会把同一段前缀算两遍(正是上面扣减要防的事,评审 P2)。
51
+ if (fallback && !(prompt > 0 || output > 0 || cacheRead > 0 || cacheWrite > 0)) return { ...usage, ...(fallback() || {}) };
52
+ return usage;
53
+ }
54
+
55
+ function toStopReason(finish, hasToolUse) {
56
+ if (hasToolUse || finish === "tool_calls" || finish === "function_call") return "tool_use";
57
+ if (finish === "length") return "max_tokens";
58
+ return "end_turn";
59
+ }
60
+
61
+ // ---------- 非流式响应:chat JSON → Anthropic message ----------
62
+
63
+ function parseToolInput(args) {
64
+ const raw = String(args || "");
65
+ if (!raw.trim()) return {};
66
+ try {
67
+ const v = JSON.parse(raw);
68
+ return v && typeof v === "object" && !Array.isArray(v) ? v : { value: v };
69
+ } catch {
70
+ return { raw }; // 上游吐出发散 JSON:退化成 raw,不整轮失败
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 上游 chat 的 `message.content` 既可能是字符串,也可能是部件数组(`[{type:"text",text:…}, {type:"image_url",…}]`,
76
+ * 多模态上游与部分聚合网关都这么回)。统一抽成正文文本;图片部件在本外壳里无法映射成 Anthropic 的 image 块,只能弃(见 ADR-0047 边界 11)。
77
+ */
78
+ export function textFromChatContent(c) {
79
+ if (typeof c === "string") return c;
80
+ if (!Array.isArray(c)) return "";
81
+ let t = "";
82
+ for (const p of c) {
83
+ if (p && typeof p === "object" && typeof p.text === "string") t += p.text;
84
+ }
85
+ return t;
86
+ }
87
+
88
+ export function chatJsonToAnthropic(chatJson = {}, model = "", opts = {}) {
89
+ const choice = chatJson.choices?.[0] || {};
90
+ const message = choice.message || {};
91
+ const content = [];
92
+ const text = textFromChatContent(message.content);
93
+ if (text) content.push({ type: "text", text });
94
+ for (const tc of message.tool_calls || []) {
95
+ content.push({ type: "tool_use", id: String(tc.id || newToolUseId()), name: String(tc.function?.name || ""), input: parseToolInput(tc.function?.arguments) });
96
+ }
97
+ return {
98
+ id: typeof chatJson.id === "string" && chatJson.id.startsWith("msg_") ? chatJson.id : newMessageId(),
99
+ type: "message",
100
+ role: "assistant",
101
+ model: model || chatJson.model || "",
102
+ content: content.length ? content : [{ type: "text", text: "" }],
103
+ stop_reason: toStopReason(choice.finish_reason, content.some((b) => b.type === "tool_use")),
104
+ stop_sequence: null,
105
+ usage: toAnthropicUsage(chatJson.usage, () => fallbackUsage(opts, content)),
106
+ };
107
+ }
108
+
109
+ // 上游整条未报 usage 时的兜底(ADR-0055 决策 3):请求侧用注入的 estInput,生成侧按已产出的块估。
110
+ function fallbackUsage(opts = {}, content = []) {
111
+ const counter = createTokenCounter();
112
+ for (const b of content) {
113
+ if (b?.type === "text") counter.add(b.text || "");
114
+ else if (b?.type === "tool_use") counter.add(JSON.stringify(b.input ?? {})); // 只计参数:与流式 pushTool 同口径(name 从不进 gen)
115
+ }
116
+ const n = typeof opts.estInput === "function" ? Number(opts.estInput()) : 0;
117
+ return { input_tokens: Number.isFinite(n) && n > 0 ? Math.floor(n) : 0, output_tokens: counter.tokens() };
118
+ }
119
+
120
+ // ---------- 流式响应:chat SSE chunk → Anthropic 具名事件 ----------
121
+
122
+ // thinking 块必须在 content_block_stop **之前**交一帧 signature_delta,缺它 Claude Code 把整块静默丢弃
123
+ // (routatic/proxy#51 真机复现、PR#133 修)。下面是**身份占位**:不含思考原文、不冒充密码学签名,理由与风险见 ADR-0052。
124
+ const SIG_PREFIX = "mslxdff:thinking:v1:";
125
+ function thinkingSignature(messageId, index) {
126
+ return Buffer.from(`${SIG_PREFIX}${messageId}:${index}`, "utf8").toString("base64url");
127
+ }
128
+
129
+ /**
130
+ * @param model 回显给客户端的模型名
131
+ * @param opts { id?, thinking?: boolean } thinking=true 才把 reasoning_content 发成 thinking 块
132
+ */
133
+ export function createAnthropicChunkTranslator(model = "", opts = {}) {
134
+ const id = opts.id || newMessageId();
135
+ const emitThinking = opts.thinking === true;
136
+ const created = Math.floor(Date.now() / 1000);
137
+ const ev = (type, extra = {}) => ({ type, ...extra });
138
+
139
+ // 请求侧 token 估算(ADR-0055 决策 3/5):路由注入 `opts.estInput()`,memoize —— `message_start` 必须在
140
+ // 首块前就带 input_tokens,全文只能扫一遍;上游后报实测 usage 时估算只留在首帧,不进 message_delta。
141
+ let estCached = null;
142
+ const estInput = () => {
143
+ if (estCached == null) {
144
+ const n = typeof opts.estInput === "function" ? Number(opts.estInput()) : 0;
145
+ estCached = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
146
+ }
147
+ return estCached;
148
+ };
149
+ const gen = createTokenCounter(); // 生成侧累计(text + thinking + tool 参数增量)
150
+
151
+ let buf = "";
152
+ let nextIndex = 0;
153
+ let open = null; // 当前未封口块:{ index, kind: "text"|"thinking"|"tool_use", text? }
154
+ const tools = new Map(); // 上游 tc.index → { id, name, args, announced, index }
155
+ let sawToolUse = false;
156
+ let lastFinish = null;
157
+ let lastUsage = null;
158
+ const dbg = { chunks: 0, textChars: 0, toolDeltas: 0, thinkingChars: 0, skippedLines: 0, jsonFails: 0, lateToolDeltas: 0, blocks: 0 };
159
+
160
+ function closeOpen(out) {
161
+ if (!open) return;
162
+ if (open.kind === "thinking") out.push(ev("content_block_delta", { index: open.index, delta: { type: "signature_delta", signature: thinkingSignature(id, open.index) } }));
163
+ out.push(ev("content_block_stop", { index: open.index }));
164
+ open = null;
165
+ }
166
+ function openBlock(kind, contentBlock, out) {
167
+ closeOpen(out);
168
+ open = { index: nextIndex++, kind, text: "" };
169
+ dbg.blocks = open.index + 1;
170
+ out.push(ev("content_block_start", { index: open.index, content_block: contentBlock }));
171
+ return open;
172
+ }
173
+ function pushText(delta) {
174
+ const out = [];
175
+ if (!open || open.kind !== "text") openBlock("text", { type: "text", text: "" }, out);
176
+ open.text += delta;
177
+ gen.add(delta); // 生成侧只累计字数、末帧一次套公式:逐帧套会把参照实现的 +1 下限乘上帧数
178
+ out.push(ev("content_block_delta", { index: open.index, delta: { type: "text_delta", text: delta } }));
179
+ return out;
180
+ }
181
+ function pushThinking(delta) {
182
+ const out = [];
183
+ if (!open || open.kind !== "thinking") openBlock("thinking", { type: "thinking", thinking: "", signature: "" }, out);
184
+ open.text += delta;
185
+ gen.add(delta); // thinking 也按 output 计费(ADR-0055 grill Q6)
186
+ out.push(ev("content_block_delta", { index: open.index, delta: { type: "thinking_delta", thinking: delta } }));
187
+ return out;
188
+ }
189
+ function announceTool(t, out) {
190
+ t.announced = true;
191
+ sawToolUse = true;
192
+ // 统一走 openBlock:自动封掉当前 text/thinking/上一个工具块,本块成为新的待封 open
193
+ openBlock("tool_use", { type: "tool_use", id: t.id, name: t.name, input: {} }, out);
194
+ t.index = open.index;
195
+ // 增量早于 id/name 到达时先攒着,announce 后补发(事件顺序仍合规)
196
+ if (t.args) out.push(ev("content_block_delta", { index: t.index, delta: { type: "input_json_delta", partial_json: t.args } }));
197
+ }
198
+ function pushTool(tc) {
199
+ const slot = Number(tc?.index ?? 0);
200
+ const out = [];
201
+ let t = tools.get(slot);
202
+ if (!t) { t = { id: "", name: "", args: "", announced: false, index: null }; tools.set(slot, t); }
203
+ if (tc?.id) t.id = String(tc.id);
204
+ if (tc?.function?.name) t.name += String(tc.function.name);
205
+ const frag = tc?.function?.arguments ? String(tc.function.arguments) : "";
206
+ if (frag) { dbg.toolDeltas++; t.args += frag; gen.add(frag); }
207
+ if (!t.announced && t.id && t.name) announceTool(t, out);
208
+ else if (t.announced && frag && open && open.index === t.index) out.push(ev("content_block_delta", { index: t.index, delta: { type: "input_json_delta", partial_json: frag } }));
209
+ else if (t.announced && frag) dbg.lateToolDeltas++; // 块已封口后才来的参数增量:发出即违规帧,丢弃但上面已计入 gen(output_tokens 取计费口径:模型确实生成了这些字节)
210
+ return out;
211
+ }
212
+
213
+ function begin() {
214
+ return [ev("message_start", { message: { id, type: "message", role: "assistant", model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: estInput(), output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 } } })];
215
+ }
216
+
217
+ function push(data) {
218
+ buf += chunkToString(data);
219
+ const out = [];
220
+ let nl;
221
+ while ((nl = buf.indexOf("\n")) >= 0) {
222
+ const line = buf.slice(0, nl).trim();
223
+ buf = buf.slice(nl + 1);
224
+ // 注释帧/keepalive(`:` 开头)与上游 `[DONE]` 都不进 Anthropic 事件序列(grill Q6)
225
+ if (!line || line.startsWith(":")) continue;
226
+ if (!line.startsWith("data:")) { dbg.skippedLines++; continue; }
227
+ const payload = line.slice(5).trim();
228
+ if (!payload || payload === "[DONE]") continue;
229
+ let chunk;
230
+ try { chunk = JSON.parse(payload); } catch { dbg.jsonFails++; continue; }
231
+ dbg.chunks++;
232
+ const choice = chunk.choices?.[0] || {};
233
+ const delta = choice.delta || {};
234
+ if (choice.finish_reason) lastFinish = choice.finish_reason;
235
+ if (chunk.usage) lastUsage = chunk.usage;
236
+ if (typeof delta.content === "string" && delta.content) { dbg.textChars += delta.content.length; out.push(...pushText(delta.content)); }
237
+ for (const tc of delta.tool_calls || []) out.push(...pushTool(tc));
238
+ const rc = typeof delta.reasoning_content === "string" ? delta.reasoning_content : "";
239
+ if (rc) {
240
+ dbg.thinkingChars += rc.length;
241
+ if (emitThinking) out.push(...pushThinking(rc)); // 只发 thinking_delta,无 signature 可发
242
+ else gen.add(rc); // `MSLXDFF_ANTHROPIC_THINKING=0` 时思考字节仍是真实生成,兜底 output 照计(在此加、不在 pushThinking 内加,避免双计)
243
+ }
244
+ }
245
+ return out;
246
+ }
247
+
248
+ function end({ finish = null, usage = null } = {}) {
249
+ const out = [];
250
+ const fr = finish || lastFinish;
251
+ for (const t of tools.values()) {
252
+ if (!t.announced && (t.id || t.name || t.args)) announceTool(t, out); // 只有 arguments 无 name 的退化收尾
253
+ }
254
+ closeOpen(out);
255
+ out.push(ev("message_delta", { delta: { stop_reason: toStopReason(fr, sawToolUse), stop_sequence: null }, usage: toAnthropicUsage(usage || lastUsage, () => ({ input_tokens: estInput(), output_tokens: gen.tokens() })) }));
256
+ out.push(ev("message_stop"));
257
+ return out;
258
+ }
259
+
260
+ // 上游给聚合 JSON(非 SSE,muse-spark / workbuddy 聚合 / zcode 非流式都是这种)时,
261
+ // 把整包喂进同一套 pushText/pushTool 状态机 —— 与流式共用封口规则,不出第二套形状。
262
+ function fromChatJson(chatJson = {}) {
263
+ const out = [];
264
+ const choice = chatJson.choices?.[0] || {};
265
+ const m = choice.message || {};
266
+ const ct = textFromChatContent(m.content); // 字符串或部件数组都吃,别把数组正文整段丢掉
267
+ // 聚合上游把思考放在 message.reasoning_content(muse-spark / workbuddy 聚合 / zcode 非流式):thinking 块须排 text 前。
268
+ const rt = typeof m.reasoning_content === "string" ? m.reasoning_content : "";
269
+ if (rt && emitThinking) out.push(...pushThinking(rt));
270
+ if (ct) out.push(...pushText(ct));
271
+ (Array.isArray(m.tool_calls) ? m.tool_calls : []).forEach((tc, i) => {
272
+ out.push(...pushTool({ index: i, id: tc?.id, function: { name: tc?.function?.name, arguments: tc?.function?.arguments } }));
273
+ });
274
+ if (choice.finish_reason) lastFinish = choice.finish_reason;
275
+ if (chatJson.usage) lastUsage = chatJson.usage;
276
+ return out;
277
+ }
278
+
279
+ function getFinal() {
280
+ return { finish: lastFinish, usage: lastUsage, sawToolUse };
281
+ }
282
+ function stats() {
283
+ return { ...dbg, openKind: open?.kind || null, toolSlots: tools.size };
284
+ }
285
+
286
+ return { id, created, begin, push, end, fromChatJson, getFinal, stats };
287
+ }