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.
- package/README.md +3 -3
- package/bin/mslxdff.js +1 -1
- package/docs/cli_help_mini.md +146 -0
- package/package.json +38 -38
- package/plugins/README.md +14 -14
- package/plugins/prefer-model.mjs.example +14 -14
- package/src/anthropic/canonical-json.js +55 -0
- package/src/anthropic/count-tokens.js +61 -61
- package/src/anthropic/request-body.js +183 -126
- package/src/anthropic/request-shape.js +60 -0
- package/src/anthropic/session-id.js +58 -0
- package/src/anthropic/shared.js +18 -18
- package/src/anthropic/sse-aggregate.js +56 -56
- package/src/anthropic/tool-media.js +75 -0
- package/src/anthropic/translate.js +296 -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/index.js +8 -40
- package/src/chat-pipeline/serial-trial.js +279 -283
- package/src/chat-pipeline/terminal-verdict.js +132 -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 +158 -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 +71 -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 -254
- package/src/plugins.js +107 -107
- package/src/providers/AGENTS.md +49 -0
- 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 -180
- package/src/providers/raccoon/credits.js +185 -185
- package/src/providers/raccoon/crypto.js +43 -43
- 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 -102
- 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/AGENTS.md +37 -0
- 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 +93 -83
- package/src/routes/index.js +147 -147
- package/src/routes/messages-route.js +375 -365
- package/src/routes/peers.js +10 -2
- 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/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 -120
- 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/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/cache-fields.js +58 -0
- package/src/usage/record.js +109 -106
- package/src/usage/report.js +209 -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
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
// raccoon 流处理:上游就是 OpenAI 形(`/api/web/llm/v2/chat/completions`),
|
|
2
|
-
// 因此**不做协议转换**——本文件只做三件事:形状判定、错误信封分类、非流式聚合。
|
|
3
|
-
// 流式路径直接把上游 body 透传(逐字节),避免任何整形引入的失真。
|
|
4
|
-
import { createSseParser } from "../../transport/sse.js";
|
|
5
|
-
import { isRaccoonAuthFailure, parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 额度类措辞(业务码未取证,见 ADR-0050 决策 8):**宁可漏判也不误报**。
|
|
9
|
-
* 刻意不收裸 `insufficient`/`balance`/`points` —— 那些词会命中「权限不足」这类非额度错,
|
|
10
|
-
* 一旦误判成 quota 就会长冷 1h 并回 429 `quota_exhausted`,违反 ADR-0044 的「非额度原因不得谎报」。
|
|
11
|
-
*/
|
|
12
|
-
const QUOTA_HINTS = /(积分|额度|quota|欠费)/i;
|
|
13
|
-
|
|
14
|
-
export function isRaccoonSseContentType(value) {
|
|
15
|
-
return String(value || "").includes("text/event-stream");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isQuotaLike(envelope) {
|
|
19
|
-
const text = `${envelope?.message ?? ""} ${envelope?.details ?? ""}`;
|
|
20
|
-
if (QUOTA_HINTS.test(text)) return true;
|
|
21
|
-
return envelope?.status === 402;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 失败分类 → `{ kind, code, status, message }`。
|
|
26
|
-
* kind 取值:auth(重登)/ quota(长冷却)/ rate_limit(短冷却)/ server(上游 5xx,短冷却)/ unknown(4xx 如实透出)。
|
|
27
|
-
*/
|
|
28
|
-
export function classifyRaccoonFailure(status, payload) {
|
|
29
|
-
const envelope = parseRaccoonEnvelope(payload, status);
|
|
30
|
-
let kind = "unknown";
|
|
31
|
-
if (isRaccoonAuthFailure(envelope, status)) kind = "auth";
|
|
32
|
-
else if (isQuotaLike(envelope)) kind = "quota";
|
|
33
|
-
else if (status === 429) kind = "rate_limit";
|
|
34
|
-
else if (status >= 500) kind = "server";
|
|
35
|
-
return { kind, code: envelope.code, status, message: raccoonEnvelopeText(envelope, `HTTP ${status}`) };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const ERROR_MAP = {
|
|
39
|
-
auth: { status: 401, type: "auth_error", text: (f) => `raccoon: 登录态已失效${f.code ? `(code ${f.code})` : ""}。请运行 mslxdff -provider raccoon login 重新登录` },
|
|
40
|
-
quota: { status: 429, type: "quota_exhausted", text: (f) => `raccoon: 积分不足${f.message ? `(${f.message})` : ""}。请运行 mslxdff -provider raccoon checkin 领取每日积分,或更换账号` },
|
|
41
|
-
rate_limit: { status: 429, type: "rate_limited", text: () => "raccoon: 上游限流(账号冷却中),请稍后重试" },
|
|
42
|
-
server: { status: 502, type: "upstream_error", text: (f) => `raccoon: 上游服务异常${f.code ? `(code ${f.code})` : ""}${f.message ? `:${f.message}` : ""}` },
|
|
43
|
-
// unknown 只覆盖 4xx(5xx 已被上面分流成 server):按请求缺陷如实回 400,不谎报成网关故障
|
|
44
|
-
unknown: { status: 400, type: "invalid_request", text: (f) => `raccoon: 上游拒绝请求(HTTP ${f.status || "?"}${f.code ? ` code ${f.code}` : ""})${f.message ? `:${f.message}` : ""}` },
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/** 失败 → Response(带 `x-mslxdff-raccoon-kind`,供工厂决定冷却档位)。 */
|
|
48
|
-
export function raccoonErrorResponse(fail = {}) {
|
|
49
|
-
const kind = ERROR_MAP[fail.kind] ? fail.kind : "unknown";
|
|
50
|
-
const entry = ERROR_MAP[kind];
|
|
51
|
-
const headers = { "Content-Type": "application/json" };
|
|
52
|
-
if (fail.kind) headers["x-mslxdff-raccoon-kind"] = fail.kind;
|
|
53
|
-
return new Response(JSON.stringify({ error: { message: entry.text(fail), type: entry.type, code: fail.code || 0 } }), {
|
|
54
|
-
status: entry.status,
|
|
55
|
-
headers,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 工具调用增量合并(对齐 `qwenwork/sse.js` 的 mergeToolCallDelta:id/type 取一次,name/arguments 追加)。 */
|
|
60
|
-
function mergeRaccoonToolCallDelta(merged, delta) {
|
|
61
|
-
for (const key of ["id", "type"]) {
|
|
62
|
-
if (merged[key] === undefined && typeof delta[key] === "string" && delta[key] !== "") merged[key] = delta[key];
|
|
63
|
-
}
|
|
64
|
-
const dfn = delta.function;
|
|
65
|
-
if (!dfn || typeof dfn !== "object") return;
|
|
66
|
-
if (!merged.function || typeof merged.function !== "object") merged.function = {};
|
|
67
|
-
if (typeof dfn.name === "string" && dfn.name !== "") merged.function.name = (merged.function.name || "") + dfn.name;
|
|
68
|
-
if (typeof dfn.arguments === "string" && dfn.arguments !== "") {
|
|
69
|
-
merged.function.arguments = (merged.function.arguments || "") + dfn.arguments;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** 把 SSE 文本聚合成一条 OpenAI chat completion(非流式请求、以及「header 说是 JSON 但体是 SSE」时走这里)。 */
|
|
74
|
-
export async function aggregateRaccoonSse(text, { model } = {}) {
|
|
75
|
-
const parser = createSseParser();
|
|
76
|
-
// 聚合拿到的是**完整 body**(不是分片):补一个定界符,否则末尾缺 `\n\n` 的最后一帧会被当半包丢掉
|
|
77
|
-
const events = parser.push(`${String(text || "")}\n\n`);
|
|
78
|
-
const content = [];
|
|
79
|
-
const reasoning = [];
|
|
80
|
-
const toolCalls = new Map();
|
|
81
|
-
const toolOrder = [];
|
|
82
|
-
let finishReason = "stop";
|
|
83
|
-
let usage;
|
|
84
|
-
let id = "";
|
|
85
|
-
let created = 0;
|
|
86
|
-
for (const raw of events) {
|
|
87
|
-
if (raw === "[DONE]" || !raw) continue;
|
|
88
|
-
let payload;
|
|
89
|
-
try {
|
|
90
|
-
payload = JSON.parse(raw);
|
|
91
|
-
} catch {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
// 流内错误信封:优先按失败分类返回,不要静默当成空回复
|
|
95
|
-
if (payload?.code !== undefined && Number(payload.code) !== 0) {
|
|
96
|
-
return { error: classifyRaccoonFailure(200, payload) };
|
|
97
|
-
}
|
|
98
|
-
if (payload?.error) {
|
|
99
|
-
return { error: classifyRaccoonFailure(200, payload) };
|
|
100
|
-
}
|
|
101
|
-
if (!id && typeof payload?.id === "string") id = payload.id;
|
|
102
|
-
if (!created && Number.isFinite(payload?.created)) created = Number(payload.created);
|
|
103
|
-
if (payload?.usage) usage = payload.usage;
|
|
104
|
-
const choice = Array.isArray(payload?.choices) ? payload.choices[0] : undefined;
|
|
105
|
-
const delta = choice?.delta ?? choice?.message;
|
|
106
|
-
if (typeof delta?.content === "string" && delta.content.length > 0) content.push(delta.content);
|
|
107
|
-
if (typeof delta?.reasoning_content === "string" && delta.reasoning_content.length > 0) reasoning.push(delta.reasoning_content);
|
|
108
|
-
for (const call of Array.isArray(delta?.tool_calls) ? delta.tool_calls : []) {
|
|
109
|
-
if (!call || typeof call !== "object") continue;
|
|
110
|
-
const idx = typeof call.index === "number" ? call.index : 0;
|
|
111
|
-
if (!toolCalls.has(idx)) {
|
|
112
|
-
toolCalls.set(idx, { index: idx });
|
|
113
|
-
toolOrder.push(idx);
|
|
114
|
-
}
|
|
115
|
-
mergeRaccoonToolCallDelta(toolCalls.get(idx), call);
|
|
116
|
-
}
|
|
117
|
-
if (typeof choice?.finish_reason === "string" && choice.finish_reason.length > 0) finishReason = choice.finish_reason;
|
|
118
|
-
}
|
|
119
|
-
const message = { role: "assistant", content: content.join("") || null };
|
|
120
|
-
if (reasoning.length > 0) message.reasoning_content = reasoning.join("");
|
|
121
|
-
if (toolOrder.length > 0) {
|
|
122
|
-
toolOrder.sort((a, b) => a - b);
|
|
123
|
-
message.tool_calls = toolOrder.map((idx) => toolCalls.get(idx));
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
openAi: {
|
|
127
|
-
id: id || `chatcmpl-raccoon-${Date.now()}`,
|
|
128
|
-
object: "chat.completion",
|
|
129
|
-
created: created || Math.floor(Date.now() / 1000),
|
|
130
|
-
model: model || "",
|
|
131
|
-
choices: [{ index: 0, message, finish_reason: finishReason }],
|
|
132
|
-
...(usage ? { usage } : {}),
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
}
|
|
1
|
+
// raccoon 流处理:上游就是 OpenAI 形(`/api/web/llm/v2/chat/completions`),
|
|
2
|
+
// 因此**不做协议转换**——本文件只做三件事:形状判定、错误信封分类、非流式聚合。
|
|
3
|
+
// 流式路径直接把上游 body 透传(逐字节),避免任何整形引入的失真。
|
|
4
|
+
import { createSseParser } from "../../transport/sse.js";
|
|
5
|
+
import { isRaccoonAuthFailure, parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 额度类措辞(业务码未取证,见 ADR-0050 决策 8):**宁可漏判也不误报**。
|
|
9
|
+
* 刻意不收裸 `insufficient`/`balance`/`points` —— 那些词会命中「权限不足」这类非额度错,
|
|
10
|
+
* 一旦误判成 quota 就会长冷 1h 并回 429 `quota_exhausted`,违反 ADR-0044 的「非额度原因不得谎报」。
|
|
11
|
+
*/
|
|
12
|
+
const QUOTA_HINTS = /(积分|额度|quota|欠费)/i;
|
|
13
|
+
|
|
14
|
+
export function isRaccoonSseContentType(value) {
|
|
15
|
+
return String(value || "").includes("text/event-stream");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isQuotaLike(envelope) {
|
|
19
|
+
const text = `${envelope?.message ?? ""} ${envelope?.details ?? ""}`;
|
|
20
|
+
if (QUOTA_HINTS.test(text)) return true;
|
|
21
|
+
return envelope?.status === 402;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 失败分类 → `{ kind, code, status, message }`。
|
|
26
|
+
* kind 取值:auth(重登)/ quota(长冷却)/ rate_limit(短冷却)/ server(上游 5xx,短冷却)/ unknown(4xx 如实透出)。
|
|
27
|
+
*/
|
|
28
|
+
export function classifyRaccoonFailure(status, payload) {
|
|
29
|
+
const envelope = parseRaccoonEnvelope(payload, status);
|
|
30
|
+
let kind = "unknown";
|
|
31
|
+
if (isRaccoonAuthFailure(envelope, status)) kind = "auth";
|
|
32
|
+
else if (isQuotaLike(envelope)) kind = "quota";
|
|
33
|
+
else if (status === 429) kind = "rate_limit";
|
|
34
|
+
else if (status >= 500) kind = "server";
|
|
35
|
+
return { kind, code: envelope.code, status, message: raccoonEnvelopeText(envelope, `HTTP ${status}`) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ERROR_MAP = {
|
|
39
|
+
auth: { status: 401, type: "auth_error", text: (f) => `raccoon: 登录态已失效${f.code ? `(code ${f.code})` : ""}。请运行 mslxdff -provider raccoon login 重新登录` },
|
|
40
|
+
quota: { status: 429, type: "quota_exhausted", text: (f) => `raccoon: 积分不足${f.message ? `(${f.message})` : ""}。请运行 mslxdff -provider raccoon checkin 领取每日积分,或更换账号` },
|
|
41
|
+
rate_limit: { status: 429, type: "rate_limited", text: () => "raccoon: 上游限流(账号冷却中),请稍后重试" },
|
|
42
|
+
server: { status: 502, type: "upstream_error", text: (f) => `raccoon: 上游服务异常${f.code ? `(code ${f.code})` : ""}${f.message ? `:${f.message}` : ""}` },
|
|
43
|
+
// unknown 只覆盖 4xx(5xx 已被上面分流成 server):按请求缺陷如实回 400,不谎报成网关故障
|
|
44
|
+
unknown: { status: 400, type: "invalid_request", text: (f) => `raccoon: 上游拒绝请求(HTTP ${f.status || "?"}${f.code ? ` code ${f.code}` : ""})${f.message ? `:${f.message}` : ""}` },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** 失败 → Response(带 `x-mslxdff-raccoon-kind`,供工厂决定冷却档位)。 */
|
|
48
|
+
export function raccoonErrorResponse(fail = {}) {
|
|
49
|
+
const kind = ERROR_MAP[fail.kind] ? fail.kind : "unknown";
|
|
50
|
+
const entry = ERROR_MAP[kind];
|
|
51
|
+
const headers = { "Content-Type": "application/json" };
|
|
52
|
+
if (fail.kind) headers["x-mslxdff-raccoon-kind"] = fail.kind;
|
|
53
|
+
return new Response(JSON.stringify({ error: { message: entry.text(fail), type: entry.type, code: fail.code || 0 } }), {
|
|
54
|
+
status: entry.status,
|
|
55
|
+
headers,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 工具调用增量合并(对齐 `qwenwork/sse.js` 的 mergeToolCallDelta:id/type 取一次,name/arguments 追加)。 */
|
|
60
|
+
function mergeRaccoonToolCallDelta(merged, delta) {
|
|
61
|
+
for (const key of ["id", "type"]) {
|
|
62
|
+
if (merged[key] === undefined && typeof delta[key] === "string" && delta[key] !== "") merged[key] = delta[key];
|
|
63
|
+
}
|
|
64
|
+
const dfn = delta.function;
|
|
65
|
+
if (!dfn || typeof dfn !== "object") return;
|
|
66
|
+
if (!merged.function || typeof merged.function !== "object") merged.function = {};
|
|
67
|
+
if (typeof dfn.name === "string" && dfn.name !== "") merged.function.name = (merged.function.name || "") + dfn.name;
|
|
68
|
+
if (typeof dfn.arguments === "string" && dfn.arguments !== "") {
|
|
69
|
+
merged.function.arguments = (merged.function.arguments || "") + dfn.arguments;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 把 SSE 文本聚合成一条 OpenAI chat completion(非流式请求、以及「header 说是 JSON 但体是 SSE」时走这里)。 */
|
|
74
|
+
export async function aggregateRaccoonSse(text, { model } = {}) {
|
|
75
|
+
const parser = createSseParser();
|
|
76
|
+
// 聚合拿到的是**完整 body**(不是分片):补一个定界符,否则末尾缺 `\n\n` 的最后一帧会被当半包丢掉
|
|
77
|
+
const events = parser.push(`${String(text || "")}\n\n`);
|
|
78
|
+
const content = [];
|
|
79
|
+
const reasoning = [];
|
|
80
|
+
const toolCalls = new Map();
|
|
81
|
+
const toolOrder = [];
|
|
82
|
+
let finishReason = "stop";
|
|
83
|
+
let usage;
|
|
84
|
+
let id = "";
|
|
85
|
+
let created = 0;
|
|
86
|
+
for (const raw of events) {
|
|
87
|
+
if (raw === "[DONE]" || !raw) continue;
|
|
88
|
+
let payload;
|
|
89
|
+
try {
|
|
90
|
+
payload = JSON.parse(raw);
|
|
91
|
+
} catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// 流内错误信封:优先按失败分类返回,不要静默当成空回复
|
|
95
|
+
if (payload?.code !== undefined && Number(payload.code) !== 0) {
|
|
96
|
+
return { error: classifyRaccoonFailure(200, payload) };
|
|
97
|
+
}
|
|
98
|
+
if (payload?.error) {
|
|
99
|
+
return { error: classifyRaccoonFailure(200, payload) };
|
|
100
|
+
}
|
|
101
|
+
if (!id && typeof payload?.id === "string") id = payload.id;
|
|
102
|
+
if (!created && Number.isFinite(payload?.created)) created = Number(payload.created);
|
|
103
|
+
if (payload?.usage) usage = payload.usage;
|
|
104
|
+
const choice = Array.isArray(payload?.choices) ? payload.choices[0] : undefined;
|
|
105
|
+
const delta = choice?.delta ?? choice?.message;
|
|
106
|
+
if (typeof delta?.content === "string" && delta.content.length > 0) content.push(delta.content);
|
|
107
|
+
if (typeof delta?.reasoning_content === "string" && delta.reasoning_content.length > 0) reasoning.push(delta.reasoning_content);
|
|
108
|
+
for (const call of Array.isArray(delta?.tool_calls) ? delta.tool_calls : []) {
|
|
109
|
+
if (!call || typeof call !== "object") continue;
|
|
110
|
+
const idx = typeof call.index === "number" ? call.index : 0;
|
|
111
|
+
if (!toolCalls.has(idx)) {
|
|
112
|
+
toolCalls.set(idx, { index: idx });
|
|
113
|
+
toolOrder.push(idx);
|
|
114
|
+
}
|
|
115
|
+
mergeRaccoonToolCallDelta(toolCalls.get(idx), call);
|
|
116
|
+
}
|
|
117
|
+
if (typeof choice?.finish_reason === "string" && choice.finish_reason.length > 0) finishReason = choice.finish_reason;
|
|
118
|
+
}
|
|
119
|
+
const message = { role: "assistant", content: content.join("") || null };
|
|
120
|
+
if (reasoning.length > 0) message.reasoning_content = reasoning.join("");
|
|
121
|
+
if (toolOrder.length > 0) {
|
|
122
|
+
toolOrder.sort((a, b) => a - b);
|
|
123
|
+
message.tool_calls = toolOrder.map((idx) => toolCalls.get(idx));
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
openAi: {
|
|
127
|
+
id: id || `chatcmpl-raccoon-${Date.now()}`,
|
|
128
|
+
object: "chat.completion",
|
|
129
|
+
created: created || Math.floor(Date.now() / 1000),
|
|
130
|
+
model: model || "",
|
|
131
|
+
choices: [{ index: 0, message, finish_reason: finishReason }],
|
|
132
|
+
...(usage ? { usage } : {}),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
// 通用(带 key)供应商的 responses 通道:muse-spark* 这类模型上游只挂 /responses,
|
|
2
|
-
// 打同 host 的 /chat/completions 会 503 "Endpoint is unavailable"(ocgo 实测 2026-09-22)。
|
|
3
|
-
// 判定单一来源 = isResponsesModel(models.dev 模型级 provider.npm + muse-spark 前缀兜底),
|
|
4
|
-
// 与 GET /v1/models 的 capabilities.upstreamApi 同源,新模型无需改码。
|
|
5
|
-
//
|
|
6
|
-
// 契约:run() 的出参恒为 **chat 形状**(SSE 或 JSON),调用方无需再判分支。
|
|
7
|
-
// · 流式优先 @ai-sdk/openai 的 responses 适配器(含加密思考往返,ADR-0017 复用);
|
|
8
|
-
// · SDK 不可用 / 非流式 → 原生 fetch + chatToResponsesBody 正转换 + 反向整形。
|
|
9
|
-
// key 轮换、退避重试、_t 计时与 createChatRunner(base.js)同语义,避免两条通道行为漂移。
|
|
10
|
-
import { chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "../upstream-responses.js";
|
|
11
|
-
import { attemptOnceResponsesSdk } from "../upstream-engine/sdk/responses.js";
|
|
12
|
-
import { ENGINE_MARKER } from "../upstream-engine/sdk/chat.js";
|
|
13
|
-
import { sleep } from "./base.js";
|
|
14
|
-
|
|
15
|
-
/** 供应商 id → env 片段(`my-api` → `MY_API`),与 providerKeyEnv 同规则 */
|
|
16
|
-
export function envSlug(id) {
|
|
17
|
-
return String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* responses 端点路径:显式入参 > env `MSLXDFF_<ID>_RESPONSES_PATH` > 缺省 `/responses`。
|
|
22
|
-
* 归一为以 `/` 开头(joinUrl 会再拼 baseUrl)。
|
|
23
|
-
*/
|
|
24
|
-
export function resolveResponsesPath(id, responsesPath) {
|
|
25
|
-
const raw = responsesPath || process.env[`MSLXDFF_${envSlug(id)}_RESPONSES_PATH`] || "";
|
|
26
|
-
const s = String(raw).trim();
|
|
27
|
-
if (!s) return "/responses";
|
|
28
|
-
return s.startsWith("/") ? s : `/${s}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 建 responses 通道。
|
|
33
|
-
* @param {object} o
|
|
34
|
-
* @param {string} o.id 供应商 id(用于报错文案与 providerName)
|
|
35
|
-
* @param {string} o.url 已拼好的 responses 绝对 URL
|
|
36
|
-
* @param {number} o.connectTimeoutMs 单次尝试超时
|
|
37
|
-
* @param {object} o.retry 重试表(与 createChatRunner 同形状:network/429/50x → {attempts, delayMs})
|
|
38
|
-
* @param {number} o.cooldownMs 全 key 冷却时的人话报错文案用
|
|
39
|
-
* @param {boolean} o.sdkEnabled 是否允许走 AI SDK responses 适配器(流式路径)
|
|
40
|
-
* @param {Function} o.buildHeaders (body, key, opts) → headers
|
|
41
|
-
* @param {Function} o.fetchImpl 注入的 fetch(连接池/测试接缝)
|
|
42
|
-
* @param {object} [o.dispatcher] undici Agent
|
|
43
|
-
*/
|
|
44
|
-
export function createResponsesChannel({
|
|
45
|
-
id,
|
|
46
|
-
url,
|
|
47
|
-
connectTimeoutMs = 30_000,
|
|
48
|
-
retry = {},
|
|
49
|
-
cooldownMs = 30_000,
|
|
50
|
-
sdkEnabled = true,
|
|
51
|
-
buildHeaders,
|
|
52
|
-
fetchImpl,
|
|
53
|
-
dispatcher = null,
|
|
54
|
-
} = {}) {
|
|
55
|
-
async function attemptOnce(body, key, opts) {
|
|
56
|
-
const headers = buildHeaders ? buildHeaders(body, key, opts) : {};
|
|
57
|
-
const wantsStream = body?.stream !== false;
|
|
58
|
-
if (sdkEnabled && wantsStream) {
|
|
59
|
-
try {
|
|
60
|
-
const r = await attemptOnceResponsesSdk({
|
|
61
|
-
url, body, headers, providerName: id, marker: ENGINE_MARKER, fetchImpl,
|
|
62
|
-
});
|
|
63
|
-
if (r) return r; // 适配器已产出 chat SSE
|
|
64
|
-
} catch (e) {
|
|
65
|
-
if (!e || (!e._sdkLoadFailed && !e._sdkUnsupported)) throw e;
|
|
66
|
-
// SDK 装载失败 / URL 异形 → 落原生兜底(保底不变差)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const controller = new AbortController();
|
|
70
|
-
const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
|
|
71
|
-
try {
|
|
72
|
-
const fetchOpts = { method: "POST", headers, body: JSON.stringify(chatToResponsesBody(body)), signal: controller.signal };
|
|
73
|
-
if (dispatcher) fetchOpts.dispatcher = dispatcher;
|
|
74
|
-
const res = await fetchImpl(url, fetchOpts);
|
|
75
|
-
if (!res.ok) return res;
|
|
76
|
-
if (wantsStream) return reshapeResponsesSse(res, body?.model);
|
|
77
|
-
const json = await res.json().catch(() => null);
|
|
78
|
-
return json ? toChatResponse(res, json) : res;
|
|
79
|
-
} finally {
|
|
80
|
-
clearTimeout(timer);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** key 轮换 + 退避重试 + _t 计时;语义对齐 base.js 的 createChatRunner.runChat */
|
|
85
|
-
async function run(body, activeRing, sourceKey, opts) {
|
|
86
|
-
const t0 = performance.now();
|
|
87
|
-
const attempts = [];
|
|
88
|
-
let waitMs = 0;
|
|
89
|
-
const key = activeRing.next();
|
|
90
|
-
if (!key && activeRing.size > 0) {
|
|
91
|
-
const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
|
|
92
|
-
err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
|
|
93
|
-
throw err;
|
|
94
|
-
}
|
|
95
|
-
for (let attempt = 0; ; attempt++) {
|
|
96
|
-
const t = performance.now();
|
|
97
|
-
let result;
|
|
98
|
-
try {
|
|
99
|
-
result = await attemptOnce(body, key, opts);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
result = err;
|
|
102
|
-
}
|
|
103
|
-
attempts.push({ attempt, type: result instanceof Error ? "network" : `http${result?.status}`, ms: Math.round(performance.now() - t) });
|
|
104
|
-
if (result instanceof Error) {
|
|
105
|
-
const entry = retry?.network;
|
|
106
|
-
if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
|
|
107
|
-
activeRing.onError(key);
|
|
108
|
-
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
109
|
-
throw result;
|
|
110
|
-
}
|
|
111
|
-
const entry = retry?.[result.status];
|
|
112
|
-
if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
|
|
113
|
-
if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
|
|
114
|
-
try { result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) }; } catch {}
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return { url, attemptOnce, run };
|
|
120
|
-
}
|
|
1
|
+
// 通用(带 key)供应商的 responses 通道:muse-spark* 这类模型上游只挂 /responses,
|
|
2
|
+
// 打同 host 的 /chat/completions 会 503 "Endpoint is unavailable"(ocgo 实测 2026-09-22)。
|
|
3
|
+
// 判定单一来源 = isResponsesModel(models.dev 模型级 provider.npm + muse-spark 前缀兜底),
|
|
4
|
+
// 与 GET /v1/models 的 capabilities.upstreamApi 同源,新模型无需改码。
|
|
5
|
+
//
|
|
6
|
+
// 契约:run() 的出参恒为 **chat 形状**(SSE 或 JSON),调用方无需再判分支。
|
|
7
|
+
// · 流式优先 @ai-sdk/openai 的 responses 适配器(含加密思考往返,ADR-0017 复用);
|
|
8
|
+
// · SDK 不可用 / 非流式 → 原生 fetch + chatToResponsesBody 正转换 + 反向整形。
|
|
9
|
+
// key 轮换、退避重试、_t 计时与 createChatRunner(base.js)同语义,避免两条通道行为漂移。
|
|
10
|
+
import { chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "../upstream-responses.js";
|
|
11
|
+
import { attemptOnceResponsesSdk } from "../upstream-engine/sdk/responses.js";
|
|
12
|
+
import { ENGINE_MARKER } from "../upstream-engine/sdk/chat.js";
|
|
13
|
+
import { sleep } from "./base.js";
|
|
14
|
+
|
|
15
|
+
/** 供应商 id → env 片段(`my-api` → `MY_API`),与 providerKeyEnv 同规则 */
|
|
16
|
+
export function envSlug(id) {
|
|
17
|
+
return String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* responses 端点路径:显式入参 > env `MSLXDFF_<ID>_RESPONSES_PATH` > 缺省 `/responses`。
|
|
22
|
+
* 归一为以 `/` 开头(joinUrl 会再拼 baseUrl)。
|
|
23
|
+
*/
|
|
24
|
+
export function resolveResponsesPath(id, responsesPath) {
|
|
25
|
+
const raw = responsesPath || process.env[`MSLXDFF_${envSlug(id)}_RESPONSES_PATH`] || "";
|
|
26
|
+
const s = String(raw).trim();
|
|
27
|
+
if (!s) return "/responses";
|
|
28
|
+
return s.startsWith("/") ? s : `/${s}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 建 responses 通道。
|
|
33
|
+
* @param {object} o
|
|
34
|
+
* @param {string} o.id 供应商 id(用于报错文案与 providerName)
|
|
35
|
+
* @param {string} o.url 已拼好的 responses 绝对 URL
|
|
36
|
+
* @param {number} o.connectTimeoutMs 单次尝试超时
|
|
37
|
+
* @param {object} o.retry 重试表(与 createChatRunner 同形状:network/429/50x → {attempts, delayMs})
|
|
38
|
+
* @param {number} o.cooldownMs 全 key 冷却时的人话报错文案用
|
|
39
|
+
* @param {boolean} o.sdkEnabled 是否允许走 AI SDK responses 适配器(流式路径)
|
|
40
|
+
* @param {Function} o.buildHeaders (body, key, opts) → headers
|
|
41
|
+
* @param {Function} o.fetchImpl 注入的 fetch(连接池/测试接缝)
|
|
42
|
+
* @param {object} [o.dispatcher] undici Agent
|
|
43
|
+
*/
|
|
44
|
+
export function createResponsesChannel({
|
|
45
|
+
id,
|
|
46
|
+
url,
|
|
47
|
+
connectTimeoutMs = 30_000,
|
|
48
|
+
retry = {},
|
|
49
|
+
cooldownMs = 30_000,
|
|
50
|
+
sdkEnabled = true,
|
|
51
|
+
buildHeaders,
|
|
52
|
+
fetchImpl,
|
|
53
|
+
dispatcher = null,
|
|
54
|
+
} = {}) {
|
|
55
|
+
async function attemptOnce(body, key, opts) {
|
|
56
|
+
const headers = buildHeaders ? buildHeaders(body, key, opts) : {};
|
|
57
|
+
const wantsStream = body?.stream !== false;
|
|
58
|
+
if (sdkEnabled && wantsStream) {
|
|
59
|
+
try {
|
|
60
|
+
const r = await attemptOnceResponsesSdk({
|
|
61
|
+
url, body, headers, providerName: id, marker: ENGINE_MARKER, fetchImpl,
|
|
62
|
+
});
|
|
63
|
+
if (r) return r; // 适配器已产出 chat SSE
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (!e || (!e._sdkLoadFailed && !e._sdkUnsupported)) throw e;
|
|
66
|
+
// SDK 装载失败 / URL 异形 → 落原生兜底(保底不变差)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
|
|
71
|
+
try {
|
|
72
|
+
const fetchOpts = { method: "POST", headers, body: JSON.stringify(chatToResponsesBody(body)), signal: controller.signal };
|
|
73
|
+
if (dispatcher) fetchOpts.dispatcher = dispatcher;
|
|
74
|
+
const res = await fetchImpl(url, fetchOpts);
|
|
75
|
+
if (!res.ok) return res;
|
|
76
|
+
if (wantsStream) return reshapeResponsesSse(res, body?.model);
|
|
77
|
+
const json = await res.json().catch(() => null);
|
|
78
|
+
return json ? toChatResponse(res, json) : res;
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** key 轮换 + 退避重试 + _t 计时;语义对齐 base.js 的 createChatRunner.runChat */
|
|
85
|
+
async function run(body, activeRing, sourceKey, opts) {
|
|
86
|
+
const t0 = performance.now();
|
|
87
|
+
const attempts = [];
|
|
88
|
+
let waitMs = 0;
|
|
89
|
+
const key = activeRing.next();
|
|
90
|
+
if (!key && activeRing.size > 0) {
|
|
91
|
+
const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
|
|
92
|
+
err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
for (let attempt = 0; ; attempt++) {
|
|
96
|
+
const t = performance.now();
|
|
97
|
+
let result;
|
|
98
|
+
try {
|
|
99
|
+
result = await attemptOnce(body, key, opts);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
result = err;
|
|
102
|
+
}
|
|
103
|
+
attempts.push({ attempt, type: result instanceof Error ? "network" : `http${result?.status}`, ms: Math.round(performance.now() - t) });
|
|
104
|
+
if (result instanceof Error) {
|
|
105
|
+
const entry = retry?.network;
|
|
106
|
+
if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
|
|
107
|
+
activeRing.onError(key);
|
|
108
|
+
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
109
|
+
throw result;
|
|
110
|
+
}
|
|
111
|
+
const entry = retry?.[result.status];
|
|
112
|
+
if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
|
|
113
|
+
if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
|
|
114
|
+
try { result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) }; } catch {}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { url, attemptOnce, run };
|
|
120
|
+
}
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
// ADR-0019:借道 = 用你的 key —— 转发时自动借出,无开关、无白名单(组内互信是前提)。
|
|
2
|
-
// header: `x-mslxdff-share-keys: <providerId>=<key1>,<key2>;<providerId2>=<key3>`
|
|
3
|
-
import { splitModelId, DEFAULT_PROVIDER } from "./model-id.js";
|
|
4
|
-
import { classifyProvider } from "./classify.js";
|
|
5
|
-
import { loadProviderKeys, listProviderIdsWithKeys } from "../state.js";
|
|
6
|
-
import { isRefreshToken } from "./cline/headers.js";
|
|
7
|
-
|
|
8
|
-
export const SHARE_KEYS_HEADER = "x-mslxdff-share-keys";
|
|
9
|
-
|
|
10
|
-
// 硬排除:refresh-token 型凭据(对端刷新会轮换 token,与本机互相踢下线)——正确性问题。
|
|
11
|
-
// 二层保护:① 按 id 硬排除(cline 恒 local-only);② 按凭据形状兜底(见 shareableProviderIds)。
|
|
12
|
-
// traework/workbuddy/zcode 同为本机账号绑定型(auths/*.json + uid;zcode 还带 per-account deviceMid),恒不借出。
|
|
13
|
-
const NEVER_SHARE_IDS = new Set(["cline", "codearts", "traework", "qoder", "zcode", "raccoon"]);
|
|
14
|
-
// 本节点应 cast key 到出站转发的供应商 id 集合 = 所有「本机有 key」的供应商,
|
|
15
|
-
// 减去:opencode(无 key 且恒排除)、local-only(workbuddy/cline系,本就不走组员)、刷新型凭据。
|
|
16
|
-
export function shareableProviderIds({ file } = {}) {
|
|
17
|
-
const ids = [];
|
|
18
|
-
for (const id of listProviderIdsWithKeys({ file })) {
|
|
19
|
-
if (id === DEFAULT_PROVIDER) continue;
|
|
20
|
-
if (NEVER_SHARE_IDS.has(id)) continue;
|
|
21
|
-
try { if (classifyProvider(id) === "local-only") continue; } catch {}
|
|
22
|
-
// 形状兜底:id 白名单可能有残留(迁移未跑/回滚/手改 config),只要该供应商现有 key 里
|
|
23
|
-
// 有一个是 refreshToken 形态,就一律不外借——refreshToken 借出去会被对端刷新轮换,双向踢下线。
|
|
24
|
-
try { if (loadProviderKeys(id, { file }).some((k) => isRefreshToken(k, id))) continue; } catch {}
|
|
25
|
-
ids.push(id);
|
|
26
|
-
}
|
|
27
|
-
return ids;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// 组装 share header 值(只含 shareable 且当前请求命中前缀的供应商)。
|
|
31
|
-
// splitModelId 用 shareableProviderIds() 作为已知集合:命中 → 该供应商在此节点 shareable;
|
|
32
|
-
// 未命中或裸 id(默认供应商 opencode)→ 不附带。返回 null 表示本请求不需要附带。
|
|
33
|
-
export function buildShareKeysHeader(model, { file } = {}) {
|
|
34
|
-
const split = splitModelId(model, shareableProviderIds({ file }));
|
|
35
|
-
if (!split.provider || split.provider === DEFAULT_PROVIDER) return null;
|
|
36
|
-
const keys = loadProviderKeys(split.provider, { file });
|
|
37
|
-
if (!keys.length) return null;
|
|
38
|
-
return `${split.provider}=${keys.join(",")}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 组员侧:解析 share header 为 { providerId -> keys[] }
|
|
42
|
-
// 防御:默认供应商(opencode)即使出现在 header 一律忽略——它不该有共享 key。
|
|
43
|
-
export function parseShareKeysHeader(value) {
|
|
44
|
-
const out = {};
|
|
45
|
-
if (!value) return out;
|
|
46
|
-
for (const seg of String(value).split(";")) {
|
|
47
|
-
const [provider, keysPart] = seg.split("=");
|
|
48
|
-
const id = String(provider || "").trim();
|
|
49
|
-
if (!id || id === DEFAULT_PROVIDER || !keysPart) continue;
|
|
50
|
-
const keys = String(keysPart).split(",").map((k) => k.trim()).filter(Boolean);
|
|
51
|
-
if (keys.length) out[id] = keys;
|
|
52
|
-
}
|
|
53
|
-
return out;
|
|
1
|
+
// ADR-0019:借道 = 用你的 key —— 转发时自动借出,无开关、无白名单(组内互信是前提)。
|
|
2
|
+
// header: `x-mslxdff-share-keys: <providerId>=<key1>,<key2>;<providerId2>=<key3>`
|
|
3
|
+
import { splitModelId, DEFAULT_PROVIDER } from "./model-id.js";
|
|
4
|
+
import { classifyProvider } from "./classify.js";
|
|
5
|
+
import { loadProviderKeys, listProviderIdsWithKeys } from "../state.js";
|
|
6
|
+
import { isRefreshToken } from "./cline/headers.js";
|
|
7
|
+
|
|
8
|
+
export const SHARE_KEYS_HEADER = "x-mslxdff-share-keys";
|
|
9
|
+
|
|
10
|
+
// 硬排除:refresh-token 型凭据(对端刷新会轮换 token,与本机互相踢下线)——正确性问题。
|
|
11
|
+
// 二层保护:① 按 id 硬排除(cline 恒 local-only);② 按凭据形状兜底(见 shareableProviderIds)。
|
|
12
|
+
// traework/workbuddy/zcode 同为本机账号绑定型(auths/*.json + uid;zcode 还带 per-account deviceMid),恒不借出。
|
|
13
|
+
const NEVER_SHARE_IDS = new Set(["cline", "codearts", "traework", "qoder", "zcode", "raccoon"]);
|
|
14
|
+
// 本节点应 cast key 到出站转发的供应商 id 集合 = 所有「本机有 key」的供应商,
|
|
15
|
+
// 减去:opencode(无 key 且恒排除)、local-only(workbuddy/cline系,本就不走组员)、刷新型凭据。
|
|
16
|
+
export function shareableProviderIds({ file } = {}) {
|
|
17
|
+
const ids = [];
|
|
18
|
+
for (const id of listProviderIdsWithKeys({ file })) {
|
|
19
|
+
if (id === DEFAULT_PROVIDER) continue;
|
|
20
|
+
if (NEVER_SHARE_IDS.has(id)) continue;
|
|
21
|
+
try { if (classifyProvider(id) === "local-only") continue; } catch {}
|
|
22
|
+
// 形状兜底:id 白名单可能有残留(迁移未跑/回滚/手改 config),只要该供应商现有 key 里
|
|
23
|
+
// 有一个是 refreshToken 形态,就一律不外借——refreshToken 借出去会被对端刷新轮换,双向踢下线。
|
|
24
|
+
try { if (loadProviderKeys(id, { file }).some((k) => isRefreshToken(k, id))) continue; } catch {}
|
|
25
|
+
ids.push(id);
|
|
26
|
+
}
|
|
27
|
+
return ids;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 组装 share header 值(只含 shareable 且当前请求命中前缀的供应商)。
|
|
31
|
+
// splitModelId 用 shareableProviderIds() 作为已知集合:命中 → 该供应商在此节点 shareable;
|
|
32
|
+
// 未命中或裸 id(默认供应商 opencode)→ 不附带。返回 null 表示本请求不需要附带。
|
|
33
|
+
export function buildShareKeysHeader(model, { file } = {}) {
|
|
34
|
+
const split = splitModelId(model, shareableProviderIds({ file }));
|
|
35
|
+
if (!split.provider || split.provider === DEFAULT_PROVIDER) return null;
|
|
36
|
+
const keys = loadProviderKeys(split.provider, { file });
|
|
37
|
+
if (!keys.length) return null;
|
|
38
|
+
return `${split.provider}=${keys.join(",")}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 组员侧:解析 share header 为 { providerId -> keys[] }
|
|
42
|
+
// 防御:默认供应商(opencode)即使出现在 header 一律忽略——它不该有共享 key。
|
|
43
|
+
export function parseShareKeysHeader(value) {
|
|
44
|
+
const out = {};
|
|
45
|
+
if (!value) return out;
|
|
46
|
+
for (const seg of String(value).split(";")) {
|
|
47
|
+
const [provider, keysPart] = seg.split("=");
|
|
48
|
+
const id = String(provider || "").trim();
|
|
49
|
+
if (!id || id === DEFAULT_PROVIDER || !keysPart) continue;
|
|
50
|
+
const keys = String(keysPart).split(",").map((k) => k.trim()).filter(Boolean);
|
|
51
|
+
if (keys.length) out[id] = keys;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
54
|
}
|