mslxdff 0.1.155 → 0.1.158

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 (151) hide show
  1. package/bin/mslxdff.js +4 -4
  2. package/docs/ARCHITECTURE.md +426 -0
  3. package/docs/FEATURE_TREE.md +164 -0
  4. package/docs/MOBILE.md +82 -0
  5. package/docs/adr/0001-reasoning-content-injection.md +14 -0
  6. package/docs/adr/0002-models-free-filter.md +12 -0
  7. package/docs/adr/0003-zero-state-no-auth.md +10 -0
  8. package/docs/adr/0004-bearer-token.md +18 -0
  9. package/docs/adr/0005-peer-mesh.md +53 -0
  10. package/docs/adr/0006-broadband-member.md +103 -0
  11. package/docs/adr/0007-multi-provider-prefix.md +25 -0
  12. package/docs/adr/0008-share-keys-to-peers.md +52 -0
  13. package/docs/adr/0009-chat-repl.md +37 -0
  14. package/docs/adr/0010-allowlist.md +36 -0
  15. package/docs/adr/0011-broadband-stream.md +30 -0
  16. package/docs/adr/0012-responses-endpoint-codex-sync.md +48 -0
  17. package/docs/adr/0013-node16-compat.md +41 -0
  18. package/docs/adr/0014-deepseek-provider.md +52 -0
  19. package/docs/adr/0015-upstream-probe-routing.md +50 -0
  20. package/docs/adr/0016-model-capabilities.md +27 -0
  21. package/docs/adr/0017-ai-sdk-upstream-engine.md +59 -0
  22. package/docs/adr/0018-zen-client-identity.md +52 -0
  23. package/docs/adr/0019-share-keys-always-lend.md +59 -0
  24. package/docs/adr/0020-zen-free-lane-agent-shape.md +52 -0
  25. package/docs/adr/0021-usage-report-jsonl.md +47 -0
  26. package/docs/adr/0022-models-capability-merge.md +61 -0
  27. package/docs/adr/0023-key-provider-default-direct.md +58 -0
  28. package/docs/adr/0024-node18-baseline.md +63 -0
  29. package/docs/adr/0025-workbuddy-authdir-follows-state.md +71 -0
  30. package/docs/adr/0026-cline-provider-id-unify.md +67 -0
  31. package/docs/adr/0027-codearts-provider.md +48 -0
  32. package/docs/adr/0028-traework-provider.md +34 -0
  33. package/docs/adr/0029-qoder-native-provider.md +60 -0
  34. package/docs/adr/0030-models-list-scoped-by-picks.md +53 -0
  35. package/docs/adr/0031-qoder-true-streaming.md +50 -0
  36. package/docs/adr/0032-generic-responses-channel.md +72 -0
  37. package/docs/adr/0033-cline-allowlist-auto-sync.md +75 -0
  38. package/docs/adr/0034-request-level-human-readable-observability.md +60 -0
  39. package/docs/adr/0035-sdk-channel-headers-timeout.md +49 -0
  40. package/docs/adr/0036-qoder-per-request-sticky-account.md +82 -0
  41. package/docs/adr/0037-qwenwork-independent-provider.md +82 -0
  42. package/docs/adr/0038-zcode-provider.md +140 -0
  43. package/docs/agents/domain.md +51 -0
  44. package/docs/agents/issue-tracker.md +30 -0
  45. package/docs/agents/triage-labels.md +15 -0
  46. package/docs/cli_help.md +1391 -0
  47. package/docs/cli_help_mini.md +133 -0
  48. package/docs/plans/bench-via-latency-2026-09-01.md +215 -0
  49. package/docs/plugins.md +187 -0
  50. package/package.json +1 -1
  51. package/src/auto.js +254 -254
  52. package/src/bench/cline-bench.js +42 -42
  53. package/src/bench/probe.js +70 -70
  54. package/src/bench/report.js +162 -162
  55. package/src/bench/runner.js +77 -77
  56. package/src/bench/via-probe.js +124 -124
  57. package/src/bench/via-routes.js +87 -87
  58. package/src/bench/workbuddy-bench.js +54 -54
  59. package/src/chat/engine.js +160 -160
  60. package/src/chat/gateway.js +163 -163
  61. package/src/chat/orchestrator.js +234 -234
  62. package/src/chat/prompt.js +70 -70
  63. package/src/chat/repl.js +88 -88
  64. package/src/chat/terminal.js +135 -135
  65. package/src/chat/tools.js +306 -306
  66. package/src/chat-pipeline/index.js +123 -123
  67. package/src/chat-pipeline/policy.js +76 -76
  68. package/src/chat-pipeline/serial-trial.js +210 -210
  69. package/src/cli/commands/group.js +249 -249
  70. package/src/cli/commands/model/list-providers.js +4 -3
  71. package/src/cli/commands/model/list-render.js +7 -9
  72. package/src/cli/commands/model/list-sort.js +52 -0
  73. package/src/cli/commands/model/list-tty.js +76 -0
  74. package/src/cli/commands/model/list.js +5 -62
  75. package/src/cli/commands/model/picks.js +50 -50
  76. package/src/cli/commands/provider/bench-via.js +247 -247
  77. package/src/cli/commands/provider/bench.js +141 -141
  78. package/src/cli/commands/provider/index.js +124 -116
  79. package/src/cli/commands/provider/models.js +139 -128
  80. package/src/cli/commands/provider/qwenwork-login.js +119 -0
  81. package/src/cli/commands/provider/zcode-login.js +77 -0
  82. package/src/cli/commands/provider/zcode-quota.js +55 -0
  83. package/src/cli/commands/sync.js +232 -232
  84. package/src/cli/provider-row.js +2 -2
  85. package/src/cli/status.js +279 -279
  86. package/src/daemon.js +96 -96
  87. package/src/model-capabilities/enrich.js +86 -86
  88. package/src/model-capabilities/index.js +183 -183
  89. package/src/model-capabilities/parse.js +70 -70
  90. package/src/model-trace.js +1 -0
  91. package/src/models.js +225 -225
  92. package/src/providers/classify.js +1 -1
  93. package/src/providers/cline/auth.js +228 -228
  94. package/src/providers/cline/chat.js +307 -307
  95. package/src/providers/cline.js +2 -2
  96. package/src/providers/keyring.js +60 -56
  97. package/src/providers/qoder/chat.js +183 -100
  98. package/src/providers/qoder/index.js +230 -184
  99. package/src/providers/qoder/sse.js +103 -46
  100. package/src/providers/qoder/sticky.js +1 -0
  101. package/src/providers/qoder/stream.js +32 -9
  102. package/src/providers/qwenwork/account-store.js +133 -0
  103. package/src/providers/qwenwork/constants.js +67 -0
  104. package/src/providers/qwenwork/cosy.js +120 -0
  105. package/src/providers/qwenwork/crypto.js +218 -0
  106. package/src/providers/qwenwork/http.js +20 -0
  107. package/src/providers/qwenwork/index.js +327 -0
  108. package/src/providers/qwenwork/payload.js +142 -0
  109. package/src/providers/qwenwork/rsa.js +54 -0
  110. package/src/providers/qwenwork/sse.js +268 -0
  111. package/src/providers/qwenwork/stream.js +130 -0
  112. package/src/providers/qwenwork/upstream.js +120 -0
  113. package/src/providers/qwenwork.js +1 -0
  114. package/src/providers/registry.js +66 -56
  115. package/src/providers/share-keys.js +2 -2
  116. package/src/providers/workbuddy/chat.js +248 -248
  117. package/src/providers/workbuddy/reshape.js +152 -152
  118. package/src/providers/workbuddy.js +2 -2
  119. package/src/providers/zcode/account-store.js +129 -0
  120. package/src/providers/zcode/auth.js +28 -0
  121. package/src/providers/zcode/chat.js +171 -0
  122. package/src/providers/zcode/const.js +54 -0
  123. package/src/providers/zcode/headers.js +55 -0
  124. package/src/providers/zcode/index.js +124 -0
  125. package/src/providers/zcode/models.js +65 -0
  126. package/src/providers/zcode/oauth.js +120 -0
  127. package/src/providers/zcode/quota.js +176 -0
  128. package/src/providers/zcode/sse.js +179 -0
  129. package/src/reasoning.js +32 -32
  130. package/src/routes/chat/gateway.js +46 -46
  131. package/src/routes/chat/relay-pipeline.js +250 -246
  132. package/src/routes/chat/via-route-handler.js +144 -144
  133. package/src/routes/hedge.js +255 -255
  134. package/src/routes/models-route.js +167 -167
  135. package/src/routes/peers.js +273 -273
  136. package/src/routes/stream.js +438 -399
  137. package/src/runtime/bootstrap.js +45 -45
  138. package/src/runtime/provider-gate.js +33 -30
  139. package/src/runtime/providers-setup.js +165 -165
  140. package/src/server.js +64 -64
  141. package/src/state/schemas/allowlist.js +92 -92
  142. package/src/sync-opencode.js +280 -280
  143. package/src/transport/index.js +244 -244
  144. package/src/transport/pool.js +56 -56
  145. package/src/transport/retry.js +24 -24
  146. package/src/transport/sse.js +93 -93
  147. package/src/upstream-probe/display.js +52 -52
  148. package/src/upstream-probe/probe.js +49 -49
  149. package/src/upstream-probe/rotate.js +110 -110
  150. package/src/upstream-probe/start.js +45 -45
  151. package/src/upstream.js +289 -289
@@ -0,0 +1,36 @@
1
+ # ADR-0010: 供应商模型白名单(防昂贵/奇怪模型)
2
+
3
+ - 状态:已接受
4
+ - 日期:2026-08-28
5
+ - 版本:0.1.60
6
+
7
+ ## 背景
8
+
9
+ 通用 OpenAI 兼容供应商(`providerConfigs.<id>={baseUrl,keys}`)代理任意上游后,客户端可任意指定 `model` 透传;若上游按量计费或存在昂贵模型,误用会产生意外费用。需要一种**显式白名单**:只有名单内的模型才放行,其余一律拦截,且 `/v1/models` 亦不暴露。
10
+
11
+ ## 决策
12
+
13
+ - 存储:`state.json providerConfigs.<id>.allowedModels: string[]`(`src/state.js`)。空数组或缺失 = 不限(向后兼容);非空 = 仅名单内可用。模型存 `raw`(去前缀后,如 `gpt-4`),输入时支持 `myapi/gpt-4` 或 `gpt-4` 自动归一。
14
+ - 接入:`mslxdff -provider add <id> <baseUrl> <key> [model1 model2 ...]` 末尾可选白名单;后续 `mslxdff -provider <id> allowlist [list|set|add|remove|clear]` 增量管理(`allow / allowed / whitelist` 为同义别名)。
15
+ - 拦截:`src/providers/dispatcher.js` 的 `chat` 前置 `isModelAllowed(providerId, raw)` 校验,不通过则立即返回 `403 {error:"model not allowed..."}`,不计入冷却、不触发 fallback;`listModels` 聚合时亦按白名单过滤(按 `raw` 精确匹配)。
16
+ - 展示:`mslxdff -provider <id> list` 与 `mslxdff -providers list` 均展示 `allowlist` 摘要(`allow=all` 或 `allow=N(...)`);`allowlist list` 展示明细。
17
+ - 生效:改动后需重启 daemon(与 `keys/baseUrl` 同步);`opencode` 亦支持白名单(`mslxdff -provider opencode allowlist set big-pickle ...`)。
18
+
19
+ ## 后果
20
+
21
+ - 正面:显式控制成本与暴露面,防止客户端误调昂贵模型;名单与 `keys/baseUrl` 同生命周期,操作一致。
22
+ - 负面:白名单非空时新增模型需手动 `allowlist add`,否则 403;空=不限的语义需文档强调。
23
+ - 兼容:空名单保持原有“全部放行”行为;旧 state 无该字段自动视为不限。
24
+
25
+ ## 备选
26
+
27
+ - 在 `generic.js`/`openrouter.js` 各自校验:被否,中心化在 `dispatcher` 更一致,且覆盖 `opencode`。
28
+ - 用黑名单而非白名单:被否,白名单更安全(默认拒绝未知模型)。
29
+ - 存 `providerAllowedModels` 独立顶层:被否,复用 `providerConfigs` 更内聚,与 `baseUrl/keys` 同条目管理。
30
+
31
+ ## 关联
32
+
33
+ - `src/state.js`(`loadProviderAllowedModels`/`saveProviderAllowedModels`/`isModelAllowed`)
34
+ - `src/providers/dispatcher.js`(`chat` 403 与 `listModels` 过滤)
35
+ - `bin/mslxdff.js`(`allowlist` 子命令与 `add` 白名单参数,`printHelp`)
36
+ - `docs/cli_help.md §6` 与 `docs/ARCHITECTURE.md §5/§6/§7`
@@ -0,0 +1,30 @@
1
+ # ADR-0011: 宽带中继 SSE 长连(替代 1s 轮询)
2
+
3
+ - 状态:已接受
4
+ - 日期:2026-09-03
5
+ - 版本:0.1.88
6
+
7
+ ## 背景
8
+
9
+ `broadband` 组员 `relay://` 无法被 Leader 直连,旧实现靠 `POST /v1/groups/relay/poll` 每秒 1 次空轮询(`src/runtime/bootstrap.js:386`)+ `30s heartbeat` 取任务,空闲即 `86400次/天/组`、`~0.2k/s` 上行,与用户“WS 聊天室 30s ping”的心智不符,抓包即暴露高频空流。
10
+
11
+ ## 决策
12
+
13
+ - 新增 `GET /v1/groups/relay/stream?name=` SSE 长连(`src/routes/groups-relay.js:streamHandler`),`Bearer` 复用 `membersForToken` 仅 `broadband/relay://` 可建,头 `text/event-stream / no-cache / X-Accel-Buffering: no`,首帧 `:connected`,每 `MSLXDFF_BROADBAND_PING_MS`(25s) 写 `:ping` 并顺带刷新 `lastSeen` 防 `stale`。
14
+ - 队列 `src/routes/relay-queue.js` 新增 `streamSubscribers: Map<key,Set<res>>`,`subscribeStream/unsubscribeStream/pushToStream`,`enqueueRelay` 先试 `pushToStream`(`event: relay\ndata: {reqId,body,hops}\n\n`),有订阅者则不入 `poll` 队列,仅留 `pendingByReqId` 等 `resolveRelay`;无订阅者回退进队列供 `poll` 拉取。
15
+ - 客户端 `src/runtime/bootstrap.js` 的 `broadbandGroups` 块:`MSLXDFF_BROADBAND_STREAM` 默认开时改单 SSE 长连+指数退避重连(1s*2^n 至 30s),解析 `event: relay` 后复用 `upstream.chat` 并 `POST /result`;`MSLXDFF_BROADBAND_STREAM=0` 回退旧 `heartbeat 30s + poll 1s`。流存活期不再单独 `heartbeat`(靠 `ping` 保 `lastSeen`)。
16
+ - `POST /poll|/heartbeat|/result|/forward` 保留兼容,`static` 直连不受影响。
17
+
18
+ ## 后果
19
+
20
+ - 正面:空闲从 1/s 降至 1/25s(-96%),延迟从 500ms 中位降至推送 <200ms,单组单连符合聊天室心智,断线自动重连不丢任务(队列双通道)。
21
+ - 负面:SSE 长连需 `close` 时清理,代理需 `X-Accel-Buffering: no`;`Uint8Array` 需 `Buffer.from` 解码(Node fetch)。
22
+ - 兼容:旧客户端/旧 Leader 无 `stream` 仍走 `poll` 互通。
23
+
24
+ ## 关联
25
+
26
+ - `src/routes/groups-relay.js`(`streamHandler` + `isStreamEnabled`)
27
+ - `src/routes/relay-queue.js`(`subscribeStream/pushToStream`)
28
+ - `src/routes/index.js`(`GET /v1/groups/relay/stream`)
29
+ - `src/runtime/bootstrap.js`(`execAndPost` + `streamEnabled` 分支)
30
+ - `docs/ARCHITECTURE.md §5/§6` Env `MSLXDFF_BROADBAND_STREAM/_PING_MS/_STALE_MS`
@@ -0,0 +1,48 @@
1
+ # ADR-0012: Responses 端点与 Codex 同步(-setto chatgpt)
2
+
3
+ - 状态:已接受(v0.1.94)
4
+ - 日期:2026-09-04
5
+
6
+ ## 背景
7
+
8
+ Codex CLI/IDE/桌面三端共用 `~/.codex/config.toml`,自定义 provider 只认
9
+ Responses API(`wire_api` 唯一合法值 `responses`,chat 线已砍),且其 Rust
10
+ 解码器对缺字段零容忍(缺一个字段整轮作废)。桌面模型 picker 不显示自定义
11
+ 模型是上游已知 issue(openai/codex#37379 等),但配 `model=` 照走。
12
+
13
+ ## 决策
14
+
15
+ 1. 网关新增 `POST /v1/responses`(`src/routes/responses-route.js`),复用
16
+ ChatPipeline 全链路,只做 Responses⇄Chat 形状翻译(纯函数下沉
17
+ `src/responses/translate.js`,单测 11 项)。
18
+ 2. stateless:`previous_response_id` 不支持,每轮全量 input;thinking 模型
19
+ reasoning 暂不透传(先用非 thinking 模型如 `big-pickle`)。
20
+ 3. Codex 严格解码三处特例(学 OmniRoute + 实测收敛):
21
+ - `GET /v1/models` 对 Codex 调用者(UA/originator `codex_*` 或
22
+ `?client_version=`)追加顶层 `models: []`——必须空,填真目录会覆盖其
23
+ 内置 agent prompt;
24
+ - `response.completed` 的 `usage` 转 Responses 口径
25
+ (`input_tokens/output_tokens` + details);
26
+ - done 事件 `text` 带累积全文。
27
+ 4. `-setto chatgpt [modelId]`(`src/sync-codex.js`)写 Codex 三端共用配置:
28
+ `model`/`model_provider="mslxdff"` + `[model_providers.mslxdff]`
29
+ (`base_url=http://127.0.0.1:<port>/v1`,`wire_api` 显式 `"responses"`),
30
+ 鉴权写绝对路径 `node + bin/mslxdff.js -showtoken`(Codex 子进程不继承
31
+ PATH,裸命令报 `program not found`)。换模型:重跑 setto /
32
+ `codex exec -m <id>` 单次覆盖 / 手改 `model=` 行。Codex 单键覆盖无需剪枝。
33
+ 5. 同批给 `-setto workbuddy|opencode` 加剪枝:`modelPicks` 非空时摘除归一化
34
+ (去 `mslxdff-` 前缀 + `/`→`-`)后不在 `picks ∪ {本次id}` 的旧模型
35
+ (opencode 剪 `provider.mslxdff.models`,workbuddy 只剪本地
36
+ `127.0.0.1` 条目、非本地永不动;picks 空则不动),输出
37
+ `pruned N 个失效模型`。
38
+ 6. 排障:`MSLXDFF_RESPONSES_DEBUG=1` 打 `[responses]` 四段日志到
39
+ `daemon.log`(req/chat/done-json/done-stream);Codex 侧看
40
+ `~/.codex/logs_2.sqlite`(`Request completed ... status=`)与
41
+ `thread_history_1.sqlite`(正文)。
42
+
43
+ ## 后果
44
+
45
+ - Codex 三端可用本地免费池(真机中文对话已验证);`@ai-sdk/openai`
46
+ 是客户端包,替代不了服务端手写翻译层。
47
+ - Qwen 上游走 `/v1/messages`(Anthropic 协议),以后接 Codex 要多一跳
48
+ Responses→Messages,未做。
@@ -0,0 +1,41 @@
1
+ # ADR-0013: Node 16 兼容层(compat.js + undici 降级 5.x)
2
+
3
+ - 状态:**已被 ADR-0024 取代**(Node 16 缺 Response/Headers/ReadableStream/TransformStream,实测必 ReferenceError;运行底线改回 Node 18+)
4
+ - 日期:2026-09-05
5
+
6
+ ## 背景
7
+
8
+ VPS(CentOS 实例)Node v16.14.2 跑不了既有代码:
9
+ `undici@8`(需 22.19+)直接崩;`globalThis.fetch`(18+)、
10
+ `AbortSignal.timeout`(17.3+)、`structuredClone`(17+)、
11
+ 裸全局 `crypto.randomUUID`(webcrypto 版 19+)全部缺失;
12
+ `node:readline/promises`(17+)已在 0.1.95 兼容。
13
+
14
+ ## 决策
15
+
16
+ 1. 依赖 `undici ^8.10.0` → `^5.28.4`(engines ≥10,老 Node 可跑;
17
+ `fetch/Agent/ProxyAgent` 与我们用到的面 API 兼容)。
18
+ 2. `package.json engines` `>=20` → `>=16`;`-chat` 版本门同降 16
19
+ (fetch 由 undici polyfill 后 16 可跑 chat)。
20
+ 3. 新增 `src/compat.js` 单一兼容出口:
21
+ - `compatFetch`:undici 优先,原生 fetch 兜底,都没有给人话报错;
22
+ - `timeoutSignal(ms)`:`AbortSignal.timeout` 缺失时手写 abort 定时器
23
+ (setTimeout unref,reason 含 `timeout` 字样便于错误分类);
24
+ - `clone(v)`:`structuredClone` 缺失时 JSON 深拷贝(state 本就是 JSON
25
+ 语义,等价);
26
+ - `uuid()`:固定走 `node:crypto.randomUUID`(14.17+),不用裸全局;
27
+ - `getUndici()`:统一动态加载 undici 的唯一入口。
28
+ 4. 全仓 codemod:`globalThis.fetch`→`compatFetch`、裸 `fetch(`→`compatFetch(`、
29
+ `AbortSignal.timeout(`→`timeoutSignal(`、`structuredClone(`→`clone(`、
30
+ `crypto.randomUUID()`→`uuid()`,共 34 文件;undici 动态 import 5 处收拢。
31
+ 5. `providers/base.js` 的 `getUndici()`(返回 `{UndiciAgent,UndiciFetch}`)
32
+ 被 4 个下游引用,保留同名导出做适配器(形状不变,加 `fetch/Agent` 直取)。
33
+
34
+ ## 后果
35
+
36
+ - Node 16 全功能可用(daemon + `-chat` + bench + providers);
37
+ `node:test` 仍需 18+(仅开发期,不影响运行时)。
38
+ - 流式消费依赖 `res.body` async iteration(undici 5 + Node 16 web streams),
39
+ 理论上等价,需 VPS 真机验证;Node 20/22 仍是推荐版本。
40
+ - 源码规范:不再直接使用 `globalThis.fetch`/`AbortSignal.timeout`/
41
+ `structuredClone`/裸 `crypto.randomUUID`,一律走 `src/compat.js`。
@@ -0,0 +1,52 @@
1
+ # ADR-0014: DeepSeek Web 逆向供应商(deepseek)
2
+
3
+ 日期:2026-09-05 · 状态:**已废弃(0.1.106 撤销——chat.deepseek.com 风控封号严重,用户账号被封,整套代码已删;本文档保留历史与协议考古记录)** · 关联:ADR-0007(多供应商前缀)、ADR-0010(allowlist)
4
+
5
+ ## 背景
6
+
7
+ chat.deepseek.com 的 web/移动端对话是免费额度(DeepSeekHashV1 PoW 保护 + 无浏览器强风控),社区已有多个把该能力转成 OpenAI 兼容 API 的成熟项目。用户希望以 `deepseek` 特殊上游供应商身份接入 mslxdff,获得 4 个免费模型(`deepseek/chat`、`deepseek/reasoner`、`deepseek/chat-search`、`deepseek/reasoner-search`),并复用现有 keyring 轮换 / 冷却 / bench / `-model pick` 体系。
8
+
9
+ ## 调研选型(结论)
10
+
11
+ | 参考项目 | 结论 |
12
+ |---|---|
13
+ | iidamie/deepseek2api(625★,Python,GPL-3.0) | **采用其 Android 客户端协议**:`x-client-platform: android` + UA `DeepSeek/1.0.13 Android/35`,`device_id` 任意字符串——免浏览器指纹,风控最松 |
14
+ | diegosouzapw/OmniRoute(61k★,MIT) | **采用其纯 JS DeepSeekHashV1 实现**(clean-room NIST FIPS 202 逆向:SHA3-256 去最后一轮 = KECCAK-p[1600,23])+ web 指纹头备注 |
15
+ | TQZHR/deepseek2api(107★,MIT) | 风控分类/重试模式参考(captcha/INVALID_POW_RESPONSE/Retry-After) |
16
+
17
+ 不采用的路线:官方 wasm 求解器(26KB fixture 保留于 test/ 做互证,但运行时不依赖——纯 JS Uint32 解 difficulty=144000 实测 ~625ms,无 wasm/无网络依赖);web 客户端 2.0.0 协议(需伪造完整浏览器指纹 + `x-hif-leim` attestation 风险)。
18
+
19
+ ## 决策
20
+
21
+ 1. **通道**:Android 客户端协议(`/api/v0/*`,base `https://chat.deepseek.com`,`x-client-version: 2.0.0`——真机实测 1.x 全被 `CLIENT_VERSION_TOO_LOW` 拒,8 版本探针定位)。
22
+ 2. **PoW**:纯 JS KECCAK-p[1600,23](`src/providers/deepseek/hash.js`,MIT 出处注释),challenge 是上游预选 nonce∈[0,difficulty) 的 `hash(salt_expireAt_nonce)`,客户端穷举还原。与官方 wasm 黑盒互证(test fixture)。
23
+ 3. **无痕会话**:每次 completion 前 `chat_session/create`、完成后 `delete`,静默失败不抛。新版 session id 在 `biz_data.chat_session.id`(新旧结构兼容)。
24
+ 4. **凭据**:`providerConfigs.deepseek.keys` 即 token 列表;`-provider deepseek login --token <userToken>`(浏览器贴,推荐)或 `login <email|mobile> <password>`(账密换 token)。
25
+ 5. **单路限制**:同账号同时仅 1 路输出 → authPool 串行 requireToken + 冷却 + 多账号轮换(401/403/429/风控自动切号)。
26
+ 6. **tools 不做**:web 上游无原生 tool calling,v1 明确不支持。
27
+ 7. **SSE v2(JSON-Patch 增强,2.0.0 抓包)**:`event:` 行(ready/update_session/title/close)+ fragments 全量快照(unseen 后缀去重)+ `o:"APPEND"` 定向增量 + 裸 `{v}` 沿用上一 patch 上下文 + `elapsed_secs` SET 为 fragment 边界(THINK→RESPONSE 静默切换)+ `response/status` FINISHED 终止。`THINK`/`THINKING` fragment → `reasoning_content`。
28
+ 8. **模型 6 个**(对外统一 `-free` 后缀对齐 ADR-0002 免费过滤):`chat-free`(快速)/`reasoner-free`(快速+思考)/`chat-search-free`/`reasoner-search-free`(联网)/`chat-expert-free`/`reasoner-expert-free`(官网专家模式,completion body 显式 `model_type:"expert"`,其余不传走 session default)。
29
+ 9. **安全默认**:与其他供应商一致走 `allowAnyModels:false`——新接入 `allowlist` 空 → 全 blocked,需显式 `allowAny on` 或 `allowlist set`。
30
+
31
+ ## 模块边界(先拆后写)
32
+
33
+ ```
34
+ src/providers/deepseek.js 桶文件
35
+ src/providers/deepseek/index.js 工厂 + 静态 listModels + OpenAI SSE 翻译出口
36
+ src/providers/deepseek/chat.js 编排:token→session→pow→completion→(流|聚合)→delete + 风控分类
37
+ src/providers/deepseek/auth.js 登录(email/mobile)+ 多账号轮换池
38
+ src/providers/deepseek/pow.js challenge 获取 + 求解编排 + x-ds-pow-response 编码 + Android 指纹头
39
+ src/providers/deepseek/hash.js DeepSeekHashV1(Uint32 优化)+ findPowNonce
40
+ src/providers/deepseek/hash-reference.js BigInt FIPS 202 参考实现(仅测试对拍)
41
+ src/providers/deepseek/bridge.js prompt 标签构造 + mapModelToFlags + SSE 解析器(纯函数)
42
+ src/providers/deepseek/session.js 会话 create/delete(无痕)
43
+ test/fixtures/sha3_wasm_bg.wasm 官方 wasm(26KB),求解互证 fixture
44
+ ```
45
+
46
+ 全部 ≤10KB;测试 5 文件 50 用例。
47
+
48
+ ## 后果
49
+
50
+ - 免费获得 DeepSeek V3/R1 系模型(网页免费额度,无 API 计费)。
51
+ - 逆向 API 本质不稳定:上游改版/风控升级需跟随维护;captcha(数美)无法自动解,报人话引导换号。
52
+ - 单账号并发 1 路由池串行保证;多账号可横向扩并发。
@@ -0,0 +1,50 @@
1
+ # ADR-0015: 上游探针与三类路由
2
+
3
+ **日期**: 2026-09-09
4
+ **状态**: 已实施
5
+
6
+ ## 背景
7
+
8
+ 组员转发上游存在误配:组员选路只看"本机→组员"延迟,不看"组员→上游",出现"离我最近的组员离上游最远"。且不同供应商的转发收益完全不同——用户在 0.1.109 后明确提出三类约束:
9
+
10
+ 1. **workbuddy 不参与组员转发**:本机账号绑定(auths/workbuddy-*.json + uid),组员没有该账号,转过去也用不了,纯浪费一跳。
11
+ 2. **opencode free 图额度不图速度**:本机额度用完后才需要组员转发(限流按 IP 算,组员 IP = 额外免费额度),直连可用时无需抢先走组员。
12
+ 3. **其他 key/token 类图速度**:同一把 key 谁发都一样,应比较「本机→上游 direct」vs「经组员 via」谁小用谁。
13
+
14
+ 同时用户要求**简化命令行参数**:不为新能力新增任何命令/旗标。
15
+
16
+ ## 决策
17
+
18
+ ### 1. 供应商三态分类(`src/providers/classify.js`,纯函数零依赖)
19
+
20
+ | 类 | 供应商 | 路由行为 |
21
+ |---|---|---|
22
+ | `local-only` | workbuddy | 恒不走组员(`shouldUseGroupForModel` 单点收敛,peer/broadband/hedge 全禁) |
23
+ | `quota-pool` | opencode | 永不走 via-route 单路径(`handleViaRoute` 提前 `handled:false`),保留既有 429 后 peer/broadband 兜底 |
24
+ | `latency-compare` | 其余 | 读 `via-routes.json` 择路,无数据时直连 + peer-race 兜底(现状) |
25
+
26
+ ### 2. 后台探针自动保鲜 `via-routes.json`(`src/upstream-probe/`)
27
+
28
+ - 节拍:`MSLXDFF_UPSTREAM_PROBE_MS` 默认 60s(0=关),daemon 装配于 bootstrap(`start.js`)。
29
+ - 每 tick 轮转探 **1 家** latency-compare 供应商(`rotate.js`):本机 `GET <baseUrl><modelsPath>` 测 TTFB(direct)+ 逐 peer 经现成 `POST /v1/relay { targetUrl, method:"GET" }` 代发同一 GET 测端到端(via),peer 间 `MSLXDFF_BENCH_DELAY_MS`(120ms) 礼貌间隔。
30
+ - 只测 GET models,不烧 token;5s 超时、单次不重试、失败只记 `ok:false` 不杀号。
31
+ - EMA α=0.3(`emaMerge`,0ms 为合法样本)合并旧值后 `saveViaRoutes` 落盘,写 **`provider:<id>` 键**(探针按供应商测,表按模型查——`getViaRoute` 精确模型键优先、未命中回退 `provider:<前缀>` 级条目;`bench --via --apply` 的手工精确键不受影响)。
32
+ - `meta.probe=true` 标记自动来源,`via-route-hit` 事件带 `probe:true` 供 events.log 对账。
33
+ - TTL 默认从"不过期"改为 **5 分钟**(`MSLXDFF_VIA_ROUTE_TTL_MS` 可覆盖,0=不过期)——探针自动保鲜下陈旧数据不该继续择路。
34
+
35
+ ### 3. 展示零新增命令(`display.js`)
36
+
37
+ - `-group list`:静态成员为某供应商 best 时行尾缀 ` via-routes: <provider> <±delta>ms`,无数据输出与旧版完全一致。
38
+ - `-status`:failover 段后追加 `via-routes:` 汇总(条数/刷新时间/前 5 条 best 摘要;空数据给引导文案)。
39
+ - 放弃此前提议的 `mslxdff -peer upstream` 新命令。
40
+
41
+ ## 后果
42
+
43
+ - 组员转发从"盲选赛跑"升级为"数据驱动单路径",hedge/peer-race 降级为无探针数据时的兜底。
44
+ - 探针只依赖既有 `/v1/relay` 端点与 GET models,组员侧零改动;broadband(relay://)成员无法代发 HTTP,自动跳过。
45
+ - `MSLXDFF_VIA_ROUTE_TTL_MS` 默认值变更:手工 `bench --via --apply` 的表 5min 后过期(探针开着会被自动刷新,无感;探针关闭且依赖手工表的用户需设 `MSLXDFF_VIA_ROUTE_TTL_MS=0`)。
46
+ - cline 等 auth 复杂供应商若 GET models 需鉴权失败,该供应商探针记 `ok:false`、不参与择路——不影响主链路。
47
+
48
+ ## 相关文件
49
+
50
+ `src/upstream-probe/{probe,rotate,start,display}.js`、`src/providers/classify.js`、`src/bench/via-routes.js`、`src/routes/chat/via-route-handler.js`、`src/state/schemas/use-group.js`、`src/runtime/bootstrap.js`、`src/cli/commands/group.js`、`src/cli/status.js`
@@ -0,0 +1,27 @@
1
+ # ADR-0016: 模型能力元数据(model-capabilities)
2
+
3
+ 日期:2026-09-11 · 状态:已采纳
4
+
5
+ ## 背景
6
+
7
+ 用户需要知道各模型的能力:推理档位(low/medium/high/max 等 reasoning effort)、是否支持图片输入、tool_call、上下文长度、价格。
8
+
9
+ 实测证据(`.scratch/opencode-model-capabilities/evidence/`):
10
+
11
+ - opencode zen `GET /zen/v1/models`:70 个模型全部仅 `id/object/created/owned_by`,**零能力字段**;`/zen/v1/models/<id>` 404;`?verbose=1` 无效 → 上游 API 不可行。
12
+ - opencode 官方源码 `packages/core/src/models-dev.ts:160`:官方自己就从 `https://models.opencode.ai/api.json`(models.dev 官方镜像)拉能力目录;`provider/transform.ts:1654` 消费 `reasoning_options` 三形态(effort/toggle/budget_tokens)。
13
+ - `models.opencode.ai/api.json` 实测 200 / 4.5MB / 213 providers;opencode 条目 102 模型,**覆盖 zen 全部 70 个(missing=0)**;76 个模型带档位、64 个支持图片输入。
14
+
15
+ ## 决策
16
+
17
+ 1. **数据源与 opencode 官方同源**:默认 `MSLXDFF_MODELS_DEV_URL=https://models.opencode.ai/api.json`,不用裸 models.dev(opencode 镜像即权威)。
18
+ 2. **独立端点** `GET /v1/models/capabilities[?provider=&id=]`,不改 `/v1/models` 原 OpenAI 形状(Codex 兼容红线,ADR-0012)。
19
+ 3. **新能力目录** `src/model-capabilities/`:`parse.js` 纯函数(S1 接缝)+ `index.js` 服务(S2 接缝:fetch + 磁盘缓存 + TTL 24h `MSLXDFF_MODELS_DEV_TTL_MS` + staleness 降级——fetch 失败回退旧缓存,完全无数据才 502)。
20
+ 4. **缓存落盘** `~/.config/mslxdff/models-dev.json`(`MSLXDFF_MODELS_DEV_CACHE` 覆盖),4.5MB 目录每日至多拉一次,不进聊天链路零延迟成本。
21
+ 5. provider 路由:裸 id 归 `opencode`,`<prov>/<id>` 前缀按 models.dev 顶层 provider 键直查(workbuddy 等未收录自然返回 null/404,不硬造)。
22
+
23
+ ## 后果
24
+
25
+ - +`GET /v1/models/capabilities`(需 Bearer,同 `/v1/models`);+3 个 env(URL/TTL/CACHE)。
26
+ -FEATURE_TREE 0.3 补叶;ARCHITECTURE §6/§7/§8 联动。
27
+ - 后续可挂:`-provider <id> models` 加能力列、auto 候选按能力过滤(如"读图任务只选 imageInput")。
@@ -0,0 +1,59 @@
1
+ # ADR-0017: 上游引擎迁移到 AI SDK(wire 层唯一实现)
2
+
3
+ 日期:2026-09-12 · 状态:已接受(分阶段实施;P0/P1 已落地;P4 转正——缺省 sdk,legacy 收敛为显式/Node16 回退)
4
+
5
+ ## 背景
6
+
7
+ 自研上游 wire 层反复出现协议级故障:workbuddy 思考流 pull 停摆(0.1.111 前)、
8
+ cline 强制流式聚合、muse-spark responses 翻译、SSE 分帧边缘。每次都是"解析/组装"层
9
+ 的实现缺陷,而非业务策略问题。而 opencode 生产栈用 `@ai-sdk/*` 包(经其 TUI
10
+ 千万级对话验证)处理同一批上游,且实测我们的网关与 AI SDK 客户端栈完全兼容
11
+ (`.scratch/ai-sdk-usage/README.md`);workbuddy 直连用 `@ai-sdk/openai-compatible`
12
+ 实测正常(流式/推理/工具/usage 全通)。调研确认 opencode 的模型→SDK 映射由
13
+ models.dev 目录 `provider.npm` 数据驱动(`.scratch/opencode-sdk-routing/RESEARCH.md`)。
14
+
15
+ 用户决策:**上游 wire 层改用 AI SDK,自研只保留业务层**(账号轮换/冷却/对冲/转发)。
16
+
17
+ ## 决策
18
+
19
+ 1. **AI SDK 为唯一上游 wire 层**,按 `.scratch/ai-sdk-upstream/PLAN.md` 分
20
+ P0→P4 迁移;`ai` 高层包(streamText/generateText)不引入(保重试/超时控制权),
21
+ 只用 provider 层 `doStream/doGenerate`。
22
+ 2. **引擎开关** `MSLXDFF_UPSTREAM_ENGINE`(缺省 `sdk`;显式 `legacy` 或关闭词
23
+ `0/off/false/no/disable` 回退原实现;SDK 不可用自动回退 legacy,Node16 亦然;
24
+ 推荐入口 `chatPath` 的 SDK 请求复用 legacy keep-alive dispatcher)。供应商级开关
25
+ (如 workbuddy `MSLXDFF_WORKBUDDY_SDK`)复用同一 `resolveEngineMode` 语义:显式设置即
26
+ 局部生效,未设置继承全局 `MSLXDFF_UPSTREAM_ENGINE`,故全局 `legacy` 为全链路一键熔断。
27
+ 接缝在组装层(`src/runtime/providers-setup.js`),引擎与 `createUpstreamClient` 同形
28
+ (`chat/preheat/close/headers`),Pipeline/转发/组员零改动。
29
+ 3. **共用库** `src/upstream-engine/sdk/`:`convert.js`(OpenAI 请求→SDK 入参)、
30
+ `sse.js`(parts→OpenAI SSE 帧)、`attempt.js`(请求+错误映射,Authorization
31
+ 随 headers 透传)、`chat.js`(适配器工厂)、`dispatch.js`(供应商级 SDK 分派:
32
+ 缺省 sdk、异形 chatPath/装载失败回退原生、统一标记头);workbuddy 通道与本库同源,
33
+ 通用 OpenAI 兼容族与 cline 经 `dispatch.js` 接入。
34
+ 4. **版本 pin**(对齐 opencode):`@ai-sdk/openai-compatible@2.0.41`、
35
+ `@ai-sdk/openai@3.0.84`、`zod@3.25.76`,进 optionalDependencies
36
+ (SDK engines>=18 与本仓 engines>=16 冲突,Node16 自动降级 legacy)。
37
+ 5. **委派与供应商接入边界**:responses 类模型(`muse-spark*`)由 `sdk/responses.js`
38
+ (`@ai-sdk/openai` 的 `provider.responses`,落 `/responses`,复用同一序列化器与错误映射)
39
+ 承接;通用 OpenAI 兼容族(generic/openrouter)与 cline 的流式 chat 经 `sdk/dispatch.js`
40
+ 缺省走 `@ai-sdk/openai-compatible`(非流式、异形 chatPath 回退原生);非流式请求与
41
+ `!anonFirst` 的 free 429 匿名重试仍委派 legacy(后者保留唯一的自研重试语义)。
42
+ 6. **验收门槛**:P2 一致性夹具(双引擎同 stub 语义帧 diff)是每个供应商
43
+ 转正的准入条件,opencode 已过(`test/upstream-engine.test.js`);P4 后删除 legacy
44
+ 解析代码,ADR-0013(Node16)随 VPS 升级废弃或收窄为"仅 legacy 路径"。
45
+
46
+ ## 后果
47
+
48
+ - +env `MSLXDFF_UPSTREAM_ENGINE`(供应商级 `MSLXDFF_<ID>_SDK` 覆盖,未设置继承);
49
+ +2 可选依赖;过渡期双引擎并存(成本明确)。
50
+ - 四族(opencode/workbuddy/cline/通用)流式 chat 缺省走 SDK;**行为收窄**:原生 transport 的
51
+ 「请求 stream 但上游返回 JSON → 回退非流式」兼容在 SDK 通道不存在,此类异构上游的流式请求
52
+ 会失败;非流式仍走原生,JSON 客户端不受影响。
53
+ - muse-spark(responses)由 `@ai-sdk/openai` 承接(端到端 200,文本完整);非流式请求仍走
54
+ legacy(仅少收益)。缺省即 sdk 且复用 legacy keep-alive 连接池,实测延迟与 legacy 同量级
55
+ (big-pickle 2430→2566ms,mimo-v2.5-free 3247→3612ms,均 200)。
56
+ - 免费层身份头(x-opencode-client/session/UA 组)收敛到
57
+ `createOpencodeHeaderBuilder` 单一来源,legacy 与 SDK 引擎共用。
58
+ - P4 完成后净删自研解析代码(预计 >1500 行),协议扩展(responses/anthropic)
59
+ 由 SDK 承接;风险(重编码保真、SDK quirk、版本漂移)对策见 PLAN.md。
@@ -0,0 +1,52 @@
1
+ # ADR-0018: zen 免费层客户端身份规格(UA + Identifier 形状)
2
+
3
+ 日期:2026-09-17 · 状态:已接受(0.1.130 落地)
4
+
5
+ ## 背景
6
+
7
+ 2026-09-17 08:31 起,opencode.ai zen 免费池对我们的所有请求返回
8
+ `403 FreeTierError: Error from provider (Console): OpenCode's free tier can only be used from within OpenCode`。
9
+ 当时最近的代码改动是 v0.1.129(chat-pipeline 内部 TDZ 修复,未碰上游头),且裸 curl
10
+ (完全绕过本项目)同样被拒,说明是上游新增门禁。
11
+
12
+ 排查(对照官方 CLI 与源码 `D:\www\wwwroot\cmdhelp\opencode-src`,anomalyco/opencode 分支):
13
+
14
+ | 变量 | 结果 |
15
+ |---|---|
16
+ | 裸 curl(任意 Authorization / 任意 UA 版本缺失) | 403 |
17
+ | 本机官方 CLI `opencode run -m opencode/big-pickle` | 200(同机同 IP) |
18
+ | curl + 官方会话真实 `ses_`/`msg_` id 重放 | 200 |
19
+ | curl + 自造 ULID / 32 位 hex / 改长度 | 403 |
20
+ | curl + opencode 同构 id(12 hex + 14 base62)+ `User-Agent: opencode/<semver>` | 200(含匿名 hermes 形态与 SSE 流式) |
21
+
22
+ 结论:门禁是**数据层校验**(不是 TLS 指纹/IP/额度),要求请求身份头符合官方客户端
23
+ 规格。源码依据:
24
+
25
+ - `packages/schema/src/identifier.ts`:id = `<prefix>_` + 12 位 hex
26
+ (`BigInt(timestamp)*0x1000n + 同毫秒计数`,高位截断 48bit)+ 14 位 base62 随机,共 26 字符。
27
+ - `packages/opencode/src/session/llm/request.ts`:providerID 以 `opencode` 开头时恒发
28
+ `x-opencode-project` / `x-opencode-session` / `x-opencode-request`(=user 消息 id) /
29
+ `x-opencode-client`(app|cli|desktop) / `User-Agent: opencode/<版本>`。
30
+ - zen handler `packages/console/app/src/routes/zen/util/handler.ts`:把上述头原样转发给
31
+ Console 推理服务(`isNewInference` 支路),由下游服务做该校验。
32
+
33
+ ## 决策
34
+
35
+ 1. **身份头单一来源**留在 `src/upstream.js`:`opencodeIdTail()`(26 位规格生成器,
36
+ 同毫秒计数保唯一)、`digestIdTail()`(sha1 摘要确定性派生,供 `sessionFromMessages`
37
+ 会话亲和,形态仍合规)、`opencodeUa()`(`MSLXDFF_OPENCODE_UA` 覆盖,默认
38
+ `opencode/1.17.20`)、`opencodeClientIdentity()`(UA + session + request 三件套)。
39
+ 2. `buildHeaders()` 全部形态(opencode 匿名 hermes / `Bearer public`)统一使用上述生成器;
40
+ 原先 uuid-hex(36 位)与 `User-Agent: opencode`(缺版本)作废。
41
+ 3. **旁路共用**:`-chat` 的 curl 工具(opencode.ai 目标)与 `bench --via` 直连 opencode
42
+ 分支都改用 `opencodeClientIdentity()`,避免各自伪造头再次踩 403。
43
+ 4. 上游若再次调整规格(如换 id 版本),只需改这一个文件;`MSLXDFF_OPENCODE_UA`
44
+ 提供不停机跟随版本号的逃生口。
45
+
46
+ ## 影响
47
+
48
+ - 免费池恢复可用(scratch daemon 实测 `big-pickle` / `mimo-v2.5-free` 200 + SSE)。
49
+ - 免费目录中 `nemotron-3-ultra-free`、`deepseek-v4-flash-free` 已被上游下线
50
+ (官方客户端亦报 `Model is unavailable`),与本决策无关,`/models` 目录为准。
51
+ - 若上游未来升级为签名/认证机制(如绑定真实会话注册表,本次排查曾见「改一位即 403」
52
+ 的模糊态),本方案可能失效;届时需重估(候选:本地 `opencode serve` 作真实客户端桥)。
@@ -0,0 +1,59 @@
1
+ # ADR-0019:key 随转发自动借出(删除 share 开关)
2
+
3
+ - 状态:**已采纳**
4
+ - 日期:2026-09-17
5
+ - 关联:ADR-0008(被本 ADR 取代其开关部分)、`src/providers/share-keys.js`、`src/routes/peers.js`、`src/routes/chat/via-route-handler.js`
6
+
7
+ ## 背景
8
+
9
+ ADR-0008 引入 `shareKeysToPeers`(默认 false)控制"转发时是否把本机供应商 key 附带组员"。
10
+ 实施后发现两件事:
11
+
12
+ 1. **开关是绊脚石而非安全边界**。A 借 B 的网络访问供应商 GGG 时,key 必须到达 B 才能让 B 代为调用;
13
+ B 拿到请求就必然能看到 header 里的明文 key。"是否附带"不是 A 能靠开关阻止的泄露——
14
+ A 选择借道这个动作本身就是同意。开关唯一效果是让"借道成功但 B 用不了 key"(403 → 回落),
15
+ 功能看起来坏了,用户需要先知道有 `share on` 这回事才能用。
16
+ 2. **组网信任模型已经是全信任**。组 = 组名+key 加入;join 响应就把所有成员的 master token 交给新成员
17
+ (`src/routes/groups.js:58,86-90`),拿到 token 即可用该节点的代理(含其 key)消费额度。
18
+ 在这个模型下再要一个"是否借 key"的开关没有边际保护。
19
+
20
+ ## 决策
21
+
22
+ **默认借出,删除开关**:转发(peer 接力 / via-route / broadband 待接)时,命中本机有 key 的供应商即自动附带
23
+ `x-mslxdff-share-keys`;组员仅本次请求借用、用完即弃(不变)。
24
+
25
+ 保留的硬边界(不是同意,而是正确性):
26
+
27
+ - `opencode`:无 key,恒排除。
28
+ - `workbuddy`:local-only(ADR-0015),本就不走组员;其 `chatWithKeys` 的接收侧开关一并删除。
29
+ - `cline` / `clinebot`:key 实为 refresh-token,借出后对端刷新会轮换 token,与本机互相踢下线 —— 硬排除。
30
+
31
+ 同步删除:`providerShareKeys` state 字段、`loadProviderShareKeys`/`saveProviderShareKeys`/`loadProviderShareOptOut`、
32
+ `providerShareEnv`、`MSLXDFF_SHARE_PROVIDERS` 白名单、`-provider <id> share on|off` CLI、`-status` 的 share 列语义(改为固定文案)。
33
+ 无开关、无白名单 —— 唯一的分享边界是"本机有 key"减去上述硬排除。
34
+
35
+ ## 备选
36
+
37
+ - **保留为 opt-out(默认借出 + `share off` 不外借)**:最强论据是给"这把 key 绝不外借"留一个按凭据粒度的表达
38
+ (尤其自动 failover 路径);否决——本软件定位为个人私用、组员互信,逃生门带来的状态/CLI/文档成本
39
+ 大于它挡住的场景。
40
+ - **保留白名单 env(`MSLXDFF_SHARE_PROVIDERS`)**:同上,是开关的粗粒度版本;否决,一并删除。
41
+ - **保持默认 off**:否决——把"借道"这个功能的必要环节藏在开关后面,是 v0.1.131~0.1.132 期间实际踩到的坑。
42
+ - **把借网络做成 CONNECT 隧道(B 看不到 key)**:技术上这是唯一能让"带不带 key"重新成为真正选择的设计;
43
+ 否决(本次不做)——L7 转发改隧道是独立大改,且隧道下 key 使用策略(谁的额度)另需设计。
44
+
45
+ ## 后果
46
+
47
+ - 借道即用你的 key:A 到 GGG 不通时,转给 B 后 B 用 A 的凭据调 GGG,一次成功,无额外配置。
48
+ - 泄露面:key 会随转发到达组员节点与中间链路(peer 多为 `http://` 明文)。接受范围 = 组内互信;
49
+ 要防中间人需 https/隧道,与开关无关。
50
+ - 迁移:旧 state 里的 `providerShareKeys` 字段变为惰性数据(不读不写);`MSLXDFF_<ID>_SHARE_KEYS` 失效;
51
+ 显式 `share off` 的用户行为回到默认借出。
52
+
53
+ ## 验证
54
+
55
+ - `test/providers-share-keys.test.js`:默认借出 / opencode 恒排除 / cline+clinebot 硬排除(含 env 白名单)/ header 组装解析。
56
+ - `test/via-route-share.test.js`:真 stub peer 抓出站头——默认携带 `openrouter=sk-test-1,sk-test-2`;opencode 裸 id 不携带。
57
+ - 全量 `node --test --test-concurrency=1 test/*.test.js` 与 `node scripts/docs-check.js`。
58
+
59
+ > 备注(2026-09-20 追加,原文不改):0.1.x 起 Cline 供应商 id 统一为 `cline`(历史 `clinebot` / `cline-bot` 仅作一次性入站归一,见 ADR-0026)。
@@ -0,0 +1,52 @@
1
+ # ADR-0020: zen 免费层 agent 形状门禁(流式 + 核心五工具)
2
+
3
+ 日期:2026-09-18 · 状态:已接受(本地 0.1.133+ 修复,待发版)
4
+
5
+ ## 背景
6
+
7
+ 2026-09-18 上午起,zen 免费池对**所有**请求(含官方身份头)返回
8
+ `403 FreeTierError: Error from provider (Console): OpenCode's free tier can only be used from within OpenCode`。
9
+ 本机表现:`-chat` 直连 mimo/big-pickle 全 403,daemon 各路径同挂,组员节点也 403(全线,不是本机问题)。
10
+ 先例:ADR-0018 是第一次身份门禁(2026-09-17),本次是第二次收紧。
11
+
12
+ 实测 bisect(`Bearer public` + `opencode/1.18.31`):
13
+
14
+ | 组合 | 结果 |
15
+ |---|---|
16
+ | `stream:true` 无 tools / `stream:false` 带五工具 | 403 |
17
+ | `stream:true` + 4 个核心工具(bash/edit/glob/grep) | 403 |
18
+ | `stream:true` + 本客户端工具名(run_command/read_file) | 403 |
19
+ | `stream:true` + `tools` 含 **bash+edit+glob+grep+read** 五名(描述/schema 随便是 `{type:object}`) | **200** |
20
+ | UA `opencode/1.17.20` + 五工具 | **426 `UpgradeRequired: OpenCode 1.18.0 or …`** |
21
+ | UA `opencode/1.18.31` + 五工具(mimo / big-pickle / responses 类 muse) | 200 |
22
+
23
+ 社区同源结论:`jasonxu114514/opencode2api` PR #31(2026-09-18T01:11Z)独立 bisect 出同一结果
24
+ (tools 阶梯 0-4→403、5+/11→200;五名子集全过;Responses 形状同理)。
25
+
26
+ ## 决策
27
+
28
+ 1. **新增 `src/free-lane.js`** 作为"免费层形状"单一来源(≤150 行):
29
+ - `CORE_AGENT_TOOL_NAMES = [bash, edit, glob, grep, read]`;
30
+ - `ensureFreeLaneShape(body)`:缺的核心工具补最小 `{type:object}` 定义(幂等、不动调用方已有工具)、
31
+ 强制 `stream:true`、返回 `{injected, forcedStream, disabled}`;chat 与 responses 两种形状;
32
+ - `aggregateChatSse(res)`:把被迫流式后的 SSE 聚合回非流式 chat completion JSON
33
+ (content/reasoning/tool_calls 增量拼接、usage/finish_reason、纯错误帧→502 JSON);
34
+ - 逃生阀 `MSLXDFF_FREE_LANE=0`(完全不动,上游撤门禁时回退)。
35
+ 2. **双点注入**(缺一不可):
36
+ - `src/upstream.js`(legacy):`chat()` 内对 `authToken==="public" && isFreeModel(model)` 的
37
+ `reqBody` 注入;非流式调用者由 `agentJson()` 聚合回 JSON;
38
+ - `src/upstream-engine/index.js`(缺省 SDK 引擎):**流式请求不走 legacy**,所以在派发前注入
39
+ (`wantsStream` 先算好、非流式仍委派 legacy 由其聚合,注入幂等)。
40
+ 3. **默认 UA 提升** `opencode/1.17.20 → opencode/1.18.31`(npm latest),`MSLXDFF_OPENCODE_UA` 仍可覆盖;
41
+ 版本 <1.18.0 一律 426,所有出口(daemon/-chat/bench 直连/curl 工具)共用 `opencodeUa()`。
42
+ 4. **可观测**:`MSLXDFF_FREE_LANE_DEBUG=1` 时 daemon.log 打 `[free-lane] send/resp`
43
+ (model/url/stream/tools 数/UA/状态),下次门禁再变时 30 秒定位。
44
+
45
+ ## 影响
46
+
47
+ - 免费池恢复:`-chat` 直连 mimo/big-pickle 200(3.9s)、daemon 流式/非流式与 muse responses 均 200;
48
+ 全量测试 860 例 858 过 0 挂(2 skip)。
49
+ - 代价:免费模型的请求都会带上五个占位工具定义;`-chat` 已有"禁工具模式下收到 tool_calls 即拦截"
50
+ 守卫(engine.js),裸客户端若被模型真调用这五个名字需自行忽略——实测未观察到幻觉调用。
51
+ - 若上游下次改成"必须真工具结果往返"或签名机制(社区已有 `opencode serve` 桥接方案讨论),
52
+ 本方案失效;届时优先评估桥接官方二进制(本机已装场景)而非继续逆向形状。
@@ -0,0 +1,47 @@
1
+ # ADR-0021 — 模型用量报表数据层(逐请求 JSONL,与 state EMA 分离)
2
+
3
+ 日期: 2026-09-19
4
+ 状态: 已采纳
5
+
6
+ ## 背景
7
+
8
+ `-status`/`-model stats`/`-chat /stats` 的每模型统计(`state.json → modelStats`)是**终生 EMA**
9
+ (α=0.3):`avgTtfbMs/avgTotalMs/avgTps/avgCompTok` 只有平滑均值,没有时间窗口;completion
10
+ tokens 只有单次均值(`avgCompTok`),prompt/total tokens 全仓不落 state。带完整 usage 的唯一
11
+ 落点是 `events.log`(`relay-done`/`result` 行),但它受 `logs.js` "超 1MB 裁到末 100 行"支配,
12
+ 实测只覆盖约 30 分钟,不可作为 24h 报表数据源。需求:`mslxdff -stats` 回答"近 24h 哪个模型
13
+ 吃了多少 token、跑多快"。
14
+
15
+ ## 决策
16
+
17
+ - 新增能力目录 `src/usage/`:
18
+ - `record.js` — 逐请求 usage 落 `<logDir>/usage/YYYY-MM-DD.jsonl`(append-only 按日分片)。
19
+ 行形状由 `recordChatUsage()` 拥有(`model/via/prompt_tokens/completion_tokens/total_tokens/
20
+ reasoning_tokens/ttfbMs/totalMs/tps/ts`),缺失字段落 `0`/`null`,不落 `undefined`。写入
21
+ 异步 append 不阻塞流式响应;retention 惰性且每天最多触发一次(`MSLXDFF_USAGE_KEEP_DAYS`
22
+ 默认 2,删字典序早于 cutoff 的日文件);`MSLXDFF_USAGE_LOG=0` 整体关闭。
23
+ - `report.js` — `aggregateUsage()` 纯函数(rows + 窗口 → 每模型聚合 + totals)与薄 IO
24
+ (`readUsageRows`/`usageReport`)分离;速度 = **窗口加权** `Σ输出 ÷ Σ生成耗时`
25
+ (生成耗时 = 总耗时 − 首字,首字缺失按 0),不用算术平均(短回答拉飞均值);`total_tokens`
26
+ 缺失用 `prompt+completion` 兜底;脏行/窗口外行丢弃。
27
+ - CLI `src/cli/commands/stats.js`:`-stats [--hours N](默认24,上限168) [--json] [--model <id>]`。
28
+ - 写入点唯一:`relay-pipeline.js` 既有 `status===200` 记账块内一次 `recordChatUsage`,只记
29
+ canonical 全称(`normalizeFullId`),不双记裸名(token 会双计);usage 解析沿用
30
+ `metrics.js` 单一口径(ADR 记忆:metrics-seam 2026-09-17,禁止内联新解析)。
31
+ - **双事实源是有意取舍**:`modelStats` 终生 EMA 服务"下一次选谁"(auto 排序、-status 体检表),
32
+ usage JSONL 服务"历史发生了啥"(窗口报表)。不合并——合并会让排序语义依赖日志 IO。
33
+ - 明确不做:HTTP `/v1/stats` 端点(无消费方)、按供应商/key 成本分摊、HTML 看板。
34
+
35
+ ## 备选(否决理由见 `.scratch/stats-report/SPEC.md` §3)
36
+
37
+ state.json 按小时分桶(`writeStateImmediate` 热路径全量重写 + 答不了 p95);modelStats 加累计
38
+ 字段(答不了"最近 24h");复用 events.log(1MB 环形截断实测只留 30 分钟,功能上不可用)。
39
+
40
+ ## 后果
41
+
42
+ - 收益:首个可切时间窗的用量视图;加权速度口径;未来 p95/分摊只需扩 `report.js`,写入侧不动。
43
+ - 代价:多一个事实源(双写);磁盘 ~150B/请求(1000 请求/天 ≈ 150KB);强杀进程可能丢末几行
44
+ (报表容忍,不引入同步写);`-chat` 直连 mimo/big-pickle 不经 8989 不计入(报表内注明);
45
+ 失败请求无 usage 不计入("请求数" = 成功请求数)。
46
+ - 顺带收口既有缺陷:`-status` recent calls 曾渲染 calls.log 从不写入的 `ttfbMs/tps/usage`
47
+ (恒空),`src/cli/status.js` 与 `cli_help` 双镜像已改为只打印真实字段并指向 `-stats`。