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