mslxdff 0.1.231 → 0.1.232

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +254 -254
  4. package/src/bench/cline-bench.js +42 -42
  5. package/src/bench/probe.js +70 -70
  6. package/src/bench/report.js +162 -162
  7. package/src/bench/runner.js +77 -77
  8. package/src/bench/via-probe.js +124 -124
  9. package/src/bench/via-routes.js +87 -87
  10. package/src/bench/workbuddy-bench.js +54 -54
  11. package/src/chat/engine.js +160 -160
  12. package/src/chat/gateway.js +163 -163
  13. package/src/chat/orchestrator.js +234 -234
  14. package/src/chat/prompt.js +70 -70
  15. package/src/chat/repl.js +88 -88
  16. package/src/chat/terminal.js +135 -135
  17. package/src/chat/tools.js +306 -306
  18. package/src/chat-pipeline/index.js +145 -145
  19. package/src/chat-pipeline/policy.js +84 -84
  20. package/src/cli/commands/group.js +266 -266
  21. package/src/cli/commands/model/picks.js +50 -50
  22. package/src/cli/commands/provider/bench-via.js +247 -247
  23. package/src/cli/commands/provider/bench.js +141 -141
  24. package/src/cli/commands/provider/index.js +139 -139
  25. package/src/cli/commands/provider/models.js +149 -149
  26. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  27. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  28. package/src/cli/commands/sync.js +310 -310
  29. package/src/cli/status.js +279 -279
  30. package/src/daemon.js +246 -246
  31. package/src/model-capabilities/enrich.js +86 -86
  32. package/src/model-capabilities/index.js +183 -183
  33. package/src/model-capabilities/parse.js +70 -70
  34. package/src/models.js +225 -225
  35. package/src/peers.js +345 -345
  36. package/src/providers/agentrouter.js +238 -238
  37. package/src/providers/base.js +164 -164
  38. package/src/providers/cline/auth.js +228 -228
  39. package/src/providers/cline/chat.js +307 -307
  40. package/src/providers/cline.js +2 -2
  41. package/src/providers/codearts/chat.js +207 -207
  42. package/src/providers/dispatcher.js +147 -147
  43. package/src/providers/globalqwenwork/cosy.js +123 -123
  44. package/src/providers/globalqwenwork/crypto.js +218 -218
  45. package/src/providers/globalqwenwork/http.js +20 -20
  46. package/src/providers/globalqwenwork/rsa.js +54 -54
  47. package/src/providers/globalqwenwork/sse.js +268 -268
  48. package/src/providers/globalqwenwork/stream.js +130 -130
  49. package/src/providers/globalqwenwork/upstream.js +122 -122
  50. package/src/providers/keyring.js +60 -60
  51. package/src/providers/qoder/chat.js +203 -203
  52. package/src/providers/qoder/index.js +249 -249
  53. package/src/providers/qoder/sse.js +145 -145
  54. package/src/providers/qwenwork/account-store.js +133 -133
  55. package/src/providers/qwenwork/constants.js +67 -67
  56. package/src/providers/qwenwork/cosy.js +120 -120
  57. package/src/providers/qwenwork/crypto.js +218 -218
  58. package/src/providers/qwenwork/http.js +20 -20
  59. package/src/providers/qwenwork/index.js +337 -337
  60. package/src/providers/qwenwork/payload.js +144 -144
  61. package/src/providers/qwenwork/rsa.js +54 -54
  62. package/src/providers/qwenwork/sse.js +268 -268
  63. package/src/providers/qwenwork/stream.js +130 -130
  64. package/src/providers/qwenwork/upstream.js +120 -120
  65. package/src/providers/qwenwork.js +1 -1
  66. package/src/providers/raccoon/const.js +202 -202
  67. package/src/providers/raccoon/credits.js +285 -285
  68. package/src/providers/registry.js +89 -89
  69. package/src/providers/workbuddy/auth.js +16 -0
  70. package/src/providers/workbuddy/chat.js +298 -264
  71. package/src/providers/workbuddy/client-headers.js +148 -0
  72. package/src/providers/workbuddy/desktop-info.js +17 -1
  73. package/src/providers/workbuddy/models.js +8 -20
  74. package/src/providers/workbuddy/reshape.js +152 -152
  75. package/src/providers/workbuddy.js +2 -2
  76. package/src/reasoning.js +32 -32
  77. package/src/routes/chat/broadband-handler.js +80 -80
  78. package/src/routes/chat/gateway.js +46 -46
  79. package/src/routes/chat/relay-pipeline.js +282 -282
  80. package/src/routes/chat/via-route-handler.js +156 -156
  81. package/src/routes/hedge.js +260 -260
  82. package/src/routes/models-route.js +167 -167
  83. package/src/routes/peers.js +373 -373
  84. package/src/routes/stream.js +347 -347
  85. package/src/runtime/bootstrap.js +75 -75
  86. package/src/runtime/keep-warm.js +135 -135
  87. package/src/runtime/provider-gate.js +75 -75
  88. package/src/runtime/providers-setup.js +187 -187
  89. package/src/runtime/raccoon-checkin.js +175 -175
  90. package/src/server.js +85 -85
  91. package/src/state/schemas/allowlist.js +92 -92
  92. package/src/sync-opencode.js +280 -280
  93. package/src/transport/index.js +244 -244
  94. package/src/transport/pool.js +56 -56
  95. package/src/transport/retry.js +24 -24
  96. package/src/transport/sse.js +93 -93
  97. package/src/upstream-probe/display.js +52 -52
  98. package/src/upstream-probe/probe.js +49 -49
  99. package/src/upstream-probe/rotate.js +110 -110
  100. package/src/upstream-probe/start.js +45 -45
  101. package/src/upstream.js +355 -355
  102. package/docs/cli_help_mini.md +0 -148
  103. package/src/providers/AGENTS.md +0 -49
  104. package/src/routes/AGENTS.md +0 -37
@@ -1,203 +1,203 @@
1
- // qoder 对话服务:上游恒 stream:true;请求方 stream=true → 真流式转发边收边吐,
2
- // false → 聚合回 JSON。装配走 request.js,帧格式走 sse.js,两条管线共享。
3
- import { compatFetch, timeoutSignal } from "../../compat.js";
4
- import { mapModel } from "./payload.js";
5
- import { effortFromBody, isReasoningEnabled } from "../../thinking-effort.js";
6
- import { normalizeRegion } from "./constants.js";
7
- import { buildUpstreamRequest } from "./request.js";
8
- import { reshapeQoderStream } from "./stream.js";
9
- import { aggregateQoderStream, toCompletionJson } from "./aggregate.js";
10
- import { errorStatus, extractDelta } from "./sse.js";
11
-
12
- function hostOf(url) {
13
- try { return new URL(String(url)).host; } catch { return "-"; }
14
- }
15
-
16
- function errRes(status, msg, type = "upstream_error") {
17
- return new Response(JSON.stringify({ error: { message: msg, type } }), { status, headers: { "Content-Type": "application/json" } });
18
- }
19
-
20
- function mapUpstreamError(status, detail) {
21
- return { kind: status === 401 || status === 403 ? "auth" : "upstream", status, detail: String(detail || "").slice(0, 300) };
22
- }
23
-
24
- // 预读上限:判决恒在首帧(实测 457 字节),8 帧足够覆盖"角色帧先到、判决帧随后",
25
- // 又不会把首字节延迟拖到不可控。超窗未得判决即放弃(退化为无冷却的流内 error,见 SPEC 已知上限)。
26
- const PEEK_MAX_FRAMES = 8;
27
-
28
- // 预读上游首帧只为取「流内判决」:qoder 把限流/鉴权判决裹在 HTTP 200 的 SSE 信封里
29
- //(statusCodeValue:403 + serviceAvailable:false),upRes.status 恒 200,判决只有读完帧才知道。
30
- // 不预读则状态码永远出不来流式路径 → 门面不冷却该号(ADR-0036 的粘号会把重试继续粘在它上面)。
31
- // 返回 { res, status, frames }:res 是把预读帧回灌后的等价上游响应,frames 交回灌用。
32
- // 任何异常都降级为「无判决」(status=0):排障手段不得变成故障源。
33
- async function peekEnvelopeVerdict(upRes, cdbg) {
34
- if (!upRes?.body) return { res: upRes, status: 0, quota: false, queued: false, frames: [] };
35
- let reader;
36
- try { reader = upRes.body.getReader(); } catch { return { res: upRes, status: 0, quota: false, queued: false, frames: [] }; }
37
- const dec = new TextDecoder();
38
- const frames = [];
39
- let text = "";
40
- let status = 0;
41
- let quota = false;
42
- let queued = false;
43
- let decided = false;
44
- try {
45
- for (let i = 0; i < PEEK_MAX_FRAMES && !decided; i++) {
46
- const { done, value } = await reader.read();
47
- if (done) break;
48
- if (value) { frames.push(value); text += dec.decode(value, { stream: true }); }
49
- // 逐行判定:先到判决(err)或先到有效载荷即停,避免无谓等待
50
- for (const raw of text.split("\n")) {
51
- const line = raw.trim();
52
- if (!line.startsWith("data:")) continue;
53
- const p = line.slice(5).trim();
54
- if (!p || p === "[DONE]") continue;
55
- const d = extractDelta(p);
56
- if (d.err) { status = Number(d.err.status) || 0; quota = d.err.kind === "quota"; queued = d.err.queued === true; decided = true; break; }
57
- if (d.content || d.reasoning || d.toolCalls || d.usageIn || d.usageOut) { decided = true; break; }
58
- }
59
- }
60
- } catch (e) {
61
- cdbg(`[peek-throw] ${String(e?.message || e).slice(0, 160)}`);
62
- }
63
- cdbg(`[peek] frames=${frames.length} verdict=${status || "none(正常流)"} quota=${quota} queued=${queued}`);
64
- // 回灌:已读帧先按原样吐出,再接续同一个 reader 的剩余部分
65
- const body = new ReadableStream({
66
- start(ctrl) {
67
- for (const f of frames) { try { ctrl.enqueue(f); } catch {} }
68
- (async () => {
69
- try {
70
- for (;;) {
71
- const { done, value } = await reader.read();
72
- if (done) break;
73
- if (value) ctrl.enqueue(value);
74
- }
75
- } catch {}
76
- try { ctrl.close(); } catch {}
77
- })();
78
- },
79
- });
80
- return { res: new Response(body, { status: upRes.status, headers: upRes.headers }), status, quota, queued, frames };
81
- }
82
- export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModelMeta } = {}) {
83
- if (!fetchImpl) fetchImpl = compatFetch;
84
-
85
- // body: OpenAI chat 请求;sess: 已建 COSY 会话;region: 该账号所属区(每号独立选端点)。返回 Response。
86
- // 回显(可观测,不落凭据):x-mslxdff-upstream=本次请求的 host、x-mslxdff-qoder-region=cn|global;
87
- // 管线层据此把"这次谁上的、打哪个站"投进模型日志(此前 provider 内部选号/切 URL 完全静默,排障无据)。
88
- const withEcho = (res, url, region, pick) => {
89
- try {
90
- res.headers.set("x-mslxdff-upstream", hostOf(url));
91
- res.headers.set("x-mslxdff-qoder-region", normalizeRegion(region));
92
- // 账号选择决定(new/sticky/switch/forced):管线把它写进模型日志,"为什么会切号"才有据可查
93
- if (pick) res.headers.set("x-mslxdff-qoder-account", String(pick));
94
- } catch {}
95
- return res;
96
- };
97
-
98
- // 上游状态回显(内部交接用,不落日志):流式路径按对外契约把非 200 整形成 "200 + 流内 error",
99
- // 真实状态码只能靠这个头带出——否则 401/403/429/5xx 到不了 provider 门面,坏号不被冷却,
100
- // 而"同请求粘号"(ADR-0036)会把后续重试继续粘在这个坏号上。
101
- // Note: 流内判决需预读首帧才能拿到(HTTP 200 里裹 statusCodeValue)— 见 .agents/notes/implemented/bug-fix/2026-09-27-qoder-envelope-verdict-cooldown.md
102
- const withUpstreamStatus = (res, st) => {
103
- try { res.headers.set("x-mslxdff-qoder-upstream-status", String(st)); } catch {}
104
- return res;
105
- };
106
- // 额度耗尽标记(内部交接,不落日志):门面据此换号(长冷却该号)而非普通短冷却
107
- const withQuota = (res) => {
108
- try { res.headers.set("x-mslxdff-qoder-quota", "1"); } catch {}
109
- return res;
110
- };
111
- // 排队标记(内部交接,不落日志):与额度同为「换号」信号但冷却档不同——
112
- // 排队是「稍后再来」,长冷却到点自动回池;额度是「今天没了」,按天重置才回。
113
- const withQueued = (res) => {
114
- try { res.headers.set("x-mslxdff-qoder-queued", "1"); } catch {}
115
- return res;
116
- };
117
-
118
- async function runChat(body, sess, region = "global", pick = "") {
119
- const stream = body?.stream !== false;
120
- const model = body?.model || "qfmodel";
121
- const chatId = "chatcmpl-qoder-" + Math.random().toString(16).slice(2, 10);
122
- const tools = Array.isArray(body?.tools) && body.tools.length ? body.tools : null;
123
- const messages = Array.isArray(body?.messages) ? body.messages : [];
124
- const maxTokens = Number(body?.max_tokens) || 0;
125
- // is_reasoning 由模型目录供数(index 注入 getModelMeta,只读 peekModels 快照缓存,chat 路径恒 0 上游调用);
126
- // 冷缓存/缺失一律降级 false(= 旧行为,不阻断对话)—— P0:推理标志此前从未出门。
127
- let isReasoning = false;
128
- try {
129
- if (getModelMeta) isReasoning = (await getModelMeta(sess, normalizeRegion(region), mapModel(model)))?.is_reasoning === true;
130
- } catch {}
131
- // 客户端显式档位优先于目录推断(grill-decisions Q3):关断→false、其余非空→true;无档位→沿用目录结论。
132
- isReasoning = isReasoningEnabled(effortFromBody(body), isReasoning);
133
-
134
- let upRes; let reqUrl = "";
135
- const echo = (res) => withEcho(res, reqUrl, region, pick);
136
- // 排障日志(QODER_DEBUG_STREAM=1 开):记录出口/状态/耗时/响应头,便于对比"200 但空轮"
137
- const cdbg = (...a) => { if (process.env.QODER_DEBUG_STREAM === "1") console.log("[qoder-chat]", ...a); };
138
- let upMs = 0;
139
- try {
140
- const req = buildUpstreamRequest({ sess, region: normalizeRegion(region), model, messages, tools, maxTokens, isReasoning });
141
- reqUrl = req.url;
142
- 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}`);
143
- const t0 = Date.now();
144
- upRes = await fetchImpl(req.url, {
145
- method: "POST",
146
- headers: req.headers,
147
- body: req.bodyStr,
148
- signal: timeoutSignal(timeoutMs || 120_000),
149
- });
150
- upMs = Date.now() - t0;
151
- cdbg(`[res] status=${upRes.status} ms=${upMs} ct=${upRes.headers.get("content-type")} host=${hostOf(reqUrl)}`);
152
- } catch (e) {
153
- const detail = String(e?.message || e).slice(0, 300);
154
- cdbg(`[res-throw] ms=${upMs} err=${detail}`);
155
- // 网络异常(超时/连接失败)折成 502:门面据此冷却该号(否则坏号恒在轮换池里)
156
- if (stream) return withUpstreamStatus(echo(reshapeQoderStream(new Response("data: [DONE]\n\n"), { model, chatId })), 502);
157
- return withUpstreamStatus(echo(errRes(502, detail)), 502);
158
- }
159
- if (upRes.status !== 200) {
160
- const detail = await upRes.text().catch(() => "");
161
- cdbg(`[res-non200] status=${upRes.status} body=${detail.slice(0, 300)}`);
162
- const e = mapUpstreamError(upRes.status, detail);
163
- if (stream) {
164
- // 首包错误也走流内 error 事件(与旧 reshapeStream catch 语义一致:200 + event:error + DONE)
165
- const enc = new TextEncoder();
166
- const s = new ReadableStream({
167
- start(c) {
168
- 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`));
169
- c.enqueue(enc.encode(`event: error\ndata: ${JSON.stringify({ message: e.detail, type: e.kind })}\n\ndata: [DONE]\n\n`));
170
- c.close();
171
- },
172
- });
173
- // 对外仍是 200(契约不变),真实状态码经回显头交门面决定是否冷却
174
- return withUpstreamStatus(echo(new Response(s, { status: 200, headers: { "Content-Type": "text/event-stream" } })), upRes.status);
175
- }
176
- return withUpstreamStatus(echo(errRes(errorStatus(e), e.detail, e.kind)), upRes.status);
177
- }
178
- if (stream) {
179
- // 预读首帧取流内判决(qoder 把 403/429 裹在 200 的信封里),预读帧原样回灌给整形器。
180
- // 判决状态码经 withUpstreamStatus 出门面 → index.js 据此 ring.onError 冷却该号 →
181
- // 粘号选择器下次因 isCooling 为真而换号。对外仍恒 200(契约不变)。
182
- const peek = await peekEnvelopeVerdict(upRes, cdbg);
183
- let shaped = echo(reshapeQoderStream(peek.res, { model, chatId, prefetched: peek.frames }));
184
- // 只在真有判决时挂状态码:正常流不写这个头(index.js 靠它区分"要不要冷却")
185
- if (peek.status) shaped = withUpstreamStatus(shaped, peek.status);
186
- if (peek.quota) shaped = withQuota(shaped);
187
- // 排队旗标(内部交接):门面据此走队列档冷却并在同一请求内换号,别把排队号送回客户端
188
- if (peek.queued) shaped = withQueued(shaped);
189
- return shaped;
190
- }
191
- try {
192
- const agg = await aggregateQoderStream(upRes);
193
- const out = toCompletionJson({ model, chatId, ...agg });
194
- return echo(new Response(JSON.stringify(out), { status: 200, headers: { "Content-Type": "application/json" } }));
195
- } catch (e) {
196
- const out = echo(errRes(errorStatus(e), String(e?.detail || e?.message || e).slice(0, 300), e?.kind || "upstream_error"));
197
- const marked = e?.kind === "quota" ? withQuota(out) : out;
198
- return e?.queued ? withQueued(marked) : marked;
199
- }
200
- }
201
-
202
- return { runChat };
203
- }
1
+ // qoder 对话服务:上游恒 stream:true;请求方 stream=true → 真流式转发边收边吐,
2
+ // false → 聚合回 JSON。装配走 request.js,帧格式走 sse.js,两条管线共享。
3
+ import { compatFetch, timeoutSignal } from "../../compat.js";
4
+ import { mapModel } from "./payload.js";
5
+ import { effortFromBody, isReasoningEnabled } from "../../thinking-effort.js";
6
+ import { normalizeRegion } from "./constants.js";
7
+ import { buildUpstreamRequest } from "./request.js";
8
+ import { reshapeQoderStream } from "./stream.js";
9
+ import { aggregateQoderStream, toCompletionJson } from "./aggregate.js";
10
+ import { errorStatus, extractDelta } from "./sse.js";
11
+
12
+ function hostOf(url) {
13
+ try { return new URL(String(url)).host; } catch { return "-"; }
14
+ }
15
+
16
+ function errRes(status, msg, type = "upstream_error") {
17
+ return new Response(JSON.stringify({ error: { message: msg, type } }), { status, headers: { "Content-Type": "application/json" } });
18
+ }
19
+
20
+ function mapUpstreamError(status, detail) {
21
+ return { kind: status === 401 || status === 403 ? "auth" : "upstream", status, detail: String(detail || "").slice(0, 300) };
22
+ }
23
+
24
+ // 预读上限:判决恒在首帧(实测 457 字节),8 帧足够覆盖"角色帧先到、判决帧随后",
25
+ // 又不会把首字节延迟拖到不可控。超窗未得判决即放弃(退化为无冷却的流内 error,见 SPEC 已知上限)。
26
+ const PEEK_MAX_FRAMES = 8;
27
+
28
+ // 预读上游首帧只为取「流内判决」:qoder 把限流/鉴权判决裹在 HTTP 200 的 SSE 信封里
29
+ //(statusCodeValue:403 + serviceAvailable:false),upRes.status 恒 200,判决只有读完帧才知道。
30
+ // 不预读则状态码永远出不来流式路径 → 门面不冷却该号(ADR-0036 的粘号会把重试继续粘在它上面)。
31
+ // 返回 { res, status, frames }:res 是把预读帧回灌后的等价上游响应,frames 交回灌用。
32
+ // 任何异常都降级为「无判决」(status=0):排障手段不得变成故障源。
33
+ async function peekEnvelopeVerdict(upRes, cdbg) {
34
+ if (!upRes?.body) return { res: upRes, status: 0, quota: false, queued: false, frames: [] };
35
+ let reader;
36
+ try { reader = upRes.body.getReader(); } catch { return { res: upRes, status: 0, quota: false, queued: false, frames: [] }; }
37
+ const dec = new TextDecoder();
38
+ const frames = [];
39
+ let text = "";
40
+ let status = 0;
41
+ let quota = false;
42
+ let queued = false;
43
+ let decided = false;
44
+ try {
45
+ for (let i = 0; i < PEEK_MAX_FRAMES && !decided; i++) {
46
+ const { done, value } = await reader.read();
47
+ if (done) break;
48
+ if (value) { frames.push(value); text += dec.decode(value, { stream: true }); }
49
+ // 逐行判定:先到判决(err)或先到有效载荷即停,避免无谓等待
50
+ for (const raw of text.split("\n")) {
51
+ const line = raw.trim();
52
+ if (!line.startsWith("data:")) continue;
53
+ const p = line.slice(5).trim();
54
+ if (!p || p === "[DONE]") continue;
55
+ const d = extractDelta(p);
56
+ if (d.err) { status = Number(d.err.status) || 0; quota = d.err.kind === "quota"; queued = d.err.queued === true; decided = true; break; }
57
+ if (d.content || d.reasoning || d.toolCalls || d.usageIn || d.usageOut) { decided = true; break; }
58
+ }
59
+ }
60
+ } catch (e) {
61
+ cdbg(`[peek-throw] ${String(e?.message || e).slice(0, 160)}`);
62
+ }
63
+ cdbg(`[peek] frames=${frames.length} verdict=${status || "none(正常流)"} quota=${quota} queued=${queued}`);
64
+ // 回灌:已读帧先按原样吐出,再接续同一个 reader 的剩余部分
65
+ const body = new ReadableStream({
66
+ start(ctrl) {
67
+ for (const f of frames) { try { ctrl.enqueue(f); } catch {} }
68
+ (async () => {
69
+ try {
70
+ for (;;) {
71
+ const { done, value } = await reader.read();
72
+ if (done) break;
73
+ if (value) ctrl.enqueue(value);
74
+ }
75
+ } catch {}
76
+ try { ctrl.close(); } catch {}
77
+ })();
78
+ },
79
+ });
80
+ return { res: new Response(body, { status: upRes.status, headers: upRes.headers }), status, quota, queued, frames };
81
+ }
82
+ export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModelMeta } = {}) {
83
+ if (!fetchImpl) fetchImpl = compatFetch;
84
+
85
+ // body: OpenAI chat 请求;sess: 已建 COSY 会话;region: 该账号所属区(每号独立选端点)。返回 Response。
86
+ // 回显(可观测,不落凭据):x-mslxdff-upstream=本次请求的 host、x-mslxdff-qoder-region=cn|global;
87
+ // 管线层据此把"这次谁上的、打哪个站"投进模型日志(此前 provider 内部选号/切 URL 完全静默,排障无据)。
88
+ const withEcho = (res, url, region, pick) => {
89
+ try {
90
+ res.headers.set("x-mslxdff-upstream", hostOf(url));
91
+ res.headers.set("x-mslxdff-qoder-region", normalizeRegion(region));
92
+ // 账号选择决定(new/sticky/switch/forced):管线把它写进模型日志,"为什么会切号"才有据可查
93
+ if (pick) res.headers.set("x-mslxdff-qoder-account", String(pick));
94
+ } catch {}
95
+ return res;
96
+ };
97
+
98
+ // 上游状态回显(内部交接用,不落日志):流式路径按对外契约把非 200 整形成 "200 + 流内 error",
99
+ // 真实状态码只能靠这个头带出——否则 401/403/429/5xx 到不了 provider 门面,坏号不被冷却,
100
+ // 而"同请求粘号"(ADR-0036)会把后续重试继续粘在这个坏号上。
101
+ // Note: 流内判决需预读首帧才能拿到(HTTP 200 里裹 statusCodeValue)— 见 .agents/notes/implemented/bug-fix/2026-09-27-qoder-envelope-verdict-cooldown.md
102
+ const withUpstreamStatus = (res, st) => {
103
+ try { res.headers.set("x-mslxdff-qoder-upstream-status", String(st)); } catch {}
104
+ return res;
105
+ };
106
+ // 额度耗尽标记(内部交接,不落日志):门面据此换号(长冷却该号)而非普通短冷却
107
+ const withQuota = (res) => {
108
+ try { res.headers.set("x-mslxdff-qoder-quota", "1"); } catch {}
109
+ return res;
110
+ };
111
+ // 排队标记(内部交接,不落日志):与额度同为「换号」信号但冷却档不同——
112
+ // 排队是「稍后再来」,长冷却到点自动回池;额度是「今天没了」,按天重置才回。
113
+ const withQueued = (res) => {
114
+ try { res.headers.set("x-mslxdff-qoder-queued", "1"); } catch {}
115
+ return res;
116
+ };
117
+
118
+ async function runChat(body, sess, region = "global", pick = "") {
119
+ const stream = body?.stream !== false;
120
+ const model = body?.model || "qfmodel";
121
+ const chatId = "chatcmpl-qoder-" + Math.random().toString(16).slice(2, 10);
122
+ const tools = Array.isArray(body?.tools) && body.tools.length ? body.tools : null;
123
+ const messages = Array.isArray(body?.messages) ? body.messages : [];
124
+ const maxTokens = Number(body?.max_tokens) || 0;
125
+ // is_reasoning 由模型目录供数(index 注入 getModelMeta,只读 peekModels 快照缓存,chat 路径恒 0 上游调用);
126
+ // 冷缓存/缺失一律降级 false(= 旧行为,不阻断对话)—— P0:推理标志此前从未出门。
127
+ let isReasoning = false;
128
+ try {
129
+ if (getModelMeta) isReasoning = (await getModelMeta(sess, normalizeRegion(region), mapModel(model)))?.is_reasoning === true;
130
+ } catch {}
131
+ // 客户端显式档位优先于目录推断(grill-decisions Q3):关断→false、其余非空→true;无档位→沿用目录结论。
132
+ isReasoning = isReasoningEnabled(effortFromBody(body), isReasoning);
133
+
134
+ let upRes; let reqUrl = "";
135
+ const echo = (res) => withEcho(res, reqUrl, region, pick);
136
+ // 排障日志(QODER_DEBUG_STREAM=1 开):记录出口/状态/耗时/响应头,便于对比"200 但空轮"
137
+ const cdbg = (...a) => { if (process.env.QODER_DEBUG_STREAM === "1") console.log("[qoder-chat]", ...a); };
138
+ let upMs = 0;
139
+ try {
140
+ const req = buildUpstreamRequest({ sess, region: normalizeRegion(region), model, messages, tools, maxTokens, isReasoning });
141
+ reqUrl = req.url;
142
+ 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}`);
143
+ const t0 = Date.now();
144
+ upRes = await fetchImpl(req.url, {
145
+ method: "POST",
146
+ headers: req.headers,
147
+ body: req.bodyStr,
148
+ signal: timeoutSignal(timeoutMs || 120_000),
149
+ });
150
+ upMs = Date.now() - t0;
151
+ cdbg(`[res] status=${upRes.status} ms=${upMs} ct=${upRes.headers.get("content-type")} host=${hostOf(reqUrl)}`);
152
+ } catch (e) {
153
+ const detail = String(e?.message || e).slice(0, 300);
154
+ cdbg(`[res-throw] ms=${upMs} err=${detail}`);
155
+ // 网络异常(超时/连接失败)折成 502:门面据此冷却该号(否则坏号恒在轮换池里)
156
+ if (stream) return withUpstreamStatus(echo(reshapeQoderStream(new Response("data: [DONE]\n\n"), { model, chatId })), 502);
157
+ return withUpstreamStatus(echo(errRes(502, detail)), 502);
158
+ }
159
+ if (upRes.status !== 200) {
160
+ const detail = await upRes.text().catch(() => "");
161
+ cdbg(`[res-non200] status=${upRes.status} body=${detail.slice(0, 300)}`);
162
+ const e = mapUpstreamError(upRes.status, detail);
163
+ if (stream) {
164
+ // 首包错误也走流内 error 事件(与旧 reshapeStream catch 语义一致:200 + event:error + DONE)
165
+ const enc = new TextEncoder();
166
+ const s = new ReadableStream({
167
+ start(c) {
168
+ 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`));
169
+ c.enqueue(enc.encode(`event: error\ndata: ${JSON.stringify({ message: e.detail, type: e.kind })}\n\ndata: [DONE]\n\n`));
170
+ c.close();
171
+ },
172
+ });
173
+ // 对外仍是 200(契约不变),真实状态码经回显头交门面决定是否冷却
174
+ return withUpstreamStatus(echo(new Response(s, { status: 200, headers: { "Content-Type": "text/event-stream" } })), upRes.status);
175
+ }
176
+ return withUpstreamStatus(echo(errRes(errorStatus(e), e.detail, e.kind)), upRes.status);
177
+ }
178
+ if (stream) {
179
+ // 预读首帧取流内判决(qoder 把 403/429 裹在 200 的信封里),预读帧原样回灌给整形器。
180
+ // 判决状态码经 withUpstreamStatus 出门面 → index.js 据此 ring.onError 冷却该号 →
181
+ // 粘号选择器下次因 isCooling 为真而换号。对外仍恒 200(契约不变)。
182
+ const peek = await peekEnvelopeVerdict(upRes, cdbg);
183
+ let shaped = echo(reshapeQoderStream(peek.res, { model, chatId, prefetched: peek.frames }));
184
+ // 只在真有判决时挂状态码:正常流不写这个头(index.js 靠它区分"要不要冷却")
185
+ if (peek.status) shaped = withUpstreamStatus(shaped, peek.status);
186
+ if (peek.quota) shaped = withQuota(shaped);
187
+ // 排队旗标(内部交接):门面据此走队列档冷却并在同一请求内换号,别把排队号送回客户端
188
+ if (peek.queued) shaped = withQueued(shaped);
189
+ return shaped;
190
+ }
191
+ try {
192
+ const agg = await aggregateQoderStream(upRes);
193
+ const out = toCompletionJson({ model, chatId, ...agg });
194
+ return echo(new Response(JSON.stringify(out), { status: 200, headers: { "Content-Type": "application/json" } }));
195
+ } catch (e) {
196
+ const out = echo(errRes(errorStatus(e), String(e?.detail || e?.message || e).slice(0, 300), e?.kind || "upstream_error"));
197
+ const marked = e?.kind === "quota" ? withQuota(out) : out;
198
+ return e?.queued ? withQueued(marked) : marked;
199
+ }
200
+ }
201
+
202
+ return { runChat };
203
+ }