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,195 +1,209 @@
1
- // 窗口用量聚合:JSONL 行数组 + 时间窗口 → 每模型 token/速度报表。
2
- // 纯函数(aggregateUsage)与薄 IO(readUsageRows / usageReport)分开,前者可离线单测。
3
- // Note: 速度用加权口径 Σcompletion / ΣcompletionMs,不用算术平均 —— 短回答会把算术均值拉飞。
4
- import { readFile, readdir } from "node:fs/promises";
5
- import { join } from "node:path";
6
- import { usageDir } from "./record.js";
7
-
8
- const HOUR_MS = 3_600_000;
9
-
10
- function tok(v) {
11
- const n = Number(v);
12
- return Number.isFinite(n) && n > 0 ? n : 0;
13
- }
14
-
15
- function ms(v) {
16
- const n = Number(v);
17
- return Number.isFinite(n) && n >= 0 ? n : null;
18
- }
19
- // 思考 tokens 的四态判定(行级):上报优先 → 有思考字符就估算 → 明确报 0 才算 0 → 否则未知。
20
- // 「上游报 0 却确有思考内容」按观测走估算:qoder 就是这形状,报 0 是上游没填,不是模型没想。
21
- export function resolveReasoning(row) {
22
- const reportedVal = Number(row?.reasoning_tokens);
23
- // 兼容判据:修复前的旧行没有 reasoning_reported,但带着真实上报值 —— 不能因缺字段退化成未知
24
- const hasReported = row?.reasoning_reported === 1 || (Number.isFinite(reportedVal) && reportedVal > 0);
25
- const chars = Number(row?.reasoning_chars);
26
- const charCount = Number.isFinite(chars) && chars > 0 ? Math.trunc(chars) : 0;
27
- if (hasReported && Number.isFinite(reportedVal) && reportedVal > 0) return { value: reportedVal, source: "reported" };
28
- if (charCount > 0) {
29
- const cap = Number(row?.completion_tokens);
30
- const est = Math.ceil(charCount / 4); // 与 src/providers/cline/usage.js 的既有估算口径一致
31
- return { value: Number.isFinite(cap) && cap > 0 ? Math.min(est, cap) : est, source: "estimated" };
32
- }
33
- if (hasReported) return { value: 0, source: "reported" }; // 该轮真的没思考
34
- return { value: 0, source: "none" }; // 无从判断 → 表格渲染 —,绝不渲染成 0
35
- }
36
-
37
-
38
- function blank(id) {
39
- return {
40
- id,
41
- requests: 0,
42
- promptTokens: 0,
43
- completionTokens: 0,
44
- totalTokens: 0,
45
- reasoningTokens: 0,
46
- ttfbSumMs: 0,
47
- ttfbN: 0,
48
- totalSumMs: 0,
49
- totalN: 0,
50
- completionMsSum: 0,
51
- tpsTokSum: 0,
52
- reasoningReported: 0,
53
- reasoningEstimated: 0,
54
- reasoningReportedRows: 0,
55
- reasoningEstimatedRows: 0,
56
- reasoningChars: 0,
57
- streamRequests: 0,
58
- };
59
- }
60
-
61
- // 把累计量收敛成对外字段:平均首字/平均总耗时/加权速度。
62
- function finalize(a) {
63
- return {
64
- id: a.id,
65
- requests: a.requests,
66
- promptTokens: a.promptTokens,
67
- completionTokens: a.completionTokens,
68
- totalTokens: a.totalTokens,
69
- reasoningTokens: a.reasoningReported + a.reasoningEstimated,
70
- avgTtfbMs: a.ttfbN ? Math.round(a.ttfbSumMs / a.ttfbN) : null,
71
- avgTotalMs: a.totalN ? Math.round(a.totalSumMs / a.totalN) : null,
72
- // 思考来源:上报与估算同时存在时是 mixed——合计值不冒充单一精确数(spec 锁死)
73
- reasoningSource: a.reasoningReportedRows > 0 && a.reasoningEstimatedRows > 0 ? "mixed"
74
- : a.reasoningReportedRows > 0 ? "reported"
75
- : a.reasoningEstimatedRows > 0 ? "estimated" : "none",
76
- reasoningChars: a.reasoningChars,
77
- ttfSamples: a.ttfbN,
78
- streamRequests: a.streamRequests,
79
- avgTps: a.completionMsSum > 0 ? Number((a.tpsTokSum / (a.completionMsSum / 1000)).toFixed(1)) : null,
80
- };
81
- }
82
-
83
- function fold(acc, r) {
84
- acc.requests++;
85
- const prompt = tok(r.prompt_tokens);
86
- const comp = tok(r.completion_tokens);
87
- acc.promptTokens += prompt;
88
- acc.completionTokens += comp;
89
- const total = tok(r.total_tokens);
90
- acc.totalTokens += total || prompt + comp;
91
- // 思考:四态分层累计(上报/估算各算各的,未知不贡献数值),原始字符数照实累加
92
- const rs = resolveReasoning(r);
93
- if (rs.source === "reported") { acc.reasoningReported += rs.value; acc.reasoningReportedRows++; }
94
- else if (rs.source === "estimated") { acc.reasoningEstimated += rs.value; acc.reasoningEstimatedRows++; }
95
- acc.reasoningChars += tok(r.reasoning_chars);
96
- // 首字样本分母:非流式行既无首字也不该摊进分母;缺 stream 的旧行按未知保守计入
97
- if (r.stream !== 0) acc.streamRequests++;
98
-
99
- const ttfb = ms(r.ttfbMs ?? r.ttfb_ms);
100
- if (ttfb != null) {
101
- acc.ttfbSumMs += ttfb;
102
- acc.ttfbN++;
103
- }
104
- const totalMs = ms(r.totalMs ?? r.total_ms);
105
- if (totalMs != null && totalMs > 0) {
106
- acc.totalSumMs += totalMs;
107
- acc.totalN++;
108
- }
109
- // 生成阶段耗时 = 总耗时 - 首字(首字缺失按 0 计)
110
- if (totalMs != null && totalMs > 0 && comp > 0) {
111
- const compMs = Math.max(0, totalMs - (ttfb ?? 0));
112
- if (compMs > 0) {
113
- acc.completionMsSum += compMs;
114
- acc.tpsTokSum += comp;
115
- }
116
- }
117
- return acc;
118
- }
119
-
120
- // 纯函数:只做过滤 + 归并,不碰磁盘。
121
- export function aggregateUsage(rows, { hours = 24, since: sinceOpt = null, now = Date.now(), model = null } = {}) {
122
- const untilN = Number(now);
123
- const until = Number.isFinite(untilN) ? untilN : Date.now();
124
- // since(绝对窗口起点,毫秒)优先于 hours:CLI 默认窗口「今日 0 点 → 现在」靠它精确落在 0 点,不随查询时刻漂移
125
- const sinceMs = sinceOpt == null ? NaN : Number(sinceOpt);
126
- const hasSince = Number.isFinite(sinceMs);
127
- const hoursN = Number(hours);
128
- const windowHours = hasSince ? (until - sinceMs) / HOUR_MS : Number.isFinite(hoursN) && hoursN > 0 ? hoursN : 24;
129
- const since = hasSince ? sinceMs : until - windowHours * HOUR_MS;
130
- const byModel = new Map();
131
- const sum = blank(null);
132
- let scanned = 0;
133
-
134
- if (Array.isArray(rows)) {
135
- for (const r of rows) {
136
- if (!r || typeof r !== "object") continue;
137
- const ts = Number(r.ts);
138
- if (!Number.isFinite(ts) || ts < since || ts > until) continue;
139
- const id = String(r.model || "").trim();
140
- if (!id) continue;
141
- // model 过滤同时接受 canonical 全称与裸 id(如 --model big-pickle 匹配 opencode/big-pickle 行)
142
- if (model && id !== model && !id.endsWith("/" + model)) continue;
143
- scanned++;
144
- let acc = byModel.get(id);
145
- if (!acc) { acc = blank(id); byModel.set(id, acc); }
146
- fold(acc, r);
147
- fold(sum, r);
148
- }
149
- }
150
- const models = [...byModel.values()].map(finalize);
151
- models.sort((a, b) => (b.totalTokens - a.totalTokens) || (b.requests - a.requests) || a.id.localeCompare(b.id));
152
- return {
153
- windowHours,
154
- since,
155
- until,
156
- rows: scanned,
157
- models,
158
- totals: finalize(sum),
159
- };
160
- }
161
-
162
- // 读保留期内的日文件并只留窗口内的行(保留期默认 2 天,文件数很少,全读即可)。
163
- export async function readUsageRows({ dir, hours = 24, since: sinceOpt = null, now = Date.now() } = {}) {
164
- const sinceMs = sinceOpt == null ? NaN : Number(sinceOpt);
165
- const hoursN = Number(hours);
166
- const since = Number.isFinite(sinceMs) ? sinceMs : Number(now) - (Number.isFinite(hoursN) && hoursN > 0 ? hoursN : 24) * HOUR_MS;
167
- let files = [];
168
- try {
169
- files = (await readdir(usageDir({ dir }))).filter((f) => f.endsWith(".jsonl")).sort();
170
- } catch {
171
- return [];
172
- }
173
- const rows = [];
174
- for (const f of files) {
175
- let text = "";
176
- try {
177
- text = await readFile(join(usageDir({ dir }), f), "utf8");
178
- } catch {
179
- continue;
180
- }
181
- for (const line of text.split("\n")) {
182
- if (!line) continue;
183
- try {
184
- const r = JSON.parse(line);
185
- if (Number(r?.ts) >= since) rows.push(r);
186
- } catch {}
187
- }
188
- }
189
- return rows;
190
- }
191
-
192
- export async function usageReport({ dir, hours = 24, since = null, now = Date.now(), model = null } = {}) {
193
- const rows = await readUsageRows({ dir, hours, since, now });
194
- return aggregateUsage(rows, { hours, since, now, model });
195
- }
1
+ // 窗口用量聚合:JSONL 行数组 + 时间窗口 → 每模型 token/速度报表。
2
+ // 纯函数(aggregateUsage)与薄 IO(readUsageRows / usageReport)分开,前者可离线单测。
3
+ // Note: 速度用加权口径 Σcompletion / ΣcompletionMs,不用算术平均 —— 短回答会把算术均值拉飞。
4
+ import { readFile, readdir } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { usageDir } from "./record.js";
7
+
8
+ const HOUR_MS = 3_600_000;
9
+
10
+ function tok(v) {
11
+ const n = Number(v);
12
+ return Number.isFinite(n) && n > 0 ? n : 0;
13
+ }
14
+
15
+ function ms(v) {
16
+ const n = Number(v);
17
+ return Number.isFinite(n) && n >= 0 ? n : null;
18
+ }
19
+ // 思考 tokens 的四态判定(行级):上报优先 → 有思考字符就估算 → 明确报 0 才算 0 → 否则未知。
20
+ // 「上游报 0 却确有思考内容」按观测走估算:qoder 就是这形状,报 0 是上游没填,不是模型没想。
21
+ export function resolveReasoning(row) {
22
+ const reportedVal = Number(row?.reasoning_tokens);
23
+ // 兼容判据:修复前的旧行没有 reasoning_reported,但带着真实上报值 —— 不能因缺字段退化成未知
24
+ const hasReported = row?.reasoning_reported === 1 || (Number.isFinite(reportedVal) && reportedVal > 0);
25
+ const chars = Number(row?.reasoning_chars);
26
+ const charCount = Number.isFinite(chars) && chars > 0 ? Math.trunc(chars) : 0;
27
+ if (hasReported && Number.isFinite(reportedVal) && reportedVal > 0) return { value: reportedVal, source: "reported" };
28
+ if (charCount > 0) {
29
+ const cap = Number(row?.completion_tokens);
30
+ const est = Math.ceil(charCount / 4); // 与 src/providers/cline/usage.js 的既有估算口径一致
31
+ return { value: Number.isFinite(cap) && cap > 0 ? Math.min(est, cap) : est, source: "estimated" };
32
+ }
33
+ if (hasReported) return { value: 0, source: "reported" }; // 该轮真的没思考
34
+ return { value: 0, source: "none" }; // 无从判断 → 表格渲染 —,绝不渲染成 0
35
+ }
36
+
37
+
38
+ function blank(id) {
39
+ return {
40
+ id,
41
+ requests: 0,
42
+ promptTokens: 0,
43
+ completionTokens: 0,
44
+ totalTokens: 0,
45
+ reasoningTokens: 0,
46
+ ttfbSumMs: 0,
47
+ ttfbN: 0,
48
+ totalSumMs: 0,
49
+ totalN: 0,
50
+ completionMsSum: 0,
51
+ tpsTokSum: 0,
52
+ reasoningReported: 0,
53
+ reasoningEstimated: 0,
54
+ reasoningReportedRows: 0,
55
+ reasoningEstimatedRows: 0,
56
+ reasoningChars: 0,
57
+ streamRequests: 0,
58
+ cacheReadTokens: 0,
59
+ cacheCreationTokens: 0,
60
+ cacheRows: 0,
61
+ };
62
+ }
63
+
64
+ // 把累计量收敛成对外字段:平均首字/平均总耗时/加权速度。
65
+ function finalize(a) {
66
+ return {
67
+ id: a.id,
68
+ requests: a.requests,
69
+ promptTokens: a.promptTokens,
70
+ completionTokens: a.completionTokens,
71
+ totalTokens: a.totalTokens,
72
+ reasoningTokens: a.reasoningReported + a.reasoningEstimated,
73
+ avgTtfbMs: a.ttfbN ? Math.round(a.ttfbSumMs / a.ttfbN) : null,
74
+ avgTotalMs: a.totalN ? Math.round(a.totalSumMs / a.totalN) : null,
75
+ // 思考来源:上报与估算同时存在时是 mixed——合计值不冒充单一精确数(spec 锁死)
76
+ reasoningSource: a.reasoningReportedRows > 0 && a.reasoningEstimatedRows > 0 ? "mixed"
77
+ : a.reasoningReportedRows > 0 ? "reported"
78
+ : a.reasoningEstimatedRows > 0 ? "estimated" : "none",
79
+ reasoningChars: a.reasoningChars,
80
+ ttfSamples: a.ttfbN,
81
+ streamRequests: a.streamRequests,
82
+ avgTps: a.completionMsSum > 0 ? Number((a.tpsTokSum / (a.completionMsSum / 1000)).toFixed(1)) : null,
83
+ cacheReadTokens: a.cacheReadTokens,
84
+ cacheCreationTokens: a.cacheCreationTokens,
85
+ cacheRows: a.cacheRows,
86
+ // 命中率 = 缓存读 ÷ 输入总数(上游 prompt_tokens **含**缓存命中,故分母用输入总数;
87
+ // 实测佐证:cache 命中那发 prompt_tokens=429 = 客户端 input 45 + cache_read 384)。
88
+ // 窗口内没有任何命中读数时给 null → 渲染成 —,绝不拿 0 冒充「没缓存」。
89
+ cacheHitRate: a.cacheRows > 0 && a.promptTokens > 0 ? Number((a.cacheReadTokens / a.promptTokens).toFixed(4)) : null,
90
+ };
91
+ }
92
+
93
+ function fold(acc, r) {
94
+ acc.requests++;
95
+ const prompt = tok(r.prompt_tokens);
96
+ const comp = tok(r.completion_tokens);
97
+ acc.promptTokens += prompt;
98
+ acc.completionTokens += comp;
99
+ const total = tok(r.total_tokens);
100
+ acc.totalTokens += total || prompt + comp;
101
+ // 思考:四态分层累计(上报/估算各算各的,未知不贡献数值),原始字符数照实累加
102
+ const rs = resolveReasoning(r);
103
+ if (rs.source === "reported") { acc.reasoningReported += rs.value; acc.reasoningReportedRows++; }
104
+ else if (rs.source === "estimated") { acc.reasoningEstimated += rs.value; acc.reasoningEstimatedRows++; }
105
+ acc.reasoningChars += tok(r.reasoning_chars);
106
+ // 首字样本分母:非流式行既无首字也不该摊进分母;缺 stream 的旧行按未知保守计入
107
+ if (r.stream !== 0) acc.streamRequests++;
108
+ // 缓存读数:只有上游真报了命中(字段存在且 >0)才算「有读数」——分母不拿未报的请求凑
109
+ const cr = tok(r.cache_read_tokens);
110
+ if (cr > 0) { acc.cacheReadTokens += cr; acc.cacheRows++; }
111
+ acc.cacheCreationTokens += tok(r.cache_creation_tokens);
112
+
113
+ const ttfb = ms(r.ttfbMs ?? r.ttfb_ms);
114
+ if (ttfb != null) {
115
+ acc.ttfbSumMs += ttfb;
116
+ acc.ttfbN++;
117
+ }
118
+ const totalMs = ms(r.totalMs ?? r.total_ms);
119
+ if (totalMs != null && totalMs > 0) {
120
+ acc.totalSumMs += totalMs;
121
+ acc.totalN++;
122
+ }
123
+ // 生成阶段耗时 = 总耗时 - 首字(首字缺失按 0 计)
124
+ if (totalMs != null && totalMs > 0 && comp > 0) {
125
+ const compMs = Math.max(0, totalMs - (ttfb ?? 0));
126
+ if (compMs > 0) {
127
+ acc.completionMsSum += compMs;
128
+ acc.tpsTokSum += comp;
129
+ }
130
+ }
131
+ return acc;
132
+ }
133
+
134
+ // 纯函数:只做过滤 + 归并,不碰磁盘。
135
+ export function aggregateUsage(rows, { hours = 24, since: sinceOpt = null, now = Date.now(), model = null } = {}) {
136
+ const untilN = Number(now);
137
+ const until = Number.isFinite(untilN) ? untilN : Date.now();
138
+ // since(绝对窗口起点,毫秒)优先于 hours:CLI 默认窗口「今日 0 点 → 现在」靠它精确落在 0 点,不随查询时刻漂移
139
+ const sinceMs = sinceOpt == null ? NaN : Number(sinceOpt);
140
+ const hasSince = Number.isFinite(sinceMs);
141
+ const hoursN = Number(hours);
142
+ const windowHours = hasSince ? (until - sinceMs) / HOUR_MS : Number.isFinite(hoursN) && hoursN > 0 ? hoursN : 24;
143
+ const since = hasSince ? sinceMs : until - windowHours * HOUR_MS;
144
+ const byModel = new Map();
145
+ const sum = blank(null);
146
+ let scanned = 0;
147
+
148
+ if (Array.isArray(rows)) {
149
+ for (const r of rows) {
150
+ if (!r || typeof r !== "object") continue;
151
+ const ts = Number(r.ts);
152
+ if (!Number.isFinite(ts) || ts < since || ts > until) continue;
153
+ const id = String(r.model || "").trim();
154
+ if (!id) continue;
155
+ // model 过滤同时接受 canonical 全称与裸 id(如 --model big-pickle 匹配 opencode/big-pickle 行)
156
+ if (model && id !== model && !id.endsWith("/" + model)) continue;
157
+ scanned++;
158
+ let acc = byModel.get(id);
159
+ if (!acc) { acc = blank(id); byModel.set(id, acc); }
160
+ fold(acc, r);
161
+ fold(sum, r);
162
+ }
163
+ }
164
+ const models = [...byModel.values()].map(finalize);
165
+ models.sort((a, b) => (b.totalTokens - a.totalTokens) || (b.requests - a.requests) || a.id.localeCompare(b.id));
166
+ return {
167
+ windowHours,
168
+ since,
169
+ until,
170
+ rows: scanned,
171
+ models,
172
+ totals: finalize(sum),
173
+ };
174
+ }
175
+
176
+ // 读保留期内的日文件并只留窗口内的行(保留期默认 2 天,文件数很少,全读即可)。
177
+ export async function readUsageRows({ dir, hours = 24, since: sinceOpt = null, now = Date.now() } = {}) {
178
+ const sinceMs = sinceOpt == null ? NaN : Number(sinceOpt);
179
+ const hoursN = Number(hours);
180
+ const since = Number.isFinite(sinceMs) ? sinceMs : Number(now) - (Number.isFinite(hoursN) && hoursN > 0 ? hoursN : 24) * HOUR_MS;
181
+ let files = [];
182
+ try {
183
+ files = (await readdir(usageDir({ dir }))).filter((f) => f.endsWith(".jsonl")).sort();
184
+ } catch {
185
+ return [];
186
+ }
187
+ const rows = [];
188
+ for (const f of files) {
189
+ let text = "";
190
+ try {
191
+ text = await readFile(join(usageDir({ dir }), f), "utf8");
192
+ } catch {
193
+ continue;
194
+ }
195
+ for (const line of text.split("\n")) {
196
+ if (!line) continue;
197
+ try {
198
+ const r = JSON.parse(line);
199
+ if (Number(r?.ts) >= since) rows.push(r);
200
+ } catch {}
201
+ }
202
+ }
203
+ return rows;
204
+ }
205
+
206
+ export async function usageReport({ dir, hours = 24, since = null, now = Date.now(), model = null } = {}) {
207
+ const rows = await readUsageRows({ dir, hours, since, now });
208
+ return aggregateUsage(rows, { hours, since, now, model });
209
+ }