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.
Files changed (326) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/package.json +38 -38
  4. package/plugins/README.md +14 -14
  5. package/plugins/prefer-model.mjs.example +14 -14
  6. package/src/anthropic/count-tokens.js +61 -61
  7. package/src/anthropic/request-body.js +126 -126
  8. package/src/anthropic/shared.js +18 -18
  9. package/src/anthropic/sse-aggregate.js +56 -56
  10. package/src/anthropic/translate.js +287 -287
  11. package/src/anthropic/web-search.js +361 -361
  12. package/src/api-kind.js +31 -31
  13. package/src/autostart.js +288 -288
  14. package/src/bench/probe.js +1 -1
  15. package/src/bench/runner.js +1 -1
  16. package/src/bench/via-probe.js +1 -1
  17. package/src/bench/via.js +144 -144
  18. package/src/bench/workbuddy-bench.js +1 -1
  19. package/src/chat/config.js +11 -11
  20. package/src/chat/cooling.js +81 -81
  21. package/src/chat/direct.js +58 -58
  22. package/src/chat/index.js +6 -6
  23. package/src/chat/spinner.js +38 -38
  24. package/src/chat/sse.js +69 -69
  25. package/src/chat/stats.js +247 -247
  26. package/src/chat/store.js +48 -48
  27. package/src/chat/tool-handlers.js +67 -67
  28. package/src/chat/tools.js +1 -1
  29. package/src/chat/upstream.js +59 -59
  30. package/src/chat-pipeline/auto-race.js +158 -158
  31. package/src/chat-pipeline/empty-turn.js +147 -147
  32. package/src/chat-pipeline/engine.js +17 -17
  33. package/src/chat-pipeline/serial-trial.js +283 -283
  34. package/src/chat-pipeline/terminal-verdict.js +147 -147
  35. package/src/chooser.js +54 -54
  36. package/src/claude-desktop/apply.js +101 -101
  37. package/src/claude-desktop/context1m.js +38 -38
  38. package/src/claude-desktop/doctor.js +146 -146
  39. package/src/claude-desktop/slots.js +152 -152
  40. package/src/cli/bootstrap.js +1 -1
  41. package/src/cli/commands/claude-desktop.js +284 -284
  42. package/src/cli/commands/daemon.js +231 -231
  43. package/src/cli/commands/model/list-live.js +22 -22
  44. package/src/cli/commands/model/list-providers.js +76 -76
  45. package/src/cli/commands/model/list-render.js +77 -77
  46. package/src/cli/commands/model/list-sort.js +52 -52
  47. package/src/cli/commands/model/list-tty.js +76 -76
  48. package/src/cli/commands/model/list.js +166 -166
  49. package/src/cli/commands/model/live-models.js +24 -24
  50. package/src/cli/commands/model/stats.js +43 -43
  51. package/src/cli/commands/model/status.js +47 -47
  52. package/src/cli/commands/model.js +51 -51
  53. package/src/cli/commands/provider/add.js +108 -108
  54. package/src/cli/commands/provider/allowlist.js +99 -99
  55. package/src/cli/commands/provider/cline-free.js +99 -99
  56. package/src/cli/commands/provider/cline-login.js +218 -218
  57. package/src/cli/commands/provider/cline-quota.js +65 -65
  58. package/src/cli/commands/provider/codearts-login.js +45 -45
  59. package/src/cli/commands/provider/config.js +119 -119
  60. package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
  61. package/src/cli/commands/provider/keys-guard.js +50 -50
  62. package/src/cli/commands/provider/keys.js +161 -161
  63. package/src/cli/commands/provider/qoder-checkin.js +81 -81
  64. package/src/cli/commands/provider/qoder-login.js +102 -102
  65. package/src/cli/commands/provider/raccoon-login.js +180 -180
  66. package/src/cli/commands/provider/raccoon-password.js +162 -162
  67. package/src/cli/commands/provider/raccoon-quota.js +109 -109
  68. package/src/cli/commands/provider/traework-checkin.js +144 -144
  69. package/src/cli/commands/provider/traework-login.js +164 -164
  70. package/src/cli/commands/provider/workbuddy-login.js +153 -153
  71. package/src/cli/commands/provider/zcode-login.js +77 -77
  72. package/src/cli/commands/provider/zcode-quota.js +55 -55
  73. package/src/cli/commands/provider.js +1 -1
  74. package/src/cli/commands/stats.js +149 -149
  75. package/src/cli/commands/system.js +261 -261
  76. package/src/cli/commands/timezone.js +47 -47
  77. package/src/cli/commands/use-group.js +60 -60
  78. package/src/cli/commands/workbuddy.js +189 -189
  79. package/src/cli/format.js +141 -141
  80. package/src/cli/group-helpers.js +70 -70
  81. package/src/cli/help.js +80 -80
  82. package/src/cli/index.js +70 -70
  83. package/src/cli/interactive.js +121 -121
  84. package/src/cli/join-core.js +99 -99
  85. package/src/cli/join-wizard.js +175 -175
  86. package/src/cli/policy.js +136 -136
  87. package/src/cli/provider-row.js +126 -126
  88. package/src/cli/util.js +24 -24
  89. package/src/compat.js +53 -53
  90. package/src/events.js +38 -38
  91. package/src/free-lane.js +131 -131
  92. package/src/free-watcher.js +63 -63
  93. package/src/groups.js +262 -262
  94. package/src/logs.js +195 -195
  95. package/src/metrics.js +62 -62
  96. package/src/model-capabilities/context-lookup.js +65 -65
  97. package/src/model-capabilities/merge.js +142 -142
  98. package/src/model-trace.js +187 -187
  99. package/src/models.js +1 -1
  100. package/src/opencode-identity.js +72 -72
  101. package/src/peers.js +254 -251
  102. package/src/plugins.js +107 -107
  103. package/src/providers/base.js +164 -164
  104. package/src/providers/classify.js +22 -22
  105. package/src/providers/cline/allowlist-sync.js +93 -93
  106. package/src/providers/cline/free-catalog.js +71 -71
  107. package/src/providers/cline/headers.js +37 -37
  108. package/src/providers/cline/index.js +130 -130
  109. package/src/providers/cline/models.js +150 -150
  110. package/src/providers/cline/usage.js +274 -274
  111. package/src/providers/codearts/auth-pool.js +148 -148
  112. package/src/providers/codearts/concurrency.js +34 -34
  113. package/src/providers/codearts/const.js +65 -65
  114. package/src/providers/codearts/dpop.js +95 -95
  115. package/src/providers/codearts/index.js +175 -175
  116. package/src/providers/codearts/login.js +214 -214
  117. package/src/providers/codearts/models.js +182 -182
  118. package/src/providers/codearts/sign.js +96 -96
  119. package/src/providers/codearts/sse.js +204 -204
  120. package/src/providers/codearts/stream.js +209 -209
  121. package/src/providers/codearts/sts.js +124 -124
  122. package/src/providers/codearts.js +1 -1
  123. package/src/providers/dispatcher.js +146 -146
  124. package/src/providers/generic.js +156 -156
  125. package/src/providers/globalqwenwork/account-store.js +141 -141
  126. package/src/providers/globalqwenwork/constants.js +73 -73
  127. package/src/providers/globalqwenwork/crypto.js +2 -2
  128. package/src/providers/globalqwenwork/http.js +2 -2
  129. package/src/providers/globalqwenwork/index.js +331 -331
  130. package/src/providers/globalqwenwork/payload.js +147 -147
  131. package/src/providers/globalqwenwork/rsa.js +2 -2
  132. package/src/providers/globalqwenwork/sse.js +2 -2
  133. package/src/providers/globalqwenwork/stream.js +2 -2
  134. package/src/providers/globalqwenwork.js +1 -1
  135. package/src/providers/globalworkbuddy/account-store.js +198 -198
  136. package/src/providers/globalworkbuddy/auth.js +97 -97
  137. package/src/providers/globalworkbuddy/catalog.js +138 -138
  138. package/src/providers/globalworkbuddy/constants.js +73 -73
  139. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  140. package/src/providers/globalworkbuddy/errors.js +98 -98
  141. package/src/providers/globalworkbuddy/headers.js +89 -89
  142. package/src/providers/globalworkbuddy/models.js +96 -96
  143. package/src/providers/globalworkbuddy/payload.js +80 -80
  144. package/src/providers/index.js +5 -5
  145. package/src/providers/model-id.js +172 -172
  146. package/src/providers/opencode.js +19 -19
  147. package/src/providers/openrouter.js +31 -31
  148. package/src/providers/qoder/account-store.js +106 -106
  149. package/src/providers/qoder/aggregate.js +60 -60
  150. package/src/providers/qoder/baseprompt.json +591 -591
  151. package/src/providers/qoder/checkin.js +160 -160
  152. package/src/providers/qoder/constants.js +44 -44
  153. package/src/providers/qoder/encode.js +41 -41
  154. package/src/providers/qoder/fingerprint.js +38 -38
  155. package/src/providers/qoder/models.js +77 -77
  156. package/src/providers/qoder/oauth.js +84 -84
  157. package/src/providers/qoder/payload.js +162 -162
  158. package/src/providers/qoder/request.js +27 -27
  159. package/src/providers/qoder/session.js +134 -134
  160. package/src/providers/qoder/sticky.js +38 -38
  161. package/src/providers/qoder/stream.js +116 -116
  162. package/src/providers/qoder.js +1 -1
  163. package/src/providers/raccoon/account-store.js +216 -216
  164. package/src/providers/raccoon/auth.js +67 -67
  165. package/src/providers/raccoon/chat.js +228 -228
  166. package/src/providers/raccoon/const.js +180 -165
  167. package/src/providers/raccoon/credits.js +185 -175
  168. package/src/providers/raccoon/crypto.js +43 -0
  169. package/src/providers/raccoon/envelope.js +31 -31
  170. package/src/providers/raccoon/headers.js +45 -45
  171. package/src/providers/raccoon/index.js +216 -216
  172. package/src/providers/raccoon/login.js +279 -279
  173. package/src/providers/raccoon/models.js +160 -160
  174. package/src/providers/raccoon/password-login.js +102 -87
  175. package/src/providers/raccoon/qr-codewords.js +115 -115
  176. package/src/providers/raccoon/qr.js +236 -236
  177. package/src/providers/raccoon/sse.js +135 -135
  178. package/src/providers/responses-channel.js +120 -120
  179. package/src/providers/share-keys.js +53 -53
  180. package/src/providers/traework/account-store.js +234 -234
  181. package/src/providers/traework/chat.js +217 -217
  182. package/src/providers/traework/checkin.js +77 -77
  183. package/src/providers/traework/constants.js +41 -41
  184. package/src/providers/traework/errors.js +52 -52
  185. package/src/providers/traework/headers.js +79 -79
  186. package/src/providers/traework/index.js +132 -132
  187. package/src/providers/traework/market-identity.js +116 -116
  188. package/src/providers/traework/models.js +98 -98
  189. package/src/providers/traework/payload.js +86 -86
  190. package/src/providers/traework/sse.js +184 -184
  191. package/src/providers/traework/token.js +52 -52
  192. package/src/providers/traework.js +2 -2
  193. package/src/providers/workbuddy/account-store.js +186 -186
  194. package/src/providers/workbuddy/auth.js +158 -158
  195. package/src/providers/workbuddy/balance.js +86 -86
  196. package/src/providers/workbuddy/cat-travel.js +125 -125
  197. package/src/providers/workbuddy/checkin.js +75 -75
  198. package/src/providers/workbuddy/desktop-info.js +78 -78
  199. package/src/providers/workbuddy/growth-api.js +67 -67
  200. package/src/providers/workbuddy/growth-plans.js +63 -63
  201. package/src/providers/workbuddy/growth.js +181 -181
  202. package/src/providers/workbuddy/index.js +217 -217
  203. package/src/providers/workbuddy/models.js +126 -126
  204. package/src/providers/workbuddy/payload.js +141 -141
  205. package/src/providers/workbuddy/rotation-log.js +83 -83
  206. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  207. package/src/providers/workbuddy/sdk-chat.js +26 -26
  208. package/src/providers/workbuddy-balance.js +78 -78
  209. package/src/providers/zcode/account-store.js +129 -129
  210. package/src/providers/zcode/auth.js +28 -28
  211. package/src/providers/zcode/chat.js +179 -179
  212. package/src/providers/zcode/const.js +54 -54
  213. package/src/providers/zcode/context-shape.js +162 -162
  214. package/src/providers/zcode/headers.js +93 -93
  215. package/src/providers/zcode/index.js +124 -124
  216. package/src/providers/zcode/models.js +65 -65
  217. package/src/providers/zcode/oauth.js +120 -120
  218. package/src/providers/zcode/quota.js +176 -176
  219. package/src/providers/zcode/sse.js +196 -196
  220. package/src/readline-compat.js +25 -25
  221. package/src/responses/translate.js +311 -311
  222. package/src/routes/chat/broadband-handler.js +72 -72
  223. package/src/routes/chat/empty-turn-report.js +31 -31
  224. package/src/routes/chat/exhausted-handler.js +87 -87
  225. package/src/routes/chat/hedge-handler.js +130 -130
  226. package/src/routes/chat/index.js +1 -1
  227. package/src/routes/chat/local-handler.js +63 -63
  228. package/src/routes/chat/peer-handler.js +81 -81
  229. package/src/routes/chat.js +3 -3
  230. package/src/routes/fallback.js +88 -88
  231. package/src/routes/groups-relay.js +214 -214
  232. package/src/routes/groups.js +122 -122
  233. package/src/routes/helpers.js +83 -83
  234. package/src/routes/index.js +147 -147
  235. package/src/routes/messages-route.js +365 -365
  236. package/src/routes/peers.js +23 -1
  237. package/src/routes/relay-queue.js +181 -181
  238. package/src/routes/relay.js +60 -60
  239. package/src/routes/responses-route.js +213 -213
  240. package/src/routes/stream-body.js +84 -84
  241. package/src/routes/stream-hold.js +337 -337
  242. package/src/routes/stream-scan.js +366 -366
  243. package/src/routes.js +3 -3
  244. package/src/runtime/auto-update.js +113 -113
  245. package/src/runtime/bootstrap.js +8 -0
  246. package/src/runtime/broadband-stream.js +79 -79
  247. package/src/runtime/broadband.js +103 -103
  248. package/src/runtime/group-sync.js +81 -81
  249. package/src/runtime/keep-warm.js +120 -0
  250. package/src/runtime/lifecycle-forensics.js +116 -116
  251. package/src/runtime/lifecycle-log.js +87 -87
  252. package/src/runtime/log-stamp.js +73 -73
  253. package/src/runtime/proc-info.js +137 -137
  254. package/src/runtime/qoder-checkin.js +93 -93
  255. package/src/runtime/raccoon-checkin.js +126 -126
  256. package/src/runtime/server-lifecycle.js +189 -189
  257. package/src/runtime/traework-checkin.js +118 -118
  258. package/src/runtime/workbuddy-checkin.js +120 -120
  259. package/src/runtime/workbuddy-growth.js +133 -133
  260. package/src/server.js +7 -0
  261. package/src/state/facade.js +58 -58
  262. package/src/state/memory.js +162 -162
  263. package/src/state/merge.js +25 -25
  264. package/src/state/migrations/cline-unify.js +94 -94
  265. package/src/state/migrations.js +33 -33
  266. package/src/state/persist.js +42 -42
  267. package/src/state/provider-config.js +142 -142
  268. package/src/state/schemas/group.js +31 -31
  269. package/src/state/schemas/model.js +94 -94
  270. package/src/state/schemas/peer.js +31 -31
  271. package/src/state/schemas/port.js +10 -10
  272. package/src/state/schemas/provider.js +202 -202
  273. package/src/state/schemas/timezone.js +70 -70
  274. package/src/state/schemas/token.js +48 -48
  275. package/src/state/schemas/use-group.js +102 -102
  276. package/src/state/store.js +176 -176
  277. package/src/state.js +1 -1
  278. package/src/sync-claude-desktop.js +234 -234
  279. package/src/sync-claude.js +152 -152
  280. package/src/sync-codex.js +116 -116
  281. package/src/sync-workbuddy.js +158 -158
  282. package/src/talk-full-redact.js +129 -129
  283. package/src/talk-full.js +131 -131
  284. package/src/talk-log.js +257 -257
  285. package/src/text-tokens.js +52 -52
  286. package/src/thinking-effort.js +67 -67
  287. package/src/time.js +106 -106
  288. package/src/timeline.js +26 -26
  289. package/src/transport/index.js +1 -1
  290. package/src/upstream-engine/index.js +74 -74
  291. package/src/upstream-engine/mode.js +15 -15
  292. package/src/upstream-engine/sdk/attempt.js +221 -221
  293. package/src/upstream-engine/sdk/chat.js +28 -28
  294. package/src/upstream-engine/sdk/convert.js +192 -192
  295. package/src/upstream-engine/sdk/diagnose.js +62 -62
  296. package/src/upstream-engine/sdk/dispatch.js +55 -55
  297. package/src/upstream-engine/sdk/responses.js +176 -176
  298. package/src/upstream-engine/sdk/sse.js +166 -166
  299. package/src/upstream-responses.js +305 -305
  300. package/src/usage/record.js +106 -106
  301. package/src/usage/report.js +195 -195
  302. package/src/v6pool/claim.js +157 -157
  303. package/src/v6pool/config.js +65 -65
  304. package/src/v6pool/detect.js +130 -130
  305. package/src/v6pool/index.js +152 -152
  306. package/src/v6pool/prefix.js +140 -140
  307. package/src/v6pool/probe.js +40 -40
  308. package/src/v6pool/report.js +104 -104
  309. package/src/v6pool/rotate.js +263 -263
  310. package/src/warp-pool/config.js +74 -74
  311. package/src/warp-pool/health.js +106 -106
  312. package/src/warp-pool/identity.js +141 -141
  313. package/src/warp-pool/index.js +358 -358
  314. package/src/warp-pool/keys.js +29 -29
  315. package/src/warp-pool/nodes.js +95 -95
  316. package/src/warp-pool/ports.js +160 -160
  317. package/src/warp-pool/register.js +113 -113
  318. package/src/warp-pool/report.js +232 -232
  319. package/src/warp-pool/revive.js +168 -168
  320. package/src/warp-pool/rotate.js +214 -214
  321. package/src/warp-pool/strategy.js +43 -43
  322. package/src/warp-pool/verdict.js +69 -69
  323. package/src/warp-pool/wireproxy.js +162 -162
  324. package/docs/cli_help_mini.md +0 -146
  325. package/src/providers/AGENTS.md +0 -49
  326. package/src/routes/AGENTS.md +0 -37
@@ -1,27 +1,27 @@
1
- // qoder 上游请求装配(纯函数,无网络):OpenAI body → COSY 签名请求(url/headers/bodyStr)。
2
- // 从 chat.js 拆出,使真流式/聚合两条管线共享同一装配口(单一来源,防两处签名漂移)。
3
- import { getEndpoints, normalizeRegion } from "./constants.js";
4
- import { buildCosyHeaders, pathSigFrom } from "./session.js";
5
- import { cosyEncode } from "./encode.js";
6
- import { buildQoderBody, mapModel } from "./payload.js";
7
-
8
- export function buildUpstreamRequest({ sess, region, model, messages, tools, maxTokens, isReasoning }) {
9
- const upstreamModel = mapModel(model);
10
- const { body, mcSource } = buildQoderBody({
11
- template: undefined,
12
- userType: sess?.identity?.userType,
13
- model: upstreamModel,
14
- messages,
15
- tools,
16
- maxTokens,
17
- isReasoning,
18
- });
19
- const ep = getEndpoints(normalizeRegion(region));
20
- const url = ep.chatStreamURL;
21
- const bodyStr = cosyEncode(Buffer.from(JSON.stringify(body)));
22
- const headers = buildCosyHeaders(sess, pathSigFrom(url), bodyStr, "text/event-stream");
23
- headers["x-model-key"] = upstreamModel;
24
- headers["x-model-source"] = mcSource;
25
- headers["accept"] = "text/event-stream";
26
- return { url, headers, bodyStr, upstreamModel, mcSource };
27
- }
1
+ // qoder 上游请求装配(纯函数,无网络):OpenAI body → COSY 签名请求(url/headers/bodyStr)。
2
+ // 从 chat.js 拆出,使真流式/聚合两条管线共享同一装配口(单一来源,防两处签名漂移)。
3
+ import { getEndpoints, normalizeRegion } from "./constants.js";
4
+ import { buildCosyHeaders, pathSigFrom } from "./session.js";
5
+ import { cosyEncode } from "./encode.js";
6
+ import { buildQoderBody, mapModel } from "./payload.js";
7
+
8
+ export function buildUpstreamRequest({ sess, region, model, messages, tools, maxTokens, isReasoning }) {
9
+ const upstreamModel = mapModel(model);
10
+ const { body, mcSource } = buildQoderBody({
11
+ template: undefined,
12
+ userType: sess?.identity?.userType,
13
+ model: upstreamModel,
14
+ messages,
15
+ tools,
16
+ maxTokens,
17
+ isReasoning,
18
+ });
19
+ const ep = getEndpoints(normalizeRegion(region));
20
+ const url = ep.chatStreamURL;
21
+ const bodyStr = cosyEncode(Buffer.from(JSON.stringify(body)));
22
+ const headers = buildCosyHeaders(sess, pathSigFrom(url), bodyStr, "text/event-stream");
23
+ headers["x-model-key"] = upstreamModel;
24
+ headers["x-model-source"] = mcSource;
25
+ headers["accept"] = "text/event-stream";
26
+ return { url, headers, bodyStr, upstreamModel, mcSource };
27
+ }
@@ -1,135 +1,135 @@
1
- // qoder COSY 签名与会话(转译 qoder2api internal/cosy/session.go + signature.go + auth.go)
2
- // 会话:RSA-OAEP 加密 tempKey + AES-CBC 加密身份 payload → cosyKey + info
3
- // 签名:md5(payloadB64 + "\n" + cosyKey + "\n" + date + "\n" + body + "\n" + pathSig)
4
- import { createHash, randomBytes, createCipheriv, publicEncrypt } from "node:crypto";
5
- import { cosyEncode } from "./encode.js";
6
- import { newUUID, unixSec } from "./fingerprint.js";
7
-
8
- export const APP_CODE = "cosy";
9
- export const VERSION = "1.0.10";
10
- // base64("war, war never changes")
11
- const SECRET_B64 = "d2FyLCB3YXIgbmV2ZXIgY2hhbmdlcw==";
12
-
13
- const SERVER_PUB_KEY = `-----BEGIN PUBLIC KEY-----
14
- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
15
- 4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
16
- 6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
17
- XcW+ML9FoCI6AOvOzwIDAQAB
18
- -----END PUBLIC KEY-----`;
19
-
20
- export function signLegacy(date) {
21
- const s = APP_CODE + "&" + SECRET_B64 + "&" + date;
22
- return createHash("md5").update(s).digest("hex");
23
- }
24
-
25
- export function currentDate() {
26
- return new Date().toUTCString();
27
- }
28
-
29
- export function buildPayloadB64(info) {
30
- const m = JSON.stringify({
31
- cosyVersion: VERSION,
32
- ideVersion: "",
33
- info,
34
- requestId: newUUID(),
35
- version: "v1",
36
- });
37
- return Buffer.from(m).toString("base64");
38
- }
39
-
40
- export function signRequest(payloadB64, cosyKey, cosyDate, body, pathWithoutAlgo) {
41
- const s = payloadB64 + "\n" + cosyKey + "\n" + cosyDate + "\n" + body + "\n" + pathWithoutAlgo;
42
- return createHash("md5").update(s).digest("hex");
43
- }
44
-
45
- export function composeBearer(payloadB64, sig) {
46
- return "Bearer COSY." + payloadB64 + "." + sig;
47
- }
48
-
49
- export function psfRsaEncrypt(plaintext) {
50
- return publicEncrypt(
51
- { key: SERVER_PUB_KEY, padding: 1 }, // PKCS1v15
52
- plaintext
53
- );
54
- }
55
-
56
- function aesEncrypt(plain, key) {
57
- const bs = 16;
58
- const pad = bs - (plain.length % bs);
59
- const padded = Buffer.alloc(plain.length + pad, pad);
60
- plain.copy(padded);
61
- const iv = key.slice(0, bs);
62
- const cipher = createCipheriv("aes-128-cbc", key, iv);
63
- return Buffer.concat([cipher.update(padded), cipher.final()]);
64
- }
65
-
66
- export function authPayloadJSON(id) {
67
- return JSON.stringify({
68
- name: id.name || "",
69
- aid: id.aid || "",
70
- uid: id.uid || "",
71
- yx_uid: id.yxUid || "",
72
- organization_id: id.organizationId || "",
73
- organization_name: id.organizationName || "",
74
- user_type: id.userType || "personal_standard",
75
- security_oauth_token: id.securityOauthToken || "",
76
- refresh_token: id.refreshToken || "",
77
- });
78
- }
79
-
80
- export function newSession(id, machineId, machineToken, machineType) {
81
- const raw = randomBytes(16);
82
- const tempKey = Buffer.from(raw.toString("hex").slice(0, 16), "utf8").slice(0, 16);
83
- // 确保正好 16 字节
84
- const key16 = Buffer.alloc(16);
85
- tempKey.copy(key16);
86
-
87
- const cosyKeyBytes = psfRsaEncrypt(key16);
88
- const cosyKey = cosyKeyBytes.toString("base64");
89
-
90
- const payloadBytes = Buffer.from(authPayloadJSON(id), "utf8");
91
- const encPayload = aesEncrypt(payloadBytes, key16);
92
- const info = encPayload.toString("base64");
93
-
94
- return { tempKey: key16, cosyKey, info, identity: id, machineId, machineToken, machineType };
95
- }
96
-
97
- // 去除 /algo 前缀的 PathSig
98
- export function pathSigFrom(url) {
99
- try {
100
- const u = new URL(url);
101
- let p = u.pathname;
102
- if (p.startsWith("/algo")) p = p.slice(5);
103
- return p;
104
- } catch {
105
- return "";
106
- }
107
- }
108
-
109
- // 构造请求头
110
- export function buildCosyHeaders(sess, pathSig, bodyStr, accept) {
111
- const payloadB64 = buildPayloadB64(sess.info);
112
- const date = String(unixSec());
113
- const sig = signRequest(payloadB64, sess.cosyKey, date, bodyStr || "", pathSig);
114
- const bearer = composeBearer(payloadB64, sig);
115
- return {
116
- "cosy-data-policy": "agree",
117
- "content-type": "application/json",
118
- "cosy-machinetype": sess.machineType,
119
- "cosy-clienttype": "5",
120
- "cosy-date": date,
121
- "cosy-user": sess.identity.uid,
122
- "cosy-key": sess.cosyKey,
123
- "cache-control": "no-cache",
124
- "accept": accept || "application/json",
125
- "authorization": bearer,
126
- "cosy-version": VERSION,
127
- "cosy-machineid": sess.machineId,
128
- "cosy-machinetoken": sess.machineToken,
129
- "login-version": "v2",
130
- "user-agent": "Go-http-client/2.0",
131
- "cosy-scene": "assistant",
132
- "cosy-business-product": "ide",
133
- "cosy-business-type": "agent",
134
- };
1
+ // qoder COSY 签名与会话(转译 qoder2api internal/cosy/session.go + signature.go + auth.go)
2
+ // 会话:RSA-OAEP 加密 tempKey + AES-CBC 加密身份 payload → cosyKey + info
3
+ // 签名:md5(payloadB64 + "\n" + cosyKey + "\n" + date + "\n" + body + "\n" + pathSig)
4
+ import { createHash, randomBytes, createCipheriv, publicEncrypt } from "node:crypto";
5
+ import { cosyEncode } from "./encode.js";
6
+ import { newUUID, unixSec } from "./fingerprint.js";
7
+
8
+ export const APP_CODE = "cosy";
9
+ export const VERSION = "1.0.10";
10
+ // base64("war, war never changes")
11
+ const SECRET_B64 = "d2FyLCB3YXIgbmV2ZXIgY2hhbmdlcw==";
12
+
13
+ const SERVER_PUB_KEY = `-----BEGIN PUBLIC KEY-----
14
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
15
+ 4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
16
+ 6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
17
+ XcW+ML9FoCI6AOvOzwIDAQAB
18
+ -----END PUBLIC KEY-----`;
19
+
20
+ export function signLegacy(date) {
21
+ const s = APP_CODE + "&" + SECRET_B64 + "&" + date;
22
+ return createHash("md5").update(s).digest("hex");
23
+ }
24
+
25
+ export function currentDate() {
26
+ return new Date().toUTCString();
27
+ }
28
+
29
+ export function buildPayloadB64(info) {
30
+ const m = JSON.stringify({
31
+ cosyVersion: VERSION,
32
+ ideVersion: "",
33
+ info,
34
+ requestId: newUUID(),
35
+ version: "v1",
36
+ });
37
+ return Buffer.from(m).toString("base64");
38
+ }
39
+
40
+ export function signRequest(payloadB64, cosyKey, cosyDate, body, pathWithoutAlgo) {
41
+ const s = payloadB64 + "\n" + cosyKey + "\n" + cosyDate + "\n" + body + "\n" + pathWithoutAlgo;
42
+ return createHash("md5").update(s).digest("hex");
43
+ }
44
+
45
+ export function composeBearer(payloadB64, sig) {
46
+ return "Bearer COSY." + payloadB64 + "." + sig;
47
+ }
48
+
49
+ export function psfRsaEncrypt(plaintext) {
50
+ return publicEncrypt(
51
+ { key: SERVER_PUB_KEY, padding: 1 }, // PKCS1v15
52
+ plaintext
53
+ );
54
+ }
55
+
56
+ function aesEncrypt(plain, key) {
57
+ const bs = 16;
58
+ const pad = bs - (plain.length % bs);
59
+ const padded = Buffer.alloc(plain.length + pad, pad);
60
+ plain.copy(padded);
61
+ const iv = key.slice(0, bs);
62
+ const cipher = createCipheriv("aes-128-cbc", key, iv);
63
+ return Buffer.concat([cipher.update(padded), cipher.final()]);
64
+ }
65
+
66
+ export function authPayloadJSON(id) {
67
+ return JSON.stringify({
68
+ name: id.name || "",
69
+ aid: id.aid || "",
70
+ uid: id.uid || "",
71
+ yx_uid: id.yxUid || "",
72
+ organization_id: id.organizationId || "",
73
+ organization_name: id.organizationName || "",
74
+ user_type: id.userType || "personal_standard",
75
+ security_oauth_token: id.securityOauthToken || "",
76
+ refresh_token: id.refreshToken || "",
77
+ });
78
+ }
79
+
80
+ export function newSession(id, machineId, machineToken, machineType) {
81
+ const raw = randomBytes(16);
82
+ const tempKey = Buffer.from(raw.toString("hex").slice(0, 16), "utf8").slice(0, 16);
83
+ // 确保正好 16 字节
84
+ const key16 = Buffer.alloc(16);
85
+ tempKey.copy(key16);
86
+
87
+ const cosyKeyBytes = psfRsaEncrypt(key16);
88
+ const cosyKey = cosyKeyBytes.toString("base64");
89
+
90
+ const payloadBytes = Buffer.from(authPayloadJSON(id), "utf8");
91
+ const encPayload = aesEncrypt(payloadBytes, key16);
92
+ const info = encPayload.toString("base64");
93
+
94
+ return { tempKey: key16, cosyKey, info, identity: id, machineId, machineToken, machineType };
95
+ }
96
+
97
+ // 去除 /algo 前缀的 PathSig
98
+ export function pathSigFrom(url) {
99
+ try {
100
+ const u = new URL(url);
101
+ let p = u.pathname;
102
+ if (p.startsWith("/algo")) p = p.slice(5);
103
+ return p;
104
+ } catch {
105
+ return "";
106
+ }
107
+ }
108
+
109
+ // 构造请求头
110
+ export function buildCosyHeaders(sess, pathSig, bodyStr, accept) {
111
+ const payloadB64 = buildPayloadB64(sess.info);
112
+ const date = String(unixSec());
113
+ const sig = signRequest(payloadB64, sess.cosyKey, date, bodyStr || "", pathSig);
114
+ const bearer = composeBearer(payloadB64, sig);
115
+ return {
116
+ "cosy-data-policy": "agree",
117
+ "content-type": "application/json",
118
+ "cosy-machinetype": sess.machineType,
119
+ "cosy-clienttype": "5",
120
+ "cosy-date": date,
121
+ "cosy-user": sess.identity.uid,
122
+ "cosy-key": sess.cosyKey,
123
+ "cache-control": "no-cache",
124
+ "accept": accept || "application/json",
125
+ "authorization": bearer,
126
+ "cosy-version": VERSION,
127
+ "cosy-machineid": sess.machineId,
128
+ "cosy-machinetoken": sess.machineToken,
129
+ "login-version": "v2",
130
+ "user-agent": "Go-http-client/2.0",
131
+ "cosy-scene": "assistant",
132
+ "cosy-business-product": "ide",
133
+ "cosy-business-type": "agent",
134
+ };
135
135
  }
@@ -1,38 +1,38 @@
1
- // qoder 同请求粘号:一个客户端请求内的多次上游调用(空转重试/换路重试)复用同一个号,
2
- // 只有该号进入冷却(401/403/429/5xx → ring.onError)才切到下一个号。
3
- // 背景:keyring 每次 next() 都前进一格,而 qoder 两个号分属 cn/global 两区(region 决定 URL),
4
- // 于是「重试」会把同一个请求甩到另一个区——排障时表现为"URL 莫名在切"。粘号把"切号"收回到
5
- // 唯一正当理由:该号已被冷却。无 scope(拿不到请求标识)时退化为原 round-robin,与旧行为一致。
6
- // Note: 切号的唯一正当理由是冷却(401/403/429/5xx)——空转等其它失败不换号,故选择器要能问 keyring "上次这个号还在冷却吗" — 见 .agents/notes/implemented/feature/2026-09-27-qoder-per-request-sticky-account.md
7
- export function createStickyPicker({ pick, isCooling = () => false, now = Date.now, ttlMs = 600_000, maxEntries = 200 } = {}) {
8
- const seen = new Map(); // scope → { key, at }
9
-
10
- function evict() {
11
- const t = now();
12
- for (const [scope, hit] of seen) if (t - hit.at >= ttlMs) seen.delete(scope);
13
- while (seen.size > maxEntries) seen.delete(seen.keys().next().value);
14
- }
15
-
16
- // onDecision(可选):把"这次为什么选它"回报给调用方 —— 决定必须可观测。
17
- // sticky=同请求复用 / new=新请求轮转选中 / switch=上次的号在冷却(或 TTL 过期)故换 / rr=无 scope 退回轮转
18
- return function pickSticky(scope, onDecision) {
19
- if (!scope) {
20
- onDecision?.("rr");
21
- return pick();
22
- }
23
- const hit = seen.get(scope);
24
- // 粘住:同一请求内复用上次的号,除非它已被冷却(那时必须换)
25
- if (hit && now() - hit.at < ttlMs && !isCooling(hit.key)) {
26
- hit.at = now();
27
- onDecision?.("sticky");
28
- return hit.key;
29
- }
30
- const key = pick();
31
- onDecision?.(hit ? "switch" : "new");
32
- if (key) {
33
- seen.set(scope, { key, at: now() });
34
- if (seen.size > maxEntries) evict();
35
- }
36
- return key;
37
- };
38
- }
1
+ // qoder 同请求粘号:一个客户端请求内的多次上游调用(空转重试/换路重试)复用同一个号,
2
+ // 只有该号进入冷却(401/403/429/5xx → ring.onError)才切到下一个号。
3
+ // 背景:keyring 每次 next() 都前进一格,而 qoder 两个号分属 cn/global 两区(region 决定 URL),
4
+ // 于是「重试」会把同一个请求甩到另一个区——排障时表现为"URL 莫名在切"。粘号把"切号"收回到
5
+ // 唯一正当理由:该号已被冷却。无 scope(拿不到请求标识)时退化为原 round-robin,与旧行为一致。
6
+ // Note: 切号的唯一正当理由是冷却(401/403/429/5xx)——空转等其它失败不换号,故选择器要能问 keyring "上次这个号还在冷却吗" — 见 .agents/notes/implemented/feature/2026-09-27-qoder-per-request-sticky-account.md
7
+ export function createStickyPicker({ pick, isCooling = () => false, now = Date.now, ttlMs = 600_000, maxEntries = 200 } = {}) {
8
+ const seen = new Map(); // scope → { key, at }
9
+
10
+ function evict() {
11
+ const t = now();
12
+ for (const [scope, hit] of seen) if (t - hit.at >= ttlMs) seen.delete(scope);
13
+ while (seen.size > maxEntries) seen.delete(seen.keys().next().value);
14
+ }
15
+
16
+ // onDecision(可选):把"这次为什么选它"回报给调用方 —— 决定必须可观测。
17
+ // sticky=同请求复用 / new=新请求轮转选中 / switch=上次的号在冷却(或 TTL 过期)故换 / rr=无 scope 退回轮转
18
+ return function pickSticky(scope, onDecision) {
19
+ if (!scope) {
20
+ onDecision?.("rr");
21
+ return pick();
22
+ }
23
+ const hit = seen.get(scope);
24
+ // 粘住:同一请求内复用上次的号,除非它已被冷却(那时必须换)
25
+ if (hit && now() - hit.at < ttlMs && !isCooling(hit.key)) {
26
+ hit.at = now();
27
+ onDecision?.("sticky");
28
+ return hit.key;
29
+ }
30
+ const key = pick();
31
+ onDecision?.(hit ? "switch" : "new");
32
+ if (key) {
33
+ seen.set(scope, { key, at: now() });
34
+ if (seen.size > maxEntries) evict();
35
+ }
36
+ return key;
37
+ };
38
+ }
@@ -1,116 +1,116 @@
1
- // qoder 真流式转发:上游 Response.body → OpenAI SSE Response(边收边转,不攒数组)。
2
- // 对标 traework chat.js reshapeSoloStream(reader.read 循环 + sendChunk 即时写)。
3
- // usage 只在尾帧出现:收到即记,附在 finish chunk 上一次发出(与旧回放语义一致)。
4
- import { extractDelta } from "./sse.js";
5
-
6
- // SSE 缓冲上限:未完结行超 2MiB 立即中断(内存保护,对齐外部参照实现 defaultMaxSseBufferChars)
7
- const MAX_BUFFER_CHARS = 2 * 1024 * 1024;
8
-
9
- // 空轮排障用的临时日志开关:QODER_DEBUG_STREAM=1 才输出,默认静默(问题定位后可摘)
10
- const DBG_STREAM = () => process.env.QODER_DEBUG_STREAM === "1";
11
- const sdbg = (...a) => { if (DBG_STREAM()) console.log("[qoder-stream]", ...a); };
12
- function openaiChunk({ model, chatId, delta, finish, usage }) {
13
- const c = {
14
- id: chatId,
15
- object: "chat.completion.chunk",
16
- created: Math.floor(Date.now() / 1000),
17
- model,
18
- choices: [{ index: 0, delta, finish_reason: finish || null }],
19
- };
20
- if (usage && finish) c.usage = usage;
21
- return `data: ${JSON.stringify(c)}\n\n`;
22
- }
23
-
24
- function errorEvent(e) {
25
- return `event: error\ndata: ${JSON.stringify({ message: String(e?.detail || e?.message || e), type: e?.kind || "upstream_error" })}\n\n`;
26
- }
27
-
28
- export function reshapeQoderStream(upstreamRes, { model, chatId, prefetched = [] }) {
29
- const src = upstreamRes.body;
30
- const enc = new TextEncoder();
31
- const dec = new TextDecoder();
32
- // 预读帧(chat.js 为取流内判决而先读的字节)原样回灌:既不丢内容也不重复,
33
- // 判决帧会在这里被正常解析并抛错,同时 chat.js 已用同一批帧定出状态码挂上门面。
34
- let buf = prefetched.map((b) => dec.decode(b, { stream: true })).join("");
35
- let usage = null;
36
- // 收尾判定与 aggregate 同构:上游给过 finish_reason 一律优先,全程没给才兜底(tool_calls > stop)
37
- let upstreamFinish = "";
38
- let sawToolCalls = false;
39
- let sawContent = false;
40
- let finishSent = false;
41
- const stream = new ReadableStream({
42
- async start(ctrl) {
43
- const reader = src.getReader();
44
- const send = (t) => ctrl.enqueue(enc.encode(t));
45
- let chunkIdx = 0;
46
- let contentLen = 0;
47
- let lineIdx = 0;
48
- try {
49
- sdbg(`[enter] model=${model} chatId=${chatId} ct=${upstreamRes.headers.get("content-type")} status=${upstreamRes.status}`);
50
- // role: assistant will be attached on first real delta to avoid premature payload commit
51
- for (;;) {
52
- const { done, value } = await reader.read();
53
- if (value) {
54
- const text = dec.decode(value, { stream: !done });
55
- buf += text;
56
- sdbg(`[chunk#${chunkIdx++}] bytes=${value.byteLength} done=${done} decodedLen=${text.length} raw=${JSON.stringify(text.slice(0, 500))}`);
57
- }
58
- const lines = buf.split("\n");
59
- buf = lines.pop();
60
- sdbg(`[buf] lines=${lines.length} leftover=${JSON.stringify(buf.slice(0, 200))}`);
61
- for (const raw of lines) {
62
- const line = raw.replace(/\r$/, "");
63
- lineIdx++;
64
- if (!line.startsWith("data:")) { sdbg(`[line#${lineIdx}] non-data: ${JSON.stringify(line.slice(0, 200))}`); continue; }
65
- const payload = line.slice(5).trim();
66
- if (!payload || payload === "[DONE]") { sdbg(`[line#${lineIdx}] skip: ${JSON.stringify(payload)}`); continue; }
67
- const d = extractDelta(payload);
68
- sdbg(`[line#${lineIdx}] payload=${JSON.stringify(payload.slice(0, 800))}`);
69
- sdbg(`[line#${lineIdx}] parsed keys=${Object.keys(d).join(",")} content=${JSON.stringify((d.content || "").slice(0, 200))} reasoning=${JSON.stringify((d.reasoning || "").slice(0, 200))} toolCalls=${(d.toolCalls || []).length} usageIn=${d.usageIn} usageOut=${d.usageOut} err=${!!d.err}`);
70
- if (d.err) { sdbg(`[line#${lineIdx}] ERROR from extractDelta: ${JSON.stringify(d.err)}`); throw d.err; }
71
- if (d.usageIn || d.usageOut) {
72
- usage = { prompt_tokens: d.usageIn, completion_tokens: d.usageOut, total_tokens: d.usageIn + d.usageOut };
73
- sdbg(`[usage] ${JSON.stringify(usage)}`);
74
- }
75
- if (d.finishReason) upstreamFinish = d.finishReason;
76
- const delta = {};
77
- if (!sawContent) delta.role = "assistant";
78
- if (d.content) { delta.content = d.content; contentLen += d.content.length; sawContent = true; }
79
- if (d.reasoning) { delta.reasoning_content = d.reasoning; sawContent = true; }
80
- if (d.toolCalls) { delta.tool_calls = d.toolCalls; sawToolCalls = true; sawContent = true; }
81
- if (Object.keys(delta).length) send(openaiChunk({ model, chatId, delta }));
82
- sdbg(`[acc] line#=${lineIdx} sawContent=${sawContent} contentLen=${contentLen} upstreamFinish=${upstreamFinish} usage=${JSON.stringify(usage)}`);
83
- }
84
- // 完整行消费完毕后再判越限:leftover 超大不得把同 chunk 里的合法帧连坐丢弃(评审路1 P1#4)
85
- if (buf.length > MAX_BUFFER_CHARS) {
86
- sdbg(`[buffer-limit] leftover=${buf.length} chars → abort stream`);
87
- throw { kind: "upstream", status: 502, detail: "qoder SSE buffer exceeded 2MiB limit" };
88
- }
89
- if (done) break;
90
- }
91
- sdbg(`[end] totalChunks=${chunkIdx} totalLines=${lineIdx} sawContent=${sawContent} contentLen=${contentLen} upstreamFinish=${upstreamFinish} usage=${JSON.stringify(usage)}`);
92
- if (!sawContent && !usage) {
93
- sdbg(`[empty-stream] sawContent=false usage=null → throw 502 empty upstream stream`);
94
- throw { kind: "upstream", status: 502, detail: "empty upstream stream" };
95
- }
96
- const finish = upstreamFinish || (sawToolCalls ? "tool_calls" : "stop");
97
- send(openaiChunk({ model, chatId, delta: {}, finish, usage }));
98
- send("data: [DONE]\n\n");
99
- finishSent = true;
100
- } catch (e) {
101
- sdbg(`[catch] finishSent=${finishSent} e=${JSON.stringify(e?.message || e)}`);
102
- if (!finishSent) {
103
- if (sawContent) { try { send(openaiChunk({ model, chatId, delta: {} })); } catch {} }
104
- try { send(errorEvent(e)); } catch {}
105
- try { send("data: [DONE]\n\n"); } catch {}
106
- finishSent = true;
107
- }
108
- }
109
- try { ctrl.close(); } catch {}
110
- },
111
- });
112
- return new Response(stream, {
113
- status: 200,
114
- headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
115
- });
116
- }
1
+ // qoder 真流式转发:上游 Response.body → OpenAI SSE Response(边收边转,不攒数组)。
2
+ // 对标 traework chat.js reshapeSoloStream(reader.read 循环 + sendChunk 即时写)。
3
+ // usage 只在尾帧出现:收到即记,附在 finish chunk 上一次发出(与旧回放语义一致)。
4
+ import { extractDelta } from "./sse.js";
5
+
6
+ // SSE 缓冲上限:未完结行超 2MiB 立即中断(内存保护,对齐外部参照实现 defaultMaxSseBufferChars)
7
+ const MAX_BUFFER_CHARS = 2 * 1024 * 1024;
8
+
9
+ // 空轮排障用的临时日志开关:QODER_DEBUG_STREAM=1 才输出,默认静默(问题定位后可摘)
10
+ const DBG_STREAM = () => process.env.QODER_DEBUG_STREAM === "1";
11
+ const sdbg = (...a) => { if (DBG_STREAM()) console.log("[qoder-stream]", ...a); };
12
+ function openaiChunk({ model, chatId, delta, finish, usage }) {
13
+ const c = {
14
+ id: chatId,
15
+ object: "chat.completion.chunk",
16
+ created: Math.floor(Date.now() / 1000),
17
+ model,
18
+ choices: [{ index: 0, delta, finish_reason: finish || null }],
19
+ };
20
+ if (usage && finish) c.usage = usage;
21
+ return `data: ${JSON.stringify(c)}\n\n`;
22
+ }
23
+
24
+ function errorEvent(e) {
25
+ return `event: error\ndata: ${JSON.stringify({ message: String(e?.detail || e?.message || e), type: e?.kind || "upstream_error" })}\n\n`;
26
+ }
27
+
28
+ export function reshapeQoderStream(upstreamRes, { model, chatId, prefetched = [] }) {
29
+ const src = upstreamRes.body;
30
+ const enc = new TextEncoder();
31
+ const dec = new TextDecoder();
32
+ // 预读帧(chat.js 为取流内判决而先读的字节)原样回灌:既不丢内容也不重复,
33
+ // 判决帧会在这里被正常解析并抛错,同时 chat.js 已用同一批帧定出状态码挂上门面。
34
+ let buf = prefetched.map((b) => dec.decode(b, { stream: true })).join("");
35
+ let usage = null;
36
+ // 收尾判定与 aggregate 同构:上游给过 finish_reason 一律优先,全程没给才兜底(tool_calls > stop)
37
+ let upstreamFinish = "";
38
+ let sawToolCalls = false;
39
+ let sawContent = false;
40
+ let finishSent = false;
41
+ const stream = new ReadableStream({
42
+ async start(ctrl) {
43
+ const reader = src.getReader();
44
+ const send = (t) => ctrl.enqueue(enc.encode(t));
45
+ let chunkIdx = 0;
46
+ let contentLen = 0;
47
+ let lineIdx = 0;
48
+ try {
49
+ sdbg(`[enter] model=${model} chatId=${chatId} ct=${upstreamRes.headers.get("content-type")} status=${upstreamRes.status}`);
50
+ // role: assistant will be attached on first real delta to avoid premature payload commit
51
+ for (;;) {
52
+ const { done, value } = await reader.read();
53
+ if (value) {
54
+ const text = dec.decode(value, { stream: !done });
55
+ buf += text;
56
+ sdbg(`[chunk#${chunkIdx++}] bytes=${value.byteLength} done=${done} decodedLen=${text.length} raw=${JSON.stringify(text.slice(0, 500))}`);
57
+ }
58
+ const lines = buf.split("\n");
59
+ buf = lines.pop();
60
+ sdbg(`[buf] lines=${lines.length} leftover=${JSON.stringify(buf.slice(0, 200))}`);
61
+ for (const raw of lines) {
62
+ const line = raw.replace(/\r$/, "");
63
+ lineIdx++;
64
+ if (!line.startsWith("data:")) { sdbg(`[line#${lineIdx}] non-data: ${JSON.stringify(line.slice(0, 200))}`); continue; }
65
+ const payload = line.slice(5).trim();
66
+ if (!payload || payload === "[DONE]") { sdbg(`[line#${lineIdx}] skip: ${JSON.stringify(payload)}`); continue; }
67
+ const d = extractDelta(payload);
68
+ sdbg(`[line#${lineIdx}] payload=${JSON.stringify(payload.slice(0, 800))}`);
69
+ sdbg(`[line#${lineIdx}] parsed keys=${Object.keys(d).join(",")} content=${JSON.stringify((d.content || "").slice(0, 200))} reasoning=${JSON.stringify((d.reasoning || "").slice(0, 200))} toolCalls=${(d.toolCalls || []).length} usageIn=${d.usageIn} usageOut=${d.usageOut} err=${!!d.err}`);
70
+ if (d.err) { sdbg(`[line#${lineIdx}] ERROR from extractDelta: ${JSON.stringify(d.err)}`); throw d.err; }
71
+ if (d.usageIn || d.usageOut) {
72
+ usage = { prompt_tokens: d.usageIn, completion_tokens: d.usageOut, total_tokens: d.usageIn + d.usageOut };
73
+ sdbg(`[usage] ${JSON.stringify(usage)}`);
74
+ }
75
+ if (d.finishReason) upstreamFinish = d.finishReason;
76
+ const delta = {};
77
+ if (!sawContent) delta.role = "assistant";
78
+ if (d.content) { delta.content = d.content; contentLen += d.content.length; sawContent = true; }
79
+ if (d.reasoning) { delta.reasoning_content = d.reasoning; sawContent = true; }
80
+ if (d.toolCalls) { delta.tool_calls = d.toolCalls; sawToolCalls = true; sawContent = true; }
81
+ if (Object.keys(delta).length) send(openaiChunk({ model, chatId, delta }));
82
+ sdbg(`[acc] line#=${lineIdx} sawContent=${sawContent} contentLen=${contentLen} upstreamFinish=${upstreamFinish} usage=${JSON.stringify(usage)}`);
83
+ }
84
+ // 完整行消费完毕后再判越限:leftover 超大不得把同 chunk 里的合法帧连坐丢弃(评审路1 P1#4)
85
+ if (buf.length > MAX_BUFFER_CHARS) {
86
+ sdbg(`[buffer-limit] leftover=${buf.length} chars → abort stream`);
87
+ throw { kind: "upstream", status: 502, detail: "qoder SSE buffer exceeded 2MiB limit" };
88
+ }
89
+ if (done) break;
90
+ }
91
+ sdbg(`[end] totalChunks=${chunkIdx} totalLines=${lineIdx} sawContent=${sawContent} contentLen=${contentLen} upstreamFinish=${upstreamFinish} usage=${JSON.stringify(usage)}`);
92
+ if (!sawContent && !usage) {
93
+ sdbg(`[empty-stream] sawContent=false usage=null → throw 502 empty upstream stream`);
94
+ throw { kind: "upstream", status: 502, detail: "empty upstream stream" };
95
+ }
96
+ const finish = upstreamFinish || (sawToolCalls ? "tool_calls" : "stop");
97
+ send(openaiChunk({ model, chatId, delta: {}, finish, usage }));
98
+ send("data: [DONE]\n\n");
99
+ finishSent = true;
100
+ } catch (e) {
101
+ sdbg(`[catch] finishSent=${finishSent} e=${JSON.stringify(e?.message || e)}`);
102
+ if (!finishSent) {
103
+ if (sawContent) { try { send(openaiChunk({ model, chatId, delta: {} })); } catch {} }
104
+ try { send(errorEvent(e)); } catch {}
105
+ try { send("data: [DONE]\n\n"); } catch {}
106
+ finishSent = true;
107
+ }
108
+ }
109
+ try { ctrl.close(); } catch {}
110
+ },
111
+ });
112
+ return new Response(stream, {
113
+ status: 200,
114
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
115
+ });
116
+ }
@@ -1,2 +1,2 @@
1
- // Shim — 保持历史导入路径兼容,真实实现下沉至 ./qoder/ 深模块
1
+ // Shim — 保持历史导入路径兼容,真实实现下沉至 ./qoder/ 深模块
2
2
  export * from "./qoder/index.js";