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