mslxdff 0.1.203 → 0.1.208

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 (332) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/docs/cli_help_mini.md +147 -0
  4. package/package.json +38 -38
  5. package/plugins/README.md +14 -14
  6. package/plugins/prefer-model.mjs.example +14 -14
  7. package/src/anthropic/canonical-json.js +55 -0
  8. package/src/anthropic/count-tokens.js +61 -61
  9. package/src/anthropic/request-body.js +183 -126
  10. package/src/anthropic/request-shape.js +60 -0
  11. package/src/anthropic/session-id.js +58 -0
  12. package/src/anthropic/shared.js +18 -18
  13. package/src/anthropic/sse-aggregate.js +56 -56
  14. package/src/anthropic/tool-media.js +75 -0
  15. package/src/anthropic/translate.js +296 -287
  16. package/src/anthropic/web-search.js +361 -361
  17. package/src/api-kind.js +31 -31
  18. package/src/autostart.js +288 -288
  19. package/src/bench/probe.js +1 -1
  20. package/src/bench/runner.js +1 -1
  21. package/src/bench/via-probe.js +1 -1
  22. package/src/bench/via.js +144 -144
  23. package/src/bench/workbuddy-bench.js +1 -1
  24. package/src/chat/config.js +11 -11
  25. package/src/chat/cooling.js +81 -81
  26. package/src/chat/direct.js +58 -58
  27. package/src/chat/index.js +6 -6
  28. package/src/chat/spinner.js +38 -38
  29. package/src/chat/sse.js +69 -69
  30. package/src/chat/stats.js +247 -247
  31. package/src/chat/store.js +48 -48
  32. package/src/chat/tool-handlers.js +67 -67
  33. package/src/chat/tools.js +1 -1
  34. package/src/chat/upstream.js +59 -59
  35. package/src/chat-pipeline/auto-race.js +158 -158
  36. package/src/chat-pipeline/empty-turn.js +147 -147
  37. package/src/chat-pipeline/engine.js +17 -17
  38. package/src/chat-pipeline/index.js +8 -40
  39. package/src/chat-pipeline/serial-trial.js +279 -283
  40. package/src/chat-pipeline/terminal-verdict.js +132 -147
  41. package/src/chooser.js +54 -54
  42. package/src/claude-desktop/apply.js +101 -101
  43. package/src/claude-desktop/context1m.js +38 -38
  44. package/src/claude-desktop/doctor.js +146 -146
  45. package/src/claude-desktop/slots.js +152 -152
  46. package/src/cli/bootstrap.js +1 -1
  47. package/src/cli/commands/claude-desktop.js +284 -284
  48. package/src/cli/commands/daemon.js +231 -231
  49. package/src/cli/commands/model/list-live.js +22 -22
  50. package/src/cli/commands/model/list-providers.js +76 -76
  51. package/src/cli/commands/model/list-render.js +77 -77
  52. package/src/cli/commands/model/list-sort.js +52 -52
  53. package/src/cli/commands/model/list-tty.js +76 -76
  54. package/src/cli/commands/model/list.js +166 -166
  55. package/src/cli/commands/model/live-models.js +24 -24
  56. package/src/cli/commands/model/stats.js +43 -43
  57. package/src/cli/commands/model/status.js +47 -47
  58. package/src/cli/commands/model.js +51 -51
  59. package/src/cli/commands/provider/add.js +108 -108
  60. package/src/cli/commands/provider/allowlist.js +99 -99
  61. package/src/cli/commands/provider/cline-free.js +99 -99
  62. package/src/cli/commands/provider/cline-login.js +218 -218
  63. package/src/cli/commands/provider/cline-quota.js +65 -65
  64. package/src/cli/commands/provider/codearts-login.js +45 -45
  65. package/src/cli/commands/provider/config.js +119 -119
  66. package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
  67. package/src/cli/commands/provider/keys-guard.js +50 -50
  68. package/src/cli/commands/provider/keys.js +161 -161
  69. package/src/cli/commands/provider/qoder-checkin.js +81 -81
  70. package/src/cli/commands/provider/qoder-login.js +102 -102
  71. package/src/cli/commands/provider/raccoon-login.js +180 -180
  72. package/src/cli/commands/provider/raccoon-password.js +162 -162
  73. package/src/cli/commands/provider/raccoon-quota.js +109 -109
  74. package/src/cli/commands/provider/traework-checkin.js +144 -144
  75. package/src/cli/commands/provider/traework-login.js +164 -164
  76. package/src/cli/commands/provider/workbuddy-login.js +153 -153
  77. package/src/cli/commands/provider/zcode-login.js +77 -77
  78. package/src/cli/commands/provider/zcode-quota.js +55 -55
  79. package/src/cli/commands/provider.js +1 -1
  80. package/src/cli/commands/stats.js +158 -149
  81. package/src/cli/commands/system.js +261 -261
  82. package/src/cli/commands/timezone.js +47 -47
  83. package/src/cli/commands/use-group.js +60 -60
  84. package/src/cli/commands/v6-pool.js +123 -0
  85. package/src/cli/commands/workbuddy.js +189 -189
  86. package/src/cli/format.js +141 -141
  87. package/src/cli/group-helpers.js +70 -70
  88. package/src/cli/help.js +80 -80
  89. package/src/cli/index.js +73 -70
  90. package/src/cli/interactive.js +121 -121
  91. package/src/cli/join-core.js +99 -99
  92. package/src/cli/join-wizard.js +175 -175
  93. package/src/cli/policy.js +136 -136
  94. package/src/cli/provider-row.js +126 -126
  95. package/src/cli/util.js +24 -24
  96. package/src/compat.js +53 -53
  97. package/src/events.js +38 -38
  98. package/src/free-lane.js +131 -131
  99. package/src/free-watcher.js +63 -63
  100. package/src/groups.js +262 -262
  101. package/src/logs.js +195 -195
  102. package/src/metrics.js +71 -62
  103. package/src/model-capabilities/context-lookup.js +65 -65
  104. package/src/model-capabilities/merge.js +142 -142
  105. package/src/model-trace.js +187 -187
  106. package/src/models.js +1 -1
  107. package/src/opencode-identity.js +72 -72
  108. package/src/peers.js +254 -254
  109. package/src/plugins.js +107 -107
  110. package/src/providers/AGENTS.md +49 -0
  111. package/src/providers/base.js +164 -164
  112. package/src/providers/classify.js +22 -22
  113. package/src/providers/cline/allowlist-sync.js +93 -93
  114. package/src/providers/cline/free-catalog.js +71 -71
  115. package/src/providers/cline/headers.js +37 -37
  116. package/src/providers/cline/index.js +130 -130
  117. package/src/providers/cline/models.js +150 -150
  118. package/src/providers/cline/usage.js +274 -274
  119. package/src/providers/codearts/auth-pool.js +148 -148
  120. package/src/providers/codearts/concurrency.js +34 -34
  121. package/src/providers/codearts/const.js +65 -65
  122. package/src/providers/codearts/dpop.js +95 -95
  123. package/src/providers/codearts/index.js +175 -175
  124. package/src/providers/codearts/login.js +214 -214
  125. package/src/providers/codearts/models.js +182 -182
  126. package/src/providers/codearts/sign.js +96 -96
  127. package/src/providers/codearts/sse.js +204 -204
  128. package/src/providers/codearts/stream.js +209 -209
  129. package/src/providers/codearts/sts.js +124 -124
  130. package/src/providers/codearts.js +1 -1
  131. package/src/providers/dispatcher.js +146 -146
  132. package/src/providers/generic.js +156 -156
  133. package/src/providers/globalqwenwork/account-store.js +141 -141
  134. package/src/providers/globalqwenwork/constants.js +73 -73
  135. package/src/providers/globalqwenwork/crypto.js +2 -2
  136. package/src/providers/globalqwenwork/http.js +2 -2
  137. package/src/providers/globalqwenwork/index.js +331 -331
  138. package/src/providers/globalqwenwork/payload.js +147 -147
  139. package/src/providers/globalqwenwork/rsa.js +2 -2
  140. package/src/providers/globalqwenwork/sse.js +2 -2
  141. package/src/providers/globalqwenwork/stream.js +2 -2
  142. package/src/providers/globalqwenwork.js +1 -1
  143. package/src/providers/globalworkbuddy/account-store.js +198 -198
  144. package/src/providers/globalworkbuddy/auth.js +97 -97
  145. package/src/providers/globalworkbuddy/catalog.js +138 -138
  146. package/src/providers/globalworkbuddy/constants.js +73 -73
  147. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  148. package/src/providers/globalworkbuddy/errors.js +98 -98
  149. package/src/providers/globalworkbuddy/headers.js +89 -89
  150. package/src/providers/globalworkbuddy/models.js +96 -96
  151. package/src/providers/globalworkbuddy/payload.js +80 -80
  152. package/src/providers/index.js +5 -5
  153. package/src/providers/model-id.js +172 -172
  154. package/src/providers/opencode.js +19 -19
  155. package/src/providers/openrouter.js +31 -31
  156. package/src/providers/qoder/account-store.js +106 -106
  157. package/src/providers/qoder/aggregate.js +60 -60
  158. package/src/providers/qoder/baseprompt.json +591 -591
  159. package/src/providers/qoder/checkin.js +160 -160
  160. package/src/providers/qoder/constants.js +44 -44
  161. package/src/providers/qoder/encode.js +41 -41
  162. package/src/providers/qoder/fingerprint.js +38 -38
  163. package/src/providers/qoder/models.js +77 -77
  164. package/src/providers/qoder/oauth.js +84 -84
  165. package/src/providers/qoder/payload.js +162 -162
  166. package/src/providers/qoder/request.js +27 -27
  167. package/src/providers/qoder/session.js +134 -134
  168. package/src/providers/qoder/sticky.js +38 -38
  169. package/src/providers/qoder/stream.js +116 -116
  170. package/src/providers/qoder.js +1 -1
  171. package/src/providers/raccoon/account-store.js +216 -216
  172. package/src/providers/raccoon/auth.js +67 -67
  173. package/src/providers/raccoon/chat.js +228 -228
  174. package/src/providers/raccoon/const.js +180 -180
  175. package/src/providers/raccoon/credits.js +185 -185
  176. package/src/providers/raccoon/crypto.js +43 -43
  177. package/src/providers/raccoon/envelope.js +31 -31
  178. package/src/providers/raccoon/headers.js +45 -45
  179. package/src/providers/raccoon/index.js +216 -216
  180. package/src/providers/raccoon/login.js +279 -279
  181. package/src/providers/raccoon/models.js +160 -160
  182. package/src/providers/raccoon/password-login.js +102 -102
  183. package/src/providers/raccoon/qr-codewords.js +115 -115
  184. package/src/providers/raccoon/qr.js +236 -236
  185. package/src/providers/raccoon/sse.js +135 -135
  186. package/src/providers/responses-channel.js +120 -120
  187. package/src/providers/share-keys.js +53 -53
  188. package/src/providers/traework/account-store.js +234 -234
  189. package/src/providers/traework/chat.js +217 -217
  190. package/src/providers/traework/checkin.js +77 -77
  191. package/src/providers/traework/constants.js +41 -41
  192. package/src/providers/traework/errors.js +52 -52
  193. package/src/providers/traework/headers.js +79 -79
  194. package/src/providers/traework/index.js +132 -132
  195. package/src/providers/traework/market-identity.js +116 -116
  196. package/src/providers/traework/models.js +98 -98
  197. package/src/providers/traework/payload.js +86 -86
  198. package/src/providers/traework/sse.js +184 -184
  199. package/src/providers/traework/token.js +52 -52
  200. package/src/providers/traework.js +2 -2
  201. package/src/providers/workbuddy/account-store.js +186 -186
  202. package/src/providers/workbuddy/auth.js +158 -158
  203. package/src/providers/workbuddy/balance.js +86 -86
  204. package/src/providers/workbuddy/cat-travel.js +125 -125
  205. package/src/providers/workbuddy/checkin.js +75 -75
  206. package/src/providers/workbuddy/desktop-info.js +78 -78
  207. package/src/providers/workbuddy/growth-api.js +67 -67
  208. package/src/providers/workbuddy/growth-plans.js +63 -63
  209. package/src/providers/workbuddy/growth.js +181 -181
  210. package/src/providers/workbuddy/index.js +217 -217
  211. package/src/providers/workbuddy/models.js +126 -126
  212. package/src/providers/workbuddy/payload.js +141 -141
  213. package/src/providers/workbuddy/rotation-log.js +83 -83
  214. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  215. package/src/providers/workbuddy/sdk-chat.js +26 -26
  216. package/src/providers/workbuddy-balance.js +78 -78
  217. package/src/providers/zcode/account-store.js +129 -129
  218. package/src/providers/zcode/auth.js +28 -28
  219. package/src/providers/zcode/chat.js +179 -179
  220. package/src/providers/zcode/const.js +54 -54
  221. package/src/providers/zcode/context-shape.js +162 -162
  222. package/src/providers/zcode/headers.js +93 -93
  223. package/src/providers/zcode/index.js +124 -124
  224. package/src/providers/zcode/models.js +65 -65
  225. package/src/providers/zcode/oauth.js +120 -120
  226. package/src/providers/zcode/quota.js +176 -176
  227. package/src/providers/zcode/sse.js +196 -196
  228. package/src/readline-compat.js +25 -25
  229. package/src/responses/translate.js +311 -311
  230. package/src/routes/AGENTS.md +37 -0
  231. package/src/routes/chat/broadband-handler.js +72 -72
  232. package/src/routes/chat/empty-turn-report.js +31 -31
  233. package/src/routes/chat/exhausted-handler.js +87 -87
  234. package/src/routes/chat/hedge-handler.js +130 -130
  235. package/src/routes/chat/index.js +1 -1
  236. package/src/routes/chat/local-handler.js +63 -63
  237. package/src/routes/chat/peer-handler.js +81 -81
  238. package/src/routes/chat.js +3 -3
  239. package/src/routes/fallback.js +88 -88
  240. package/src/routes/groups-relay.js +214 -214
  241. package/src/routes/groups.js +122 -122
  242. package/src/routes/helpers.js +93 -83
  243. package/src/routes/index.js +147 -147
  244. package/src/routes/messages-route.js +375 -365
  245. package/src/routes/peers.js +10 -2
  246. package/src/routes/relay-queue.js +181 -181
  247. package/src/routes/relay.js +60 -60
  248. package/src/routes/responses-route.js +213 -213
  249. package/src/routes/stream-body.js +84 -84
  250. package/src/routes/stream-hold.js +337 -337
  251. package/src/routes/stream-scan.js +366 -366
  252. package/src/routes.js +3 -3
  253. package/src/runtime/auto-update.js +113 -113
  254. package/src/runtime/broadband-stream.js +79 -79
  255. package/src/runtime/broadband.js +103 -103
  256. package/src/runtime/group-sync.js +81 -81
  257. package/src/runtime/keep-warm.js +120 -120
  258. package/src/runtime/lifecycle-forensics.js +116 -116
  259. package/src/runtime/lifecycle-log.js +87 -87
  260. package/src/runtime/log-stamp.js +73 -73
  261. package/src/runtime/proc-info.js +137 -137
  262. package/src/runtime/qoder-checkin.js +93 -93
  263. package/src/runtime/raccoon-checkin.js +126 -126
  264. package/src/runtime/server-lifecycle.js +189 -189
  265. package/src/runtime/traework-checkin.js +118 -118
  266. package/src/runtime/workbuddy-checkin.js +120 -120
  267. package/src/runtime/workbuddy-growth.js +133 -133
  268. package/src/state/facade.js +58 -58
  269. package/src/state/memory.js +162 -162
  270. package/src/state/merge.js +25 -25
  271. package/src/state/migrations/cline-unify.js +94 -94
  272. package/src/state/migrations.js +33 -33
  273. package/src/state/persist.js +42 -42
  274. package/src/state/provider-config.js +142 -142
  275. package/src/state/schemas/group.js +31 -31
  276. package/src/state/schemas/model.js +94 -94
  277. package/src/state/schemas/peer.js +31 -31
  278. package/src/state/schemas/port.js +10 -10
  279. package/src/state/schemas/provider.js +202 -202
  280. package/src/state/schemas/timezone.js +70 -70
  281. package/src/state/schemas/token.js +48 -48
  282. package/src/state/schemas/use-group.js +102 -102
  283. package/src/state/store.js +176 -176
  284. package/src/state.js +1 -1
  285. package/src/sync-claude-desktop.js +234 -234
  286. package/src/sync-claude.js +152 -152
  287. package/src/sync-codex.js +116 -116
  288. package/src/sync-workbuddy.js +158 -158
  289. package/src/talk-full-redact.js +129 -129
  290. package/src/talk-full.js +131 -131
  291. package/src/talk-log.js +257 -257
  292. package/src/text-tokens.js +52 -52
  293. package/src/thinking-effort.js +67 -67
  294. package/src/time.js +106 -106
  295. package/src/timeline.js +26 -26
  296. package/src/transport/index.js +1 -1
  297. package/src/upstream-engine/index.js +74 -74
  298. package/src/upstream-engine/mode.js +15 -15
  299. package/src/upstream-engine/sdk/attempt.js +221 -221
  300. package/src/upstream-engine/sdk/chat.js +28 -28
  301. package/src/upstream-engine/sdk/convert.js +192 -192
  302. package/src/upstream-engine/sdk/diagnose.js +62 -62
  303. package/src/upstream-engine/sdk/dispatch.js +55 -55
  304. package/src/upstream-engine/sdk/responses.js +176 -176
  305. package/src/upstream-engine/sdk/sse.js +166 -166
  306. package/src/upstream-responses.js +305 -305
  307. package/src/usage/cache-fields.js +58 -0
  308. package/src/usage/record.js +109 -106
  309. package/src/usage/report.js +209 -195
  310. package/src/v6pool/claim.js +157 -157
  311. package/src/v6pool/config.js +65 -65
  312. package/src/v6pool/detect.js +130 -130
  313. package/src/v6pool/index.js +152 -152
  314. package/src/v6pool/prefix.js +140 -140
  315. package/src/v6pool/probe.js +40 -40
  316. package/src/v6pool/report.js +104 -104
  317. package/src/v6pool/rotate.js +263 -263
  318. package/src/v6pool/selftest.js +154 -0
  319. package/src/warp-pool/config.js +74 -74
  320. package/src/warp-pool/health.js +106 -106
  321. package/src/warp-pool/identity.js +141 -141
  322. package/src/warp-pool/index.js +358 -358
  323. package/src/warp-pool/keys.js +29 -29
  324. package/src/warp-pool/nodes.js +95 -95
  325. package/src/warp-pool/ports.js +160 -160
  326. package/src/warp-pool/register.js +113 -113
  327. package/src/warp-pool/report.js +232 -232
  328. package/src/warp-pool/revive.js +168 -168
  329. package/src/warp-pool/rotate.js +214 -214
  330. package/src/warp-pool/strategy.js +43 -43
  331. package/src/warp-pool/verdict.js +69 -69
  332. package/src/warp-pool/wireproxy.js +162 -162
@@ -1,287 +1,296 @@
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
+ import { cacheReadTokens, cacheWriteTokens } from "../usage/cache-fields.js";
21
+
22
+ // re-export:拆档不改对外接口,调用方与测试仍从本文件取这些符号
23
+ export { messagesToChatBody, toolResultTextOf } from "./request-body.js";
24
+ export { newMessageId, newToolUseId, anthropicError } from "./shared.js";
25
+
26
+ // ---------- usage / stop_reason ----------
27
+
28
+ /**
29
+ * chat 口径 usage → Anthropic 口径。
30
+ * Anthropic 的 `input_tokens` **不含**缓存读命中的部分(两者分开计费),而上游
31
+ * `prompt_tokens` 是含缓存的总输入 —— 故命中数要从 input 里扣掉,否则客户端把同一段
32
+ * 前缀算两遍(读数虚高)。
33
+ * `fallback()` 只在**上游整条未报**(缺字段,或 prompt/output/cache_read/cache_creation 四项全为 0)时才用:
34
+ * 真实模型对非空 prompt 不可能报 0,照抄 0 会让客户端的上下文计度停在原地、永不压实(ADR-0055 决策 3)。
35
+ */
36
+ export function toAnthropicUsage(u = {}, fallback = null) {
37
+ const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
38
+ // 缓存读数候选链的**单一来源**在 ../usage/cache-fields.js(metrics.js 的日志投影同源):
39
+ // 命中=「存在且 >0」才采信(显式 0 视为未报,防中转桩 0 掩蔽真命中);写入=字段存在即采信。
40
+ const src = u && typeof u === "object" ? u : {};
41
+ const prompt = num(src.prompt_tokens ?? src.input_tokens);
42
+ const output = num(src.completion_tokens ?? src.output_tokens);
43
+ const cacheRead = cacheReadTokens(src);
44
+ const cacheWrite = cacheWriteTokens(src) ?? 0;
45
+ const usage = {
46
+ input_tokens: Math.max(0, prompt - cacheRead - cacheWrite),
47
+ output_tokens: output,
48
+ cache_read_input_tokens: cacheRead,
49
+ cache_creation_input_tokens: cacheWrite,
50
+ };
51
+ // cache 非零也算「报了」:SDK 通道会输出 `{prompt_tokens:0, …, prompt_tokens_details:{cached_tokens:N}}`,
52
+ // 若当未报整条覆盖,客户端「input + cache_read」会把同一段前缀算两遍。
53
+ if (fallback && !(prompt > 0 || output > 0 || cacheRead > 0 || cacheWrite > 0)) return { ...usage, ...(fallback() || {}) };
54
+ return usage;
55
+ }
56
+
57
+ function toStopReason(finish, hasToolUse) {
58
+ if (hasToolUse || finish === "tool_calls" || finish === "function_call") return "tool_use";
59
+ if (finish === "length") return "max_tokens";
60
+ return "end_turn";
61
+ }
62
+
63
+ // ---------- 非流式响应:chat JSON → Anthropic message ----------
64
+
65
+ function parseToolInput(args) {
66
+ const raw = String(args || "");
67
+ if (!raw.trim()) return {};
68
+ try {
69
+ const v = JSON.parse(raw);
70
+ return v && typeof v === "object" && !Array.isArray(v) ? v : { value: v };
71
+ } catch {
72
+ return { raw }; // 上游吐出发散 JSON:退化成 raw,不整轮失败
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 上游 chat 的 `message.content` 既可能是字符串,也可能是部件数组(`[{type:"text",text:…}, {type:"image_url",…}]`,
78
+ * 多模态上游与部分聚合网关都这么回)。统一抽成正文文本;图片部件在本外壳里无法映射成 Anthropic 的 image 块,只能弃(见 ADR-0047 边界 11)。
79
+ */
80
+ export function textFromChatContent(c) {
81
+ if (typeof c === "string") return c;
82
+ if (!Array.isArray(c)) return "";
83
+ let t = "";
84
+ for (const p of c) {
85
+ if (p && typeof p === "object" && typeof p.text === "string") t += p.text;
86
+ }
87
+ return t;
88
+ }
89
+
90
+ export function chatJsonToAnthropic(chatJson = {}, model = "", opts = {}) {
91
+ const choice = chatJson.choices?.[0] || {};
92
+ const message = choice.message || {};
93
+ const id = typeof chatJson.id === "string" && chatJson.id.startsWith("msg_") ? chatJson.id : newMessageId();
94
+ const content = [];
95
+ // 思考块排在正文前:上游把思考放在 message.reasoning_content(DeepSeek/MiMo/聚合网关);
96
+ // 照抄 cc-switch transform.rs:614-641 的前置顺序。signature 用本仓占位(ADR-0052:缺签名
97
+ // Claude Code 直接丢弃整块);外发开关与流式同源(MSLXDFF_ANTHROPIC_THINKING=0 关)。
98
+ const rt = typeof message.reasoning_content === "string" ? message.reasoning_content : "";
99
+ if (opts.thinking === true && rt) content.push({ type: "thinking", thinking: rt, signature: thinkingSignature(id, 0) });
100
+ const text = textFromChatContent(message.content);
101
+ if (text) content.push({ type: "text", text });
102
+ for (const tc of message.tool_calls || []) {
103
+ content.push({ type: "tool_use", id: String(tc.id || newToolUseId()), name: String(tc.function?.name || ""), input: parseToolInput(tc.function?.arguments) });
104
+ }
105
+ return {
106
+ id,
107
+ type: "message",
108
+ role: "assistant",
109
+ model: model || chatJson.model || "",
110
+ content: content.length ? content : [{ type: "text", text: "" }],
111
+ stop_reason: toStopReason(choice.finish_reason, content.some((b) => b.type === "tool_use")),
112
+ stop_sequence: null,
113
+ usage: toAnthropicUsage(chatJson.usage, () => fallbackUsage(opts, content)),
114
+ };
115
+ }
116
+
117
+ // 上游整条未报 usage 时的兜底(ADR-0055 决策 3):请求侧用注入的 estInput,生成侧按已产出的块估。
118
+ function fallbackUsage(opts = {}, content = []) {
119
+ const counter = createTokenCounter();
120
+ for (const b of content) {
121
+ if (b?.type === "text") counter.add(b.text || "");
122
+ else if (b?.type === "tool_use") counter.add(JSON.stringify(b.input ?? {})); // 只计参数:与流式 pushTool 同口径(name 从不进 gen)
123
+ else if (b?.type === "thinking") counter.add(b.thinking || ""); // thinking 也按 output 计费(与流式 pushThinking 同口径)
124
+ }
125
+ const n = typeof opts.estInput === "function" ? Number(opts.estInput()) : 0;
126
+ return { input_tokens: Number.isFinite(n) && n > 0 ? Math.floor(n) : 0, output_tokens: counter.tokens() };
127
+ }
128
+
129
+ // ---------- 流式响应:chat SSE chunk → Anthropic 具名事件 ----------
130
+
131
+ // thinking 块必须在 content_block_stop **之前**交一帧 signature_delta,缺它 Claude Code 把整块静默丢弃
132
+ // (routatic/proxy#51 真机复现、PR#133 修)。下面是**身份占位**:不含思考原文、不冒充密码学签名,理由与风险见 ADR-0052。
133
+ const SIG_PREFIX = "mslxdff:thinking:v1:";
134
+ function thinkingSignature(messageId, index) {
135
+ return Buffer.from(`${SIG_PREFIX}${messageId}:${index}`, "utf8").toString("base64url");
136
+ }
137
+
138
+ /**
139
+ * @param model 回显给客户端的模型名
140
+ * @param opts { id?, thinking?: boolean } thinking=true 才把 reasoning_content 发成 thinking 块
141
+ */
142
+ export function createAnthropicChunkTranslator(model = "", opts = {}) {
143
+ const id = opts.id || newMessageId();
144
+ const emitThinking = opts.thinking === true;
145
+ const created = Math.floor(Date.now() / 1000);
146
+ const ev = (type, extra = {}) => ({ type, ...extra });
147
+
148
+ // 请求侧 token 估算(ADR-0055 决策 3/5):路由注入 `opts.estInput()`,memoize —— `message_start` 必须在
149
+ // 首块前就带 input_tokens,全文只能扫一遍;上游后报实测 usage 时估算只留在首帧,不进 message_delta。
150
+ let estCached = null;
151
+ const estInput = () => {
152
+ if (estCached == null) {
153
+ const n = typeof opts.estInput === "function" ? Number(opts.estInput()) : 0;
154
+ estCached = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
155
+ }
156
+ return estCached;
157
+ };
158
+ const gen = createTokenCounter(); // 生成侧累计(text + thinking + tool 参数增量)
159
+
160
+ let buf = "";
161
+ let nextIndex = 0;
162
+ let open = null; // 当前未封口块:{ index, kind: "text"|"thinking"|"tool_use", text? }
163
+ const tools = new Map(); // 上游 tc.index → { id, name, args, announced, index }
164
+ let sawToolUse = false;
165
+ let lastFinish = null;
166
+ let lastUsage = null;
167
+ const dbg = { chunks: 0, textChars: 0, toolDeltas: 0, thinkingChars: 0, skippedLines: 0, jsonFails: 0, lateToolDeltas: 0, blocks: 0 };
168
+
169
+ function closeOpen(out) {
170
+ if (!open) return;
171
+ if (open.kind === "thinking") out.push(ev("content_block_delta", { index: open.index, delta: { type: "signature_delta", signature: thinkingSignature(id, open.index) } }));
172
+ out.push(ev("content_block_stop", { index: open.index }));
173
+ open = null;
174
+ }
175
+ function openBlock(kind, contentBlock, out) {
176
+ closeOpen(out);
177
+ open = { index: nextIndex++, kind, text: "" };
178
+ dbg.blocks = open.index + 1;
179
+ out.push(ev("content_block_start", { index: open.index, content_block: contentBlock }));
180
+ return open;
181
+ }
182
+ function pushText(delta) {
183
+ const out = [];
184
+ if (!open || open.kind !== "text") openBlock("text", { type: "text", text: "" }, out);
185
+ open.text += delta;
186
+ gen.add(delta); // 生成侧只累计字数、末帧一次套公式:逐帧套会把参照实现的 +1 下限乘上帧数
187
+ out.push(ev("content_block_delta", { index: open.index, delta: { type: "text_delta", text: delta } }));
188
+ return out;
189
+ }
190
+ function pushThinking(delta) {
191
+ const out = [];
192
+ if (!open || open.kind !== "thinking") openBlock("thinking", { type: "thinking", thinking: "", signature: "" }, out);
193
+ open.text += delta;
194
+ gen.add(delta); // thinking 也按 output 计费(ADR-0055 grill Q6)
195
+ out.push(ev("content_block_delta", { index: open.index, delta: { type: "thinking_delta", thinking: delta } }));
196
+ return out;
197
+ }
198
+ function announceTool(t, out) {
199
+ t.announced = true;
200
+ sawToolUse = true;
201
+ // 统一走 openBlock:自动封掉当前 text/thinking/上一个工具块,本块成为新的待封 open
202
+ openBlock("tool_use", { type: "tool_use", id: t.id, name: t.name, input: {} }, out);
203
+ t.index = open.index;
204
+ // 增量早于 id/name 到达时先攒着,announce 后补发(事件顺序仍合规)
205
+ if (t.args) out.push(ev("content_block_delta", { index: t.index, delta: { type: "input_json_delta", partial_json: t.args } }));
206
+ }
207
+ function pushTool(tc) {
208
+ const slot = Number(tc?.index ?? 0);
209
+ const out = [];
210
+ let t = tools.get(slot);
211
+ if (!t) { t = { id: "", name: "", args: "", announced: false, index: null }; tools.set(slot, t); }
212
+ if (tc?.id) t.id = String(tc.id);
213
+ if (tc?.function?.name) t.name += String(tc.function.name);
214
+ const frag = tc?.function?.arguments ? String(tc.function.arguments) : "";
215
+ if (frag) { dbg.toolDeltas++; t.args += frag; gen.add(frag); }
216
+ if (!t.announced && t.id && t.name) announceTool(t, out);
217
+ 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 } }));
218
+ else if (t.announced && frag) dbg.lateToolDeltas++; // 块已封口后才来的参数增量:发出即违规帧,丢弃但上面已计入 gen(output_tokens 取计费口径:模型确实生成了这些字节)
219
+ return out;
220
+ }
221
+
222
+ function begin() {
223
+ 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 } } })];
224
+ }
225
+
226
+ function push(data) {
227
+ buf += chunkToString(data);
228
+ const out = [];
229
+ let nl;
230
+ while ((nl = buf.indexOf("\n")) >= 0) {
231
+ const line = buf.slice(0, nl).trim();
232
+ buf = buf.slice(nl + 1);
233
+ // 注释帧/keepalive(`:` 开头)与上游 `[DONE]` 都不进 Anthropic 事件序列(grill Q6)
234
+ if (!line || line.startsWith(":")) continue;
235
+ if (!line.startsWith("data:")) { dbg.skippedLines++; continue; }
236
+ const payload = line.slice(5).trim();
237
+ if (!payload || payload === "[DONE]") continue;
238
+ let chunk;
239
+ try { chunk = JSON.parse(payload); } catch { dbg.jsonFails++; continue; }
240
+ dbg.chunks++;
241
+ const choice = chunk.choices?.[0] || {};
242
+ const delta = choice.delta || {};
243
+ if (choice.finish_reason) lastFinish = choice.finish_reason;
244
+ if (chunk.usage) lastUsage = chunk.usage;
245
+ if (typeof delta.content === "string" && delta.content) { dbg.textChars += delta.content.length; out.push(...pushText(delta.content)); }
246
+ for (const tc of delta.tool_calls || []) out.push(...pushTool(tc));
247
+ const rc = typeof delta.reasoning_content === "string" ? delta.reasoning_content : "";
248
+ if (rc) {
249
+ dbg.thinkingChars += rc.length;
250
+ if (emitThinking) out.push(...pushThinking(rc)); // 只发 thinking_delta,无 signature 可发
251
+ else gen.add(rc); // `MSLXDFF_ANTHROPIC_THINKING=0` 时思考字节仍是真实生成,兜底 output 照计(在此加、不在 pushThinking 内加,避免双计)
252
+ }
253
+ }
254
+ return out;
255
+ }
256
+
257
+ function end({ finish = null, usage = null } = {}) {
258
+ const out = [];
259
+ const fr = finish || lastFinish;
260
+ for (const t of tools.values()) {
261
+ if (!t.announced && (t.id || t.name || t.args)) announceTool(t, out); // 只有 arguments 无 name 的退化收尾
262
+ }
263
+ closeOpen(out);
264
+ 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() })) }));
265
+ out.push(ev("message_stop"));
266
+ return out;
267
+ }
268
+
269
+ // 上游给聚合 JSON(非 SSE,muse-spark / workbuddy 聚合 / zcode 非流式都是这种)时,
270
+ // 把整包喂进同一套 pushText/pushTool 状态机 —— 与流式共用封口规则,不出第二套形状。
271
+ function fromChatJson(chatJson = {}) {
272
+ const out = [];
273
+ const choice = chatJson.choices?.[0] || {};
274
+ const m = choice.message || {};
275
+ const ct = textFromChatContent(m.content); // 字符串或部件数组都吃,别把数组正文整段丢掉
276
+ // 聚合上游把思考放在 message.reasoning_content(muse-spark / workbuddy 聚合 / zcode 非流式):thinking 块须排 text 前。
277
+ const rt = typeof m.reasoning_content === "string" ? m.reasoning_content : "";
278
+ if (rt && emitThinking) out.push(...pushThinking(rt));
279
+ if (ct) out.push(...pushText(ct));
280
+ (Array.isArray(m.tool_calls) ? m.tool_calls : []).forEach((tc, i) => {
281
+ out.push(...pushTool({ index: i, id: tc?.id, function: { name: tc?.function?.name, arguments: tc?.function?.arguments } }));
282
+ });
283
+ if (choice.finish_reason) lastFinish = choice.finish_reason;
284
+ if (chatJson.usage) lastUsage = chatJson.usage;
285
+ return out;
286
+ }
287
+
288
+ function getFinal() {
289
+ return { finish: lastFinish, usage: lastUsage, sawToolUse };
290
+ }
291
+ function stats() {
292
+ return { ...dbg, openKind: open?.kind || null, toolSlots: tools.size };
293
+ }
294
+
295
+ return { id, created, begin, push, end, fromChatJson, getFinal, stats };
296
+ }