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.
- package/bin/mslxdff.js +3 -3
- package/package.json +1 -1
- package/src/auto.js +264 -264
- package/src/autostart.js +62 -15
- 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 +155 -155
- package/src/chat-pipeline/policy.js +84 -84
- package/src/cli/commands/group.js +266 -266
- 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 +139 -139
- package/src/cli/commands/provider/models.js +149 -149
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/provider/raccoon-quota.js +156 -156
- package/src/cli/commands/sync.js +310 -310
- package/src/cli/status.js +282 -282
- package/src/daemon.js +246 -246
- 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/models.js +293 -293
- package/src/peers.js +389 -389
- package/src/providers/agentrouter.js +238 -238
- package/src/providers/base.js +164 -164
- 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/codearts/chat.js +207 -207
- package/src/providers/dispatcher.js +147 -147
- package/src/providers/globalqwenwork/cosy.js +123 -123
- package/src/providers/globalqwenwork/crypto.js +218 -218
- package/src/providers/globalqwenwork/http.js +20 -20
- package/src/providers/globalqwenwork/rsa.js +54 -54
- package/src/providers/globalqwenwork/sse.js +268 -268
- package/src/providers/globalqwenwork/stream.js +130 -130
- package/src/providers/globalqwenwork/upstream.js +122 -122
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +292 -292
- package/src/providers/qoder/index.js +311 -311
- package/src/providers/qoder/sse.js +233 -233
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +337 -337
- package/src/providers/qwenwork/payload.js +144 -144
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/raccoon/const.js +219 -219
- package/src/providers/raccoon/credits.js +285 -285
- package/src/providers/registry.js +89 -89
- package/src/providers/workbuddy/chat.js +298 -298
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/reasoning.js +32 -32
- package/src/routes/chat/broadband-handler.js +80 -80
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +282 -282
- package/src/routes/chat/via-route-handler.js +158 -158
- package/src/routes/hedge.js +260 -260
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +373 -373
- package/src/routes/stream.js +364 -364
- package/src/runtime/bootstrap.js +75 -75
- package/src/runtime/keep-warm.js +135 -135
- package/src/runtime/provider-gate.js +75 -75
- package/src/runtime/providers-setup.js +187 -187
- package/src/runtime/raccoon-checkin.js +175 -175
- package/src/runtime/server-lifecycle.js +34 -10
- package/src/server.js +85 -85
- 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 +355 -355
- package/docs/cli_help_mini.md +0 -149
- package/src/providers/AGENTS.md +0 -49
- package/src/routes/AGENTS.md +0 -37
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
// workbuddy 流式 reasoning_content 整形:上游思考模型(如 glm-5.3-flash)把思考过程一词一 chunk
|
|
2
|
-
// 地放进 delta.reasoning_content,客户端会把每个增量渲染成独立 "- Thought" 条目。
|
|
3
|
-
// 旧版:连续 reasoning 增量缓冲到首个 content/finish 才一次性 flush → 思考期 10~30s 界面完全无变化(用户看到 workbuddy 后台流量走但 opencode 界面卡住)
|
|
4
|
-
// 新版:增量聚合 + 阈值分片:缓冲 reasoning,达到 REASONING_CHUNK_SIZE(默认 100 字符)即分片 flush,
|
|
5
|
-
// 期间仅首个 role 帧透传保持 TTFB,其余纯 reasoning 空帧吞掉(之前会透传 800+ 空 role 帧);
|
|
6
|
-
// 尾段不足阈值的在 content/finish/流结束时兜底 flush。恒空 reasoning(deepseek-v4-flash)零改写。
|
|
7
|
-
// 仅包 workbuddy 出口的 SSE 响应,不影响其他供应商与全局聚合器。
|
|
8
|
-
|
|
9
|
-
const REASONING_CHUNK_SIZE = 150;
|
|
10
|
-
|
|
11
|
-
export function reshapeWorkbuddySse(res) {
|
|
12
|
-
try {
|
|
13
|
-
const ct = res.headers?.get?.("content-type") || "";
|
|
14
|
-
if (res.status !== 200 || !ct.includes("text/event-stream") || !res.body) return res;
|
|
15
|
-
} catch { return res; }
|
|
16
|
-
|
|
17
|
-
const reader = res.body.getReader();
|
|
18
|
-
const decoder = new TextDecoder();
|
|
19
|
-
const encoder = new TextEncoder();
|
|
20
|
-
let buf = "";
|
|
21
|
-
let reasoningBuf = "";
|
|
22
|
-
let lastMeta = null;
|
|
23
|
-
let closed = false;
|
|
24
|
-
let hasSentRole = false;
|
|
25
|
-
let lastFlushAt = Date.now();
|
|
26
|
-
|
|
27
|
-
function flushReasoningInto(out) {
|
|
28
|
-
if (!reasoningBuf) return;
|
|
29
|
-
const meta = lastMeta || {};
|
|
30
|
-
out.push(`data: ${JSON.stringify({
|
|
31
|
-
id: meta.id || "chatcmpl-workbuddy-reshape",
|
|
32
|
-
object: meta.object || "chat.completion.chunk",
|
|
33
|
-
created: meta.created || Math.floor(Date.now() / 1000),
|
|
34
|
-
model: meta.model || "",
|
|
35
|
-
choices: [{ index: 0, delta: { reasoning_content: reasoningBuf }, finish_reason: null }],
|
|
36
|
-
})}\n\n`);
|
|
37
|
-
reasoningBuf = "";
|
|
38
|
-
lastFlushAt = Date.now();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let blankStreak = false;
|
|
42
|
-
// 处理一段完整行文本(以 \n 结尾),产出的行/帧推进 out(连续空行合并为单个事件分隔)
|
|
43
|
-
function processInto(text, out) {
|
|
44
|
-
for (const line of text.split("\n")) {
|
|
45
|
-
if (!line) { if (!blankStreak) out.push("\n"); blankStreak = true; continue; }
|
|
46
|
-
blankStreak = false;
|
|
47
|
-
if (!line.startsWith("data:")) { out.push(line + "\n"); continue; }
|
|
48
|
-
const payload = line.slice(5).trim();
|
|
49
|
-
if (!payload || payload === "[DONE]") { out.push(line + "\n"); continue; }
|
|
50
|
-
let obj;
|
|
51
|
-
try { obj = JSON.parse(payload); } catch { out.push(line + "\n"); continue; }
|
|
52
|
-
const ch = obj.choices?.[0];
|
|
53
|
-
if (ch && typeof ch === "object") {
|
|
54
|
-
if (obj.id) lastMeta = { id: obj.id, model: obj.model, object: obj.object, created: obj.created };
|
|
55
|
-
const d = ch.delta && typeof ch.delta === "object" ? ch.delta : null;
|
|
56
|
-
const rc = d ? d.reasoning_content : undefined;
|
|
57
|
-
if (typeof rc === "string" && rc) {
|
|
58
|
-
reasoningBuf += rc;
|
|
59
|
-
d.reasoning_content = "";
|
|
60
|
-
const isBoundary = (typeof d.content === "string" && d.content) || !!ch.finish_reason;
|
|
61
|
-
// 首个 reasoning 帧的 role 透传一次,保 TTFB;后续纯 reasoning 的 role 重复帧直接吞掉
|
|
62
|
-
if (!hasSentRole && d.role !== undefined) {
|
|
63
|
-
hasSentRole = true;
|
|
64
|
-
const rm = lastMeta || obj;
|
|
65
|
-
out.push(`data: ${JSON.stringify({
|
|
66
|
-
id: rm.id || obj.id || "chatcmpl-workbuddy-reshape",
|
|
67
|
-
object: rm.object || obj.object || "chat.completion.chunk",
|
|
68
|
-
created: rm.created || obj.created || Math.floor(Date.now() / 1000),
|
|
69
|
-
model: rm.model || obj.model || "",
|
|
70
|
-
choices: [{ index: 0, delta: { role: d.role }, finish_reason: null }],
|
|
71
|
-
})}\n\n`);
|
|
72
|
-
} else if (d.role !== undefined) {
|
|
73
|
-
// 后续重复 role 帧不再透传,避免 800+ 空帧洪泛
|
|
74
|
-
// 标记已发送过,避免后续非 reasoning 帧再误判为首次
|
|
75
|
-
hasSentRole = true;
|
|
76
|
-
}
|
|
77
|
-
// 阈值分片:攒够 100 字符即向前吐一块,期间界面可见增量而非全程静默
|
|
78
|
-
// 时间阈值:若距上次 flush 已超 700ms 且已攒 20+ 字符,也吐一块(避免小尾巴长时间不更新)
|
|
79
|
-
const now = Date.now();
|
|
80
|
-
const shouldChunk = reasoningBuf.length >= REASONING_CHUNK_SIZE
|
|
81
|
-
|| (reasoningBuf.length >= 50 && now - lastFlushAt > 1500);
|
|
82
|
-
if (shouldChunk) flushReasoningInto(out);
|
|
83
|
-
if (isBoundary) {
|
|
84
|
-
// 兜底:content/finish 前把剩余思考一次性吐出,再透传本帧
|
|
85
|
-
flushReasoningInto(out);
|
|
86
|
-
const hasPassthrough = isBoundary || (Array.isArray(d.tool_calls) && d.tool_calls.length);
|
|
87
|
-
if (hasPassthrough) out.push(`data: ${JSON.stringify(obj)}\n\n`);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
// 纯 reasoning 碎片帧已缓冲/或已分片吐出,原空帧吞掉
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
// 非 reasoning 帧:若有 content/finish 先兜底 flush
|
|
94
|
-
if ((typeof d?.content === "string" && d.content) || ch.finish_reason) flushReasoningInto(out);
|
|
95
|
-
if (!hasSentRole && d?.role !== undefined) {
|
|
96
|
-
hasSentRole = true;
|
|
97
|
-
} else if (hasSentRole && d?.role !== undefined) {
|
|
98
|
-
// 去重:首个 role 后续重复 role 去掉,避免下游每帧都带 role
|
|
99
|
-
try { const clone = JSON.parse(payload); delete clone.choices[0].delta.role; out.push(`data: ${JSON.stringify(clone)}\n\n`); continue; } catch { /* fallback透传 */ }
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
out.push(line + "\n");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const body = new ReadableStream({
|
|
107
|
-
async pull(controller) {
|
|
108
|
-
if (closed) { try { controller.close(); } catch {} return; }
|
|
109
|
-
try {
|
|
110
|
-
// 循环读源,直到"有输出 / 流结束 / 出错"才返回。
|
|
111
|
-
// WHATWG 流规范:pull 的 promise 解析后若未 enqueue,规范不会再自动调度 pull
|
|
112
|
-
// (仅当解析期间有新读请求触发 pullAgain 才重调)——消费者只挂一个待处理 read 时,
|
|
113
|
-
// 任何一次"只吞帧不出数据"的拉取都会让整流永久停摆。workbuddy 思考流按帧到达
|
|
114
|
-
// (role 帧后每个 reasoning 碎片都不足 150 字符阈值)时必现,即"首帧后卡死"根因。
|
|
115
|
-
for (;;) {
|
|
116
|
-
const { done, value } = await reader.read();
|
|
117
|
-
if (done) {
|
|
118
|
-
closed = true;
|
|
119
|
-
const out = [];
|
|
120
|
-
if (buf) { processInto(buf + "\n", out); buf = ""; }
|
|
121
|
-
flushReasoningInto(out);
|
|
122
|
-
if (out.length) controller.enqueue(encoder.encode(out.join("")));
|
|
123
|
-
controller.close();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
buf += decoder.decode(value, { stream: true });
|
|
127
|
-
const idx = buf.lastIndexOf("\n");
|
|
128
|
-
if (idx < 0) continue;
|
|
129
|
-
const complete = buf.slice(0, idx + 1);
|
|
130
|
-
buf = buf.slice(idx + 1);
|
|
131
|
-
const out = [];
|
|
132
|
-
processInto(complete, out);
|
|
133
|
-
if (out.length) { controller.enqueue(encoder.encode(out.join(""))); return; }
|
|
134
|
-
}
|
|
135
|
-
} catch {
|
|
136
|
-
closed = true;
|
|
137
|
-
try { controller.close(); } catch {}
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
cancel() {
|
|
141
|
-
closed = true;
|
|
142
|
-
try {
|
|
143
|
-
const p = reader.cancel();
|
|
144
|
-
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
145
|
-
} catch {}
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const out = new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
150
|
-
try { out._t = res._t; } catch {}
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
1
|
+
// workbuddy 流式 reasoning_content 整形:上游思考模型(如 glm-5.3-flash)把思考过程一词一 chunk
|
|
2
|
+
// 地放进 delta.reasoning_content,客户端会把每个增量渲染成独立 "- Thought" 条目。
|
|
3
|
+
// 旧版:连续 reasoning 增量缓冲到首个 content/finish 才一次性 flush → 思考期 10~30s 界面完全无变化(用户看到 workbuddy 后台流量走但 opencode 界面卡住)
|
|
4
|
+
// 新版:增量聚合 + 阈值分片:缓冲 reasoning,达到 REASONING_CHUNK_SIZE(默认 100 字符)即分片 flush,
|
|
5
|
+
// 期间仅首个 role 帧透传保持 TTFB,其余纯 reasoning 空帧吞掉(之前会透传 800+ 空 role 帧);
|
|
6
|
+
// 尾段不足阈值的在 content/finish/流结束时兜底 flush。恒空 reasoning(deepseek-v4-flash)零改写。
|
|
7
|
+
// 仅包 workbuddy 出口的 SSE 响应,不影响其他供应商与全局聚合器。
|
|
8
|
+
|
|
9
|
+
const REASONING_CHUNK_SIZE = 150;
|
|
10
|
+
|
|
11
|
+
export function reshapeWorkbuddySse(res) {
|
|
12
|
+
try {
|
|
13
|
+
const ct = res.headers?.get?.("content-type") || "";
|
|
14
|
+
if (res.status !== 200 || !ct.includes("text/event-stream") || !res.body) return res;
|
|
15
|
+
} catch { return res; }
|
|
16
|
+
|
|
17
|
+
const reader = res.body.getReader();
|
|
18
|
+
const decoder = new TextDecoder();
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
let buf = "";
|
|
21
|
+
let reasoningBuf = "";
|
|
22
|
+
let lastMeta = null;
|
|
23
|
+
let closed = false;
|
|
24
|
+
let hasSentRole = false;
|
|
25
|
+
let lastFlushAt = Date.now();
|
|
26
|
+
|
|
27
|
+
function flushReasoningInto(out) {
|
|
28
|
+
if (!reasoningBuf) return;
|
|
29
|
+
const meta = lastMeta || {};
|
|
30
|
+
out.push(`data: ${JSON.stringify({
|
|
31
|
+
id: meta.id || "chatcmpl-workbuddy-reshape",
|
|
32
|
+
object: meta.object || "chat.completion.chunk",
|
|
33
|
+
created: meta.created || Math.floor(Date.now() / 1000),
|
|
34
|
+
model: meta.model || "",
|
|
35
|
+
choices: [{ index: 0, delta: { reasoning_content: reasoningBuf }, finish_reason: null }],
|
|
36
|
+
})}\n\n`);
|
|
37
|
+
reasoningBuf = "";
|
|
38
|
+
lastFlushAt = Date.now();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let blankStreak = false;
|
|
42
|
+
// 处理一段完整行文本(以 \n 结尾),产出的行/帧推进 out(连续空行合并为单个事件分隔)
|
|
43
|
+
function processInto(text, out) {
|
|
44
|
+
for (const line of text.split("\n")) {
|
|
45
|
+
if (!line) { if (!blankStreak) out.push("\n"); blankStreak = true; continue; }
|
|
46
|
+
blankStreak = false;
|
|
47
|
+
if (!line.startsWith("data:")) { out.push(line + "\n"); continue; }
|
|
48
|
+
const payload = line.slice(5).trim();
|
|
49
|
+
if (!payload || payload === "[DONE]") { out.push(line + "\n"); continue; }
|
|
50
|
+
let obj;
|
|
51
|
+
try { obj = JSON.parse(payload); } catch { out.push(line + "\n"); continue; }
|
|
52
|
+
const ch = obj.choices?.[0];
|
|
53
|
+
if (ch && typeof ch === "object") {
|
|
54
|
+
if (obj.id) lastMeta = { id: obj.id, model: obj.model, object: obj.object, created: obj.created };
|
|
55
|
+
const d = ch.delta && typeof ch.delta === "object" ? ch.delta : null;
|
|
56
|
+
const rc = d ? d.reasoning_content : undefined;
|
|
57
|
+
if (typeof rc === "string" && rc) {
|
|
58
|
+
reasoningBuf += rc;
|
|
59
|
+
d.reasoning_content = "";
|
|
60
|
+
const isBoundary = (typeof d.content === "string" && d.content) || !!ch.finish_reason;
|
|
61
|
+
// 首个 reasoning 帧的 role 透传一次,保 TTFB;后续纯 reasoning 的 role 重复帧直接吞掉
|
|
62
|
+
if (!hasSentRole && d.role !== undefined) {
|
|
63
|
+
hasSentRole = true;
|
|
64
|
+
const rm = lastMeta || obj;
|
|
65
|
+
out.push(`data: ${JSON.stringify({
|
|
66
|
+
id: rm.id || obj.id || "chatcmpl-workbuddy-reshape",
|
|
67
|
+
object: rm.object || obj.object || "chat.completion.chunk",
|
|
68
|
+
created: rm.created || obj.created || Math.floor(Date.now() / 1000),
|
|
69
|
+
model: rm.model || obj.model || "",
|
|
70
|
+
choices: [{ index: 0, delta: { role: d.role }, finish_reason: null }],
|
|
71
|
+
})}\n\n`);
|
|
72
|
+
} else if (d.role !== undefined) {
|
|
73
|
+
// 后续重复 role 帧不再透传,避免 800+ 空帧洪泛
|
|
74
|
+
// 标记已发送过,避免后续非 reasoning 帧再误判为首次
|
|
75
|
+
hasSentRole = true;
|
|
76
|
+
}
|
|
77
|
+
// 阈值分片:攒够 100 字符即向前吐一块,期间界面可见增量而非全程静默
|
|
78
|
+
// 时间阈值:若距上次 flush 已超 700ms 且已攒 20+ 字符,也吐一块(避免小尾巴长时间不更新)
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const shouldChunk = reasoningBuf.length >= REASONING_CHUNK_SIZE
|
|
81
|
+
|| (reasoningBuf.length >= 50 && now - lastFlushAt > 1500);
|
|
82
|
+
if (shouldChunk) flushReasoningInto(out);
|
|
83
|
+
if (isBoundary) {
|
|
84
|
+
// 兜底:content/finish 前把剩余思考一次性吐出,再透传本帧
|
|
85
|
+
flushReasoningInto(out);
|
|
86
|
+
const hasPassthrough = isBoundary || (Array.isArray(d.tool_calls) && d.tool_calls.length);
|
|
87
|
+
if (hasPassthrough) out.push(`data: ${JSON.stringify(obj)}\n\n`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// 纯 reasoning 碎片帧已缓冲/或已分片吐出,原空帧吞掉
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// 非 reasoning 帧:若有 content/finish 先兜底 flush
|
|
94
|
+
if ((typeof d?.content === "string" && d.content) || ch.finish_reason) flushReasoningInto(out);
|
|
95
|
+
if (!hasSentRole && d?.role !== undefined) {
|
|
96
|
+
hasSentRole = true;
|
|
97
|
+
} else if (hasSentRole && d?.role !== undefined) {
|
|
98
|
+
// 去重:首个 role 后续重复 role 去掉,避免下游每帧都带 role
|
|
99
|
+
try { const clone = JSON.parse(payload); delete clone.choices[0].delta.role; out.push(`data: ${JSON.stringify(clone)}\n\n`); continue; } catch { /* fallback透传 */ }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
out.push(line + "\n");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const body = new ReadableStream({
|
|
107
|
+
async pull(controller) {
|
|
108
|
+
if (closed) { try { controller.close(); } catch {} return; }
|
|
109
|
+
try {
|
|
110
|
+
// 循环读源,直到"有输出 / 流结束 / 出错"才返回。
|
|
111
|
+
// WHATWG 流规范:pull 的 promise 解析后若未 enqueue,规范不会再自动调度 pull
|
|
112
|
+
// (仅当解析期间有新读请求触发 pullAgain 才重调)——消费者只挂一个待处理 read 时,
|
|
113
|
+
// 任何一次"只吞帧不出数据"的拉取都会让整流永久停摆。workbuddy 思考流按帧到达
|
|
114
|
+
// (role 帧后每个 reasoning 碎片都不足 150 字符阈值)时必现,即"首帧后卡死"根因。
|
|
115
|
+
for (;;) {
|
|
116
|
+
const { done, value } = await reader.read();
|
|
117
|
+
if (done) {
|
|
118
|
+
closed = true;
|
|
119
|
+
const out = [];
|
|
120
|
+
if (buf) { processInto(buf + "\n", out); buf = ""; }
|
|
121
|
+
flushReasoningInto(out);
|
|
122
|
+
if (out.length) controller.enqueue(encoder.encode(out.join("")));
|
|
123
|
+
controller.close();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
buf += decoder.decode(value, { stream: true });
|
|
127
|
+
const idx = buf.lastIndexOf("\n");
|
|
128
|
+
if (idx < 0) continue;
|
|
129
|
+
const complete = buf.slice(0, idx + 1);
|
|
130
|
+
buf = buf.slice(idx + 1);
|
|
131
|
+
const out = [];
|
|
132
|
+
processInto(complete, out);
|
|
133
|
+
if (out.length) { controller.enqueue(encoder.encode(out.join(""))); return; }
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
closed = true;
|
|
137
|
+
try { controller.close(); } catch {}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
cancel() {
|
|
141
|
+
closed = true;
|
|
142
|
+
try {
|
|
143
|
+
const p = reader.cancel();
|
|
144
|
+
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
145
|
+
} catch {}
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const out = new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
150
|
+
try { out._t = res._t; } catch {}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// Shim — 保持历史导入路径兼容,真实实现下沉至 ./workbuddy/ 深模块
|
|
2
|
-
export * from "./workbuddy/index.js";
|
|
1
|
+
// Shim — 保持历史导入路径兼容,真实实现下沉至 ./workbuddy/ 深模块
|
|
2
|
+
export * from "./workbuddy/index.js";
|
package/src/reasoning.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
const PLACEHOLDER = " ";
|
|
2
|
-
|
|
3
|
-
const MODEL_RULES = [
|
|
4
|
-
{ match: (m) => /^kimi-/i.test(m || ""), scope: "toolCalls" },
|
|
5
|
-
{ match: (m) => /deepseek/i.test(m || ""), scope: "all" },
|
|
6
|
-
];
|
|
7
|
-
|
|
8
|
-
export function normalizeModel(model) {
|
|
9
|
-
return model.startsWith("oc/") ? model.slice(3) : model;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function shouldInject(message, scope) {
|
|
13
|
-
if (message?.role !== "assistant") return false;
|
|
14
|
-
const rc = message.reasoning_content;
|
|
15
|
-
if (typeof rc === "string" && rc.length > 0) return false;
|
|
16
|
-
if (scope === "toolCalls") {
|
|
17
|
-
return Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
18
|
-
}
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function applyRule(body, rule) {
|
|
23
|
-
if (!rule || !body?.messages) return body;
|
|
24
|
-
const messages = body.messages.map((m) =>
|
|
25
|
-
shouldInject(m, rule.scope) ? { ...m, reasoning_content: PLACEHOLDER } : m
|
|
26
|
-
);
|
|
27
|
-
return { ...body, messages };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function injectReasoningContent(model, body) {
|
|
31
|
-
const rule = MODEL_RULES.find((r) => r.match(model));
|
|
32
|
-
return applyRule(body, rule);
|
|
1
|
+
const PLACEHOLDER = " ";
|
|
2
|
+
|
|
3
|
+
const MODEL_RULES = [
|
|
4
|
+
{ match: (m) => /^kimi-/i.test(m || ""), scope: "toolCalls" },
|
|
5
|
+
{ match: (m) => /deepseek/i.test(m || ""), scope: "all" },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export function normalizeModel(model) {
|
|
9
|
+
return model.startsWith("oc/") ? model.slice(3) : model;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shouldInject(message, scope) {
|
|
13
|
+
if (message?.role !== "assistant") return false;
|
|
14
|
+
const rc = message.reasoning_content;
|
|
15
|
+
if (typeof rc === "string" && rc.length > 0) return false;
|
|
16
|
+
if (scope === "toolCalls") {
|
|
17
|
+
return Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function applyRule(body, rule) {
|
|
23
|
+
if (!rule || !body?.messages) return body;
|
|
24
|
+
const messages = body.messages.map((m) =>
|
|
25
|
+
shouldInject(m, rule.scope) ? { ...m, reasoning_content: PLACEHOLDER } : m
|
|
26
|
+
);
|
|
27
|
+
return { ...body, messages };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function injectReasoningContent(model, body) {
|
|
31
|
+
const rule = MODEL_RULES.find((r) => r.match(model));
|
|
32
|
+
return applyRule(body, rule);
|
|
33
33
|
}
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import { relay, SLOW_TOTAL_MS, STREAM_TIMEOUT_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS } from "../stream.js";
|
|
2
|
-
import { buildFallbackInfo } from "../fallback.js";
|
|
3
|
-
import { createRelayPipeline } from "./relay-pipeline.js";
|
|
4
|
-
import { tryBroadbandRelay } from "../relay-queue.js";
|
|
5
|
-
import { overForwardLimit, contextGuardMessage } from "../../chat-pipeline/context-guard.js";
|
|
6
|
-
|
|
7
|
-
/** 薄适配:broadband 两种形态合一经由 pipeline */
|
|
8
|
-
export async function handleBroadbandRelay({
|
|
9
|
-
model,
|
|
10
|
-
body,
|
|
11
|
-
hops,
|
|
12
|
-
lastErr,
|
|
13
|
-
requested,
|
|
14
|
-
useAuto,
|
|
15
|
-
lockModel,
|
|
16
|
-
auto,
|
|
17
|
-
groups,
|
|
18
|
-
token,
|
|
19
|
-
bus,
|
|
20
|
-
logs,
|
|
21
|
-
handlerCtx,
|
|
22
|
-
evt,
|
|
23
|
-
mark,
|
|
24
|
-
perf0,
|
|
25
|
-
attemptStartMs,
|
|
26
|
-
stages,
|
|
27
|
-
res,
|
|
28
|
-
startedAt,
|
|
29
|
-
plugins,
|
|
30
|
-
deps = {},
|
|
31
|
-
}) {
|
|
32
|
-
const { createPipeline = createRelayPipeline, tryBroadbandRelay: tryRelay = tryBroadbandRelay } = deps;
|
|
33
|
-
// 转发前上下文预检:宽带成员是完全不同的另一台机器,上下文撑爆的代价(白烧它的额度)最实在。
|
|
34
|
-
const guard = overForwardLimit(body);
|
|
35
|
-
if (guard) {
|
|
36
|
-
const msg = contextGuardMessage(guard);
|
|
37
|
-
evt("context-guard", { reqId: handlerCtx.reqId, model, via: "broadband", estimated: guard.estimated, limit: guard.limit, over: guard.over });
|
|
38
|
-
return { handled: false, lastErr: { model, upstream: null, status: 400, message: msg, contextGuard: true } };
|
|
39
|
-
}
|
|
40
|
-
const bb = await tryRelay({ groups, token, model, body, hops, bus, logs, reqId: handlerCtx.reqId, evt, res, mark, perf0, stages });
|
|
41
|
-
if (!bb) {
|
|
42
|
-
evt("relay-miss", { reqId: handlerCtx.reqId, model });
|
|
43
|
-
return { handled: false };
|
|
44
|
-
}
|
|
45
|
-
const pipeline = createPipeline({
|
|
46
|
-
relay,
|
|
47
|
-
buildFallbackInfo,
|
|
48
|
-
auto,
|
|
49
|
-
plugins,
|
|
50
|
-
evt,
|
|
51
|
-
mark,
|
|
52
|
-
logCall: () => {},
|
|
53
|
-
logError: () => {},
|
|
54
|
-
constants: { STREAM_TIMEOUT_MS, SLOW_TOTAL_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS },
|
|
55
|
-
startedAt,
|
|
56
|
-
stages,
|
|
57
|
-
});
|
|
58
|
-
const isResponse = bb.result && typeof bb.result.status === "number" && typeof bb.result.headers?.get === "function";
|
|
59
|
-
if (isResponse) {
|
|
60
|
-
const r = await pipeline.execute({ res, upRes: bb.result, body, requested, actual: model, lastErr, via: "broadband", lockModel, useAuto, handlerCtx, mark, perf0, attemptStartMs, stages, startedAt });
|
|
61
|
-
if (!r.handled) return { handled: false, upRes: null, lastErr: r.lastErr };
|
|
62
|
-
return { handled: true };
|
|
63
|
-
}
|
|
64
|
-
if (bb.result && typeof bb.result.status === "number") {
|
|
65
|
-
const b = bb.result.body || "";
|
|
66
|
-
const str = typeof b === "string" ? b : JSON.stringify(b);
|
|
67
|
-
const isSSE = bb.result.headers?.["Content-Type"]?.includes("text/event-stream");
|
|
68
|
-
const fakeRes = {
|
|
69
|
-
status: bb.result.status,
|
|
70
|
-
headers: { get: (k) => bb.result.headers?.[k] || bb.result.headers?.[k.toLowerCase()] || null },
|
|
71
|
-
text: async () => str,
|
|
72
|
-
body: isSSE ? (async function* () { yield Buffer.from(str); })() : null,
|
|
73
|
-
};
|
|
74
|
-
const r = await pipeline.execute({ res, upRes: fakeRes, body, requested, actual: model, lastErr, via: "broadband-local", lockModel, useAuto, handlerCtx, mark, perf0, attemptStartMs, stages, startedAt });
|
|
75
|
-
if (!r.handled) return { handled: false, upRes: null, lastErr: r.lastErr };
|
|
76
|
-
return { handled: true };
|
|
77
|
-
}
|
|
78
|
-
evt("relay-miss", { reqId: handlerCtx.reqId, model });
|
|
79
|
-
return { handled: false };
|
|
80
|
-
}
|
|
1
|
+
import { relay, SLOW_TOTAL_MS, STREAM_TIMEOUT_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS } from "../stream.js";
|
|
2
|
+
import { buildFallbackInfo } from "../fallback.js";
|
|
3
|
+
import { createRelayPipeline } from "./relay-pipeline.js";
|
|
4
|
+
import { tryBroadbandRelay } from "../relay-queue.js";
|
|
5
|
+
import { overForwardLimit, contextGuardMessage } from "../../chat-pipeline/context-guard.js";
|
|
6
|
+
|
|
7
|
+
/** 薄适配:broadband 两种形态合一经由 pipeline */
|
|
8
|
+
export async function handleBroadbandRelay({
|
|
9
|
+
model,
|
|
10
|
+
body,
|
|
11
|
+
hops,
|
|
12
|
+
lastErr,
|
|
13
|
+
requested,
|
|
14
|
+
useAuto,
|
|
15
|
+
lockModel,
|
|
16
|
+
auto,
|
|
17
|
+
groups,
|
|
18
|
+
token,
|
|
19
|
+
bus,
|
|
20
|
+
logs,
|
|
21
|
+
handlerCtx,
|
|
22
|
+
evt,
|
|
23
|
+
mark,
|
|
24
|
+
perf0,
|
|
25
|
+
attemptStartMs,
|
|
26
|
+
stages,
|
|
27
|
+
res,
|
|
28
|
+
startedAt,
|
|
29
|
+
plugins,
|
|
30
|
+
deps = {},
|
|
31
|
+
}) {
|
|
32
|
+
const { createPipeline = createRelayPipeline, tryBroadbandRelay: tryRelay = tryBroadbandRelay } = deps;
|
|
33
|
+
// 转发前上下文预检:宽带成员是完全不同的另一台机器,上下文撑爆的代价(白烧它的额度)最实在。
|
|
34
|
+
const guard = overForwardLimit(body);
|
|
35
|
+
if (guard) {
|
|
36
|
+
const msg = contextGuardMessage(guard);
|
|
37
|
+
evt("context-guard", { reqId: handlerCtx.reqId, model, via: "broadband", estimated: guard.estimated, limit: guard.limit, over: guard.over });
|
|
38
|
+
return { handled: false, lastErr: { model, upstream: null, status: 400, message: msg, contextGuard: true } };
|
|
39
|
+
}
|
|
40
|
+
const bb = await tryRelay({ groups, token, model, body, hops, bus, logs, reqId: handlerCtx.reqId, evt, res, mark, perf0, stages });
|
|
41
|
+
if (!bb) {
|
|
42
|
+
evt("relay-miss", { reqId: handlerCtx.reqId, model });
|
|
43
|
+
return { handled: false };
|
|
44
|
+
}
|
|
45
|
+
const pipeline = createPipeline({
|
|
46
|
+
relay,
|
|
47
|
+
buildFallbackInfo,
|
|
48
|
+
auto,
|
|
49
|
+
plugins,
|
|
50
|
+
evt,
|
|
51
|
+
mark,
|
|
52
|
+
logCall: () => {},
|
|
53
|
+
logError: () => {},
|
|
54
|
+
constants: { STREAM_TIMEOUT_MS, SLOW_TOTAL_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS },
|
|
55
|
+
startedAt,
|
|
56
|
+
stages,
|
|
57
|
+
});
|
|
58
|
+
const isResponse = bb.result && typeof bb.result.status === "number" && typeof bb.result.headers?.get === "function";
|
|
59
|
+
if (isResponse) {
|
|
60
|
+
const r = await pipeline.execute({ res, upRes: bb.result, body, requested, actual: model, lastErr, via: "broadband", lockModel, useAuto, handlerCtx, mark, perf0, attemptStartMs, stages, startedAt });
|
|
61
|
+
if (!r.handled) return { handled: false, upRes: null, lastErr: r.lastErr };
|
|
62
|
+
return { handled: true };
|
|
63
|
+
}
|
|
64
|
+
if (bb.result && typeof bb.result.status === "number") {
|
|
65
|
+
const b = bb.result.body || "";
|
|
66
|
+
const str = typeof b === "string" ? b : JSON.stringify(b);
|
|
67
|
+
const isSSE = bb.result.headers?.["Content-Type"]?.includes("text/event-stream");
|
|
68
|
+
const fakeRes = {
|
|
69
|
+
status: bb.result.status,
|
|
70
|
+
headers: { get: (k) => bb.result.headers?.[k] || bb.result.headers?.[k.toLowerCase()] || null },
|
|
71
|
+
text: async () => str,
|
|
72
|
+
body: isSSE ? (async function* () { yield Buffer.from(str); })() : null,
|
|
73
|
+
};
|
|
74
|
+
const r = await pipeline.execute({ res, upRes: fakeRes, body, requested, actual: model, lastErr, via: "broadband-local", lockModel, useAuto, handlerCtx, mark, perf0, attemptStartMs, stages, startedAt });
|
|
75
|
+
if (!r.handled) return { handled: false, upRes: null, lastErr: r.lastErr };
|
|
76
|
+
return { handled: true };
|
|
77
|
+
}
|
|
78
|
+
evt("relay-miss", { reqId: handlerCtx.reqId, model });
|
|
79
|
+
return { handled: false };
|
|
80
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import { clientIp, json, readBody, parseHops } from "../helpers.js";
|
|
2
|
-
import { runHook } from "../../plugins.js";
|
|
3
|
-
import { createChatPipeline } from "../../chat-pipeline/index.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* ChatGateway 薄适配层:仅负责 HTTP I/O(readBody + request:received hook)→ 委托 ChatPipeline.execute
|
|
7
|
-
* Policy/Order/AutoRace/hedge/peer/broadband 全部在 chat-pipeline 深模块内。
|
|
8
|
-
*/
|
|
9
|
-
export function createChatGateway(deps = {}) {
|
|
10
|
-
const pipeline = createChatPipeline(deps);
|
|
11
|
-
|
|
12
|
-
async function handle({ req, res }) {
|
|
13
|
-
let body;
|
|
14
|
-
try {
|
|
15
|
-
body = await readBody(req);
|
|
16
|
-
} catch {
|
|
17
|
-
return json(res, 400, { error: "Invalid JSON body" });
|
|
18
|
-
}
|
|
19
|
-
if (deps.plugins?.length) {
|
|
20
|
-
const rc = await runHook(deps.plugins, "request:received", {
|
|
21
|
-
ip: clientIp(req),
|
|
22
|
-
hops: parseHops(req.headers["x-mslxdff-hops"]),
|
|
23
|
-
headers: { "content-type": req.headers["content-type"] },
|
|
24
|
-
body,
|
|
25
|
-
});
|
|
26
|
-
for (const e of rc.errors) deps.logs?.appendEvent?.({ ts: Date.now(), type: "plugin-hook-error", hook: "request:received", plugin: e.plugin, error: e.error });
|
|
27
|
-
const respond = rc.value?.respond;
|
|
28
|
-
if (respond && typeof respond === "object") return json(res, respond.status || 200, respond.body ?? {});
|
|
29
|
-
}
|
|
30
|
-
req.body = body;
|
|
31
|
-
try {
|
|
32
|
-
await pipeline.execute({ req, res });
|
|
33
|
-
} catch (err) {
|
|
34
|
-
const msg = err?.message || String(err);
|
|
35
|
-
if (!res.headersSent) return json(res, 502, { error: msg });
|
|
36
|
-
try { res.end(); } catch {}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return { handle };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// 薄适配:保持原 chatHandler 签名兼容
|
|
44
|
-
export async function chatHandler(ctx) {
|
|
45
|
-
const gw = createChatGateway(ctx);
|
|
46
|
-
return gw.handle(ctx);
|
|
1
|
+
import { clientIp, json, readBody, parseHops } from "../helpers.js";
|
|
2
|
+
import { runHook } from "../../plugins.js";
|
|
3
|
+
import { createChatPipeline } from "../../chat-pipeline/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ChatGateway 薄适配层:仅负责 HTTP I/O(readBody + request:received hook)→ 委托 ChatPipeline.execute
|
|
7
|
+
* Policy/Order/AutoRace/hedge/peer/broadband 全部在 chat-pipeline 深模块内。
|
|
8
|
+
*/
|
|
9
|
+
export function createChatGateway(deps = {}) {
|
|
10
|
+
const pipeline = createChatPipeline(deps);
|
|
11
|
+
|
|
12
|
+
async function handle({ req, res }) {
|
|
13
|
+
let body;
|
|
14
|
+
try {
|
|
15
|
+
body = await readBody(req);
|
|
16
|
+
} catch {
|
|
17
|
+
return json(res, 400, { error: "Invalid JSON body" });
|
|
18
|
+
}
|
|
19
|
+
if (deps.plugins?.length) {
|
|
20
|
+
const rc = await runHook(deps.plugins, "request:received", {
|
|
21
|
+
ip: clientIp(req),
|
|
22
|
+
hops: parseHops(req.headers["x-mslxdff-hops"]),
|
|
23
|
+
headers: { "content-type": req.headers["content-type"] },
|
|
24
|
+
body,
|
|
25
|
+
});
|
|
26
|
+
for (const e of rc.errors) deps.logs?.appendEvent?.({ ts: Date.now(), type: "plugin-hook-error", hook: "request:received", plugin: e.plugin, error: e.error });
|
|
27
|
+
const respond = rc.value?.respond;
|
|
28
|
+
if (respond && typeof respond === "object") return json(res, respond.status || 200, respond.body ?? {});
|
|
29
|
+
}
|
|
30
|
+
req.body = body;
|
|
31
|
+
try {
|
|
32
|
+
await pipeline.execute({ req, res });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const msg = err?.message || String(err);
|
|
35
|
+
if (!res.headersSent) return json(res, 502, { error: msg });
|
|
36
|
+
try { res.end(); } catch {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { handle };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 薄适配:保持原 chatHandler 签名兼容
|
|
44
|
+
export async function chatHandler(ctx) {
|
|
45
|
+
const gw = createChatGateway(ctx);
|
|
46
|
+
return gw.handle(ctx);
|
|
47
47
|
}
|