mslxdff 0.1.236 → 0.1.237

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 (102) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +264 -264
  4. package/src/autostart.js +62 -15
  5. package/src/bench/cline-bench.js +42 -42
  6. package/src/bench/probe.js +70 -70
  7. package/src/bench/report.js +162 -162
  8. package/src/bench/runner.js +77 -77
  9. package/src/bench/via-probe.js +124 -124
  10. package/src/bench/via-routes.js +87 -87
  11. package/src/bench/workbuddy-bench.js +54 -54
  12. package/src/chat/engine.js +160 -160
  13. package/src/chat/gateway.js +163 -163
  14. package/src/chat/orchestrator.js +234 -234
  15. package/src/chat/prompt.js +70 -70
  16. package/src/chat/repl.js +88 -88
  17. package/src/chat/terminal.js +135 -135
  18. package/src/chat/tools.js +306 -306
  19. package/src/chat-pipeline/index.js +155 -155
  20. package/src/chat-pipeline/policy.js +84 -84
  21. package/src/cli/commands/group.js +266 -266
  22. package/src/cli/commands/model/picks.js +50 -50
  23. package/src/cli/commands/provider/bench-via.js +247 -247
  24. package/src/cli/commands/provider/bench.js +141 -141
  25. package/src/cli/commands/provider/index.js +139 -139
  26. package/src/cli/commands/provider/models.js +149 -149
  27. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  28. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  29. package/src/cli/commands/sync.js +310 -310
  30. package/src/cli/status.js +282 -282
  31. package/src/daemon.js +246 -246
  32. package/src/model-capabilities/enrich.js +86 -86
  33. package/src/model-capabilities/index.js +183 -183
  34. package/src/model-capabilities/parse.js +70 -70
  35. package/src/models.js +293 -293
  36. package/src/peers.js +389 -389
  37. package/src/providers/agentrouter.js +238 -238
  38. package/src/providers/base.js +164 -164
  39. package/src/providers/cline/auth.js +228 -228
  40. package/src/providers/cline/chat.js +307 -307
  41. package/src/providers/cline.js +2 -2
  42. package/src/providers/codearts/chat.js +207 -207
  43. package/src/providers/dispatcher.js +147 -147
  44. package/src/providers/globalqwenwork/cosy.js +123 -123
  45. package/src/providers/globalqwenwork/crypto.js +218 -218
  46. package/src/providers/globalqwenwork/http.js +20 -20
  47. package/src/providers/globalqwenwork/rsa.js +54 -54
  48. package/src/providers/globalqwenwork/sse.js +268 -268
  49. package/src/providers/globalqwenwork/stream.js +130 -130
  50. package/src/providers/globalqwenwork/upstream.js +122 -122
  51. package/src/providers/keyring.js +60 -60
  52. package/src/providers/qoder/chat.js +292 -292
  53. package/src/providers/qoder/index.js +311 -311
  54. package/src/providers/qoder/sse.js +233 -233
  55. package/src/providers/qwenwork/account-store.js +133 -133
  56. package/src/providers/qwenwork/constants.js +67 -67
  57. package/src/providers/qwenwork/cosy.js +120 -120
  58. package/src/providers/qwenwork/crypto.js +218 -218
  59. package/src/providers/qwenwork/http.js +20 -20
  60. package/src/providers/qwenwork/index.js +337 -337
  61. package/src/providers/qwenwork/payload.js +144 -144
  62. package/src/providers/qwenwork/rsa.js +54 -54
  63. package/src/providers/qwenwork/sse.js +268 -268
  64. package/src/providers/qwenwork/stream.js +130 -130
  65. package/src/providers/qwenwork/upstream.js +120 -120
  66. package/src/providers/qwenwork.js +1 -1
  67. package/src/providers/raccoon/const.js +219 -219
  68. package/src/providers/raccoon/credits.js +285 -285
  69. package/src/providers/registry.js +89 -89
  70. package/src/providers/workbuddy/chat.js +298 -298
  71. package/src/providers/workbuddy/reshape.js +152 -152
  72. package/src/providers/workbuddy.js +2 -2
  73. package/src/reasoning.js +32 -32
  74. package/src/routes/chat/broadband-handler.js +80 -80
  75. package/src/routes/chat/gateway.js +46 -46
  76. package/src/routes/chat/relay-pipeline.js +282 -282
  77. package/src/routes/chat/via-route-handler.js +158 -158
  78. package/src/routes/hedge.js +260 -260
  79. package/src/routes/models-route.js +167 -167
  80. package/src/routes/peers.js +373 -373
  81. package/src/routes/stream.js +364 -364
  82. package/src/runtime/bootstrap.js +75 -75
  83. package/src/runtime/keep-warm.js +135 -135
  84. package/src/runtime/provider-gate.js +75 -75
  85. package/src/runtime/providers-setup.js +187 -187
  86. package/src/runtime/raccoon-checkin.js +175 -175
  87. package/src/runtime/server-lifecycle.js +34 -10
  88. package/src/server.js +85 -85
  89. package/src/state/schemas/allowlist.js +92 -92
  90. package/src/sync-opencode.js +280 -280
  91. package/src/transport/index.js +244 -244
  92. package/src/transport/pool.js +56 -56
  93. package/src/transport/retry.js +24 -24
  94. package/src/transport/sse.js +93 -93
  95. package/src/upstream-probe/display.js +52 -52
  96. package/src/upstream-probe/probe.js +49 -49
  97. package/src/upstream-probe/rotate.js +110 -110
  98. package/src/upstream-probe/start.js +45 -45
  99. package/src/upstream.js +355 -355
  100. package/docs/cli_help_mini.md +0 -149
  101. package/src/providers/AGENTS.md +0 -49
  102. package/src/routes/AGENTS.md +0 -37
@@ -1,292 +1,292 @@
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, effortLevelFromBody, 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, findQuotaSignal, findLoginSignal, findQueueSignal, retryAfterFrom } 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
- // 判决旗标(统一出口):登录过期/额度/排队(含无容量子型)+ retryAfter 秒数。
29
- // 非 200 直出与 200 信封裹身两条路径共用同一解析口,防两处漂移。
30
- function verdictFlagsFrom(objOrText) {
31
- const v = { login: false, quota: false, queued: false, noCapacity: false, retryAfter: 0 };
32
- if (findLoginSignal(objOrText)) v.login = true;
33
- else if (findQuotaSignal(objOrText)) v.quota = true;
34
- else {
35
- const q = findQueueSignal(objOrText);
36
- if (q) { v.queued = true; v.noCapacity = q.noCapacity === true; }
37
- }
38
- const ra = retryAfterFrom(typeof objOrText === "string" ? objOrText : JSON.stringify(objOrText ?? ""));
39
- if (ra.retryAfter) v.retryAfter = ra.retryAfter;
40
- return v;
41
- }
42
-
43
- // 预读上游首帧只为取「流内判决」:qoder 把限流/鉴权判决裹在 HTTP 200 的 SSE 信封里
44
- //(statusCodeValue:403 + serviceAvailable:false),upRes.status 恒 200,判决只有读完帧才知道。
45
- // 不预读则状态码永远出不来流式路径 → 门面不冷却该号(ADR-0036 的粘号会把重试继续粘在它上面)。
46
- // 返回 { res, status, frames }:res 是把预读帧回灌后的等价上游响应,frames 交回灌用。
47
- // 任何异常都降级为「无判决」(status=0):排障手段不得变成故障源。
48
- async function peekEnvelopeVerdict(upRes, cdbg) {
49
- if (!upRes?.body) return { res: upRes, status: 0, quota: false, queued: false, noCapacity: false, retryAfter: 0, login: false, frames: [] };
50
- let reader;
51
- try { reader = upRes.body.getReader(); } catch { return { res: upRes, status: 0, quota: false, queued: false, noCapacity: false, retryAfter: 0, login: false, frames: [] }; }
52
- const dec = new TextDecoder();
53
- const frames = [];
54
- let text = "";
55
- let status = 0;
56
- let quota = false;
57
- let queued = false;
58
- let noCapacity = false;
59
- let retryAfter = 0;
60
- let login = false;
61
- let decided = false;
62
- try {
63
- for (let i = 0; i < PEEK_MAX_FRAMES && !decided; i++) {
64
- const { done, value } = await reader.read();
65
- if (done) break;
66
- if (value) { frames.push(value); text += dec.decode(value, { stream: true }); }
67
- // 逐行判定:先到判决(err)或先到有效载荷即停,避免无谓等待
68
- for (const raw of text.split("\n")) {
69
- const line = raw.trim();
70
- if (!line.startsWith("data:")) continue;
71
- const p = line.slice(5).trim();
72
- if (!p || p === "[DONE]") continue;
73
- const d = extractDelta(p);
74
- if (d.err) {
75
- status = Number(d.err.status) || 0;
76
- quota = d.err.kind === "quota";
77
- queued = d.err.queued === true;
78
- noCapacity = d.err.noCapacity === true;
79
- retryAfter = Number(d.err.retryAfter) || 0;
80
- login = d.err.loginExpired === true;
81
- decided = true; break;
82
- }
83
- if (d.content || d.reasoning || d.toolCalls || d.usageIn || d.usageOut) { decided = true; break; }
84
- }
85
- }
86
- } catch (e) {
87
- cdbg(`[peek-throw] ${String(e?.message || e).slice(0, 160)}`);
88
- }
89
- cdbg(`[peek] frames=${frames.length} verdict=${status || "none(正常流)"} quota=${quota} queued=${queued} noCapacity=${noCapacity} ra=${retryAfter} login=${login}`);
90
- // 回灌:已读帧先按原样吐出,再接续同一个 reader 的剩余部分
91
- const body = new ReadableStream({
92
- start(ctrl) {
93
- for (const f of frames) { try { ctrl.enqueue(f); } catch {} }
94
- (async () => {
95
- try {
96
- for (;;) {
97
- const { done, value } = await reader.read();
98
- if (done) break;
99
- if (value) ctrl.enqueue(value);
100
- }
101
- } catch {}
102
- try { ctrl.close(); } catch {}
103
- })();
104
- },
105
- });
106
- return { res: new Response(body, { status: upRes.status, headers: upRes.headers }), status, quota, queued, noCapacity, retryAfter, login, frames };
107
- }
108
- export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModelMeta } = {}) {
109
- if (!fetchImpl) fetchImpl = compatFetch;
110
-
111
- // body: OpenAI chat 请求;sess: 已建 COSY 会话;region: 该账号所属区(每号独立选端点)。返回 Response。
112
- // 回显(可观测,不落凭据):x-mslxdff-upstream=本次请求的 host、x-mslxdff-qoder-region=cn|global;
113
- // 管线层据此把"这次谁上的、打哪个站"投进模型日志(此前 provider 内部选号/切 URL 完全静默,排障无据)。
114
- const withEcho = (res, url, region, pick) => {
115
- try {
116
- res.headers.set("x-mslxdff-upstream", hostOf(url));
117
- res.headers.set("x-mslxdff-qoder-region", normalizeRegion(region));
118
- // 账号选择决定(new/sticky/switch/forced):管线把它写进模型日志,"为什么会切号"才有据可查
119
- if (pick) res.headers.set("x-mslxdff-qoder-account", String(pick));
120
- } catch {}
121
- return res;
122
- };
123
-
124
- // 上游状态回显(内部交接用,不落日志):流式路径按对外契约把非 200 整形成 "200 + 流内 error",
125
- // 真实状态码只能靠这个头带出——否则 401/403/429/5xx 到不了 provider 门面,坏号不被冷却,
126
- // 而"同请求粘号"(ADR-0036)会把后续重试继续粘在这个坏号上。
127
- // Note: 流内判决需预读首帧才能拿到(HTTP 200 里裹 statusCodeValue)— 见 .agents/notes/implemented/bug-fix/2026-09-27-qoder-envelope-verdict-cooldown.md
128
- const withUpstreamStatus = (res, st) => {
129
- try { res.headers.set("x-mslxdff-qoder-upstream-status", String(st)); } catch {}
130
- return res;
131
- };
132
- // 额度耗尽标记(内部交接,不落日志):门面据此换号(长冷却该号)而非普通短冷却
133
- const withQuota = (res) => {
134
- try { res.headers.set("x-mslxdff-qoder-quota", "1"); } catch {}
135
- return res;
136
- };
137
- // 排队标记(内部交接,不落日志):与额度同为「换号」信号但冷却档不同——
138
- // 排队是「稍后再来」,长冷却到点自动回池;额度是「今天没了」,按天重置才回。
139
- const withQueued = (res) => {
140
- try { res.headers.set("x-mslxdff-qoder-queued", "1"); } catch {}
141
- return res;
142
- };
143
- // 无容量旗标:10605 且 queueCount:0 + serviceAvailable:false = 整池共享同一 p3 队列、
144
- // 换号救不了,只能等 Retry-After。门面据此**不换号轮询**,带 429+Retry-After 如实透给客户端。
145
- const withNoCapacity = (res, retryAfter) => {
146
- try {
147
- res.headers.set("x-mslxdff-qoder-nocapacity", "1");
148
- if (retryAfter) {
149
- res.headers.set("x-mslxdff-qoder-retry-after", String(retryAfter));
150
- res.headers.set("Retry-After", String(retryAfter)); // 对客户端的标准交代(relay 层原样转发响应头)
151
- }
152
- } catch {}
153
- return res;
154
- };
155
- // 登录过期标记(内部交接,不落日志):门面据此走「刷新令牌→原地重发」,不当普通坏号冷却轮询。
156
- const withLogin = (res) => {
157
- try { res.headers.set("x-mslxdff-qoder-login", "1"); } catch {}
158
- return res;
159
- };
160
- // 判决结果钉头(2026-10-22 用户实证「没多打日志→排查全靠猜」后补):verdict 取值
161
- // login/quota/queue/queue-nocapacity/none,由 model-trace.upstreamEcho 落进模型日志。
162
- // 关键在 none 也要显式钉——「压根没命中」与「命中但走别的分支」在 403 盲视图里必须分得开。
163
- const verdictName = (v) => (v.login ? "login" : v.quota ? "quota" : v.queued ? (v.noCapacity ? "queue-nocapacity" : "queue") : "none");
164
- const withVerdict = (res, v) => {
165
- try { res.headers.set("x-mslxdff-qoder-verdict", verdictName(v)); } catch {}
166
- return res;
167
- };
168
-
169
- async function runChat(body, sess, region = "global", pick = "") {
170
- const stream = body?.stream !== false;
171
- const model = body?.model || "qfmodel";
172
- const chatId = "chatcmpl-qoder-" + Math.random().toString(16).slice(2, 10);
173
- const tools = Array.isArray(body?.tools) && body.tools.length ? body.tools : null;
174
- const messages = Array.isArray(body?.messages) ? body.messages : [];
175
- const maxTokens = Number(body?.max_tokens) || 0;
176
- // is_reasoning 与能力元数据(窗口/档位)由模型目录一次供数(index 注入 getModelMeta,
177
- // 只读 peekModels 快照缓存,chat 路径恒 0 额外上游调用;preheat 负责灌缓存)。
178
- // 冷缓存/缺失一律降级:isReasoning=false、无 meta(= 旧行为,不阻断对话)。
179
- let meta = null;
180
- let isReasoning = false;
181
- try {
182
- if (getModelMeta) {
183
- meta = (await getModelMeta(sess, normalizeRegion(region), mapModel(model))) || null;
184
- isReasoning = meta?.is_reasoning === true;
185
- }
186
- } catch {}
187
- // 客户端显式档位优先于目录推断(grill-decisions Q3):关断→false、其余非空→true;无档位→沿用目录结论。
188
- isReasoning = isReasoningEnabled(effortFromBody(body), isReasoning);
189
- const effortLevel = effortLevelFromBody(body); // 真档位串(low/medium/high…),透传给装配口对齐模型档位
190
-
191
- let upRes; let reqUrl = "";
192
- const echo = (res) => withEcho(res, reqUrl, region, pick);
193
- // 排障日志(QODER_DEBUG_STREAM=1 开):记录出口/状态/耗时/响应头,便于对比"200 但空轮"
194
- const cdbg = (...a) => { if (process.env.QODER_DEBUG_STREAM === "1") console.log("[qoder-chat]", ...a); };
195
- let upMs = 0;
196
- try {
197
- const req = buildUpstreamRequest({ sess, region: normalizeRegion(region), model, messages, tools, maxTokens, isReasoning, meta, effort: effortLevel });
198
- reqUrl = req.url;
199
- 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}`);
200
- const t0 = Date.now();
201
- upRes = await fetchImpl(req.url, {
202
- method: "POST",
203
- headers: req.headers,
204
- body: req.bodyStr,
205
- signal: timeoutSignal(timeoutMs || 120_000),
206
- });
207
- upMs = Date.now() - t0;
208
- cdbg(`[res] status=${upRes.status} ms=${upMs} ct=${upRes.headers.get("content-type")} host=${hostOf(reqUrl)}`);
209
- } catch (e) {
210
- const detail = String(e?.message || e).slice(0, 300);
211
- cdbg(`[res-throw] ms=${upMs} err=${detail}`);
212
- // 网络异常(超时/连接失败)折成 502:门面据此冷却该号(否则坏号恒在轮换池里)
213
- if (stream) return withUpstreamStatus(echo(reshapeQoderStream(new Response("data: [DONE]\n\n"), { model, chatId })), 502);
214
- return withUpstreamStatus(echo(errRes(502, detail)), 502);
215
- }
216
- if (upRes.status !== 200) {
217
- const detail = await upRes.text().catch(() => "");
218
- cdbg(`[res-non200] status=${upRes.status} body=${detail.slice(0, 300)}`);
219
- // 真 HTTP 403 也可能直出 10605 排队/110 额度/105 登录(不只裹 200 信封)。
220
- // 旧版一律 mapUpstreamError(403)→auth→冷却换号,把「整池共享的 p3 无容量」当死号轮询白烧。
221
- // 先解析 body 定旗标:无容量 → 不换号、带 Retry-After 如实透;额度/登录 → 交门面对应处理。
222
- const vf = verdictFlagsFrom(detail);
223
- cdbg(`[verdict] status=${upRes.status} login=${vf.login} quota=${vf.quota} queued=${vf.queued} noCapacity=${vf.noCapacity} ra=${vf.retryAfter} bodyLen=${detail.length} body=${detail.slice(0, 300)}`);
224
- const e = vf.login ? { kind: "auth", status: 401, detail }
225
- : vf.quota ? { kind: "quota", status: 429, detail }
226
- : vf.queued ? { kind: "upstream", status: 429, detail }
227
- : mapUpstreamError(upRes.status, detail);
228
- if (stream) {
229
- // 首包错误也走流内 error 事件(与旧 reshapeStream catch 语义一致:200 + event:error + DONE)
230
- const enc = new TextEncoder();
231
- const s = new ReadableStream({
232
- start(c) {
233
- 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`));
234
- c.enqueue(enc.encode(`event: error\ndata: ${JSON.stringify({ message: e.detail, type: e.kind })}\n\ndata: [DONE]\n\n`));
235
- c.close();
236
- },
237
- });
238
- // 对外仍是 200(契约不变),真实状态码经回显头交门面决定是否冷却
239
- let shaped = withUpstreamStatus(echo(new Response(s, { status: 200, headers: { "Content-Type": "text/event-stream" } })), upRes.status);
240
- shaped = withVerdict(shaped, vf);
241
- if (vf.quota) shaped = withQuota(shaped);
242
- if (vf.login) shaped = withLogin(shaped);
243
- if (vf.queued) shaped = vf.noCapacity ? withNoCapacity(shaped, vf.retryAfter) : withQueued(shaped);
244
- return shaped;
245
- }
246
- let out = withUpstreamStatus(echo(errRes(errorStatus(e), e.detail, e.kind)), upRes.status);
247
- out = withVerdict(out, vf);
248
- if (vf.quota) out = withQuota(out);
249
- if (vf.login) out = withLogin(out);
250
- if (vf.queued) out = vf.noCapacity ? withNoCapacity(out, vf.retryAfter) : withQueued(out);
251
- return out;
252
- }
253
- if (stream) {
254
- // 预读首帧取流内判决(qoder 把 403/429 裹在 200 的信封里),预读帧原样回灌给整形器。
255
- // 判决状态码经 withUpstreamStatus 出门面 → index.js 据此 ring.onError 冷却该号 →
256
- // 粘号选择器下次因 isCooling 为真而换号。对外仍恒 200(契约不变)。
257
- const peek = await peekEnvelopeVerdict(upRes, cdbg);
258
- const pvf = { login: peek.login, quota: peek.quota, queued: peek.queued, noCapacity: peek.noCapacity };
259
- let shaped = echo(reshapeQoderStream(peek.res, { model, chatId, prefetched: peek.frames }));
260
- // 只在真有判决时挂状态码:正常流不写这个头(index.js 靠它区分"要不要冷却")
261
- if (peek.status) shaped = withUpstreamStatus(shaped, peek.status);
262
- // verdict 钉头同理只钉有判决的流——正常流不写,避免成功请求日志被 verdict=none 刷屏
263
- if (peek.status) shaped = withVerdict(shaped, pvf);
264
- if (peek.quota) shaped = withQuota(shaped);
265
- // 排队旗标(内部交接):真排队(有人等位)→ 门面换号碰别的号;无容量 → 门面不换号透 Retry-After
266
- if (peek.queued) shaped = peek.noCapacity ? withNoCapacity(shaped, peek.retryAfter) : withQueued(shaped);
267
- // 登录过期旗标:门面据此刷新令牌原地重发(不冷却换号轮询整池过期号)
268
- if (peek.login) shaped = withLogin(shaped);
269
- return shaped;
270
- }
271
- try {
272
- const agg = await aggregateQoderStream(upRes);
273
- const out = toCompletionJson({ model, chatId, ...agg });
274
- return echo(new Response(JSON.stringify(out), { status: 200, headers: { "Content-Type": "application/json" } }));
275
- } catch (e) {
276
- // 聚合出口的判决折态(2026-10-22 真机实锤 req=mvj6u7wj:HTTP 恒 200、403 裹在 SSE 信封
277
- // statusCodeValue 里,extractDelta 的 err.status=403 原样 errRes 出去 → nocapacity 头挂了、
278
- // 状态却还是 403。无容量档必须与非 200 直出路径同口径折 429,否则门面 break 后
279
- // lastRes 透给客户端仍是 403,Retry-After 语义全丢)。
280
- const cvf = { login: e?.loginExpired === true, quota: e?.kind === "quota", queued: e?.queued === true, noCapacity: e?.noCapacity === true };
281
- const aggStatus = cvf.noCapacity ? 429 : errorStatus(e);
282
- let out = echo(errRes(aggStatus, String(e?.detail || e?.message || e).slice(0, 300), e?.kind || "upstream_error"));
283
- out = withVerdict(out, cvf);
284
- if (cvf.noCapacity && e?.retryAfter) out = withNoCapacity(out, e.retryAfter);
285
- let marked = cvf.quota ? withQuota(out) : out;
286
- if (cvf.queued && !cvf.noCapacity) marked = withQueued(marked);
287
- return cvf.login ? withLogin(marked) : marked;
288
- }
289
- }
290
-
291
- return { runChat };
292
- }
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, effortLevelFromBody, 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, findQuotaSignal, findLoginSignal, findQueueSignal, retryAfterFrom } 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
+ // 判决旗标(统一出口):登录过期/额度/排队(含无容量子型)+ retryAfter 秒数。
29
+ // 非 200 直出与 200 信封裹身两条路径共用同一解析口,防两处漂移。
30
+ function verdictFlagsFrom(objOrText) {
31
+ const v = { login: false, quota: false, queued: false, noCapacity: false, retryAfter: 0 };
32
+ if (findLoginSignal(objOrText)) v.login = true;
33
+ else if (findQuotaSignal(objOrText)) v.quota = true;
34
+ else {
35
+ const q = findQueueSignal(objOrText);
36
+ if (q) { v.queued = true; v.noCapacity = q.noCapacity === true; }
37
+ }
38
+ const ra = retryAfterFrom(typeof objOrText === "string" ? objOrText : JSON.stringify(objOrText ?? ""));
39
+ if (ra.retryAfter) v.retryAfter = ra.retryAfter;
40
+ return v;
41
+ }
42
+
43
+ // 预读上游首帧只为取「流内判决」:qoder 把限流/鉴权判决裹在 HTTP 200 的 SSE 信封里
44
+ //(statusCodeValue:403 + serviceAvailable:false),upRes.status 恒 200,判决只有读完帧才知道。
45
+ // 不预读则状态码永远出不来流式路径 → 门面不冷却该号(ADR-0036 的粘号会把重试继续粘在它上面)。
46
+ // 返回 { res, status, frames }:res 是把预读帧回灌后的等价上游响应,frames 交回灌用。
47
+ // 任何异常都降级为「无判决」(status=0):排障手段不得变成故障源。
48
+ async function peekEnvelopeVerdict(upRes, cdbg) {
49
+ if (!upRes?.body) return { res: upRes, status: 0, quota: false, queued: false, noCapacity: false, retryAfter: 0, login: false, frames: [] };
50
+ let reader;
51
+ try { reader = upRes.body.getReader(); } catch { return { res: upRes, status: 0, quota: false, queued: false, noCapacity: false, retryAfter: 0, login: false, frames: [] }; }
52
+ const dec = new TextDecoder();
53
+ const frames = [];
54
+ let text = "";
55
+ let status = 0;
56
+ let quota = false;
57
+ let queued = false;
58
+ let noCapacity = false;
59
+ let retryAfter = 0;
60
+ let login = false;
61
+ let decided = false;
62
+ try {
63
+ for (let i = 0; i < PEEK_MAX_FRAMES && !decided; i++) {
64
+ const { done, value } = await reader.read();
65
+ if (done) break;
66
+ if (value) { frames.push(value); text += dec.decode(value, { stream: true }); }
67
+ // 逐行判定:先到判决(err)或先到有效载荷即停,避免无谓等待
68
+ for (const raw of text.split("\n")) {
69
+ const line = raw.trim();
70
+ if (!line.startsWith("data:")) continue;
71
+ const p = line.slice(5).trim();
72
+ if (!p || p === "[DONE]") continue;
73
+ const d = extractDelta(p);
74
+ if (d.err) {
75
+ status = Number(d.err.status) || 0;
76
+ quota = d.err.kind === "quota";
77
+ queued = d.err.queued === true;
78
+ noCapacity = d.err.noCapacity === true;
79
+ retryAfter = Number(d.err.retryAfter) || 0;
80
+ login = d.err.loginExpired === true;
81
+ decided = true; break;
82
+ }
83
+ if (d.content || d.reasoning || d.toolCalls || d.usageIn || d.usageOut) { decided = true; break; }
84
+ }
85
+ }
86
+ } catch (e) {
87
+ cdbg(`[peek-throw] ${String(e?.message || e).slice(0, 160)}`);
88
+ }
89
+ cdbg(`[peek] frames=${frames.length} verdict=${status || "none(正常流)"} quota=${quota} queued=${queued} noCapacity=${noCapacity} ra=${retryAfter} login=${login}`);
90
+ // 回灌:已读帧先按原样吐出,再接续同一个 reader 的剩余部分
91
+ const body = new ReadableStream({
92
+ start(ctrl) {
93
+ for (const f of frames) { try { ctrl.enqueue(f); } catch {} }
94
+ (async () => {
95
+ try {
96
+ for (;;) {
97
+ const { done, value } = await reader.read();
98
+ if (done) break;
99
+ if (value) ctrl.enqueue(value);
100
+ }
101
+ } catch {}
102
+ try { ctrl.close(); } catch {}
103
+ })();
104
+ },
105
+ });
106
+ return { res: new Response(body, { status: upRes.status, headers: upRes.headers }), status, quota, queued, noCapacity, retryAfter, login, frames };
107
+ }
108
+ export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModelMeta } = {}) {
109
+ if (!fetchImpl) fetchImpl = compatFetch;
110
+
111
+ // body: OpenAI chat 请求;sess: 已建 COSY 会话;region: 该账号所属区(每号独立选端点)。返回 Response。
112
+ // 回显(可观测,不落凭据):x-mslxdff-upstream=本次请求的 host、x-mslxdff-qoder-region=cn|global;
113
+ // 管线层据此把"这次谁上的、打哪个站"投进模型日志(此前 provider 内部选号/切 URL 完全静默,排障无据)。
114
+ const withEcho = (res, url, region, pick) => {
115
+ try {
116
+ res.headers.set("x-mslxdff-upstream", hostOf(url));
117
+ res.headers.set("x-mslxdff-qoder-region", normalizeRegion(region));
118
+ // 账号选择决定(new/sticky/switch/forced):管线把它写进模型日志,"为什么会切号"才有据可查
119
+ if (pick) res.headers.set("x-mslxdff-qoder-account", String(pick));
120
+ } catch {}
121
+ return res;
122
+ };
123
+
124
+ // 上游状态回显(内部交接用,不落日志):流式路径按对外契约把非 200 整形成 "200 + 流内 error",
125
+ // 真实状态码只能靠这个头带出——否则 401/403/429/5xx 到不了 provider 门面,坏号不被冷却,
126
+ // 而"同请求粘号"(ADR-0036)会把后续重试继续粘在这个坏号上。
127
+ // Note: 流内判决需预读首帧才能拿到(HTTP 200 里裹 statusCodeValue)— 见 .agents/notes/implemented/bug-fix/2026-09-27-qoder-envelope-verdict-cooldown.md
128
+ const withUpstreamStatus = (res, st) => {
129
+ try { res.headers.set("x-mslxdff-qoder-upstream-status", String(st)); } catch {}
130
+ return res;
131
+ };
132
+ // 额度耗尽标记(内部交接,不落日志):门面据此换号(长冷却该号)而非普通短冷却
133
+ const withQuota = (res) => {
134
+ try { res.headers.set("x-mslxdff-qoder-quota", "1"); } catch {}
135
+ return res;
136
+ };
137
+ // 排队标记(内部交接,不落日志):与额度同为「换号」信号但冷却档不同——
138
+ // 排队是「稍后再来」,长冷却到点自动回池;额度是「今天没了」,按天重置才回。
139
+ const withQueued = (res) => {
140
+ try { res.headers.set("x-mslxdff-qoder-queued", "1"); } catch {}
141
+ return res;
142
+ };
143
+ // 无容量旗标:10605 且 queueCount:0 + serviceAvailable:false = 整池共享同一 p3 队列、
144
+ // 换号救不了,只能等 Retry-After。门面据此**不换号轮询**,带 429+Retry-After 如实透给客户端。
145
+ const withNoCapacity = (res, retryAfter) => {
146
+ try {
147
+ res.headers.set("x-mslxdff-qoder-nocapacity", "1");
148
+ if (retryAfter) {
149
+ res.headers.set("x-mslxdff-qoder-retry-after", String(retryAfter));
150
+ res.headers.set("Retry-After", String(retryAfter)); // 对客户端的标准交代(relay 层原样转发响应头)
151
+ }
152
+ } catch {}
153
+ return res;
154
+ };
155
+ // 登录过期标记(内部交接,不落日志):门面据此走「刷新令牌→原地重发」,不当普通坏号冷却轮询。
156
+ const withLogin = (res) => {
157
+ try { res.headers.set("x-mslxdff-qoder-login", "1"); } catch {}
158
+ return res;
159
+ };
160
+ // 判决结果钉头(2026-10-22 用户实证「没多打日志→排查全靠猜」后补):verdict 取值
161
+ // login/quota/queue/queue-nocapacity/none,由 model-trace.upstreamEcho 落进模型日志。
162
+ // 关键在 none 也要显式钉——「压根没命中」与「命中但走别的分支」在 403 盲视图里必须分得开。
163
+ const verdictName = (v) => (v.login ? "login" : v.quota ? "quota" : v.queued ? (v.noCapacity ? "queue-nocapacity" : "queue") : "none");
164
+ const withVerdict = (res, v) => {
165
+ try { res.headers.set("x-mslxdff-qoder-verdict", verdictName(v)); } catch {}
166
+ return res;
167
+ };
168
+
169
+ async function runChat(body, sess, region = "global", pick = "") {
170
+ const stream = body?.stream !== false;
171
+ const model = body?.model || "qfmodel";
172
+ const chatId = "chatcmpl-qoder-" + Math.random().toString(16).slice(2, 10);
173
+ const tools = Array.isArray(body?.tools) && body.tools.length ? body.tools : null;
174
+ const messages = Array.isArray(body?.messages) ? body.messages : [];
175
+ const maxTokens = Number(body?.max_tokens) || 0;
176
+ // is_reasoning 与能力元数据(窗口/档位)由模型目录一次供数(index 注入 getModelMeta,
177
+ // 只读 peekModels 快照缓存,chat 路径恒 0 额外上游调用;preheat 负责灌缓存)。
178
+ // 冷缓存/缺失一律降级:isReasoning=false、无 meta(= 旧行为,不阻断对话)。
179
+ let meta = null;
180
+ let isReasoning = false;
181
+ try {
182
+ if (getModelMeta) {
183
+ meta = (await getModelMeta(sess, normalizeRegion(region), mapModel(model))) || null;
184
+ isReasoning = meta?.is_reasoning === true;
185
+ }
186
+ } catch {}
187
+ // 客户端显式档位优先于目录推断(grill-decisions Q3):关断→false、其余非空→true;无档位→沿用目录结论。
188
+ isReasoning = isReasoningEnabled(effortFromBody(body), isReasoning);
189
+ const effortLevel = effortLevelFromBody(body); // 真档位串(low/medium/high…),透传给装配口对齐模型档位
190
+
191
+ let upRes; let reqUrl = "";
192
+ const echo = (res) => withEcho(res, reqUrl, region, pick);
193
+ // 排障日志(QODER_DEBUG_STREAM=1 开):记录出口/状态/耗时/响应头,便于对比"200 但空轮"
194
+ const cdbg = (...a) => { if (process.env.QODER_DEBUG_STREAM === "1") console.log("[qoder-chat]", ...a); };
195
+ let upMs = 0;
196
+ try {
197
+ const req = buildUpstreamRequest({ sess, region: normalizeRegion(region), model, messages, tools, maxTokens, isReasoning, meta, effort: effortLevel });
198
+ reqUrl = req.url;
199
+ 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}`);
200
+ const t0 = Date.now();
201
+ upRes = await fetchImpl(req.url, {
202
+ method: "POST",
203
+ headers: req.headers,
204
+ body: req.bodyStr,
205
+ signal: timeoutSignal(timeoutMs || 120_000),
206
+ });
207
+ upMs = Date.now() - t0;
208
+ cdbg(`[res] status=${upRes.status} ms=${upMs} ct=${upRes.headers.get("content-type")} host=${hostOf(reqUrl)}`);
209
+ } catch (e) {
210
+ const detail = String(e?.message || e).slice(0, 300);
211
+ cdbg(`[res-throw] ms=${upMs} err=${detail}`);
212
+ // 网络异常(超时/连接失败)折成 502:门面据此冷却该号(否则坏号恒在轮换池里)
213
+ if (stream) return withUpstreamStatus(echo(reshapeQoderStream(new Response("data: [DONE]\n\n"), { model, chatId })), 502);
214
+ return withUpstreamStatus(echo(errRes(502, detail)), 502);
215
+ }
216
+ if (upRes.status !== 200) {
217
+ const detail = await upRes.text().catch(() => "");
218
+ cdbg(`[res-non200] status=${upRes.status} body=${detail.slice(0, 300)}`);
219
+ // 真 HTTP 403 也可能直出 10605 排队/110 额度/105 登录(不只裹 200 信封)。
220
+ // 旧版一律 mapUpstreamError(403)→auth→冷却换号,把「整池共享的 p3 无容量」当死号轮询白烧。
221
+ // 先解析 body 定旗标:无容量 → 不换号、带 Retry-After 如实透;额度/登录 → 交门面对应处理。
222
+ const vf = verdictFlagsFrom(detail);
223
+ cdbg(`[verdict] status=${upRes.status} login=${vf.login} quota=${vf.quota} queued=${vf.queued} noCapacity=${vf.noCapacity} ra=${vf.retryAfter} bodyLen=${detail.length} body=${detail.slice(0, 300)}`);
224
+ const e = vf.login ? { kind: "auth", status: 401, detail }
225
+ : vf.quota ? { kind: "quota", status: 429, detail }
226
+ : vf.queued ? { kind: "upstream", status: 429, detail }
227
+ : mapUpstreamError(upRes.status, detail);
228
+ if (stream) {
229
+ // 首包错误也走流内 error 事件(与旧 reshapeStream catch 语义一致:200 + event:error + DONE)
230
+ const enc = new TextEncoder();
231
+ const s = new ReadableStream({
232
+ start(c) {
233
+ 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`));
234
+ c.enqueue(enc.encode(`event: error\ndata: ${JSON.stringify({ message: e.detail, type: e.kind })}\n\ndata: [DONE]\n\n`));
235
+ c.close();
236
+ },
237
+ });
238
+ // 对外仍是 200(契约不变),真实状态码经回显头交门面决定是否冷却
239
+ let shaped = withUpstreamStatus(echo(new Response(s, { status: 200, headers: { "Content-Type": "text/event-stream" } })), upRes.status);
240
+ shaped = withVerdict(shaped, vf);
241
+ if (vf.quota) shaped = withQuota(shaped);
242
+ if (vf.login) shaped = withLogin(shaped);
243
+ if (vf.queued) shaped = vf.noCapacity ? withNoCapacity(shaped, vf.retryAfter) : withQueued(shaped);
244
+ return shaped;
245
+ }
246
+ let out = withUpstreamStatus(echo(errRes(errorStatus(e), e.detail, e.kind)), upRes.status);
247
+ out = withVerdict(out, vf);
248
+ if (vf.quota) out = withQuota(out);
249
+ if (vf.login) out = withLogin(out);
250
+ if (vf.queued) out = vf.noCapacity ? withNoCapacity(out, vf.retryAfter) : withQueued(out);
251
+ return out;
252
+ }
253
+ if (stream) {
254
+ // 预读首帧取流内判决(qoder 把 403/429 裹在 200 的信封里),预读帧原样回灌给整形器。
255
+ // 判决状态码经 withUpstreamStatus 出门面 → index.js 据此 ring.onError 冷却该号 →
256
+ // 粘号选择器下次因 isCooling 为真而换号。对外仍恒 200(契约不变)。
257
+ const peek = await peekEnvelopeVerdict(upRes, cdbg);
258
+ const pvf = { login: peek.login, quota: peek.quota, queued: peek.queued, noCapacity: peek.noCapacity };
259
+ let shaped = echo(reshapeQoderStream(peek.res, { model, chatId, prefetched: peek.frames }));
260
+ // 只在真有判决时挂状态码:正常流不写这个头(index.js 靠它区分"要不要冷却")
261
+ if (peek.status) shaped = withUpstreamStatus(shaped, peek.status);
262
+ // verdict 钉头同理只钉有判决的流——正常流不写,避免成功请求日志被 verdict=none 刷屏
263
+ if (peek.status) shaped = withVerdict(shaped, pvf);
264
+ if (peek.quota) shaped = withQuota(shaped);
265
+ // 排队旗标(内部交接):真排队(有人等位)→ 门面换号碰别的号;无容量 → 门面不换号透 Retry-After
266
+ if (peek.queued) shaped = peek.noCapacity ? withNoCapacity(shaped, peek.retryAfter) : withQueued(shaped);
267
+ // 登录过期旗标:门面据此刷新令牌原地重发(不冷却换号轮询整池过期号)
268
+ if (peek.login) shaped = withLogin(shaped);
269
+ return shaped;
270
+ }
271
+ try {
272
+ const agg = await aggregateQoderStream(upRes);
273
+ const out = toCompletionJson({ model, chatId, ...agg });
274
+ return echo(new Response(JSON.stringify(out), { status: 200, headers: { "Content-Type": "application/json" } }));
275
+ } catch (e) {
276
+ // 聚合出口的判决折态(2026-10-22 真机实锤 req=mvj6u7wj:HTTP 恒 200、403 裹在 SSE 信封
277
+ // statusCodeValue 里,extractDelta 的 err.status=403 原样 errRes 出去 → nocapacity 头挂了、
278
+ // 状态却还是 403。无容量档必须与非 200 直出路径同口径折 429,否则门面 break 后
279
+ // lastRes 透给客户端仍是 403,Retry-After 语义全丢)。
280
+ const cvf = { login: e?.loginExpired === true, quota: e?.kind === "quota", queued: e?.queued === true, noCapacity: e?.noCapacity === true };
281
+ const aggStatus = cvf.noCapacity ? 429 : errorStatus(e);
282
+ let out = echo(errRes(aggStatus, String(e?.detail || e?.message || e).slice(0, 300), e?.kind || "upstream_error"));
283
+ out = withVerdict(out, cvf);
284
+ if (cvf.noCapacity && e?.retryAfter) out = withNoCapacity(out, e.retryAfter);
285
+ let marked = cvf.quota ? withQuota(out) : out;
286
+ if (cvf.queued && !cvf.noCapacity) marked = withQueued(marked);
287
+ return cvf.login ? withLogin(marked) : marked;
288
+ }
289
+ }
290
+
291
+ return { runChat };
292
+ }