mslxdff 0.1.155 → 0.1.158
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mslxdff.js +4 -4
- package/docs/ARCHITECTURE.md +426 -0
- package/docs/FEATURE_TREE.md +164 -0
- package/docs/MOBILE.md +82 -0
- package/docs/adr/0001-reasoning-content-injection.md +14 -0
- package/docs/adr/0002-models-free-filter.md +12 -0
- package/docs/adr/0003-zero-state-no-auth.md +10 -0
- package/docs/adr/0004-bearer-token.md +18 -0
- package/docs/adr/0005-peer-mesh.md +53 -0
- package/docs/adr/0006-broadband-member.md +103 -0
- package/docs/adr/0007-multi-provider-prefix.md +25 -0
- package/docs/adr/0008-share-keys-to-peers.md +52 -0
- package/docs/adr/0009-chat-repl.md +37 -0
- package/docs/adr/0010-allowlist.md +36 -0
- package/docs/adr/0011-broadband-stream.md +30 -0
- package/docs/adr/0012-responses-endpoint-codex-sync.md +48 -0
- package/docs/adr/0013-node16-compat.md +41 -0
- package/docs/adr/0014-deepseek-provider.md +52 -0
- package/docs/adr/0015-upstream-probe-routing.md +50 -0
- package/docs/adr/0016-model-capabilities.md +27 -0
- package/docs/adr/0017-ai-sdk-upstream-engine.md +59 -0
- package/docs/adr/0018-zen-client-identity.md +52 -0
- package/docs/adr/0019-share-keys-always-lend.md +59 -0
- package/docs/adr/0020-zen-free-lane-agent-shape.md +52 -0
- package/docs/adr/0021-usage-report-jsonl.md +47 -0
- package/docs/adr/0022-models-capability-merge.md +61 -0
- package/docs/adr/0023-key-provider-default-direct.md +58 -0
- package/docs/adr/0024-node18-baseline.md +63 -0
- package/docs/adr/0025-workbuddy-authdir-follows-state.md +71 -0
- package/docs/adr/0026-cline-provider-id-unify.md +67 -0
- package/docs/adr/0027-codearts-provider.md +48 -0
- package/docs/adr/0028-traework-provider.md +34 -0
- package/docs/adr/0029-qoder-native-provider.md +60 -0
- package/docs/adr/0030-models-list-scoped-by-picks.md +53 -0
- package/docs/adr/0031-qoder-true-streaming.md +50 -0
- package/docs/adr/0032-generic-responses-channel.md +72 -0
- package/docs/adr/0033-cline-allowlist-auto-sync.md +75 -0
- package/docs/adr/0034-request-level-human-readable-observability.md +60 -0
- package/docs/adr/0035-sdk-channel-headers-timeout.md +49 -0
- package/docs/adr/0036-qoder-per-request-sticky-account.md +82 -0
- package/docs/adr/0037-qwenwork-independent-provider.md +82 -0
- package/docs/adr/0038-zcode-provider.md +140 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/cli_help.md +1391 -0
- package/docs/cli_help_mini.md +133 -0
- package/docs/plans/bench-via-latency-2026-09-01.md +215 -0
- package/docs/plugins.md +187 -0
- package/package.json +1 -1
- package/src/auto.js +254 -254
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +123 -123
- package/src/chat-pipeline/policy.js +76 -76
- package/src/chat-pipeline/serial-trial.js +210 -210
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/list-providers.js +4 -3
- package/src/cli/commands/model/list-render.js +7 -9
- package/src/cli/commands/model/list-sort.js +52 -0
- package/src/cli/commands/model/list-tty.js +76 -0
- package/src/cli/commands/model/list.js +5 -62
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +124 -116
- package/src/cli/commands/provider/models.js +139 -128
- package/src/cli/commands/provider/qwenwork-login.js +119 -0
- package/src/cli/commands/provider/zcode-login.js +77 -0
- package/src/cli/commands/provider/zcode-quota.js +55 -0
- package/src/cli/commands/sync.js +232 -232
- package/src/cli/provider-row.js +2 -2
- package/src/cli/status.js +279 -279
- package/src/daemon.js +96 -96
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/model-trace.js +1 -0
- package/src/models.js +225 -225
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/keyring.js +60 -56
- package/src/providers/qoder/chat.js +183 -100
- package/src/providers/qoder/index.js +230 -184
- package/src/providers/qoder/sse.js +103 -46
- package/src/providers/qoder/sticky.js +1 -0
- package/src/providers/qoder/stream.js +32 -9
- package/src/providers/qwenwork/account-store.js +133 -0
- package/src/providers/qwenwork/constants.js +67 -0
- package/src/providers/qwenwork/cosy.js +120 -0
- package/src/providers/qwenwork/crypto.js +218 -0
- package/src/providers/qwenwork/http.js +20 -0
- package/src/providers/qwenwork/index.js +327 -0
- package/src/providers/qwenwork/payload.js +142 -0
- package/src/providers/qwenwork/rsa.js +54 -0
- package/src/providers/qwenwork/sse.js +268 -0
- package/src/providers/qwenwork/stream.js +130 -0
- package/src/providers/qwenwork/upstream.js +120 -0
- package/src/providers/qwenwork.js +1 -0
- package/src/providers/registry.js +66 -56
- package/src/providers/share-keys.js +2 -2
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/account-store.js +129 -0
- package/src/providers/zcode/auth.js +28 -0
- package/src/providers/zcode/chat.js +171 -0
- package/src/providers/zcode/const.js +54 -0
- package/src/providers/zcode/headers.js +55 -0
- package/src/providers/zcode/index.js +124 -0
- package/src/providers/zcode/models.js +65 -0
- package/src/providers/zcode/oauth.js +120 -0
- package/src/providers/zcode/quota.js +176 -0
- package/src/providers/zcode/sse.js +179 -0
- package/src/reasoning.js +32 -32
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +250 -246
- package/src/routes/chat/via-route-handler.js +144 -144
- package/src/routes/hedge.js +255 -255
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream.js +438 -399
- package/src/runtime/bootstrap.js +45 -45
- package/src/runtime/provider-gate.js +33 -30
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +64 -64
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +289 -289
package/src/providers/keyring.js
CHANGED
|
@@ -1,57 +1,61 @@
|
|
|
1
|
-
// 多 key 轮转器:round-robin 取键 + 出错冷却隔离。
|
|
2
|
-
// 语义:某个 key 出错后 cooldownMs 内不再使用;全部 key 都在冷却 → next() 返回 null
|
|
3
|
-
// (调用方应视为该供应商暂时失效,直接报错而非降级为单 key)。
|
|
4
|
-
export const DEFAULT_COOLDOWN_MS = 30_000;
|
|
5
|
-
|
|
6
|
-
export function createKeyRing(keys = [], { cooldownMs = DEFAULT_COOLDOWN_MS, now = Date.now } = {}) {
|
|
7
|
-
const list = [...new Set(keys.filter((k) => typeof k === "string" && k.trim().length))];
|
|
8
|
-
const errAt = new Map();
|
|
9
|
-
let cursor = 0;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
// 多 key 轮转器:round-robin 取键 + 出错冷却隔离。
|
|
2
|
+
// 语义:某个 key 出错后 cooldownMs 内不再使用;全部 key 都在冷却 → next() 返回 null
|
|
3
|
+
// (调用方应视为该供应商暂时失效,直接报错而非降级为单 key)。
|
|
4
|
+
export const DEFAULT_COOLDOWN_MS = 30_000;
|
|
5
|
+
|
|
6
|
+
export function createKeyRing(keys = [], { cooldownMs = DEFAULT_COOLDOWN_MS, now = Date.now } = {}) {
|
|
7
|
+
const list = [...new Set(keys.filter((k) => typeof k === "string" && k.trim().length))];
|
|
8
|
+
const errAt = new Map();
|
|
9
|
+
let cursor = 0;
|
|
10
|
+
|
|
11
|
+
// errAt 存「冷却到期时间戳」:onError(key, durationMs?) 可自定义时长(额度错长冷却用),
|
|
12
|
+
// 缺省用构造时的 cooldownMs —— 旧的 onError(key) 调用方行为不变。
|
|
13
|
+
function isCooling(key) {
|
|
14
|
+
const e = errAt.get(key);
|
|
15
|
+
return e != null && now() < e;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function next() {
|
|
19
|
+
if (!list.length) return null;
|
|
20
|
+
for (let i = 0; i < list.length; i++) {
|
|
21
|
+
const idx = (cursor + i) % list.length;
|
|
22
|
+
const key = list[idx];
|
|
23
|
+
if (!isCooling(key)) {
|
|
24
|
+
cursor = (idx + 1) % list.length;
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function onError(key, durationMs) {
|
|
32
|
+
if (!list.includes(key)) return;
|
|
33
|
+
const dur = Number.isFinite(durationMs) && durationMs >= 0 ? durationMs : cooldownMs;
|
|
34
|
+
errAt.set(key, now() + dur);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function replace(oldKey, newKey) {
|
|
38
|
+
const idx = list.indexOf(oldKey);
|
|
39
|
+
if (idx < 0) {
|
|
40
|
+
if (newKey && !list.includes(newKey)) list.push(newKey);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (newKey && oldKey !== newKey) {
|
|
44
|
+
list[idx] = newKey;
|
|
45
|
+
// 迁移冷却状态:旧 key 的冷却移到新 key,避免新 token 立即被误判可用
|
|
46
|
+
if (errAt.has(oldKey)) {
|
|
47
|
+
const t = errAt.get(oldKey);
|
|
48
|
+
errAt.delete(oldKey);
|
|
49
|
+
// 新 token 刚刷新,不应继承旧冷却,直接清掉
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function available() {
|
|
56
|
+
return list.filter((k) => !isCooling(k)).length;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// isCooling 对外暴露:粘号选择器要判断"上次这个号还在冷却吗"(决定是否必须换号)
|
|
60
|
+
return { next, onError, replace, available, isCooling, size: list.length, cooldownMs, keys: [...list] };
|
|
57
61
|
}
|
|
@@ -1,100 +1,183 @@
|
|
|
1
|
-
// qoder 对话服务:上游恒 stream:true;请求方 stream=true → 真流式转发边收边吐,
|
|
2
|
-
// false → 聚合回 JSON。装配走 request.js,帧格式走 sse.js,两条管线共享。
|
|
3
|
-
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
4
|
-
import { normalizeRegion } from "./constants.js";
|
|
5
|
-
import { buildUpstreamRequest } from "./request.js";
|
|
6
|
-
import { reshapeQoderStream } from "./stream.js";
|
|
7
|
-
import { aggregateQoderStream, toCompletionJson } from "./aggregate.js";
|
|
8
|
-
import { errorStatus } from "./sse.js";
|
|
9
|
-
|
|
10
|
-
function hostOf(url) {
|
|
11
|
-
try { return new URL(String(url)).host; } catch { return "-"; }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function errRes(status, msg, type = "upstream_error") {
|
|
15
|
-
return new Response(JSON.stringify({ error: { message: msg, type } }), { status, headers: { "Content-Type": "application/json" } });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function mapUpstreamError(status, detail) {
|
|
19
|
-
return { kind: status === 401 || status === 403 ? "auth" : "upstream", status, detail: String(detail || "").slice(0, 300) };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
1
|
+
// qoder 对话服务:上游恒 stream:true;请求方 stream=true → 真流式转发边收边吐,
|
|
2
|
+
// false → 聚合回 JSON。装配走 request.js,帧格式走 sse.js,两条管线共享。
|
|
3
|
+
import { compatFetch, timeoutSignal } from "../../compat.js";
|
|
4
|
+
import { normalizeRegion } from "./constants.js";
|
|
5
|
+
import { buildUpstreamRequest } from "./request.js";
|
|
6
|
+
import { reshapeQoderStream } from "./stream.js";
|
|
7
|
+
import { aggregateQoderStream, toCompletionJson } from "./aggregate.js";
|
|
8
|
+
import { errorStatus, extractDelta } from "./sse.js";
|
|
9
|
+
|
|
10
|
+
function hostOf(url) {
|
|
11
|
+
try { return new URL(String(url)).host; } catch { return "-"; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function errRes(status, msg, type = "upstream_error") {
|
|
15
|
+
return new Response(JSON.stringify({ error: { message: msg, type } }), { status, headers: { "Content-Type": "application/json" } });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mapUpstreamError(status, detail) {
|
|
19
|
+
return { kind: status === 401 || status === 403 ? "auth" : "upstream", status, detail: String(detail || "").slice(0, 300) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 预读上限:判决恒在首帧(实测 457 字节),8 帧足够覆盖"角色帧先到、判决帧随后",
|
|
23
|
+
// 又不会把首字节延迟拖到不可控。超窗未得判决即放弃(退化为无冷却的流内 error,见 SPEC 已知上限)。
|
|
24
|
+
const PEEK_MAX_FRAMES = 8;
|
|
25
|
+
|
|
26
|
+
// 预读上游首帧只为取「流内判决」:qoder 把限流/鉴权判决裹在 HTTP 200 的 SSE 信封里
|
|
27
|
+
//(statusCodeValue:403 + serviceAvailable:false),upRes.status 恒 200,判决只有读完帧才知道。
|
|
28
|
+
// 不预读则状态码永远出不来流式路径 → 门面不冷却该号(ADR-0036 的粘号会把重试继续粘在它上面)。
|
|
29
|
+
// 返回 { res, status, frames }:res 是把预读帧回灌后的等价上游响应,frames 交回灌用。
|
|
30
|
+
// 任何异常都降级为「无判决」(status=0):排障手段不得变成故障源。
|
|
31
|
+
async function peekEnvelopeVerdict(upRes, cdbg) {
|
|
32
|
+
if (!upRes?.body) return { res: upRes, status: 0, quota: false, frames: [] };
|
|
33
|
+
let reader;
|
|
34
|
+
try { reader = upRes.body.getReader(); } catch { return { res: upRes, status: 0, quota: false, frames: [] }; }
|
|
35
|
+
const dec = new TextDecoder();
|
|
36
|
+
const frames = [];
|
|
37
|
+
let text = "";
|
|
38
|
+
let status = 0;
|
|
39
|
+
let quota = false;
|
|
40
|
+
let decided = false;
|
|
41
|
+
try {
|
|
42
|
+
for (let i = 0; i < PEEK_MAX_FRAMES && !decided; i++) {
|
|
43
|
+
const { done, value } = await reader.read();
|
|
44
|
+
if (done) break;
|
|
45
|
+
if (value) { frames.push(value); text += dec.decode(value, { stream: true }); }
|
|
46
|
+
// 逐行判定:先到判决(err)或先到有效载荷即停,避免无谓等待
|
|
47
|
+
for (const raw of text.split("\n")) {
|
|
48
|
+
const line = raw.trim();
|
|
49
|
+
if (!line.startsWith("data:")) continue;
|
|
50
|
+
const p = line.slice(5).trim();
|
|
51
|
+
if (!p || p === "[DONE]") continue;
|
|
52
|
+
const d = extractDelta(p);
|
|
53
|
+
if (d.err) { status = Number(d.err.status) || 0; quota = d.err.kind === "quota"; decided = true; break; }
|
|
54
|
+
if (d.content || d.reasoning || d.toolCalls || d.usageIn || d.usageOut) { decided = true; break; }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
cdbg(`[peek-throw] ${String(e?.message || e).slice(0, 160)}`);
|
|
59
|
+
}
|
|
60
|
+
cdbg(`[peek] frames=${frames.length} verdict=${status || "none(正常流)"}`);
|
|
61
|
+
// 回灌:已读帧先按原样吐出,再接续同一个 reader 的剩余部分
|
|
62
|
+
const body = new ReadableStream({
|
|
63
|
+
start(ctrl) {
|
|
64
|
+
for (const f of frames) { try { ctrl.enqueue(f); } catch {} }
|
|
65
|
+
(async () => {
|
|
66
|
+
try {
|
|
67
|
+
for (;;) {
|
|
68
|
+
const { done, value } = await reader.read();
|
|
69
|
+
if (done) break;
|
|
70
|
+
if (value) ctrl.enqueue(value);
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
try { ctrl.close(); } catch {}
|
|
74
|
+
})();
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
return { res: new Response(body, { status: upRes.status, headers: upRes.headers }), status, quota, frames };
|
|
78
|
+
}
|
|
79
|
+
export function createChatService({ id = "qoder", fetchImpl, timeoutMs } = {}) {
|
|
80
|
+
if (!fetchImpl) fetchImpl = compatFetch;
|
|
81
|
+
|
|
82
|
+
// body: OpenAI chat 请求;sess: 已建 COSY 会话;region: 该账号所属区(每号独立选端点)。返回 Response。
|
|
83
|
+
// 回显(可观测,不落凭据):x-mslxdff-upstream=本次请求的 host、x-mslxdff-qoder-region=cn|global;
|
|
84
|
+
// 管线层据此把"这次谁上的、打哪个站"投进模型日志(此前 provider 内部选号/切 URL 完全静默,排障无据)。
|
|
85
|
+
const withEcho = (res, url, region, pick) => {
|
|
86
|
+
try {
|
|
87
|
+
res.headers.set("x-mslxdff-upstream", hostOf(url));
|
|
88
|
+
res.headers.set("x-mslxdff-qoder-region", normalizeRegion(region));
|
|
89
|
+
// 账号选择决定(new/sticky/switch/forced):管线把它写进模型日志,"为什么会切号"才有据可查
|
|
90
|
+
if (pick) res.headers.set("x-mslxdff-qoder-account", String(pick));
|
|
91
|
+
} catch {}
|
|
92
|
+
return res;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// 上游状态回显(内部交接用,不落日志):流式路径按对外契约把非 200 整形成 "200 + 流内 error",
|
|
96
|
+
// 真实状态码只能靠这个头带出——否则 401/403/429/5xx 到不了 provider 门面,坏号不被冷却,
|
|
97
|
+
// 而"同请求粘号"(ADR-0036)会把后续重试继续粘在这个坏号上。
|
|
98
|
+
// Note: 流内判决需预读首帧才能拿到(HTTP 200 里裹 statusCodeValue)— 见 .agents/notes/implemented/bug-fix/2026-09-27-qoder-envelope-verdict-cooldown.md
|
|
99
|
+
const withUpstreamStatus = (res, st) => {
|
|
100
|
+
try { res.headers.set("x-mslxdff-qoder-upstream-status", String(st)); } catch {}
|
|
101
|
+
return res;
|
|
102
|
+
};
|
|
103
|
+
// 额度耗尽标记(内部交接,不落日志):门面据此换号(长冷却该号)而非普通短冷却
|
|
104
|
+
const withQuota = (res) => {
|
|
105
|
+
try { res.headers.set("x-mslxdff-qoder-quota", "1"); } catch {}
|
|
106
|
+
return res;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
async function runChat(body, sess, region = "global", pick = "") {
|
|
110
|
+
const stream = body?.stream !== false;
|
|
111
|
+
const model = body?.model || "qfmodel";
|
|
112
|
+
const chatId = "chatcmpl-qoder-" + Math.random().toString(16).slice(2, 10);
|
|
113
|
+
const tools = Array.isArray(body?.tools) && body.tools.length ? body.tools : null;
|
|
114
|
+
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
|
115
|
+
const maxTokens = Number(body?.max_tokens) || 0;
|
|
116
|
+
|
|
117
|
+
let upRes; let reqUrl = "";
|
|
118
|
+
const echo = (res) => withEcho(res, reqUrl, region, pick);
|
|
119
|
+
// 排障日志(QODER_DEBUG_STREAM=1 开):记录出口/状态/耗时/响应头,便于对比"200 但空轮"
|
|
120
|
+
const cdbg = (...a) => { if (process.env.QODER_DEBUG_STREAM === "1") console.log("[qoder-chat]", ...a); };
|
|
121
|
+
let upMs = 0;
|
|
122
|
+
try {
|
|
123
|
+
const req = buildUpstreamRequest({ sess, region: normalizeRegion(region), model, messages, tools, maxTokens });
|
|
124
|
+
reqUrl = req.url;
|
|
125
|
+
cdbg(`[req] url=${hostOf(req.url)} region=${normalizeRegion(region)} model=${model} msgs=${messages.length} tools=${tools?.length ?? 0} maxTokens=${maxTokens} stream=${stream} bodyBytes=${req.bodyStr?.length ?? 0}`);
|
|
126
|
+
const t0 = Date.now();
|
|
127
|
+
upRes = await fetchImpl(req.url, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: req.headers,
|
|
130
|
+
body: req.bodyStr,
|
|
131
|
+
signal: timeoutSignal(timeoutMs || 120_000),
|
|
132
|
+
});
|
|
133
|
+
upMs = Date.now() - t0;
|
|
134
|
+
cdbg(`[res] status=${upRes.status} ms=${upMs} ct=${upRes.headers.get("content-type")} host=${hostOf(reqUrl)}`);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
const detail = String(e?.message || e).slice(0, 300);
|
|
137
|
+
cdbg(`[res-throw] ms=${upMs} err=${detail}`);
|
|
138
|
+
// 网络异常(超时/连接失败)折成 502:门面据此冷却该号(否则坏号恒在轮换池里)
|
|
139
|
+
if (stream) return withUpstreamStatus(echo(reshapeQoderStream(new Response("data: [DONE]\n\n"), { model, chatId })), 502);
|
|
140
|
+
return withUpstreamStatus(echo(errRes(502, detail)), 502);
|
|
141
|
+
}
|
|
142
|
+
if (upRes.status !== 200) {
|
|
143
|
+
const detail = await upRes.text().catch(() => "");
|
|
144
|
+
cdbg(`[res-non200] status=${upRes.status} body=${detail.slice(0, 300)}`);
|
|
145
|
+
const e = mapUpstreamError(upRes.status, detail);
|
|
146
|
+
if (stream) {
|
|
147
|
+
// 首包错误也走流内 error 事件(与旧 reshapeStream catch 语义一致:200 + event:error + DONE)
|
|
148
|
+
const enc = new TextEncoder();
|
|
149
|
+
const s = new ReadableStream({
|
|
150
|
+
start(c) {
|
|
151
|
+
c.enqueue(enc.encode(`data: ${JSON.stringify({ id: chatId, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model, choices: [{ index: 0, delta: {}, finish_reason: null }] })}\n\n`));
|
|
152
|
+
c.enqueue(enc.encode(`event: error\ndata: ${JSON.stringify({ message: e.detail, type: e.kind })}\n\ndata: [DONE]\n\n`));
|
|
153
|
+
c.close();
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
// 对外仍是 200(契约不变),真实状态码经回显头交门面决定是否冷却
|
|
157
|
+
return withUpstreamStatus(echo(new Response(s, { status: 200, headers: { "Content-Type": "text/event-stream" } })), upRes.status);
|
|
158
|
+
}
|
|
159
|
+
return withUpstreamStatus(echo(errRes(errorStatus(e), e.detail, e.kind)), upRes.status);
|
|
160
|
+
}
|
|
161
|
+
if (stream) {
|
|
162
|
+
// 预读首帧取流内判决(qoder 把 403/429 裹在 200 的信封里),预读帧原样回灌给整形器。
|
|
163
|
+
// 判决状态码经 withUpstreamStatus 出门面 → index.js 据此 ring.onError 冷却该号 →
|
|
164
|
+
// 粘号选择器下次因 isCooling 为真而换号。对外仍恒 200(契约不变)。
|
|
165
|
+
const peek = await peekEnvelopeVerdict(upRes, cdbg);
|
|
166
|
+
let shaped = echo(reshapeQoderStream(peek.res, { model, chatId, prefetched: peek.frames }));
|
|
167
|
+
// 只在真有判决时挂状态码:正常流不写这个头(index.js 靠它区分"要不要冷却")
|
|
168
|
+
if (peek.status) shaped = withUpstreamStatus(shaped, peek.status);
|
|
169
|
+
if (peek.quota) shaped = withQuota(shaped);
|
|
170
|
+
return shaped;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const agg = await aggregateQoderStream(upRes);
|
|
174
|
+
const out = toCompletionJson({ model, chatId, ...agg });
|
|
175
|
+
return echo(new Response(JSON.stringify(out), { status: 200, headers: { "Content-Type": "application/json" } }));
|
|
176
|
+
} catch (e) {
|
|
177
|
+
const out = echo(errRes(errorStatus(e), String(e?.detail || e?.message || e).slice(0, 300), e?.kind || "upstream_error"));
|
|
178
|
+
return e?.kind === "quota" ? withQuota(out) : out;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { runChat };
|
|
183
|
+
}
|