mslxdff 0.1.199 → 0.1.203

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