mslxdff 0.1.200 → 0.1.203
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/mslxdff.js +1 -1
- package/package.json +38 -38
- package/plugins/README.md +14 -14
- package/plugins/prefer-model.mjs.example +14 -14
- package/src/anthropic/count-tokens.js +61 -61
- package/src/anthropic/request-body.js +126 -126
- package/src/anthropic/shared.js +18 -18
- package/src/anthropic/sse-aggregate.js +56 -56
- package/src/anthropic/translate.js +287 -287
- package/src/anthropic/web-search.js +361 -361
- package/src/api-kind.js +31 -31
- package/src/autostart.js +288 -288
- package/src/bench/probe.js +1 -1
- package/src/bench/runner.js +1 -1
- package/src/bench/via-probe.js +1 -1
- package/src/bench/via.js +144 -144
- package/src/bench/workbuddy-bench.js +1 -1
- package/src/chat/config.js +11 -11
- package/src/chat/cooling.js +81 -81
- package/src/chat/direct.js +58 -58
- package/src/chat/index.js +6 -6
- package/src/chat/spinner.js +38 -38
- package/src/chat/sse.js +69 -69
- package/src/chat/stats.js +247 -247
- package/src/chat/store.js +48 -48
- package/src/chat/tool-handlers.js +67 -67
- package/src/chat/tools.js +1 -1
- package/src/chat/upstream.js +59 -59
- package/src/chat-pipeline/auto-race.js +158 -158
- package/src/chat-pipeline/empty-turn.js +147 -147
- package/src/chat-pipeline/engine.js +17 -17
- package/src/chat-pipeline/serial-trial.js +283 -283
- package/src/chat-pipeline/terminal-verdict.js +147 -147
- package/src/chooser.js +54 -54
- package/src/claude-desktop/apply.js +101 -101
- package/src/claude-desktop/context1m.js +38 -38
- package/src/claude-desktop/doctor.js +146 -146
- package/src/claude-desktop/slots.js +152 -152
- package/src/cli/bootstrap.js +1 -1
- package/src/cli/commands/claude-desktop.js +284 -284
- package/src/cli/commands/daemon.js +231 -231
- package/src/cli/commands/model/list-live.js +22 -22
- package/src/cli/commands/model/list-providers.js +76 -76
- package/src/cli/commands/model/list-render.js +77 -77
- package/src/cli/commands/model/list-sort.js +52 -52
- package/src/cli/commands/model/list-tty.js +76 -76
- package/src/cli/commands/model/list.js +166 -166
- package/src/cli/commands/model/live-models.js +24 -24
- package/src/cli/commands/model/stats.js +43 -43
- package/src/cli/commands/model/status.js +47 -47
- package/src/cli/commands/model.js +51 -51
- package/src/cli/commands/provider/add.js +108 -108
- package/src/cli/commands/provider/allowlist.js +99 -99
- package/src/cli/commands/provider/cline-free.js +99 -99
- package/src/cli/commands/provider/cline-login.js +218 -218
- package/src/cli/commands/provider/cline-quota.js +65 -65
- package/src/cli/commands/provider/codearts-login.js +45 -45
- package/src/cli/commands/provider/config.js +119 -119
- package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
- package/src/cli/commands/provider/keys-guard.js +50 -50
- package/src/cli/commands/provider/keys.js +161 -161
- package/src/cli/commands/provider/qoder-checkin.js +81 -81
- package/src/cli/commands/provider/qoder-login.js +102 -102
- package/src/cli/commands/provider/raccoon-login.js +180 -180
- package/src/cli/commands/provider/raccoon-password.js +162 -162
- package/src/cli/commands/provider/raccoon-quota.js +109 -109
- package/src/cli/commands/provider/traework-checkin.js +144 -144
- package/src/cli/commands/provider/traework-login.js +164 -164
- package/src/cli/commands/provider/workbuddy-login.js +153 -153
- package/src/cli/commands/provider/zcode-login.js +77 -77
- package/src/cli/commands/provider/zcode-quota.js +55 -55
- package/src/cli/commands/provider.js +1 -1
- package/src/cli/commands/stats.js +149 -149
- package/src/cli/commands/system.js +261 -261
- package/src/cli/commands/timezone.js +47 -47
- package/src/cli/commands/use-group.js +60 -60
- package/src/cli/commands/workbuddy.js +189 -189
- package/src/cli/format.js +141 -141
- package/src/cli/group-helpers.js +70 -70
- package/src/cli/help.js +80 -80
- package/src/cli/index.js +70 -70
- package/src/cli/interactive.js +121 -121
- package/src/cli/join-core.js +99 -99
- package/src/cli/join-wizard.js +175 -175
- package/src/cli/policy.js +136 -136
- package/src/cli/provider-row.js +126 -126
- package/src/cli/util.js +24 -24
- package/src/compat.js +53 -53
- package/src/events.js +38 -38
- package/src/free-lane.js +131 -131
- package/src/free-watcher.js +63 -63
- package/src/groups.js +262 -262
- package/src/logs.js +195 -195
- package/src/metrics.js +62 -62
- package/src/model-capabilities/context-lookup.js +65 -65
- package/src/model-capabilities/merge.js +142 -142
- package/src/model-trace.js +187 -187
- package/src/models.js +1 -1
- package/src/opencode-identity.js +72 -72
- package/src/peers.js +254 -251
- package/src/plugins.js +107 -107
- package/src/providers/base.js +164 -164
- package/src/providers/classify.js +22 -22
- package/src/providers/cline/allowlist-sync.js +93 -93
- package/src/providers/cline/free-catalog.js +71 -71
- package/src/providers/cline/headers.js +37 -37
- package/src/providers/cline/index.js +130 -130
- package/src/providers/cline/models.js +150 -150
- package/src/providers/cline/usage.js +274 -274
- package/src/providers/codearts/auth-pool.js +148 -148
- package/src/providers/codearts/concurrency.js +34 -34
- package/src/providers/codearts/const.js +65 -65
- package/src/providers/codearts/dpop.js +95 -95
- package/src/providers/codearts/index.js +175 -175
- package/src/providers/codearts/login.js +214 -214
- package/src/providers/codearts/models.js +182 -182
- package/src/providers/codearts/sign.js +96 -96
- package/src/providers/codearts/sse.js +204 -204
- package/src/providers/codearts/stream.js +209 -209
- package/src/providers/codearts/sts.js +124 -124
- package/src/providers/codearts.js +1 -1
- package/src/providers/dispatcher.js +146 -146
- package/src/providers/generic.js +156 -156
- package/src/providers/globalqwenwork/account-store.js +141 -141
- package/src/providers/globalqwenwork/constants.js +73 -73
- package/src/providers/globalqwenwork/crypto.js +2 -2
- package/src/providers/globalqwenwork/http.js +2 -2
- package/src/providers/globalqwenwork/index.js +331 -331
- package/src/providers/globalqwenwork/payload.js +147 -147
- package/src/providers/globalqwenwork/rsa.js +2 -2
- package/src/providers/globalqwenwork/sse.js +2 -2
- package/src/providers/globalqwenwork/stream.js +2 -2
- package/src/providers/globalqwenwork.js +1 -1
- package/src/providers/globalworkbuddy/account-store.js +198 -198
- package/src/providers/globalworkbuddy/auth.js +97 -97
- package/src/providers/globalworkbuddy/catalog.js +138 -138
- package/src/providers/globalworkbuddy/constants.js +73 -73
- package/src/providers/globalworkbuddy/device-auth.js +120 -120
- package/src/providers/globalworkbuddy/errors.js +98 -98
- package/src/providers/globalworkbuddy/headers.js +89 -89
- package/src/providers/globalworkbuddy/models.js +96 -96
- package/src/providers/globalworkbuddy/payload.js +80 -80
- package/src/providers/index.js +5 -5
- package/src/providers/model-id.js +172 -172
- package/src/providers/opencode.js +19 -19
- package/src/providers/openrouter.js +31 -31
- package/src/providers/qoder/account-store.js +106 -106
- package/src/providers/qoder/aggregate.js +60 -60
- package/src/providers/qoder/baseprompt.json +591 -591
- package/src/providers/qoder/checkin.js +160 -160
- package/src/providers/qoder/constants.js +44 -44
- package/src/providers/qoder/encode.js +41 -41
- package/src/providers/qoder/fingerprint.js +38 -38
- package/src/providers/qoder/models.js +77 -77
- package/src/providers/qoder/oauth.js +84 -84
- package/src/providers/qoder/payload.js +162 -162
- package/src/providers/qoder/request.js +27 -27
- package/src/providers/qoder/session.js +134 -134
- package/src/providers/qoder/sticky.js +38 -38
- package/src/providers/qoder/stream.js +116 -116
- package/src/providers/qoder.js +1 -1
- package/src/providers/raccoon/account-store.js +216 -216
- package/src/providers/raccoon/auth.js +67 -67
- package/src/providers/raccoon/chat.js +228 -228
- package/src/providers/raccoon/const.js +180 -165
- package/src/providers/raccoon/credits.js +185 -175
- package/src/providers/raccoon/crypto.js +43 -0
- package/src/providers/raccoon/envelope.js +31 -31
- package/src/providers/raccoon/headers.js +45 -45
- package/src/providers/raccoon/index.js +216 -216
- package/src/providers/raccoon/login.js +279 -279
- package/src/providers/raccoon/models.js +160 -160
- package/src/providers/raccoon/password-login.js +102 -87
- package/src/providers/raccoon/qr-codewords.js +115 -115
- package/src/providers/raccoon/qr.js +236 -236
- package/src/providers/raccoon/sse.js +135 -135
- package/src/providers/responses-channel.js +120 -120
- package/src/providers/share-keys.js +53 -53
- package/src/providers/traework/account-store.js +234 -234
- package/src/providers/traework/chat.js +217 -217
- package/src/providers/traework/checkin.js +77 -77
- package/src/providers/traework/constants.js +41 -41
- package/src/providers/traework/errors.js +52 -52
- package/src/providers/traework/headers.js +79 -79
- package/src/providers/traework/index.js +132 -132
- package/src/providers/traework/market-identity.js +116 -116
- package/src/providers/traework/models.js +98 -98
- package/src/providers/traework/payload.js +86 -86
- package/src/providers/traework/sse.js +184 -184
- package/src/providers/traework/token.js +52 -52
- package/src/providers/traework.js +2 -2
- package/src/providers/workbuddy/account-store.js +186 -186
- package/src/providers/workbuddy/auth.js +158 -158
- package/src/providers/workbuddy/balance.js +86 -86
- package/src/providers/workbuddy/cat-travel.js +125 -125
- package/src/providers/workbuddy/checkin.js +75 -75
- package/src/providers/workbuddy/desktop-info.js +78 -78
- package/src/providers/workbuddy/growth-api.js +67 -67
- package/src/providers/workbuddy/growth-plans.js +63 -63
- package/src/providers/workbuddy/growth.js +181 -181
- package/src/providers/workbuddy/index.js +217 -217
- package/src/providers/workbuddy/models.js +126 -126
- package/src/providers/workbuddy/payload.js +141 -141
- package/src/providers/workbuddy/rotation-log.js +83 -83
- package/src/providers/workbuddy/sanitize-tools.js +61 -61
- package/src/providers/workbuddy/sdk-chat.js +26 -26
- package/src/providers/workbuddy-balance.js +78 -78
- package/src/providers/zcode/account-store.js +129 -129
- package/src/providers/zcode/auth.js +28 -28
- package/src/providers/zcode/chat.js +179 -179
- package/src/providers/zcode/const.js +54 -54
- package/src/providers/zcode/context-shape.js +162 -162
- package/src/providers/zcode/headers.js +93 -93
- package/src/providers/zcode/index.js +124 -124
- package/src/providers/zcode/models.js +65 -65
- package/src/providers/zcode/oauth.js +120 -120
- package/src/providers/zcode/quota.js +176 -176
- package/src/providers/zcode/sse.js +196 -196
- package/src/readline-compat.js +25 -25
- package/src/responses/translate.js +311 -311
- package/src/routes/chat/broadband-handler.js +72 -72
- package/src/routes/chat/empty-turn-report.js +31 -31
- package/src/routes/chat/exhausted-handler.js +87 -87
- package/src/routes/chat/hedge-handler.js +130 -130
- package/src/routes/chat/index.js +1 -1
- package/src/routes/chat/local-handler.js +63 -63
- package/src/routes/chat/peer-handler.js +81 -81
- package/src/routes/chat.js +3 -3
- package/src/routes/fallback.js +88 -88
- package/src/routes/groups-relay.js +214 -214
- package/src/routes/groups.js +122 -122
- package/src/routes/helpers.js +83 -83
- package/src/routes/index.js +147 -147
- package/src/routes/messages-route.js +365 -365
- package/src/routes/peers.js +23 -1
- package/src/routes/relay-queue.js +181 -181
- package/src/routes/relay.js +60 -60
- package/src/routes/responses-route.js +213 -213
- package/src/routes/stream-body.js +84 -84
- package/src/routes/stream-hold.js +337 -337
- package/src/routes/stream-scan.js +366 -366
- package/src/routes.js +3 -3
- package/src/runtime/auto-update.js +113 -113
- package/src/runtime/bootstrap.js +8 -0
- package/src/runtime/broadband-stream.js +79 -79
- package/src/runtime/broadband.js +103 -103
- package/src/runtime/group-sync.js +81 -81
- package/src/runtime/keep-warm.js +120 -0
- package/src/runtime/lifecycle-forensics.js +116 -116
- package/src/runtime/lifecycle-log.js +87 -87
- package/src/runtime/log-stamp.js +73 -73
- package/src/runtime/proc-info.js +137 -137
- package/src/runtime/qoder-checkin.js +93 -93
- package/src/runtime/raccoon-checkin.js +126 -126
- package/src/runtime/server-lifecycle.js +189 -189
- package/src/runtime/traework-checkin.js +118 -118
- package/src/runtime/workbuddy-checkin.js +120 -120
- package/src/runtime/workbuddy-growth.js +133 -133
- package/src/server.js +7 -0
- package/src/state/facade.js +58 -58
- package/src/state/memory.js +162 -162
- package/src/state/merge.js +25 -25
- package/src/state/migrations/cline-unify.js +94 -94
- package/src/state/migrations.js +33 -33
- package/src/state/persist.js +42 -42
- package/src/state/provider-config.js +142 -142
- package/src/state/schemas/group.js +31 -31
- package/src/state/schemas/model.js +94 -94
- package/src/state/schemas/peer.js +31 -31
- package/src/state/schemas/port.js +10 -10
- package/src/state/schemas/provider.js +202 -202
- package/src/state/schemas/timezone.js +70 -70
- package/src/state/schemas/token.js +48 -48
- package/src/state/schemas/use-group.js +102 -102
- package/src/state/store.js +176 -176
- package/src/state.js +1 -1
- package/src/sync-claude-desktop.js +234 -234
- package/src/sync-claude.js +152 -152
- package/src/sync-codex.js +116 -116
- package/src/sync-workbuddy.js +158 -158
- package/src/talk-full-redact.js +129 -129
- package/src/talk-full.js +131 -131
- package/src/talk-log.js +257 -257
- package/src/text-tokens.js +52 -52
- package/src/thinking-effort.js +67 -67
- package/src/time.js +106 -106
- package/src/timeline.js +26 -26
- package/src/transport/index.js +1 -1
- package/src/upstream-engine/index.js +74 -74
- package/src/upstream-engine/mode.js +15 -15
- package/src/upstream-engine/sdk/attempt.js +221 -221
- package/src/upstream-engine/sdk/chat.js +28 -28
- package/src/upstream-engine/sdk/convert.js +192 -192
- package/src/upstream-engine/sdk/diagnose.js +62 -62
- package/src/upstream-engine/sdk/dispatch.js +55 -55
- package/src/upstream-engine/sdk/responses.js +176 -176
- package/src/upstream-engine/sdk/sse.js +166 -166
- package/src/upstream-responses.js +305 -305
- package/src/usage/record.js +106 -106
- package/src/usage/report.js +195 -195
- package/src/v6pool/claim.js +157 -157
- package/src/v6pool/config.js +65 -65
- package/src/v6pool/detect.js +130 -130
- package/src/v6pool/index.js +152 -152
- package/src/v6pool/prefix.js +140 -140
- package/src/v6pool/probe.js +40 -40
- package/src/v6pool/report.js +104 -104
- package/src/v6pool/rotate.js +263 -263
- package/src/warp-pool/config.js +74 -74
- package/src/warp-pool/health.js +106 -106
- package/src/warp-pool/identity.js +141 -141
- package/src/warp-pool/index.js +358 -358
- package/src/warp-pool/keys.js +29 -29
- package/src/warp-pool/nodes.js +95 -95
- package/src/warp-pool/ports.js +160 -160
- package/src/warp-pool/register.js +113 -113
- package/src/warp-pool/report.js +232 -232
- package/src/warp-pool/revive.js +168 -168
- package/src/warp-pool/rotate.js +214 -214
- package/src/warp-pool/strategy.js +43 -43
- package/src/warp-pool/verdict.js +69 -69
- package/src/warp-pool/wireproxy.js +162 -162
- package/docs/cli_help_mini.md +0 -146
- package/src/providers/AGENTS.md +0 -49
- package/src/routes/AGENTS.md +0 -37
package/src/model-trace.js
CHANGED
|
@@ -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")) ||
|
package/src/opencode-identity.js
CHANGED
|
@@ -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
|
+
}
|