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,147 +1,132 @@
1
- // 终局上游判决分类器(纯函数,零 IO):上游把「这个模型你别想用」写死在错误里时的判据。
2
- //
3
- // 立此判据的现网取证(~/.config/mslxdff/deepseek-v4.1-flash-free.log,req=mv9m1d2v-n2y5):
4
- // 22:04:41 上游 6s 就回 403 FreeTierError("OpenCode's free tier can only be used from within OpenCode"),
5
- // 网关仍把这条拒绝继续丢给 4 个组员接力 → 组员撞的是同一堵墙(组员 149.13.91.10 回**同一个**
6
- // FreeTierError,其余是 30s abort / fetch failed),客户端 22:05:46 才拿到 502 "peers failed",
7
- // total=72752ms——真相(403 + 原文)被接力产物彻底埋掉,人只看到「一直转圈圈」。
8
- //
9
- // 边界(重要):只判「换节点也不会变」的拒绝。一切可能随号/随额度/随时间恢复的都**不算**终局,
10
- // 接力与 failover 照常:401/402/429/5xx、fetch 异常,以及坏号型 403(zcode `security_reject`
11
- // 「安全校验拒绝」、qoder 限流裹 403、`isQueued`/`serviceAvailable` 这类换号就能过的)。
12
- // 本仓自己的 allowlist 403 在 serial-trial 更前面已短路,不经此处。
13
- //
14
- // ⚠ 喂进来的文本必须是**未截成 400 字符**的原文(调用方给 ≤4KB):TRANSIENT 护栏是唯一的反向刹车,
15
- // 它命中的字样若落在截断之外,就会把「换个组员可能就能用」的失败误判成终局、静默停掉组网兜底
16
- // (多 provider 明细体如 OpenRouter 的 `metadata.raw[]` 正是这种排列)。
17
-
18
- // 上游身份/资格闸门:文案是具名判决(FreeTierError),4xx 任意码都算终局——不锁死 403,
19
- // 防上游把同一判决挪到 400/404 就失明。
20
- const FREE_TIER_PATTERNS = [
21
- /FreeTierError/i,
22
- // ⚠ `FreeUsageLimitError` 曾列在此(BUG-8 于 2026-10-16 加入)**现已移除**(2026-10-17 纠正):
23
- // 它是**限额/限流**判决,不是身份闸门。三条独立实证都证明「换出口/换组员就恢复」⇒ 属可接力:
24
- // ① 本仓自测记录(warp-pool/verdict.js:9-10):同一身份连发 3 次 429 FreeUsageLimitError,
25
- // **身份一换、同一个请求立刻 200**(限额按出口 IP 计);
26
- // ② warp-pool 的决策表(verdict.js:62)对 429 FreeUsage 明写「强制换出口、跳过冷却」;
27
- // ③ v6 池(v6pool-claim-rotate.test.js:140)同样据它换源地址。
28
- // 更根本:ADR-0058 §2 本就把「401/402/**429**/5xx」明列为**可恢复、接力照常**;本文件头 :9-11 也是
29
- // 同一口径。BUG-8 把 429 的具名文案判成终局,偏离了本模块自己写下的边界 —— 现网代价是
30
- // 组员额度还在、仍被 `peersSkipped:true` 一刀跳过(2026-10-18 events.log 满屏实证)。
31
- // 改由 TRANSIENT_MARKERS 显式排除(不依赖 message 里偶然出现的 "rate limit")。
32
- // 具名资格判决:上游明说「你没订阅这个模型套餐」。它比 free 层判决更硬——免费层判决还可能随
33
- // 上游口径变化,套餐缺失则是**换号换节点都不会变**的判决,属本判据射程内却曾被漏判。
34
- // 实测(errors.log,2026-10-17 两次逐字相同,cline/cline-pass/deepseek-v4.1-flash):
35
- // `{"error":{"code":"ENTITLEMENT_ERROR","message":"Error 403: the user is not subscribed to required model plan"}}`
36
- // ⇒ 旧版 classifyTerminalUpstream 返回 {terminal:false},照跑组员接力,且 isPoolSwitchWorthy 也返回 false
37
- // (终局为假 ⇒ 同池互备路径一并关闭),指定 free 模型的调用方只能吃 403 收场。
38
- // 文案兜底另收一条:防上游改 code 名后只保留人类可读的 message。
39
- /ENTITLEMENT_ERROR/i,
40
- /not subscribed to required model plan/i,
41
- /free tier can only be used/i,
42
- /免费(?:层|额度).{0,12}(?:只能|仅限|不可)/,
43
- ];
44
-
45
- // 「模型不存在/不可用」:每条 pattern 自身就含 model 语义,故不再另设 `\bmodel\b` 独立单词门
46
- // (那道门既挡死自己的 `model_not_found`/`ProviderModelNotFoundError`,又被 `"param":"model"`
47
- // 这类 JSON 键名随手推开,纯属自相矛盾的假安全)。中英文都收:中文上游目录只有中文文案。
48
- const MODEL_GONE_PATTERNS = [
49
- /model not found/i,
50
- /providermodelnotfounderror/i,
51
- /model is unavailable/i,
52
- /model_not_found/i,
53
- /no such model/i,
54
- /unknown model/i,
55
- /requested model\b[\s\S]{0,60}\b(not available|not found|does not exist|is not supported)/i,
56
- /模型(?:不存在|未找到|不可用|已下线|已下架|无法访问)/,
57
- /(?:没有|无)权限访问(?:该|此)?模型/,
58
- ];
59
-
60
- // 反向护栏:形状像终局但语义是「这个号/这一发不行」,换号换节点可能就好——绝不终局。
61
- // 只认明确凭据/限流语义词,不收裸 `token`:`max_tokens` 之类的参数报错会被它误判成可接力。
62
- const TRANSIENT_MARKERS = [
63
- // 限额/限流具名判决 ⇒ **可恢复**(换出口换组员就恢复,见 FREE_TIER_PATTERNS 上方三条实证)。
64
- // 显式列出而非指望 message 里的 "rate limit" 字样:上游改文案时这层不能跟着失明
65
- // (`FreeUsageLimitError` 的类名比它的 message 稳定得多)。
66
- // GoUsageLimitError 与它同族(warp-pool/verdict.js:25 把两者并列),同样按出口计、同样可换。
67
- /FreeUsageLimitError|GoUsageLimitError/i,
68
- /security_reject/i,
69
- /安全校验拒绝/,
70
- /isQueued/i,
71
- /serviceAvailable["\s:]*false/i,
72
- /rate.?limit|too many requests|over.?quota|retry after/i,
73
- /invalid (api )?key|missing api key|unauthorized|expired (token|key)|no credentials|鉴权失败|无有效(?:账号|凭据|key)/i,
74
- ];
75
-
76
- /**
77
- * 有界读一份失败响应的正文供判决(串行 trial 与 auto 竞速两条出口共用同一口径):
78
- * 1s 上限防流式错误体拖慢、≤4KB 防大 body 把正则拖成长扫描,读不到就返回空串——
79
- * 判据见空串一律**不判终局**(保守回退到「照跑接力」的旧行为,绝不因读不到就停兜底)。
80
- * 取的是 `clone()`:原响应必须留给收场出口 relay 原字节。
81
- */
82
- export async function readVerdictText(res, { timeoutMs = 1000, maxChars = 4000 } = {}) {
83
- try {
84
- return String(await Promise.race([
85
- res.clone().text(),
86
- new Promise((r) => { const t = setTimeout(() => r(""), timeoutMs); t.unref?.(); }),
87
- ])).slice(0, maxChars);
88
- } catch {
89
- return "";
90
- }
91
- }
92
-
93
- function firstHit(list, text) {
94
- for (const re of list) if (re.test(text)) return re.source;
95
- return null;
96
- }
97
-
98
- /**
99
- * 上游这条失败响应是不是「换节点也不会变」的终局判决。
100
- * @param {number} status 上游 HTTP 状态
101
- * @param {string} bodyText 上游错误体原文(≤4KB,未被日志摘要截成 400 字的那份;见文件头 ⚠)
102
- * @returns {{terminal: boolean, kind?: string, marker?: string, notice?: string}}
103
- */
104
- export function classifyTerminalUpstream(status, bodyText = "") {
105
- const code = Number(status);
106
- const text = typeof bodyText === "string" ? bodyText : "";
107
- if (!Number.isInteger(code) || code < 400 || code > 499 || !text.trim()) return { terminal: false };
108
- // 顺序即优先级:① 先认「上游把免费层说死」——它的文案里也可能带 quota/limit 字样,反过来排会
109
- // 把终局判成可接力;② 再排坏号/凭据/限流类可恢复拒绝;③ 最后才判模型 id 不存在。
110
- const freeHit = firstHit(FREE_TIER_PATTERNS, text);
111
- if (freeHit) {
112
- return { terminal: true, kind: "free-tier-blocked", marker: freeHit, notice: "上游免费层资格判决(组员同源),已跳过接力" };
113
- }
114
- if (firstHit(TRANSIENT_MARKERS, text)) return { terminal: false };
115
- const goneHit = firstHit(MODEL_GONE_PATTERNS, text);
116
- if (goneHit) {
117
- return { terminal: true, kind: "model-gone", marker: goneHit, notice: `上游不认这个模型 id(status=${code}),已跳过接力` };
118
- }
119
- return { terminal: false };
120
- }
121
-
122
- /**
123
- * 逃生阀:`MSLXDFF_TERMINAL_SHORTCIRCUIT=0`(含 `false`/`off`/`no`)关掉终局短路,
124
- * 恢复「任何失败都照跑组员接力」的旧行为。缺省=开(终局判决不接力)。
125
- * 上游哪天改了文案导致误判/漏判时不停机回退(同 `MSLXDFF_FREE_LANE=0` 的先例:开关方向与名字同向,
126
- * 别再写一个「设 1 表示关掉」的反向旋钮)。
127
- */
128
- export function terminalShortcircuitOff(env = process.env) {
129
- const raw = env.MSLXDFF_TERMINAL_SHORTCIRCUIT;
130
- if (raw === undefined || raw === null || raw === "") return false;
131
- const s = String(raw).trim().toLowerCase();
132
- return s === "0" || s === "false" || s === "off" || s === "no";
133
- }
134
-
135
- /**
136
- * 「这一发失败值得换个同池模型再试」vs「换了也一样」的分界(BUG-7 的收窄口径)。
137
- * 只有额度/判决类失败才允许指定 free 模型横向切同池兄弟:
138
- * - 429:免费池的限流几乎都是额度维度,换个模型常能立刻出字;
139
- * - terminal(具名额度判决 / 模型 id 不存在):换兄弟模型可能命中另一个可用 id。
140
- * 参数错(400)、上游 5xx、fetch 异常一律**不**横向切——那会把「客户端自己发错了」伪装成换模型成功,
141
- * 也白烧别的模型额度。
142
- */
143
- export function isPoolSwitchWorthy(lastErr) {
144
- if (!lastErr) return false;
145
- if (lastErr.terminal) return true;
146
- return Number(lastErr.status) === 429;
147
- }
1
+ // 终局上游判决分类器(纯函数,零 IO):上游把「这个模型你别想用」写死在错误里时的判据。
2
+ //
3
+ // 立此判据的现网取证(~/.config/mslxdff/deepseek-v4.1-flash-free.log,req=mv9m1d2v-n2y5):
4
+ // 22:04:41 上游 6s 就回 403 FreeTierError("OpenCode's free tier can only be used from within OpenCode"),
5
+ // 网关仍把这条拒绝继续丢给 4 个组员接力 → 组员撞的是同一堵墙(组员 149.13.91.10 回**同一个**
6
+ // FreeTierError,其余是 30s abort / fetch failed),客户端 22:05:46 才拿到 502 "peers failed",
7
+ // total=72752ms——真相(403 + 原文)被接力产物彻底埋掉,人只看到「一直转圈圈」。
8
+ //
9
+ // 边界(重要):只判「换节点也不会变」的拒绝。一切可能随号/随额度/随时间恢复的都**不算**终局,
10
+ // 接力与 failover 照常:401/402/429/5xx、fetch 异常,以及坏号型 403(zcode `security_reject`
11
+ // 「安全校验拒绝」、qoder 限流裹 403、`isQueued`/`serviceAvailable` 这类换号就能过的)。
12
+ // 本仓自己的 allowlist 403 在 serial-trial 更前面已短路,不经此处。
13
+ //
14
+ // ⚠ 喂进来的文本必须是**未截成 400 字符**的原文(调用方给 ≤4KB):TRANSIENT 护栏是唯一的反向刹车,
15
+ // 它命中的字样若落在截断之外,就会把「换个组员可能就能用」的失败误判成终局、静默停掉组网兜底
16
+ // (多 provider 明细体如 OpenRouter 的 `metadata.raw[]` 正是这种排列)。
17
+
18
+ // 上游身份/资格闸门:文案是具名判决(FreeTierError),4xx 任意码都算终局——不锁死 403,
19
+ // 防上游把同一判决挪到 400/404 就失明。
20
+ const FREE_TIER_PATTERNS = [
21
+ /FreeTierError/i,
22
+ // ⚠ `FreeUsageLimitError` 曾列在此(BUG-8 于 2026-10-16 加入)**现已移除**(2026-10-17 纠正):
23
+ // 它是**限额/限流**判决,不是身份闸门。三条独立实证都证明「换出口/换组员就恢复」⇒ 属可接力:
24
+ // ① 本仓自测记录(warp-pool/verdict.js:9-10):同一身份连发 3 次 429 FreeUsageLimitError,
25
+ // **身份一换、同一个请求立刻 200**(限额按出口 IP 计);
26
+ // ② warp-pool 的决策表(verdict.js:62)对 429 FreeUsage 明写「强制换出口、跳过冷却」;
27
+ // ③ v6 池(v6pool-claim-rotate.test.js:140)同样据它换源地址。
28
+ // 更根本:ADR-0058 §2 本就把「401/402/**429**/5xx」明列为**可恢复、接力照常**;本文件头 :9-11 也是
29
+ // 同一口径。BUG-8 把 429 的具名文案判成终局,偏离了本模块自己写下的边界 —— 现网代价是
30
+ // 组员额度还在、仍被 `peersSkipped:true` 一刀跳过(2026-10-18 events.log 满屏实证)。
31
+ // 改由 TRANSIENT_MARKERS 显式排除(不依赖 message 里偶然出现的 "rate limit")。
32
+ // 具名资格判决:上游明说「你没订阅这个模型套餐」。它比 free 层判决更硬——免费层判决还可能随
33
+ // 上游口径变化,套餐缺失则是**换号换节点都不会变**的判决,属本判据射程内却曾被漏判。
34
+ // 实测(errors.log,2026-10-17 两次逐字相同,cline/cline-pass/deepseek-v4.1-flash):
35
+ // `{"error":{"code":"ENTITLEMENT_ERROR","message":"Error 403: the user is not subscribed to required model plan"}}`
36
+ // ⇒ 旧版 classifyTerminalUpstream 返回 {terminal:false},被误当普通 403 照跑组员接力收场。
37
+ // 文案兜底另收一条:防上游改 code 名后只保留人类可读的 message。
38
+ /ENTITLEMENT_ERROR/i,
39
+ /not subscribed to required model plan/i,
40
+ /free tier can only be used/i,
41
+ /免费(?:层|额度).{0,12}(?:只能|仅限|不可)/,
42
+ ];
43
+
44
+ // 「模型不存在/不可用」:每条 pattern 自身就含 model 语义,故不再另设 `\bmodel\b` 独立单词门
45
+ // (那道门既挡死自己的 `model_not_found`/`ProviderModelNotFoundError`,又被 `"param":"model"`
46
+ // 这类 JSON 键名随手推开,纯属自相矛盾的假安全)。中英文都收:中文上游目录只有中文文案。
47
+ const MODEL_GONE_PATTERNS = [
48
+ /model not found/i,
49
+ /providermodelnotfounderror/i,
50
+ /model is unavailable/i,
51
+ /model_not_found/i,
52
+ /no such model/i,
53
+ /unknown model/i,
54
+ /requested model\b[\s\S]{0,60}\b(not available|not found|does not exist|is not supported)/i,
55
+ /模型(?:不存在|未找到|不可用|已下线|已下架|无法访问)/,
56
+ /(?:没有|无)权限访问(?:该|此)?模型/,
57
+ ];
58
+
59
+ // 反向护栏:形状像终局但语义是「这个号/这一发不行」,换号换节点可能就好——绝不终局。
60
+ // 只认明确凭据/限流语义词,不收裸 `token`:`max_tokens` 之类的参数报错会被它误判成可接力。
61
+ const TRANSIENT_MARKERS = [
62
+ // 限额/限流具名判决 ⇒ **可恢复**(换出口换组员就恢复,见 FREE_TIER_PATTERNS 上方三条实证)。
63
+ // 显式列出而非指望 message 里的 "rate limit" 字样:上游改文案时这层不能跟着失明
64
+ // (`FreeUsageLimitError` 的类名比它的 message 稳定得多)。
65
+ // GoUsageLimitError 与它同族(warp-pool/verdict.js:25 把两者并列),同样按出口计、同样可换。
66
+ /FreeUsageLimitError|GoUsageLimitError/i,
67
+ /security_reject/i,
68
+ /安全校验拒绝/,
69
+ /isQueued/i,
70
+ /serviceAvailable["\s:]*false/i,
71
+ /rate.?limit|too many requests|over.?quota|retry after/i,
72
+ /invalid (api )?key|missing api key|unauthorized|expired (token|key)|no credentials|鉴权失败|无有效(?:账号|凭据|key)/i,
73
+ ];
74
+
75
+ /**
76
+ * 有界读一份失败响应的正文供判决(串行 trial 与 auto 竞速两条出口共用同一口径):
77
+ * 1s 上限防流式错误体拖慢、≤4KB 防大 body 把正则拖成长扫描,读不到就返回空串——
78
+ * 判据见空串一律**不判终局**(保守回退到「照跑接力」的旧行为,绝不因读不到就停兜底)。
79
+ * 取的是 `clone()`:原响应必须留给收场出口 relay 原字节。
80
+ */
81
+ export async function readVerdictText(res, { timeoutMs = 1000, maxChars = 4000 } = {}) {
82
+ try {
83
+ return String(await Promise.race([
84
+ res.clone().text(),
85
+ new Promise((r) => { const t = setTimeout(() => r(""), timeoutMs); t.unref?.(); }),
86
+ ])).slice(0, maxChars);
87
+ } catch {
88
+ return "";
89
+ }
90
+ }
91
+
92
+ function firstHit(list, text) {
93
+ for (const re of list) if (re.test(text)) return re.source;
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * 上游这条失败响应是不是「换节点也不会变」的终局判决。
99
+ * @param {number} status 上游 HTTP 状态
100
+ * @param {string} bodyText 上游错误体原文(≤4KB,未被日志摘要截成 400 字的那份;见文件头 ⚠)
101
+ * @returns {{terminal: boolean, kind?: string, marker?: string, notice?: string}}
102
+ */
103
+ export function classifyTerminalUpstream(status, bodyText = "") {
104
+ const code = Number(status);
105
+ const text = typeof bodyText === "string" ? bodyText : "";
106
+ if (!Number.isInteger(code) || code < 400 || code > 499 || !text.trim()) return { terminal: false };
107
+ // 顺序即优先级:① 先认「上游把免费层说死」——它的文案里也可能带 quota/limit 字样,反过来排会
108
+ // 把终局判成可接力;② 再排坏号/凭据/限流类可恢复拒绝;③ 最后才判模型 id 不存在。
109
+ const freeHit = firstHit(FREE_TIER_PATTERNS, text);
110
+ if (freeHit) {
111
+ return { terminal: true, kind: "free-tier-blocked", marker: freeHit, notice: "上游免费层资格判决(组员同源),已跳过接力" };
112
+ }
113
+ if (firstHit(TRANSIENT_MARKERS, text)) return { terminal: false };
114
+ const goneHit = firstHit(MODEL_GONE_PATTERNS, text);
115
+ if (goneHit) {
116
+ return { terminal: true, kind: "model-gone", marker: goneHit, notice: `上游不认这个模型 id(status=${code}),已跳过接力` };
117
+ }
118
+ return { terminal: false };
119
+ }
120
+
121
+ /**
122
+ * 逃生阀:`MSLXDFF_TERMINAL_SHORTCIRCUIT=0`(含 `false`/`off`/`no`)关掉终局短路,
123
+ * 恢复「任何失败都照跑组员接力」的旧行为。缺省=开(终局判决不接力)。
124
+ * 上游哪天改了文案导致误判/漏判时不停机回退(同 `MSLXDFF_FREE_LANE=0` 的先例:开关方向与名字同向,
125
+ * 别再写一个「设 1 表示关掉」的反向旋钮)。
126
+ */
127
+ export function terminalShortcircuitOff(env = process.env) {
128
+ const raw = env.MSLXDFF_TERMINAL_SHORTCIRCUIT;
129
+ if (raw === undefined || raw === null || raw === "") return false;
130
+ const s = String(raw).trim().toLowerCase();
131
+ return s === "0" || s === "false" || s === "off" || s === "no";
132
+ }
package/src/chooser.js CHANGED
@@ -1,54 +1,54 @@
1
- // 交互式模型选择器的纯渲染逻辑(便于测试);键盘循环在 interactive.js
2
-
3
- // items: [{ id, status?, ms?, fail?, current?, picked? }];cursor 当前高亮行(全局下标)
4
- // multi=true 时多选勾选(picked 显示 [x]/[ ]),否则单选默认模型(当前显示 ✓)
5
- // index/size 可选——窗口分页渲染(只画当页 + 页头),不传 size 则全量渲染(行为不变)
6
- // Note: 分页是本模块职责(键盘循环在 interactive.js),不传 size 必全量渲染 — 见 .agents/notes/implemented/feature/2026-09-21-models-paginated-chooser.md
7
- export function renderChooser(items, cursor = 0, { multi = false, index = 0, size } = {}) {
8
- if (size == null) return renderRows(items, cursor, 0, multi);
9
- const total = items.length;
10
- const sz = Math.max(1, Number(size) || 1);
11
- const pages = Math.max(1, Math.ceil(total / sz));
12
- const idx = Math.min(Math.max(Number(index) || 0, 0), pages - 1);
13
- const start = idx * sz;
14
- const rows = [`── 第 ${idx + 1}/${pages} 页 · 共 ${total} 项(←/→ 翻页) ──`];
15
- rows.push(...renderRows(items.slice(start, start + sz), cursor, start, multi));
16
- return rows;
17
- }
18
-
19
- function renderRows(slice, cursor, offset, multi) {
20
- return slice.map((it, li) => {
21
- const i = offset + li;
22
- const arrow = i === cursor ? "❯" : " ";
23
- let state = "";
24
- if (it.fail) state = ` [fail: ${it.fail}]`;
25
- else if (it.ms != null) state = ` [${it.ms}ms]`;
26
- else if (it.status && it.status !== "normal") state = ` [${it.status}]`;
27
- if (multi) {
28
- const box = it.picked ? "[✓]" : "[ ]";
29
- return `${arrow} ${box} ${it.id}${state}`;
30
- }
31
- const check = it.current ? " ✓ (current)" : "";
32
- return `${arrow} ${it.id}${check}${state}`;
33
- });
34
- }
35
-
36
- export function renderChooserHelp(multi = false, hasPages = false) {
37
- const paging = hasPages ? " · ←/→ page" : "";
38
- return multi
39
- ? ["", `↑/↓ move · Space toggle pick${paging} · Enter save picks · q/Esc cancel`]
40
- : ["", `↑/↓ move${paging} · Enter select as default · q/Esc cancel`];
41
- }
42
-
43
- // 解析按键:返回 "up" | "down" | "pageup" | "pagedown" | "enter" | "cancel" | "space" | null(忽略)
44
- export function parseKey(str) {
45
- if (!str) return null;
46
- if (str === "\x1b[A" || str === "k") return "up";
47
- if (str === "\x1b[B" || str === "j") return "down";
48
- if (str === "\x1b[D" || str === "h" || str === "\x1b[5~") return "pageup";
49
- if (str === "\x1b[C" || str === "l" || str === "\x1b[6~") return "pagedown";
50
- if (str === "\r" || str === "\n") return "enter";
51
- if (str === " " || str === "\x1b[32") return "space"; // 空格(32=0x20 的 ASC 表示)或某些终端 terminator
52
- if (str === "\x1b" || str === "q" || str === "\x03") return "cancel";
53
- return null;
54
- }
1
+ // 交互式模型选择器的纯渲染逻辑(便于测试);键盘循环在 interactive.js
2
+
3
+ // items: [{ id, status?, ms?, fail?, current?, picked? }];cursor 当前高亮行(全局下标)
4
+ // multi=true 时多选勾选(picked 显示 [x]/[ ]),否则单选默认模型(当前显示 ✓)
5
+ // index/size 可选——窗口分页渲染(只画当页 + 页头),不传 size 则全量渲染(行为不变)
6
+ // Note: 分页是本模块职责(键盘循环在 interactive.js),不传 size 必全量渲染 — 见 .agents/notes/implemented/feature/2026-09-21-models-paginated-chooser.md
7
+ export function renderChooser(items, cursor = 0, { multi = false, index = 0, size } = {}) {
8
+ if (size == null) return renderRows(items, cursor, 0, multi);
9
+ const total = items.length;
10
+ const sz = Math.max(1, Number(size) || 1);
11
+ const pages = Math.max(1, Math.ceil(total / sz));
12
+ const idx = Math.min(Math.max(Number(index) || 0, 0), pages - 1);
13
+ const start = idx * sz;
14
+ const rows = [`── 第 ${idx + 1}/${pages} 页 · 共 ${total} 项(←/→ 翻页) ──`];
15
+ rows.push(...renderRows(items.slice(start, start + sz), cursor, start, multi));
16
+ return rows;
17
+ }
18
+
19
+ function renderRows(slice, cursor, offset, multi) {
20
+ return slice.map((it, li) => {
21
+ const i = offset + li;
22
+ const arrow = i === cursor ? "❯" : " ";
23
+ let state = "";
24
+ if (it.fail) state = ` [fail: ${it.fail}]`;
25
+ else if (it.ms != null) state = ` [${it.ms}ms]`;
26
+ else if (it.status && it.status !== "normal") state = ` [${it.status}]`;
27
+ if (multi) {
28
+ const box = it.picked ? "[✓]" : "[ ]";
29
+ return `${arrow} ${box} ${it.id}${state}`;
30
+ }
31
+ const check = it.current ? " ✓ (current)" : "";
32
+ return `${arrow} ${it.id}${check}${state}`;
33
+ });
34
+ }
35
+
36
+ export function renderChooserHelp(multi = false, hasPages = false) {
37
+ const paging = hasPages ? " · ←/→ page" : "";
38
+ return multi
39
+ ? ["", `↑/↓ move · Space toggle pick${paging} · Enter save picks · q/Esc cancel`]
40
+ : ["", `↑/↓ move${paging} · Enter select as default · q/Esc cancel`];
41
+ }
42
+
43
+ // 解析按键:返回 "up" | "down" | "pageup" | "pagedown" | "enter" | "cancel" | "space" | null(忽略)
44
+ export function parseKey(str) {
45
+ if (!str) return null;
46
+ if (str === "\x1b[A" || str === "k") return "up";
47
+ if (str === "\x1b[B" || str === "j") return "down";
48
+ if (str === "\x1b[D" || str === "h" || str === "\x1b[5~") return "pageup";
49
+ if (str === "\x1b[C" || str === "l" || str === "\x1b[6~") return "pagedown";
50
+ if (str === "\r" || str === "\n") return "enter";
51
+ if (str === " " || str === "\x1b[32") return "space"; // 空格(32=0x20 的 ASC 表示)或某些终端 terminator
52
+ if (str === "\x1b" || str === "q" || str === "\x03") return "cancel";
53
+ return null;
54
+ }
@@ -1,101 +1,101 @@
1
- import { normalizeModel } from "../reasoning.js";
2
- import { registerModelAlias, persistModelAliases, loadModelAliases, modelAliasFile } from "../providers/model-id.js";
3
- import { planSlots, aliasPairs, ROLE_SLOTS, parseOneMMode, decideRows1m } from "./slots.js";
4
- import { oneMContexts } from "./context1m.js";
5
- import { syncToClaudeDesktop, retireClaudeDesktopProfile, claudeDesktopConfigDir, readClaudeDesktopProfile } from "../sync-claude-desktop.js";
6
- import { detectManagedPolicy, currentAliases, probeMessages } from "./doctor.js";
7
-
8
- /**
9
- * `-setto claude-desktop` 的编排层(副作用集中在这一个文件,CLI 只管参数与人话输出)。
10
- * 一次调用做两件事,缺一不可 —— 这正是社区工具要「profile + 本地路由」两步的原因:
11
- * ① 写 Claude Desktop 的 3P profile(`inferenceModels` 用**角色槽 id**,`labelOverride` 标真模型名);
12
- * ② 写网关自己的 alias(角色槽 → 真模型),让桌面端发来的槽位请求在入站就被翻译成能跑的真模型。
13
- * 少了 ②,桌面端每次请求都会被当未知模型丢组员转发 → 30~90 s 后 502(App 里就表现为 Test connection 红点)。
14
- * ③ `--1m` 上下文打标(ADR-0056,Claude Desktop 专用):auto 问本机 /v1/models 拿已裁决窗口,
15
- * 证据缺位时**保留盘上既有标**(无证据≠负面证据)、**绝不阻塞写盘**;all/列表为用户担保;
16
- * 打标行经 toInferenceModels 写成对官方键 `supports1m+prefer1m`。evidence 参数是测试接缝。
17
- */
18
- export async function applyClaudeDesktop({ picks = [], preferred = "", port = 8989, token = "", max, maxEffort, oneM = "auto", evidence, noAlias = false, dir, probe = false, probeTimeoutMs = 90000 } = {}) {
19
- const planned = planSlots({ picks, preferred, max });
20
- let rows = planned.rows.map((r) => ({ ...r, model: normalizeModel(r.model) || r.model }));
21
-
22
- const oneMMode = oneM && typeof oneM === "object" ? oneM : parseOneMMode(oneM);
23
- if (oneMMode.mode === "invalid") throw new Error("--1m 取值非法:只接受 off | auto | all | <逗号分隔槽位/模型id>");
24
- const desktopDir = dir || claudeDesktopConfigDir();
25
- let ev = evidence;
26
- let evidenceQueried = false;
27
- if (oneMMode.mode === "auto" && ev === undefined) {
28
- evidenceQueried = true;
29
- ev = await oneMContexts({ port, token });
30
- }
31
- // auto 的保留集:证据缺位(整体失败或该行不在目录)时沿用盘上既有 1M 标——网关停机的一轮
32
- // 不得把上次写好的标静默抹掉(评审 1 P1-2;读盘失败自然降级为空表=与旧行为一致,不新增失败模式)
33
- let preserve = null;
34
- if (oneMMode.mode === "auto") {
35
- preserve = new Map();
36
- const existing = readClaudeDesktopProfile({ dir: desktopDir });
37
- for (const m of Array.isArray(existing.profile?.inferenceModels) ? existing.profile.inferenceModels : []) {
38
- if (m && typeof m === "object" && m.supports1m === true && m.name) preserve.set(String(m.name).toLowerCase(), true);
39
- }
40
- }
41
- const decided = decideRows1m({ rows, oneM: oneMMode, contexts: ev && ev.ok ? ev.contexts : null, preserve });
42
- rows = decided.rows;
43
-
44
- const written = syncToClaudeDesktop({ port, token, rows, maxEffort, dir: desktopDir });
45
-
46
- const out = {
47
- dir: written.dir,
48
- configFile: written.configFile,
49
- metaFile: written.metaFile,
50
- configId: written.id,
51
- action: written.action,
52
- changed: written.changed,
53
- backups: written.backups || [],
54
- tmpLeftover: written.tmpLeftover || [],
55
- rows,
56
- overflow: planned.overflow,
57
- dropped: planned.dropped,
58
- slotCap: ROLE_SLOTS.length,
59
- aliasFile: modelAliasFile(),
60
- aliasAdded: [],
61
- aliasOverwritten: [],
62
- aliasSkipped: noAlias,
63
- managed: detectManagedPolicy(),
64
- probes: [],
65
- oneM: {
66
- mode: oneMMode.mode,
67
- list: oneMMode.list || [],
68
- evidenceQueried,
69
- evidenceOk: oneMMode.mode === "auto" ? Boolean(ev && ev.ok) : null,
70
- evidenceReason: oneMMode.mode === "auto" && ev && !ev.ok ? String(ev.reason || "") : "",
71
- markedCount: rows.filter((r) => r.supports1m).length,
72
- preserved: decided.preserved,
73
- unmatched: decided.unmatched,
74
- },
75
- };
76
-
77
- if (!noAlias) {
78
- const before = currentAliases();
79
- for (const [slot, model] of aliasPairs(rows)) {
80
- if (before[slot] && before[slot] !== model) out.aliasOverwritten.push({ slot, from: before[slot], to: model });
81
- else out.aliasAdded.push({ slot, to: model });
82
- registerModelAlias(slot, model);
83
- }
84
- persistModelAliases();
85
- }
86
-
87
- if (probe) {
88
- for (const r of rows) out.probes.push(await probeMessages({ port, token, model: r.slot, timeoutMs: probeTimeoutMs }));
89
- }
90
- return out;
91
- }
92
-
93
- /** `--official`:摘掉 mslxdff 那条登记(不删配置、不动别人的 entry)。 */
94
- export function retireClaudeDesktop({ port = 8989, dir, alsoAliases = [] } = {}) {
95
- const r = retireClaudeDesktopProfile({ port, dir: dir || claudeDesktopConfigDir() });
96
- const aliasFile = modelAliasFile();
97
- return { ...r, aliasFile, aliasHints: Array.isArray(alsoAliases) ? alsoAliases : [] };
98
- }
99
-
100
- /** 现成 alias 表(给 CLI 报「哪些槽位被谁占着」)。 */
101
- export { loadModelAliases };
1
+ import { normalizeModel } from "../reasoning.js";
2
+ import { registerModelAlias, persistModelAliases, loadModelAliases, modelAliasFile } from "../providers/model-id.js";
3
+ import { planSlots, aliasPairs, ROLE_SLOTS, parseOneMMode, decideRows1m } from "./slots.js";
4
+ import { oneMContexts } from "./context1m.js";
5
+ import { syncToClaudeDesktop, retireClaudeDesktopProfile, claudeDesktopConfigDir, readClaudeDesktopProfile } from "../sync-claude-desktop.js";
6
+ import { detectManagedPolicy, currentAliases, probeMessages } from "./doctor.js";
7
+
8
+ /**
9
+ * `-setto claude-desktop` 的编排层(副作用集中在这一个文件,CLI 只管参数与人话输出)。
10
+ * 一次调用做两件事,缺一不可 —— 这正是社区工具要「profile + 本地路由」两步的原因:
11
+ * ① 写 Claude Desktop 的 3P profile(`inferenceModels` 用**角色槽 id**,`labelOverride` 标真模型名);
12
+ * ② 写网关自己的 alias(角色槽 → 真模型),让桌面端发来的槽位请求在入站就被翻译成能跑的真模型。
13
+ * 少了 ②,桌面端每次请求都会被当未知模型丢组员转发 → 30~90 s 后 502(App 里就表现为 Test connection 红点)。
14
+ * ③ `--1m` 上下文打标(ADR-0056,Claude Desktop 专用):auto 问本机 /v1/models 拿已裁决窗口,
15
+ * 证据缺位时**保留盘上既有标**(无证据≠负面证据)、**绝不阻塞写盘**;all/列表为用户担保;
16
+ * 打标行经 toInferenceModels 写成对官方键 `supports1m+prefer1m`。evidence 参数是测试接缝。
17
+ */
18
+ export async function applyClaudeDesktop({ picks = [], preferred = "", port = 8989, token = "", max, maxEffort, oneM = "auto", evidence, noAlias = false, dir, probe = false, probeTimeoutMs = 90000 } = {}) {
19
+ const planned = planSlots({ picks, preferred, max });
20
+ let rows = planned.rows.map((r) => ({ ...r, model: normalizeModel(r.model) || r.model }));
21
+
22
+ const oneMMode = oneM && typeof oneM === "object" ? oneM : parseOneMMode(oneM);
23
+ if (oneMMode.mode === "invalid") throw new Error("--1m 取值非法:只接受 off | auto | all | <逗号分隔槽位/模型id>");
24
+ const desktopDir = dir || claudeDesktopConfigDir();
25
+ let ev = evidence;
26
+ let evidenceQueried = false;
27
+ if (oneMMode.mode === "auto" && ev === undefined) {
28
+ evidenceQueried = true;
29
+ ev = await oneMContexts({ port, token });
30
+ }
31
+ // auto 的保留集:证据缺位(整体失败或该行不在目录)时沿用盘上既有 1M 标——网关停机的一轮
32
+ // 不得把上次写好的标静默抹掉(评审 1 P1-2;读盘失败自然降级为空表=与旧行为一致,不新增失败模式)
33
+ let preserve = null;
34
+ if (oneMMode.mode === "auto") {
35
+ preserve = new Map();
36
+ const existing = readClaudeDesktopProfile({ dir: desktopDir });
37
+ for (const m of Array.isArray(existing.profile?.inferenceModels) ? existing.profile.inferenceModels : []) {
38
+ if (m && typeof m === "object" && m.supports1m === true && m.name) preserve.set(String(m.name).toLowerCase(), true);
39
+ }
40
+ }
41
+ const decided = decideRows1m({ rows, oneM: oneMMode, contexts: ev && ev.ok ? ev.contexts : null, preserve });
42
+ rows = decided.rows;
43
+
44
+ const written = syncToClaudeDesktop({ port, token, rows, maxEffort, dir: desktopDir });
45
+
46
+ const out = {
47
+ dir: written.dir,
48
+ configFile: written.configFile,
49
+ metaFile: written.metaFile,
50
+ configId: written.id,
51
+ action: written.action,
52
+ changed: written.changed,
53
+ backups: written.backups || [],
54
+ tmpLeftover: written.tmpLeftover || [],
55
+ rows,
56
+ overflow: planned.overflow,
57
+ dropped: planned.dropped,
58
+ slotCap: ROLE_SLOTS.length,
59
+ aliasFile: modelAliasFile(),
60
+ aliasAdded: [],
61
+ aliasOverwritten: [],
62
+ aliasSkipped: noAlias,
63
+ managed: detectManagedPolicy(),
64
+ probes: [],
65
+ oneM: {
66
+ mode: oneMMode.mode,
67
+ list: oneMMode.list || [],
68
+ evidenceQueried,
69
+ evidenceOk: oneMMode.mode === "auto" ? Boolean(ev && ev.ok) : null,
70
+ evidenceReason: oneMMode.mode === "auto" && ev && !ev.ok ? String(ev.reason || "") : "",
71
+ markedCount: rows.filter((r) => r.supports1m).length,
72
+ preserved: decided.preserved,
73
+ unmatched: decided.unmatched,
74
+ },
75
+ };
76
+
77
+ if (!noAlias) {
78
+ const before = currentAliases();
79
+ for (const [slot, model] of aliasPairs(rows)) {
80
+ if (before[slot] && before[slot] !== model) out.aliasOverwritten.push({ slot, from: before[slot], to: model });
81
+ else out.aliasAdded.push({ slot, to: model });
82
+ registerModelAlias(slot, model);
83
+ }
84
+ persistModelAliases();
85
+ }
86
+
87
+ if (probe) {
88
+ for (const r of rows) out.probes.push(await probeMessages({ port, token, model: r.slot, timeoutMs: probeTimeoutMs }));
89
+ }
90
+ return out;
91
+ }
92
+
93
+ /** `--official`:摘掉 mslxdff 那条登记(不删配置、不动别人的 entry)。 */
94
+ export function retireClaudeDesktop({ port = 8989, dir, alsoAliases = [] } = {}) {
95
+ const r = retireClaudeDesktopProfile({ port, dir: dir || claudeDesktopConfigDir() });
96
+ const aliasFile = modelAliasFile();
97
+ return { ...r, aliasFile, aliasHints: Array.isArray(alsoAliases) ? alsoAliases : [] };
98
+ }
99
+
100
+ /** 现成 alias 表(给 CLI 报「哪些槽位被谁占着」)。 */
101
+ export { loadModelAliases };