mslxdff 0.1.203 → 0.1.207

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 (330) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/docs/cli_help_mini.md +146 -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/workbuddy.js +189 -189
  85. package/src/cli/format.js +141 -141
  86. package/src/cli/group-helpers.js +70 -70
  87. package/src/cli/help.js +80 -80
  88. package/src/cli/index.js +70 -70
  89. package/src/cli/interactive.js +121 -121
  90. package/src/cli/join-core.js +99 -99
  91. package/src/cli/join-wizard.js +175 -175
  92. package/src/cli/policy.js +136 -136
  93. package/src/cli/provider-row.js +126 -126
  94. package/src/cli/util.js +24 -24
  95. package/src/compat.js +53 -53
  96. package/src/events.js +38 -38
  97. package/src/free-lane.js +131 -131
  98. package/src/free-watcher.js +63 -63
  99. package/src/groups.js +262 -262
  100. package/src/logs.js +195 -195
  101. package/src/metrics.js +71 -62
  102. package/src/model-capabilities/context-lookup.js +65 -65
  103. package/src/model-capabilities/merge.js +142 -142
  104. package/src/model-trace.js +187 -187
  105. package/src/models.js +1 -1
  106. package/src/opencode-identity.js +72 -72
  107. package/src/peers.js +254 -254
  108. package/src/plugins.js +107 -107
  109. package/src/providers/AGENTS.md +49 -0
  110. package/src/providers/base.js +164 -164
  111. package/src/providers/classify.js +22 -22
  112. package/src/providers/cline/allowlist-sync.js +93 -93
  113. package/src/providers/cline/free-catalog.js +71 -71
  114. package/src/providers/cline/headers.js +37 -37
  115. package/src/providers/cline/index.js +130 -130
  116. package/src/providers/cline/models.js +150 -150
  117. package/src/providers/cline/usage.js +274 -274
  118. package/src/providers/codearts/auth-pool.js +148 -148
  119. package/src/providers/codearts/concurrency.js +34 -34
  120. package/src/providers/codearts/const.js +65 -65
  121. package/src/providers/codearts/dpop.js +95 -95
  122. package/src/providers/codearts/index.js +175 -175
  123. package/src/providers/codearts/login.js +214 -214
  124. package/src/providers/codearts/models.js +182 -182
  125. package/src/providers/codearts/sign.js +96 -96
  126. package/src/providers/codearts/sse.js +204 -204
  127. package/src/providers/codearts/stream.js +209 -209
  128. package/src/providers/codearts/sts.js +124 -124
  129. package/src/providers/codearts.js +1 -1
  130. package/src/providers/dispatcher.js +146 -146
  131. package/src/providers/generic.js +156 -156
  132. package/src/providers/globalqwenwork/account-store.js +141 -141
  133. package/src/providers/globalqwenwork/constants.js +73 -73
  134. package/src/providers/globalqwenwork/crypto.js +2 -2
  135. package/src/providers/globalqwenwork/http.js +2 -2
  136. package/src/providers/globalqwenwork/index.js +331 -331
  137. package/src/providers/globalqwenwork/payload.js +147 -147
  138. package/src/providers/globalqwenwork/rsa.js +2 -2
  139. package/src/providers/globalqwenwork/sse.js +2 -2
  140. package/src/providers/globalqwenwork/stream.js +2 -2
  141. package/src/providers/globalqwenwork.js +1 -1
  142. package/src/providers/globalworkbuddy/account-store.js +198 -198
  143. package/src/providers/globalworkbuddy/auth.js +97 -97
  144. package/src/providers/globalworkbuddy/catalog.js +138 -138
  145. package/src/providers/globalworkbuddy/constants.js +73 -73
  146. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  147. package/src/providers/globalworkbuddy/errors.js +98 -98
  148. package/src/providers/globalworkbuddy/headers.js +89 -89
  149. package/src/providers/globalworkbuddy/models.js +96 -96
  150. package/src/providers/globalworkbuddy/payload.js +80 -80
  151. package/src/providers/index.js +5 -5
  152. package/src/providers/model-id.js +172 -172
  153. package/src/providers/opencode.js +19 -19
  154. package/src/providers/openrouter.js +31 -31
  155. package/src/providers/qoder/account-store.js +106 -106
  156. package/src/providers/qoder/aggregate.js +60 -60
  157. package/src/providers/qoder/baseprompt.json +591 -591
  158. package/src/providers/qoder/checkin.js +160 -160
  159. package/src/providers/qoder/constants.js +44 -44
  160. package/src/providers/qoder/encode.js +41 -41
  161. package/src/providers/qoder/fingerprint.js +38 -38
  162. package/src/providers/qoder/models.js +77 -77
  163. package/src/providers/qoder/oauth.js +84 -84
  164. package/src/providers/qoder/payload.js +162 -162
  165. package/src/providers/qoder/request.js +27 -27
  166. package/src/providers/qoder/session.js +134 -134
  167. package/src/providers/qoder/sticky.js +38 -38
  168. package/src/providers/qoder/stream.js +116 -116
  169. package/src/providers/qoder.js +1 -1
  170. package/src/providers/raccoon/account-store.js +216 -216
  171. package/src/providers/raccoon/auth.js +67 -67
  172. package/src/providers/raccoon/chat.js +228 -228
  173. package/src/providers/raccoon/const.js +180 -180
  174. package/src/providers/raccoon/credits.js +185 -185
  175. package/src/providers/raccoon/crypto.js +43 -43
  176. package/src/providers/raccoon/envelope.js +31 -31
  177. package/src/providers/raccoon/headers.js +45 -45
  178. package/src/providers/raccoon/index.js +216 -216
  179. package/src/providers/raccoon/login.js +279 -279
  180. package/src/providers/raccoon/models.js +160 -160
  181. package/src/providers/raccoon/password-login.js +102 -102
  182. package/src/providers/raccoon/qr-codewords.js +115 -115
  183. package/src/providers/raccoon/qr.js +236 -236
  184. package/src/providers/raccoon/sse.js +135 -135
  185. package/src/providers/responses-channel.js +120 -120
  186. package/src/providers/share-keys.js +53 -53
  187. package/src/providers/traework/account-store.js +234 -234
  188. package/src/providers/traework/chat.js +217 -217
  189. package/src/providers/traework/checkin.js +77 -77
  190. package/src/providers/traework/constants.js +41 -41
  191. package/src/providers/traework/errors.js +52 -52
  192. package/src/providers/traework/headers.js +79 -79
  193. package/src/providers/traework/index.js +132 -132
  194. package/src/providers/traework/market-identity.js +116 -116
  195. package/src/providers/traework/models.js +98 -98
  196. package/src/providers/traework/payload.js +86 -86
  197. package/src/providers/traework/sse.js +184 -184
  198. package/src/providers/traework/token.js +52 -52
  199. package/src/providers/traework.js +2 -2
  200. package/src/providers/workbuddy/account-store.js +186 -186
  201. package/src/providers/workbuddy/auth.js +158 -158
  202. package/src/providers/workbuddy/balance.js +86 -86
  203. package/src/providers/workbuddy/cat-travel.js +125 -125
  204. package/src/providers/workbuddy/checkin.js +75 -75
  205. package/src/providers/workbuddy/desktop-info.js +78 -78
  206. package/src/providers/workbuddy/growth-api.js +67 -67
  207. package/src/providers/workbuddy/growth-plans.js +63 -63
  208. package/src/providers/workbuddy/growth.js +181 -181
  209. package/src/providers/workbuddy/index.js +217 -217
  210. package/src/providers/workbuddy/models.js +126 -126
  211. package/src/providers/workbuddy/payload.js +141 -141
  212. package/src/providers/workbuddy/rotation-log.js +83 -83
  213. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  214. package/src/providers/workbuddy/sdk-chat.js +26 -26
  215. package/src/providers/workbuddy-balance.js +78 -78
  216. package/src/providers/zcode/account-store.js +129 -129
  217. package/src/providers/zcode/auth.js +28 -28
  218. package/src/providers/zcode/chat.js +179 -179
  219. package/src/providers/zcode/const.js +54 -54
  220. package/src/providers/zcode/context-shape.js +162 -162
  221. package/src/providers/zcode/headers.js +93 -93
  222. package/src/providers/zcode/index.js +124 -124
  223. package/src/providers/zcode/models.js +65 -65
  224. package/src/providers/zcode/oauth.js +120 -120
  225. package/src/providers/zcode/quota.js +176 -176
  226. package/src/providers/zcode/sse.js +196 -196
  227. package/src/readline-compat.js +25 -25
  228. package/src/responses/translate.js +311 -311
  229. package/src/routes/AGENTS.md +37 -0
  230. package/src/routes/chat/broadband-handler.js +72 -72
  231. package/src/routes/chat/empty-turn-report.js +31 -31
  232. package/src/routes/chat/exhausted-handler.js +87 -87
  233. package/src/routes/chat/hedge-handler.js +130 -130
  234. package/src/routes/chat/index.js +1 -1
  235. package/src/routes/chat/local-handler.js +63 -63
  236. package/src/routes/chat/peer-handler.js +81 -81
  237. package/src/routes/chat.js +3 -3
  238. package/src/routes/fallback.js +88 -88
  239. package/src/routes/groups-relay.js +214 -214
  240. package/src/routes/groups.js +122 -122
  241. package/src/routes/helpers.js +93 -83
  242. package/src/routes/index.js +147 -147
  243. package/src/routes/messages-route.js +375 -365
  244. package/src/routes/peers.js +10 -2
  245. package/src/routes/relay-queue.js +181 -181
  246. package/src/routes/relay.js +60 -60
  247. package/src/routes/responses-route.js +213 -213
  248. package/src/routes/stream-body.js +84 -84
  249. package/src/routes/stream-hold.js +337 -337
  250. package/src/routes/stream-scan.js +366 -366
  251. package/src/routes.js +3 -3
  252. package/src/runtime/auto-update.js +113 -113
  253. package/src/runtime/broadband-stream.js +79 -79
  254. package/src/runtime/broadband.js +103 -103
  255. package/src/runtime/group-sync.js +81 -81
  256. package/src/runtime/keep-warm.js +120 -120
  257. package/src/runtime/lifecycle-forensics.js +116 -116
  258. package/src/runtime/lifecycle-log.js +87 -87
  259. package/src/runtime/log-stamp.js +73 -73
  260. package/src/runtime/proc-info.js +137 -137
  261. package/src/runtime/qoder-checkin.js +93 -93
  262. package/src/runtime/raccoon-checkin.js +126 -126
  263. package/src/runtime/server-lifecycle.js +189 -189
  264. package/src/runtime/traework-checkin.js +118 -118
  265. package/src/runtime/workbuddy-checkin.js +120 -120
  266. package/src/runtime/workbuddy-growth.js +133 -133
  267. package/src/state/facade.js +58 -58
  268. package/src/state/memory.js +162 -162
  269. package/src/state/merge.js +25 -25
  270. package/src/state/migrations/cline-unify.js +94 -94
  271. package/src/state/migrations.js +33 -33
  272. package/src/state/persist.js +42 -42
  273. package/src/state/provider-config.js +142 -142
  274. package/src/state/schemas/group.js +31 -31
  275. package/src/state/schemas/model.js +94 -94
  276. package/src/state/schemas/peer.js +31 -31
  277. package/src/state/schemas/port.js +10 -10
  278. package/src/state/schemas/provider.js +202 -202
  279. package/src/state/schemas/timezone.js +70 -70
  280. package/src/state/schemas/token.js +48 -48
  281. package/src/state/schemas/use-group.js +102 -102
  282. package/src/state/store.js +176 -176
  283. package/src/state.js +1 -1
  284. package/src/sync-claude-desktop.js +234 -234
  285. package/src/sync-claude.js +152 -152
  286. package/src/sync-codex.js +116 -116
  287. package/src/sync-workbuddy.js +158 -158
  288. package/src/talk-full-redact.js +129 -129
  289. package/src/talk-full.js +131 -131
  290. package/src/talk-log.js +257 -257
  291. package/src/text-tokens.js +52 -52
  292. package/src/thinking-effort.js +67 -67
  293. package/src/time.js +106 -106
  294. package/src/timeline.js +26 -26
  295. package/src/transport/index.js +1 -1
  296. package/src/upstream-engine/index.js +74 -74
  297. package/src/upstream-engine/mode.js +15 -15
  298. package/src/upstream-engine/sdk/attempt.js +221 -221
  299. package/src/upstream-engine/sdk/chat.js +28 -28
  300. package/src/upstream-engine/sdk/convert.js +192 -192
  301. package/src/upstream-engine/sdk/diagnose.js +62 -62
  302. package/src/upstream-engine/sdk/dispatch.js +55 -55
  303. package/src/upstream-engine/sdk/responses.js +176 -176
  304. package/src/upstream-engine/sdk/sse.js +166 -166
  305. package/src/upstream-responses.js +305 -305
  306. package/src/usage/cache-fields.js +58 -0
  307. package/src/usage/record.js +109 -106
  308. package/src/usage/report.js +209 -195
  309. package/src/v6pool/claim.js +157 -157
  310. package/src/v6pool/config.js +65 -65
  311. package/src/v6pool/detect.js +130 -130
  312. package/src/v6pool/index.js +152 -152
  313. package/src/v6pool/prefix.js +140 -140
  314. package/src/v6pool/probe.js +40 -40
  315. package/src/v6pool/report.js +104 -104
  316. package/src/v6pool/rotate.js +263 -263
  317. package/src/warp-pool/config.js +74 -74
  318. package/src/warp-pool/health.js +106 -106
  319. package/src/warp-pool/identity.js +141 -141
  320. package/src/warp-pool/index.js +358 -358
  321. package/src/warp-pool/keys.js +29 -29
  322. package/src/warp-pool/nodes.js +95 -95
  323. package/src/warp-pool/ports.js +160 -160
  324. package/src/warp-pool/register.js +113 -113
  325. package/src/warp-pool/report.js +232 -232
  326. package/src/warp-pool/revive.js +168 -168
  327. package/src/warp-pool/rotate.js +214 -214
  328. package/src/warp-pool/strategy.js +43 -43
  329. package/src/warp-pool/verdict.js +69 -69
  330. 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
+ }