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,187 +1,187 @@
1
- // Per-model request trace formatting. Never include prompt, response body, headers or credentials.
2
- // Note: 同步 appendFileSync(异步 append 会在并发下打乱同一请求的阶段行序)— 见 .agents/notes/implemented/feature/2026-09-25-model-trace-log.md
3
- // Note: 事件面用黑名单(默认全可见,只排除噪声/敏感面)+ 决定类事件只渲染登记过的标量字段——起因是 empty-turn-retry 曾被阶段白名单静默吞掉,"关键决定不许再被漏登记" — 见 .agents/notes/implemented/feature/2026-09-27-qoder-per-request-sticky-account.md
4
- import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { logDir } from "./logs.js";
7
- import { fmtShanghaiYMDHMS } from "./time.js";
8
-
9
- const MAX_TRACE_BYTES = 1024 * 1024;
10
- // 裁剪后保留的比例(按**字节**回退)。与 logs.js 的 KEEP_RATIO 同口径:
11
- // 旧的 `slice(-100)` 是按**行数**回退,与按字节的阈值严重不匹配 —— 实测
12
- // cline-cline-pass-deepseek-v4.1-flash.log 到 1,006,482 字节 / 5346 行时,
13
- // 一次裁剪丢 5246 行(98.1% 历史),事后回溯基本不可用。按字节留 3/4 则每次只丢 ~1/4。
14
- const TRACE_KEEP_RATIO = 3 / 4;
15
-
16
- export function modelLogName(model) {
17
- const s = String(model || "unknown").trim().toLowerCase();
18
- const safe = s.replace(/[^a-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 180);
19
- return `${safe || "unknown"}.log`;
20
- }
21
-
22
- export function modelLogFile(model) {
23
- return join(logDir(), modelLogName(model));
24
- }
25
-
26
-
27
- function count(v) {
28
- const n = Number(v);
29
- return Number.isFinite(n) && n >= 0 ? n : null;
30
- }
31
-
32
- export function summarizeRequest(body = {}) {
33
- const messages = Array.isArray(body.messages) ? body.messages : [];
34
- const roles = {};
35
- for (const m of messages) roles[String(m?.role || "unknown")] = (roles[String(m?.role || "unknown")] || 0) + 1;
36
- return {
37
- stream: body.stream === true,
38
- messages: messages.length,
39
- roles,
40
- tools: Array.isArray(body.tools) ? body.tools.length : 0,
41
- maxTokens: count(body.max_tokens ?? body.max_completion_tokens),
42
- temperature: Number.isFinite(Number(body.temperature)) ? Number(body.temperature) : null,
43
- topP: Number.isFinite(Number(body.top_p)) ? Number(body.top_p) : null,
44
- };
45
- }
46
-
47
- function hostPort(value) {
48
- try { const u = new URL(String(value)); return u.port ? `${u.hostname}:${u.port}` : u.hostname; } catch { return "-"; }
49
- }
50
- // 黑名单(而非白名单):模型日志默认收下**所有**事件,只排除噪声与凭据面。
51
- // 起因:`empty-turn-retry`("切号换 URL"的真实触发者)曾被白名单静默吞掉,排障时只见现象不见原因。
52
- // 关键决定不允许再被漏登记:新增事件默认可见,确属噪声/敏感才在此登记排除。
53
- const TRACE_DENY = new Set([
54
- "peer-health", // 组员心跳,秒级高频,无决策价值
55
- "heartbeat", // 运行心跳
56
- "client-session", // 客户端会话标识(路由用),非决定且属敏感面
57
- ]);
58
-
59
- export function shouldTraceModel(type) {
60
- const t = String(type || "");
61
- if (!t) return false;
62
- if (TRACE_DENY.has(t)) return false;
63
- if (t.startsWith("upstream-probe")) return false; // 探针类(probe/probe-error/probe-skip)
64
- return true;
65
- }
66
-
67
- function safeText(value, n = 140) {
68
- return String(value ?? "")
69
- .replace(/([?&](?:token|refreshToken|access[_-]?token|refresh[_-]?token|api[_-]?key|apikey|key|secret|password|cookie)=)[^&\s]+/gi, "$1[redacted]")
70
- .replace(/(authorization|bearer|access[_-]?token|refresh[_-]?token|cookie|api[-_]?key|password|secret)\s*[:=]\s*[^,; ]+/gi, "$1=[redacted]")
71
- .replace(/\s+/g, " ").trim().slice(0, n);
72
- }
73
-
74
- function kv(obj) {
75
- return Object.entries(obj).filter(([, v]) => v != null && v !== "").map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : v}`).join(" ");
76
- }
77
-
78
- // 决定类事件的可见字段白名单:只渲染这些标量;事件里的 payload/detail/stages/请求正文等一律不落模型日志。
79
- // 「加日志」不等于「倒数据」——新增决定字段要显式登记在这里才会出现。
80
- const DECISION_FIELDS = [
81
- "status", "from", "to", "reason", "hook", "applied", "skipped", "plugin",
82
- "provider", "before", "after", "models", "skippedFaulty", "limit", "tried",
83
- "winPeer", "winTarget", "peer", "peers", "peerLabel", "routeBest", "at",
84
- "latencyMs", "ttfMs", "hedged", "hops", "hasShare", "elapsedMs", "threshold",
85
- "stallHits", "maxGapMs", "interrupted", "providers", "via",
86
- "retry", "step", "max", "delayMs", "upstream", "account", "pick", "cooled", "queued", "nudged", "retries", "waitedMs", "raiseFrom", "raiseTo",
87
- "heldOpen", "terminalForm", "requestWaitedMs", "budgetMs", "budgetOut",
88
- // 终局判决(ADR-0058):取证时第一眼要看的三件——判的哪种、命中哪条判据、是否因此没问组员
89
- "kind", "marker", "peersSkipped",
90
- ];
91
-
92
- function decisionKv(data = {}) {
93
- const out = [];
94
- for (const k of DECISION_FIELDS) {
95
- const v = data[k];
96
- if (v == null || v === "") continue;
97
- out.push(`${k}=${Array.isArray(v) ? v.join("|") : v}`);
98
- }
99
- return out.join(" ");
100
- }
101
-
102
- // provider 回显头 → 日志字段(谁上的 / 打哪个站 / 为什么选它 / 是否被冷却 / 是否被判排队):
103
- // 单一来源,防各处重复写法漂移;取不到时返回空对象(其它供应商零变化)。
104
- export function upstreamEcho(res) {
105
- const h = res?.headers;
106
- if (typeof h?.get !== "function") return {};
107
- const pick = (name) => h.get(name) || null;
108
- return {
109
- upstream: pick("x-mslxdff-upstream"),
110
- account: pick("x-mslxdff-workbuddy-uid") || pick("x-mslxdff-qoder-region"),
111
- pick: pick("x-mslxdff-qoder-account"),
112
- cooled: pick("x-mslxdff-qoder-cooldown"),
113
- queued: pick("x-mslxdff-qoder-queued"),
114
- };
115
- }
116
-
117
- export function formatModelTrace({ type, reqId, model, data = {}, request = null, totalMs = null } = {}) {
118
- const base = { time: fmtShanghaiYMDHMS(new Date()), req: reqId || "-", model: model || "-", stage: type || "-" };
119
- let detail = "";
120
- // api= 打在 incoming 行最前:门是排查第一眼要的东西,grep "incoming api=" 直接分得开三门;无值时 kv 自然不落该段(不发散成 api=other)
121
- if (type === "request") detail = `incoming ${kv({ api: data.api, ...(request || summarizeRequest(data.body)), hops: data.hops, useAuto: data.useAuto })}`;
122
- else if (type === "ordered") detail = `route order=${(data.order || []).join(" | ")} hops=${data.hops ?? "-"} fallback=${data.canFallback ? 1 : 0}`;
123
- else if (type === "model-try") detail = `target=${data.model || model || "-"} idx=${data.idx ?? "-"} remaining=${data.remaining ?? "-"}`;
124
- else if (type === "alias") detail = `rawModel=${data.rawModel || "-"} requested=${data.requested || model || "-"}`;
125
- else if (type === "exhausted-local" || type === "exhausted-all") detail = `last=${data.lastModel || model || "-"} status=${data.lastStatus ?? "-"} order=${(data.order || []).join(" | ")}`;
126
- else if (type === "upstream-try") detail = `target=${data.model || model || "-"} attempt=${data.attempt ?? "-"} payload=${kv(data.payload || {})}`;
127
- else if (type === "upstream-done" || type === "upstream-error") detail = `upstream status=${data.status ?? "-"} timing=${data.timing?.totalMs ?? "-"}ms${data.upstream ? ` via=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}${data.cooled ? ` cooled=${data.cooled}` : ""}${data.queued ? ` queued=1` : ""} ${safeText(data.message || data.error || "")}`;
128
- else if (type === "empty-turn-retry") detail = `空转重试:retry ${data.retry ?? "-"}/${data.max ?? "-"}${data.step != null ? ` step=${data.step}` : ""} delay=${data.delayMs ?? "-"}ms${data.waitedMs ? ` waited=${data.waitedMs}ms` : ""}${data.requestWaitedMs != null ? ` 请求级累计=${data.requestWaitedMs}ms/预算${data.budgetMs ?? "-"}ms` : ""}${data.raiseTo ? ` max_tokens ${data.raiseFrom}→${data.raiseTo}` : ""}${data.upstream ? ` after=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}${data.nudged ? ` nudged=1` : ""} reason=empty turn${data.reason ? ` :: ${safeText(data.reason, 120)}` : ""}`;
129
- else if (type === "empty-turn-recovered") detail = `空转重试成功:第 ${data.retries ?? "-"}/${data.max ?? "-"} 次重拉拿到真实输出,本轮共白等 ${data.waitedMs ?? "-"}ms${data.upstream ? ` via=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
130
- else if (type === "empty-turn-exhausted") detail = `空转重试已用尽:重试 ${data.retries ?? 0}/${data.max ?? "-"} 次仍零输出(累计等待 ${data.waitedMs ?? 0}ms${data.budgetOut ? `,请求级等待预算 ${data.budgetMs ?? "-"}ms 用尽` : ""}),交回换候选${data.heldOpen ? "(响应仍开着,换过去的正文还能送达)" : "(响应已封口,重拉只为省额度)"}${data.upstream ? ` after=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}${data.reason ? ` :: ${safeText(data.reason, 120)}` : ""}`;
131
- else if (type === "peer-request" || type === "peer-forward" || type === "peer-attempt" || type === "peer-error") detail = `peer=${hostPort(data.peer)}${data.name ? ` name=${data.name}` : ""} status=${data.status ?? "-"} latency=${data.latencyMs ?? "-"}ms payload=${kv(data.payload || {})} ${safeText(data.message || data.error || "")}`;
132
- else if (type === "relay-done") {
133
- const d = data.detail || {};
134
- const u = d.usage || {};
135
- // 缺 usage 时显式标注来源,别与「真的用了 0 token」混成同一个 `-`(BUG-13:经组员中继的响应
136
- // 少数轮次拿不到 usage,成本报表会静默偏低,读日志的人也会误判成"这轮没消耗")。
137
- const noUsage = u.prompt_tokens == null && u.completion_tokens == null;
138
- const usageTxt = noUsage
139
- ? `usage(none${data.via === "peer" ? ":peer 未回" : ""})`
140
- : `usage(prompt=${u.prompt_tokens ?? "-"}/completion=${u.completion_tokens ?? "-"})`;
141
- detail = `upstream_response status=${data.status ?? "-"} finish=${d.sawFinishReason || "-"} chunks=${d.receivedChunks ?? "-"} bytes=${d.receivedBytes ?? "-"} tools=${d.toolCalls ?? 0} chars=${d.chars ?? 0} ${usageTxt} elapsed=${data.totalMs ?? "-"}ms${data.upstream ? ` upstream=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
142
- } else if (type === "result" || type === "client-response") {
143
- detail = `client status=${data.status ?? "-"} via=${data.via || "-"} actual=${data.actual || model || "-"} fallback=${data.fallback?.fallback ? 1 : 0} total=${totalMs ?? data.durationMs ?? "-"}ms${data.upstream ? ` upstream=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
144
- } else detail = safeText(`${decisionKv(data)} ${data.message || data.error || ""}`, 200);
145
- return `[${base.time}] [req=${base.req}] [model=${base.model}] [stage=${base.stage}] ${detail}`;
146
- }
147
-
148
- /**
149
- * 按字节从尾部回退(不截半行)。行有长有短,固定行数 = 固定容量 ⇒ 一次裁剪就是断崖。
150
- * 与 logs.js 的 keepTailByBytes 同口径(那边不导出,这里留一份最小实现,避免为一行日志反向依赖)。
151
- */
152
- function keepTailByBytes(text, maxBytes) {
153
- const limit = Math.max(1, Math.floor(maxBytes * TRACE_KEEP_RATIO));
154
- if (Buffer.byteLength(text) <= limit) return text;
155
- const lines = text.split("\n");
156
- let bytes = 0;
157
- let i = lines.length - 1;
158
- for (; i >= 0; i--) {
159
- const b = Buffer.byteLength(lines[i]) + 1;
160
- if (bytes + b > limit) break;
161
- bytes += b;
162
- }
163
- // 至少保留最后一条:单行超 limit 时否则返回空串 = 清空整个 trace。
164
- // 判据是「结果为空」——文件尾有换行时 split 的最后一个元素是空串,`i === lines.length-1` 判不到内容行。
165
- const kept = lines.slice(i + 1).join("\n");
166
- if (!kept) {
167
- const nonEmpty = lines.filter((l) => l !== "");
168
- if (nonEmpty.length) return nonEmpty[nonEmpty.length - 1];
169
- }
170
- return kept;
171
- }
172
-
173
- export function appendModelTrace(model, entry, { file = modelLogFile(model) } = {}) {
174
- const line = `${formatModelTrace(entry)}\n`;
175
- try {
176
- mkdirSync(logDir(), { recursive: true });
177
- if (existsSync(file) && statSync(file).size > MAX_TRACE_BYTES) {
178
- // 按字节留 3/4,不再固定砍到 100 行(那一次就丢 98% 历史,见 TRACE_KEEP_RATIO 注释)。
179
- const kept = keepTailByBytes(readFileSync(file, "utf8"), MAX_TRACE_BYTES);
180
- // 末尾补换行:下一次 appendFileSync 才不会把新行接到半截行尾巴上
181
- writeFileSync(file, kept.endsWith("\n") ? kept : `${kept}\n`);
182
- }
183
- appendFileSync(file, line);
184
- } catch {
185
- // Logging must never affect the chat request.
186
- }
187
- }
1
+ // Per-model request trace formatting. Never include prompt, response body, headers or credentials.
2
+ // Note: 同步 appendFileSync(异步 append 会在并发下打乱同一请求的阶段行序)— 见 .agents/notes/implemented/feature/2026-09-25-model-trace-log.md
3
+ // Note: 事件面用黑名单(默认全可见,只排除噪声/敏感面)+ 决定类事件只渲染登记过的标量字段——起因是 empty-turn-retry 曾被阶段白名单静默吞掉,"关键决定不许再被漏登记" — 见 .agents/notes/implemented/feature/2026-09-27-qoder-per-request-sticky-account.md
4
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { logDir } from "./logs.js";
7
+ import { fmtShanghaiYMDHMS } from "./time.js";
8
+
9
+ const MAX_TRACE_BYTES = 1024 * 1024;
10
+ // 裁剪后保留的比例(按**字节**回退)。与 logs.js 的 KEEP_RATIO 同口径:
11
+ // 旧的 `slice(-100)` 是按**行数**回退,与按字节的阈值严重不匹配 —— 实测
12
+ // cline-cline-pass-deepseek-v4.1-flash.log 到 1,006,482 字节 / 5346 行时,
13
+ // 一次裁剪丢 5246 行(98.1% 历史),事后回溯基本不可用。按字节留 3/4 则每次只丢 ~1/4。
14
+ const TRACE_KEEP_RATIO = 3 / 4;
15
+
16
+ export function modelLogName(model) {
17
+ const s = String(model || "unknown").trim().toLowerCase();
18
+ const safe = s.replace(/[^a-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 180);
19
+ return `${safe || "unknown"}.log`;
20
+ }
21
+
22
+ export function modelLogFile(model) {
23
+ return join(logDir(), modelLogName(model));
24
+ }
25
+
26
+
27
+ function count(v) {
28
+ const n = Number(v);
29
+ return Number.isFinite(n) && n >= 0 ? n : null;
30
+ }
31
+
32
+ export function summarizeRequest(body = {}) {
33
+ const messages = Array.isArray(body.messages) ? body.messages : [];
34
+ const roles = {};
35
+ for (const m of messages) roles[String(m?.role || "unknown")] = (roles[String(m?.role || "unknown")] || 0) + 1;
36
+ return {
37
+ stream: body.stream === true,
38
+ messages: messages.length,
39
+ roles,
40
+ tools: Array.isArray(body.tools) ? body.tools.length : 0,
41
+ maxTokens: count(body.max_tokens ?? body.max_completion_tokens),
42
+ temperature: Number.isFinite(Number(body.temperature)) ? Number(body.temperature) : null,
43
+ topP: Number.isFinite(Number(body.top_p)) ? Number(body.top_p) : null,
44
+ };
45
+ }
46
+
47
+ function hostPort(value) {
48
+ try { const u = new URL(String(value)); return u.port ? `${u.hostname}:${u.port}` : u.hostname; } catch { return "-"; }
49
+ }
50
+ // 黑名单(而非白名单):模型日志默认收下**所有**事件,只排除噪声与凭据面。
51
+ // 起因:`empty-turn-retry`("切号换 URL"的真实触发者)曾被白名单静默吞掉,排障时只见现象不见原因。
52
+ // 关键决定不允许再被漏登记:新增事件默认可见,确属噪声/敏感才在此登记排除。
53
+ const TRACE_DENY = new Set([
54
+ "peer-health", // 组员心跳,秒级高频,无决策价值
55
+ "heartbeat", // 运行心跳
56
+ "client-session", // 客户端会话标识(路由用),非决定且属敏感面
57
+ ]);
58
+
59
+ export function shouldTraceModel(type) {
60
+ const t = String(type || "");
61
+ if (!t) return false;
62
+ if (TRACE_DENY.has(t)) return false;
63
+ if (t.startsWith("upstream-probe")) return false; // 探针类(probe/probe-error/probe-skip)
64
+ return true;
65
+ }
66
+
67
+ function safeText(value, n = 140) {
68
+ return String(value ?? "")
69
+ .replace(/([?&](?:token|refreshToken|access[_-]?token|refresh[_-]?token|api[_-]?key|apikey|key|secret|password|cookie)=)[^&\s]+/gi, "$1[redacted]")
70
+ .replace(/(authorization|bearer|access[_-]?token|refresh[_-]?token|cookie|api[-_]?key|password|secret)\s*[:=]\s*[^,; ]+/gi, "$1=[redacted]")
71
+ .replace(/\s+/g, " ").trim().slice(0, n);
72
+ }
73
+
74
+ function kv(obj) {
75
+ return Object.entries(obj).filter(([, v]) => v != null && v !== "").map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : v}`).join(" ");
76
+ }
77
+
78
+ // 决定类事件的可见字段白名单:只渲染这些标量;事件里的 payload/detail/stages/请求正文等一律不落模型日志。
79
+ // 「加日志」不等于「倒数据」——新增决定字段要显式登记在这里才会出现。
80
+ const DECISION_FIELDS = [
81
+ "status", "from", "to", "reason", "hook", "applied", "skipped", "plugin",
82
+ "provider", "before", "after", "models", "skippedFaulty", "limit", "tried",
83
+ "winPeer", "winTarget", "peer", "peers", "peerLabel", "routeBest", "at",
84
+ "latencyMs", "ttfMs", "hedged", "hops", "hasShare", "elapsedMs", "threshold",
85
+ "stallHits", "maxGapMs", "interrupted", "providers", "via",
86
+ "retry", "step", "max", "delayMs", "upstream", "account", "pick", "cooled", "queued", "nudged", "retries", "waitedMs", "raiseFrom", "raiseTo",
87
+ "heldOpen", "terminalForm", "requestWaitedMs", "budgetMs", "budgetOut",
88
+ // 终局判决(ADR-0058):取证时第一眼要看的三件——判的哪种、命中哪条判据、是否因此没问组员
89
+ "kind", "marker", "peersSkipped",
90
+ ];
91
+
92
+ function decisionKv(data = {}) {
93
+ const out = [];
94
+ for (const k of DECISION_FIELDS) {
95
+ const v = data[k];
96
+ if (v == null || v === "") continue;
97
+ out.push(`${k}=${Array.isArray(v) ? v.join("|") : v}`);
98
+ }
99
+ return out.join(" ");
100
+ }
101
+
102
+ // provider 回显头 → 日志字段(谁上的 / 打哪个站 / 为什么选它 / 是否被冷却 / 是否被判排队):
103
+ // 单一来源,防各处重复写法漂移;取不到时返回空对象(其它供应商零变化)。
104
+ export function upstreamEcho(res) {
105
+ const h = res?.headers;
106
+ if (typeof h?.get !== "function") return {};
107
+ const pick = (name) => h.get(name) || null;
108
+ return {
109
+ upstream: pick("x-mslxdff-upstream"),
110
+ account: pick("x-mslxdff-workbuddy-uid") || pick("x-mslxdff-qoder-region"),
111
+ pick: pick("x-mslxdff-qoder-account"),
112
+ cooled: pick("x-mslxdff-qoder-cooldown"),
113
+ queued: pick("x-mslxdff-qoder-queued"),
114
+ };
115
+ }
116
+
117
+ export function formatModelTrace({ type, reqId, model, data = {}, request = null, totalMs = null } = {}) {
118
+ const base = { time: fmtShanghaiYMDHMS(new Date()), req: reqId || "-", model: model || "-", stage: type || "-" };
119
+ let detail = "";
120
+ // api= 打在 incoming 行最前:门是排查第一眼要的东西,grep "incoming api=" 直接分得开三门;无值时 kv 自然不落该段(不发散成 api=other)
121
+ if (type === "request") detail = `incoming ${kv({ api: data.api, ...(request || summarizeRequest(data.body)), hops: data.hops, useAuto: data.useAuto })}`;
122
+ else if (type === "ordered") detail = `route order=${(data.order || []).join(" | ")} hops=${data.hops ?? "-"} fallback=${data.canFallback ? 1 : 0}`;
123
+ else if (type === "model-try") detail = `target=${data.model || model || "-"} idx=${data.idx ?? "-"} remaining=${data.remaining ?? "-"}`;
124
+ else if (type === "alias") detail = `rawModel=${data.rawModel || "-"} requested=${data.requested || model || "-"}`;
125
+ else if (type === "exhausted-local" || type === "exhausted-all") detail = `last=${data.lastModel || model || "-"} status=${data.lastStatus ?? "-"} order=${(data.order || []).join(" | ")}`;
126
+ else if (type === "upstream-try") detail = `target=${data.model || model || "-"} attempt=${data.attempt ?? "-"} payload=${kv(data.payload || {})}`;
127
+ else if (type === "upstream-done" || type === "upstream-error") detail = `upstream status=${data.status ?? "-"} timing=${data.timing?.totalMs ?? "-"}ms${data.upstream ? ` via=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}${data.cooled ? ` cooled=${data.cooled}` : ""}${data.queued ? ` queued=1` : ""} ${safeText(data.message || data.error || "")}`;
128
+ else if (type === "empty-turn-retry") detail = `空转重试:retry ${data.retry ?? "-"}/${data.max ?? "-"}${data.step != null ? ` step=${data.step}` : ""} delay=${data.delayMs ?? "-"}ms${data.waitedMs ? ` waited=${data.waitedMs}ms` : ""}${data.requestWaitedMs != null ? ` 请求级累计=${data.requestWaitedMs}ms/预算${data.budgetMs ?? "-"}ms` : ""}${data.raiseTo ? ` max_tokens ${data.raiseFrom}→${data.raiseTo}` : ""}${data.upstream ? ` after=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}${data.nudged ? ` nudged=1` : ""} reason=empty turn${data.reason ? ` :: ${safeText(data.reason, 120)}` : ""}`;
129
+ else if (type === "empty-turn-recovered") detail = `空转重试成功:第 ${data.retries ?? "-"}/${data.max ?? "-"} 次重拉拿到真实输出,本轮共白等 ${data.waitedMs ?? "-"}ms${data.upstream ? ` via=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
130
+ else if (type === "empty-turn-exhausted") detail = `空转重试已用尽:重试 ${data.retries ?? 0}/${data.max ?? "-"} 次仍零输出(累计等待 ${data.waitedMs ?? 0}ms${data.budgetOut ? `,请求级等待预算 ${data.budgetMs ?? "-"}ms 用尽` : ""}),交回换候选${data.heldOpen ? "(响应仍开着,换过去的正文还能送达)" : "(响应已封口,重拉只为省额度)"}${data.upstream ? ` after=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}${data.reason ? ` :: ${safeText(data.reason, 120)}` : ""}`;
131
+ else if (type === "peer-request" || type === "peer-forward" || type === "peer-attempt" || type === "peer-error") detail = `peer=${hostPort(data.peer)}${data.name ? ` name=${data.name}` : ""} status=${data.status ?? "-"} latency=${data.latencyMs ?? "-"}ms payload=${kv(data.payload || {})} ${safeText(data.message || data.error || "")}`;
132
+ else if (type === "relay-done") {
133
+ const d = data.detail || {};
134
+ const u = d.usage || {};
135
+ // 缺 usage 时显式标注来源,别与「真的用了 0 token」混成同一个 `-`(BUG-13:经组员中继的响应
136
+ // 少数轮次拿不到 usage,成本报表会静默偏低,读日志的人也会误判成"这轮没消耗")。
137
+ const noUsage = u.prompt_tokens == null && u.completion_tokens == null;
138
+ const usageTxt = noUsage
139
+ ? `usage(none${data.via === "peer" ? ":peer 未回" : ""})`
140
+ : `usage(prompt=${u.prompt_tokens ?? "-"}/completion=${u.completion_tokens ?? "-"})`;
141
+ detail = `upstream_response status=${data.status ?? "-"} finish=${d.sawFinishReason || "-"} chunks=${d.receivedChunks ?? "-"} bytes=${d.receivedBytes ?? "-"} tools=${d.toolCalls ?? 0} chars=${d.chars ?? 0} ${usageTxt} elapsed=${data.totalMs ?? "-"}ms${data.upstream ? ` upstream=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
142
+ } else if (type === "result" || type === "client-response") {
143
+ detail = `client status=${data.status ?? "-"} via=${data.via || "-"} actual=${data.actual || model || "-"} fallback=${data.fallback?.fallback ? 1 : 0} total=${totalMs ?? data.durationMs ?? "-"}ms${data.upstream ? ` upstream=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
144
+ } else detail = safeText(`${decisionKv(data)} ${data.message || data.error || ""}`, 200);
145
+ return `[${base.time}] [req=${base.req}] [model=${base.model}] [stage=${base.stage}] ${detail}`;
146
+ }
147
+
148
+ /**
149
+ * 按字节从尾部回退(不截半行)。行有长有短,固定行数 = 固定容量 ⇒ 一次裁剪就是断崖。
150
+ * 与 logs.js 的 keepTailByBytes 同口径(那边不导出,这里留一份最小实现,避免为一行日志反向依赖)。
151
+ */
152
+ function keepTailByBytes(text, maxBytes) {
153
+ const limit = Math.max(1, Math.floor(maxBytes * TRACE_KEEP_RATIO));
154
+ if (Buffer.byteLength(text) <= limit) return text;
155
+ const lines = text.split("\n");
156
+ let bytes = 0;
157
+ let i = lines.length - 1;
158
+ for (; i >= 0; i--) {
159
+ const b = Buffer.byteLength(lines[i]) + 1;
160
+ if (bytes + b > limit) break;
161
+ bytes += b;
162
+ }
163
+ // 至少保留最后一条:单行超 limit 时否则返回空串 = 清空整个 trace。
164
+ // 判据是「结果为空」——文件尾有换行时 split 的最后一个元素是空串,`i === lines.length-1` 判不到内容行。
165
+ const kept = lines.slice(i + 1).join("\n");
166
+ if (!kept) {
167
+ const nonEmpty = lines.filter((l) => l !== "");
168
+ if (nonEmpty.length) return nonEmpty[nonEmpty.length - 1];
169
+ }
170
+ return kept;
171
+ }
172
+
173
+ export function appendModelTrace(model, entry, { file = modelLogFile(model) } = {}) {
174
+ const line = `${formatModelTrace(entry)}\n`;
175
+ try {
176
+ mkdirSync(logDir(), { recursive: true });
177
+ if (existsSync(file) && statSync(file).size > MAX_TRACE_BYTES) {
178
+ // 按字节留 3/4,不再固定砍到 100 行(那一次就丢 98% 历史,见 TRACE_KEEP_RATIO 注释)。
179
+ const kept = keepTailByBytes(readFileSync(file, "utf8"), MAX_TRACE_BYTES);
180
+ // 末尾补换行:下一次 appendFileSync 才不会把新行接到半截行尾巴上
181
+ writeFileSync(file, kept.endsWith("\n") ? kept : `${kept}\n`);
182
+ }
183
+ appendFileSync(file, line);
184
+ } catch {
185
+ // Logging must never affect the chat request.
186
+ }
187
+ }
package/src/models.js CHANGED
@@ -3,7 +3,7 @@ const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
3
3
  const DEFAULT_REFRESH_MS = 2 * 60 * 60 * 1000;
4
4
  import { mkdirSync, writeFileSync } from "node:fs";
5
5
  import { dirname } from "node:path";
6
- import { compatFetch } from "./compat.js";
6
+ import { compatFetch } from "./compat.js";
7
7
 
8
8
  export function isFreeModel(id) {
9
9
  return (typeof id === "string" && id.endsWith("-free")) ||
@@ -1,72 +1,72 @@
1
- import crypto from "node:crypto";
2
-
3
- // opencode 客户端身份规格单一来源。真值 = 上游 packages/schema/src/identifier.ts:
4
- // id = <prefix>_ + 12 位 hex 时间段 + 14 位 base62;UA 必须 "opencode/<semver>"。
5
- // ⚠ 时间段是**倒序**的:value = ~(timestamp*4096 + counter) & 48bit(descending,最新排最前)。
6
- // 正序时钟不合规——zen 免费层自 2026-10-13 起把低 12 位**反解回 counter** 校验,只接受 [0,63];
7
- // 正序 id 反解出 4094 → 403 FreeTierError(现网实测:counter=1/40 → 200,counter=4094 → 403)。
8
- // 时钟半段不校验:ms*4096 如今已需 53bit,截到 48bit 必回绕——真客户端自己反解出来是 1972 年。
9
- // ⇒ 会话 id 可以继续由摘要确定性派生(跨重启稳定),只需把低 12 位强制成合法 counter。
10
- // 旧判据全部仍有效:形状正则(2026-09-17)、UA ≥ 1.18.0(2026-09-18,低版本 426 UpgradeRequired)、
11
- // 以及请求体那半道 agent 形状闸(stream + 核心五工具,见 free-lane.js)。
12
- const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
13
- const MASK48 = 0xffffffffffffn;
14
- const MASK12 = 0xfffn;
15
- // 同毫秒内第 N 个 id:免费层只认反解后 ∈[0,63],越界必 403 ⇒ 到 63 回头,不无限自增
16
- const COUNTER_MAX = 63;
17
- const DEFAULT_OPENCODE_UA = "opencode/1.18.31";
18
- let idLastTs = 0;
19
- let idCounter = 0;
20
-
21
- /** 合规时间段:倒序的 timestamp*4096+counter 截 48bit,恒 12 位小写 hex */
22
- export function identifierTimeHex(timestamp = Date.now(), counter = 1) {
23
- return ((~(BigInt(timestamp) * 0x1000n + BigInt(counter))) & MASK48).toString(16).padStart(12, "0");
24
- }
25
-
26
- /**
27
- * 从 12 位 hex 时间段反解 per-ms 计数——与免费层闸门同一判据。
28
- * 值域 0..4095,闸门只放行 ≤63;形状不符返回 NaN(绝不抛,调用方用 Number.isInteger 自检)。
29
- */
30
- export function counterFromTimeHex(hex) {
31
- if (typeof hex !== "string" || !/^[0-9a-f]{12}$/i.test(hex)) return Number.NaN;
32
- return Number((~BigInt("0x" + hex)) & MASK12);
33
- }
34
-
35
- function base62From(bytes) {
36
- let out = "";
37
- for (const b of bytes) out += ID_CHARS[b % 62];
38
- return out;
39
- }
40
-
41
- export function opencodeIdTail(timestamp = Date.now()) {
42
- if (timestamp !== idLastTs) {
43
- idLastTs = timestamp;
44
- idCounter = 0;
45
- }
46
- idCounter = (idCounter % COUNTER_MAX) + 1;
47
- return identifierTimeHex(timestamp, idCounter) + base62From(crypto.randomBytes(14));
48
- }
49
-
50
- // 摘要派生:同种子恒同 id(会话亲和——上游按 session 归组免费额度与 prompt cache,改成现取
51
- // 时钟会让同一会话每请求换号,绝不)。时钟半段照摘要填(不校验),低 12 位强制 counter=1。
52
- export function digestIdTail(digest) {
53
- const time = ((BigInt("0x" + digest.subarray(0, 6).toString("hex")) & ~MASK12) | ((~1n) & MASK12)) & MASK48;
54
- return time.toString(16).padStart(12, "0") + base62From(digest.subarray(6, 20));
55
- }
56
-
57
- export function genId(prefix) {
58
- return `${prefix}${opencodeIdTail()}`;
59
- }
60
-
61
- export function opencodeUa() {
62
- return process.env.MSLXDFF_OPENCODE_UA || DEFAULT_OPENCODE_UA;
63
- }
64
-
65
- // 旁路(-chat curl 工具 / bench 直连)共用同一身份,避免各自伪造头再被 403
66
- export function opencodeClientIdentity() {
67
- return {
68
- "User-Agent": opencodeUa(),
69
- "x-opencode-session": genId("ses_"),
70
- "x-opencode-request": genId("msg_"),
71
- };
72
- }
1
+ import crypto from "node:crypto";
2
+
3
+ // opencode 客户端身份规格单一来源。真值 = 上游 packages/schema/src/identifier.ts:
4
+ // id = <prefix>_ + 12 位 hex 时间段 + 14 位 base62;UA 必须 "opencode/<semver>"。
5
+ // ⚠ 时间段是**倒序**的:value = ~(timestamp*4096 + counter) & 48bit(descending,最新排最前)。
6
+ // 正序时钟不合规——zen 免费层自 2026-10-13 起把低 12 位**反解回 counter** 校验,只接受 [0,63];
7
+ // 正序 id 反解出 4094 → 403 FreeTierError(现网实测:counter=1/40 → 200,counter=4094 → 403)。
8
+ // 时钟半段不校验:ms*4096 如今已需 53bit,截到 48bit 必回绕——真客户端自己反解出来是 1972 年。
9
+ // ⇒ 会话 id 可以继续由摘要确定性派生(跨重启稳定),只需把低 12 位强制成合法 counter。
10
+ // 旧判据全部仍有效:形状正则(2026-09-17)、UA ≥ 1.18.0(2026-09-18,低版本 426 UpgradeRequired)、
11
+ // 以及请求体那半道 agent 形状闸(stream + 核心五工具,见 free-lane.js)。
12
+ const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
13
+ const MASK48 = 0xffffffffffffn;
14
+ const MASK12 = 0xfffn;
15
+ // 同毫秒内第 N 个 id:免费层只认反解后 ∈[0,63],越界必 403 ⇒ 到 63 回头,不无限自增
16
+ const COUNTER_MAX = 63;
17
+ const DEFAULT_OPENCODE_UA = "opencode/1.18.31";
18
+ let idLastTs = 0;
19
+ let idCounter = 0;
20
+
21
+ /** 合规时间段:倒序的 timestamp*4096+counter 截 48bit,恒 12 位小写 hex */
22
+ export function identifierTimeHex(timestamp = Date.now(), counter = 1) {
23
+ return ((~(BigInt(timestamp) * 0x1000n + BigInt(counter))) & MASK48).toString(16).padStart(12, "0");
24
+ }
25
+
26
+ /**
27
+ * 从 12 位 hex 时间段反解 per-ms 计数——与免费层闸门同一判据。
28
+ * 值域 0..4095,闸门只放行 ≤63;形状不符返回 NaN(绝不抛,调用方用 Number.isInteger 自检)。
29
+ */
30
+ export function counterFromTimeHex(hex) {
31
+ if (typeof hex !== "string" || !/^[0-9a-f]{12}$/i.test(hex)) return Number.NaN;
32
+ return Number((~BigInt("0x" + hex)) & MASK12);
33
+ }
34
+
35
+ function base62From(bytes) {
36
+ let out = "";
37
+ for (const b of bytes) out += ID_CHARS[b % 62];
38
+ return out;
39
+ }
40
+
41
+ export function opencodeIdTail(timestamp = Date.now()) {
42
+ if (timestamp !== idLastTs) {
43
+ idLastTs = timestamp;
44
+ idCounter = 0;
45
+ }
46
+ idCounter = (idCounter % COUNTER_MAX) + 1;
47
+ return identifierTimeHex(timestamp, idCounter) + base62From(crypto.randomBytes(14));
48
+ }
49
+
50
+ // 摘要派生:同种子恒同 id(会话亲和——上游按 session 归组免费额度与 prompt cache,改成现取
51
+ // 时钟会让同一会话每请求换号,绝不)。时钟半段照摘要填(不校验),低 12 位强制 counter=1。
52
+ export function digestIdTail(digest) {
53
+ const time = ((BigInt("0x" + digest.subarray(0, 6).toString("hex")) & ~MASK12) | ((~1n) & MASK12)) & MASK48;
54
+ return time.toString(16).padStart(12, "0") + base62From(digest.subarray(6, 20));
55
+ }
56
+
57
+ export function genId(prefix) {
58
+ return `${prefix}${opencodeIdTail()}`;
59
+ }
60
+
61
+ export function opencodeUa() {
62
+ return process.env.MSLXDFF_OPENCODE_UA || DEFAULT_OPENCODE_UA;
63
+ }
64
+
65
+ // 旁路(-chat curl 工具 / bench 直连)共用同一身份,避免各自伪造头再被 403
66
+ export function opencodeClientIdentity() {
67
+ return {
68
+ "User-Agent": opencodeUa(),
69
+ "x-opencode-session": genId("ses_"),
70
+ "x-opencode-request": genId("msg_"),
71
+ };
72
+ }