mslxdff 0.1.160 → 0.1.161

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 (117) hide show
  1. package/bin/mslxdff.js +4 -4
  2. package/docs/cli_help_mini.md +135 -0
  3. package/package.json +1 -1
  4. package/src/auto.js +254 -254
  5. package/src/autostart.js +3 -0
  6. package/src/bench/cline-bench.js +42 -42
  7. package/src/bench/probe.js +70 -70
  8. package/src/bench/report.js +162 -162
  9. package/src/bench/runner.js +77 -77
  10. package/src/bench/via-probe.js +124 -124
  11. package/src/bench/via-routes.js +87 -87
  12. package/src/bench/workbuddy-bench.js +54 -54
  13. package/src/chat/engine.js +160 -160
  14. package/src/chat/gateway.js +163 -163
  15. package/src/chat/orchestrator.js +234 -234
  16. package/src/chat/prompt.js +70 -70
  17. package/src/chat/repl.js +88 -88
  18. package/src/chat/terminal.js +135 -135
  19. package/src/chat/tools.js +306 -306
  20. package/src/chat-pipeline/empty-turn.js +119 -0
  21. package/src/chat-pipeline/index.js +126 -123
  22. package/src/chat-pipeline/policy.js +76 -76
  23. package/src/chat-pipeline/serial-trial.js +53 -19
  24. package/src/cli/commands/daemon.js +4 -4
  25. package/src/cli/commands/group.js +249 -249
  26. package/src/cli/commands/model/list-providers.js +1 -1
  27. package/src/cli/commands/model/picks.js +50 -50
  28. package/src/cli/commands/provider/bench-via.js +247 -247
  29. package/src/cli/commands/provider/bench.js +141 -141
  30. package/src/cli/commands/provider/globalqwenwork-login.js +126 -0
  31. package/src/cli/commands/provider/index.js +126 -124
  32. package/src/cli/commands/provider/models.js +142 -139
  33. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  34. package/src/cli/commands/sync.js +232 -232
  35. package/src/cli/commands/system.js +2 -2
  36. package/src/cli/format.js +6 -0
  37. package/src/cli/policy.js +2 -2
  38. package/src/cli/provider-row.js +2 -2
  39. package/src/cli/status.js +279 -279
  40. package/src/daemon.js +101 -96
  41. package/src/logs.js +14 -1
  42. package/src/model-capabilities/enrich.js +86 -86
  43. package/src/model-capabilities/index.js +183 -183
  44. package/src/model-capabilities/parse.js +70 -70
  45. package/src/model-trace.js +5 -2
  46. package/src/models.js +225 -225
  47. package/src/providers/AGENTS.md +46 -0
  48. package/src/providers/classify.js +1 -1
  49. package/src/providers/cline/auth.js +228 -228
  50. package/src/providers/cline/chat.js +307 -307
  51. package/src/providers/cline.js +2 -2
  52. package/src/providers/globalqwenwork/account-store.js +141 -0
  53. package/src/providers/globalqwenwork/constants.js +73 -0
  54. package/src/providers/globalqwenwork/cosy.js +123 -0
  55. package/src/providers/globalqwenwork/crypto.js +220 -0
  56. package/src/providers/globalqwenwork/http.js +22 -0
  57. package/src/providers/globalqwenwork/index.js +331 -0
  58. package/src/providers/globalqwenwork/payload.js +145 -0
  59. package/src/providers/globalqwenwork/rsa.js +56 -0
  60. package/src/providers/globalqwenwork/sse.js +270 -0
  61. package/src/providers/globalqwenwork/stream.js +132 -0
  62. package/src/providers/globalqwenwork/upstream.js +122 -0
  63. package/src/providers/globalqwenwork.js +1 -0
  64. package/src/providers/keyring.js +60 -60
  65. package/src/providers/qoder/chat.js +183 -183
  66. package/src/providers/qoder/index.js +230 -230
  67. package/src/providers/qoder/sse.js +103 -103
  68. package/src/providers/qwenwork/account-store.js +133 -133
  69. package/src/providers/qwenwork/constants.js +67 -67
  70. package/src/providers/qwenwork/cosy.js +120 -120
  71. package/src/providers/qwenwork/crypto.js +218 -218
  72. package/src/providers/qwenwork/http.js +20 -20
  73. package/src/providers/qwenwork/index.js +327 -327
  74. package/src/providers/qwenwork/payload.js +142 -142
  75. package/src/providers/qwenwork/rsa.js +54 -54
  76. package/src/providers/qwenwork/sse.js +268 -268
  77. package/src/providers/qwenwork/stream.js +130 -130
  78. package/src/providers/qwenwork/upstream.js +120 -120
  79. package/src/providers/qwenwork.js +1 -1
  80. package/src/providers/registry.js +74 -66
  81. package/src/providers/workbuddy/chat.js +248 -248
  82. package/src/providers/workbuddy/reshape.js +152 -152
  83. package/src/providers/workbuddy.js +2 -2
  84. package/src/providers/zcode/sse.js +19 -2
  85. package/src/reasoning.js +32 -32
  86. package/src/routes/AGENTS.md +37 -0
  87. package/src/routes/chat/exhausted-handler.js +23 -8
  88. package/src/routes/chat/gateway.js +46 -46
  89. package/src/routes/chat/local-handler.js +3 -1
  90. package/src/routes/chat/relay-pipeline.js +276 -264
  91. package/src/routes/chat/via-route-handler.js +146 -146
  92. package/src/routes/hedge.js +255 -255
  93. package/src/routes/helpers.js +20 -4
  94. package/src/routes/models-route.js +167 -167
  95. package/src/routes/peers.js +273 -273
  96. package/src/routes/stream-hold.js +138 -0
  97. package/src/routes/stream-scan.js +192 -0
  98. package/src/routes/stream.js +386 -393
  99. package/src/runtime/bootstrap.js +45 -45
  100. package/src/runtime/lifecycle-forensics.js +116 -0
  101. package/src/runtime/lifecycle-log.js +23 -0
  102. package/src/runtime/provider-gate.js +34 -33
  103. package/src/runtime/providers-setup.js +165 -165
  104. package/src/server.js +64 -64
  105. package/src/state/schemas/allowlist.js +92 -92
  106. package/src/sync-opencode.js +280 -280
  107. package/src/talk-log.js +226 -0
  108. package/src/timeline.js +5 -2
  109. package/src/transport/index.js +244 -244
  110. package/src/transport/pool.js +56 -56
  111. package/src/transport/retry.js +24 -24
  112. package/src/transport/sse.js +93 -93
  113. package/src/upstream-probe/display.js +52 -52
  114. package/src/upstream-probe/probe.js +49 -49
  115. package/src/upstream-probe/rotate.js +110 -110
  116. package/src/upstream-probe/start.js +45 -45
  117. package/src/upstream.js +289 -289
@@ -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";
@@ -1,5 +1,7 @@
1
1
  // Anthropic SSE → OpenAI 形状转换(流式 / 聚合两种出口)。
2
- // 承诺字段:文本增量、tool_use 增量(tool_calls)、usage、finish_reason;thinking 块丢弃(不入历史)。
2
+ // 承诺字段:文本增量、thinking 增量(reasoning_content)、tool_use 增量(tool_calls)、usage、finish_reason。
3
+ // thinking 跨轮回填:首个 thinking block 的 signature 随 reasoning_content 挂在 assistant 历史消息上(必须,
4
+ // 上游要求 signature 与 thinking 原文一起回传;与 reasoning_content_signature 拼写不同的上游见下兼容)。
3
5
  const ENCODER = new TextEncoder();
4
6
  const DECODER = new TextDecoder();
5
7
  const STOP_REASONS = { end_turn: "stop", max_tokens: "length", tool_use: "tool_calls", stop_sequence: "stop", pause_turn: "stop" };
@@ -26,6 +28,9 @@ export function createAnthropicTranslator({ model = "zcode", id = newId(), creat
26
28
  let inputTokens = null;
27
29
  let outputTokens = null;
28
30
  let text = "";
31
+ // thinking 全量文本(聚合出口回填 reasoning_content)与首个 signature(跨轮必需,见文件头)
32
+ let thinkingText = "";
33
+ let thinkingSig = null;
29
34
  let toolCount = 0;
30
35
  const toolIndexByBlock = new Map();
31
36
  const toolCalls = [];
@@ -55,7 +60,12 @@ export function createAnthropicTranslator({ model = "zcode", id = newId(), creat
55
60
  }
56
61
  } else if (evt.type === "content_block_start") {
57
62
  const block = evt.content_block || {};
58
- if (block.type === "tool_use") {
63
+ if (block.type === "thinking") {
64
+ // 首个 thinking 块的 signature 留给聚合出口的跨轮回填(后续 thinking 块没有也必须回传空串占位——
65
+ // codearts 的 DeepSeek 同款要求,见 src/providers/codearts/chat.js)
66
+ if (thinkingSig === null && typeof block.signature === "string") thinkingSig = block.signature;
67
+ if (typeof block.thinking === "string" && block.thinking) thinkingText += block.thinking;
68
+ } else if (block.type === "tool_use") {
59
69
  const idx = toolCount++;
60
70
  toolIndexByBlock.set(Number(evt.index), idx);
61
71
  toolCalls.push({ id: String(block.id || `toolu_${idx}`), name: String(block.name || ""), args: "" });
@@ -66,6 +76,10 @@ export function createAnthropicTranslator({ model = "zcode", id = newId(), creat
66
76
  if (d.type === "text_delta" && typeof d.text === "string") {
67
77
  text += d.text;
68
78
  out.push(chunkLine({ content: d.text }));
79
+ } else if (d.type === "thinking_delta" && typeof d.thinking === "string") {
80
+ thinkingText += d.thinking;
81
+ if (typeof d.signature === "string" && d.signature && thinkingSig === null) thinkingSig = d.signature;
82
+ out.push(chunkLine({ reasoning_content: d.thinking }));
69
83
  } else if (d.type === "input_json_delta" && typeof d.partial_json === "string") {
70
84
  const idx = toolIndexByBlock.has(Number(evt.index)) ? toolIndexByBlock.get(Number(evt.index)) : Math.max(0, toolCount - 1);
71
85
  if (toolCalls[idx]) toolCalls[idx].args += d.partial_json;
@@ -113,6 +127,9 @@ export function createAnthropicTranslator({ model = "zcode", id = newId(), creat
113
127
 
114
128
  function result() {
115
129
  const message = { role: "assistant", content: text || null };
130
+ // thinking 聚合回填:跨轮必需的 signature 与原文一起带回(见文件头;codearts DeepSeek 同款)
131
+ if (thinkingText) message.reasoning_content = thinkingText;
132
+ if (thinkingSig !== null) message.reasoning_content_signature = thinkingSig;
116
133
  if (toolCalls.length) {
117
134
  message.tool_calls = toolCalls.map((t) => ({ id: t.id, type: "function", function: { name: t.name, arguments: t.args || "{}" } }));
118
135
  if (!text) message.content = null;
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
  }
@@ -0,0 +1,37 @@
1
+ # src/routes — 转发链契约(改 relay / 流式 / 用量前必读)
2
+
3
+ > 从 `AGENTS.md` 下沉(2026-09-29 分层,就近生效)。改完跑:`node --test --test-concurrency=1 test/stream-*.test.js test/relay-*.test.js test/usage-*.test.js` + `npm run docs:check`。
4
+
5
+ ## 时间量测的两条口径(ADR-0039,最易被改错)
6
+
7
+ - **保护闸门的零点 = `relay()` 入口的 `t0`**(首帧超时 / stall / MAX_STREAM / keepalive 四类)。**禁止**把上报锚点接进闸门判定,否则上游排队时间会吃掉超时预算、误杀正在出字的慢模型。
8
+ - **上报时长(usage 行)的零点 = 本次上游尝试起点**(`serial-trial` 的 `tUp`,经 `attemptStartMs` 交进 relay);relay 入口算 `preflightMs = t0 − 锚点` 并随**全部 4 个 return 点**带出,`relay-pipeline` 落行时相加。锚点缺失/负值一律 0。
9
+ - 报表口径与 state 口径**彻底分离**:`recordModelStats`(→ `-status`/`-model stats` 的 EMA)仍吃转发入口相对值,勿"顺手统一"。
10
+ - 首字与总耗时同加一个常数 → `总 − 首字` 恒等 → 速度读数不变(已锁为单测)。
11
+ - usage 行**只增字段**演进(`src/usage/record.js` 拥有行形状);旧行缺字段 = 未知,不得当 0。
12
+ - 模型日志里 `relay-first-chunk ttfMs=` 是**转发入口相对值**(故意与报表不同源),排查时别混。
13
+
14
+ ## relay 的行为契约(既有测试锁死)
15
+
16
+ - **超时是显式字段 `timedOut`**,`status` 数值只是 HTTP 语义展示,不得再拿 status 当"是否可 failover"的信号。
17
+ - **注释帧(`: keepalive`)不算首块**:不解除闸门、不触发超时救回,但照常透传;能否安全 failover 看 `detail.wrotePayload`(=是否已**提交**模型产出,见下条留口)。
18
+ - 闸门到点后上游仍吐出真实数据 → **撤销超时判定照常转发**(cancel 是异步的,竞态窗口内已到达的数据是纯收益),记 `detail.recoveries`。
19
+ - 任一闸门到点且未写真实数据 → 回退候选;唯一/末位候选无 failover 去向 → 闸门放宽到 `MSLXDFF_LAST_CANDIDATE_TIMEOUT_MS`(默认 120s,0=不限),防误杀慢模型。
20
+ - **空转 200 判据单一真相** = `stream-scan.js:isEmptyTurnDetail`(`chatShaped===true` 且 `chars===0 && toolCalls===0` 且 `finish_reason ∉ {tool_calls, function_call}`);relay 侧「要不要封口」与 pipeline 5a0「要不要重拉」都必须用它,两处各写一份必然错拍。判空 → 转 failover,不记 auto 冷却(空转≠模型坏)。错误包络暂扣 `holdableChunk` 与非 chat 形状放行同在 `stream-scan.js`(**不在 stream.js**)。
21
+ - **空轮留口(ADR-0043)**:模型产出(正文/工具/**思考**)出现前,空 delta 帧与 `[DONE]` 一律暂扣不写(`stream-hold.js:createRevocablePrefix`);流结束仍零产出且未断下游 → **不 `res.end()`**,返回 `heldOpen:true` 交回重拉/换候选,正文续写同一条连接。三条铁律:① 异形/解析失败帧必须立即透传(`isTrivialFrame` 白名单式),透传契约不得为救空轮让路;② `[DONE]` 是终点,错误帧要排在它之前(tail 槽)且全程只允许一个;③ 重入同一条连接时 headers 可能已 flush → `statusCode`/`setHeader` 一律门控,收场经 `endHeldFailure`/`endEmptyTurnStream`/`helpers.json` 的 `writableEnded`+`headersSent` 守卫保证**恰好一次**,禁止把 JSON 体混进 `text/event-stream`。
22
+ - 断下游即掐上游:relay 自持 `reader`(`for-await` 会锁 ReadableStream 使 `body.cancel()` 必 reject),`reader.cancel()` 才真掐得断。
23
+
24
+ ## 组/组员与宽带中继(`src/routes/groups*.js`、`peers.js`、`relay-queue.js`)
25
+
26
+ - 慢模型不掐当前流:首块 25s 内到即视为可用、后续无限等;stall 15s 仅作质量分,耗时 >20s 降权;`MSLXDFF_HEDGE_DELAY_MS=1000` 时 1s 并发组员赛跑,赢家 `x-mslxdff-via`;指定模型 429 时按延迟 EMA 择优切最快模型。
27
+ - `SLOW_TOTAL_MS=20s` 标 slow 进 5 分钟冷却(普通 60s);上游重试 `1次×100ms`;`MSLXDFF_STREAM_TIMEOUT_MS=25s`(**仅首块未到且未写出任何字节时**才 failover;`0`=显式关闭首块超时,给重思考模型用);`MAX_STREAM_MS=120s`。
28
+ - 宽带 `relay://` 成员走 `GET /v1/groups/relay/stream` SSE 单长连 25s ping 直推(`pushToStream`),`MSLXDFF_BROADBAND_STREAM=0` 回退 1s poll,`MSLXDFF_BROADBAND_PING_MS=25000`/`_STALE_MS=90000` 可配(空闲 -96%);用户明确反对 1s 轮询的空轮询。
29
+ - 组命令语义:`-group list` 序号按 state 成员顺序(过滤 leader)固定渲染,`-group remove <seq>` 用同一顺序踢人(仅组长);组长 `-leavegroup` 提示改用 `-delgroup`,`-delgroup` 在组员节点报错并提示该组由谁领导。
30
+ - 模型可观测面:事件用**黑名单** `TRACE_DENY`(默认全可见,只排噪声与敏感面);决定类事件只渲染 `DECISION_FIELDS` 登记过的标量,**"加日志"≠"倒数据"**,payload/正文一律不落;回显取头单一入口 `upstreamEcho(res)`。
31
+
32
+ ## Responses 端点(`routes/responses-route.js` + `responses/translate.js`,ADR-0012)
33
+
34
+ - 复用 ChatPipeline 全链路,只做形状翻译;stateless(`previous_response_id` 不支持,thinking reasoning 暂不透传)。
35
+ - Codex Rust 严格解码三特例(缺一整轮作废):① 对 codex 调用者(UA/originator `codex_*` 或 `?client_version=`)`GET /v1/models` 追加顶层 `models: []`(**必须空**,填真目录会覆盖其内置 agent prompt);② completed 的 usage 转 Responses 口径(`input_tokens/output_tokens` + details);③ done 事件 `text` 带累积全文。
36
+ - `-setto chatgpt`(`src/sync-codex.js`)写三端共用 `~/.codex/config.toml`(`wire_api="responses"` 显式、鉴权绝对路径 `node + -showtoken`,换模型需重跑 setto / `codex exec -m` / 手改 `model=`,单键覆盖无需剪枝);排障 `MSLXDFF_RESPONSES_DEBUG=1` 看 `daemon.log [responses]` 四段 + Codex 侧 `~/.codex/logs_2.sqlite`(`Request completed ... status=`)与 `thread_history_1.sqlite`(正文)。
37
+ - `@ai-sdk/openai` 只是客户端包,替代不了服务端翻译层;Qwen 上游走 `/v1/messages`(Anthropic 协议),接 Codex 要多一跳(未做)。
@@ -1,7 +1,16 @@
1
1
  // Note: 失败收尾四分支在 result 后补 client-response,与成功路径对账口径一致(result 条数 == client-response 条数)— 见 .agents/notes/implemented/bug-fix/2026-09-25-sdk-headers-timeout-and-failure-client-response.md
2
2
  import { relay } from "../stream.js";
3
+ import { endHeldFailure } from "../stream-hold.js"; // 终局收场出口(502 JSON 或 SSE 错误帧 + [DONE])
3
4
  import { json } from "../helpers.js";
4
5
  import { performance } from "node:perf_hooks";
6
+ import { recordRelayTalk } from "../../talk-log.js"; // 环形对话日志:exhausted 收尾直连 relay,落盘点必须自己补一份
7
+
8
+ // 最后一站的收场:relay 的「不写响应」路径(超时 / 空轮延后封口)都汇到这里,没有上层 failover 了,
9
+ // 必须把响应「恰好一次」地关掉。形状由 endHeldFailure 按 headers 是否已 flush 决定
10
+ // (未 flush → 502 JSON;已 flush 的 SSE → 错误帧 + [DONE]),这里不再自己 json() 打已 flush 的流。
11
+ function terminal(res, message) {
12
+ endHeldFailure(res, message || "all auto models failed");
13
+ }
5
14
 
6
15
  export async function handleExhaustedLocal({ res, body, lastErr, order, handlerCtx, evt, logCall, mark, perf0, stages, done, requested, useAuto, deps = {} }) {
7
16
  const { relay: relayImpl = relay } = deps;
@@ -15,18 +24,22 @@ export async function handleExhaustedLocal({ res, body, lastErr, order, handlerC
15
24
  onDownstreamAbort: () => evt("client-abort", { reqId: handlerCtx.reqId, model: lastErr.model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
16
25
  });
17
26
  evt("relay-done", { reqId: handlerCtx.reqId, model: lastErr.model, via: "local-exhausted", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
27
+ // 绕过 relay-pipeline 的最后一站:对话日志照常落盘(正文只进 talk/*.log,不进 events.log)
28
+ try { recordRelayTalk({ reqId: handlerCtx.reqId, model: lastErr.model, via: "local-exhausted", body, out }); } catch {}
18
29
  evt("result", { reqId: handlerCtx.reqId, model: lastErr.model, status: out.status, via: "local", timing: lastErr.upstream._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null });
19
30
  evt("client-response", { requested, actual: lastErr.model, via: "local", fallback: false, status: out.status, reqId: handlerCtx.reqId });
20
- // 最后一站:relay 超时路径不写响应(设计留给上层 failover),这里没有上层,必须自己收尾
21
- if (out.timedOut) {
22
- json(res, 502, { error: out.detail?.upstreamError ? `upstream error: ${out.detail.upstreamError}` : `stream timed out after ${out.totalMs}ms` });
31
+ // 最后一站:relay 超时/空轮留口路径都不写响应,这里没有上层,必须自己收尾
32
+ if (out.timedOut || out.heldOpen) {
33
+ terminal(res, out.detail?.exitReason === "empty-turn-hold"
34
+ ? "EMPTY_MODEL_RESPONSE: 空轮重拉与换候选全部用尽,仍无正文"
35
+ : out.detail?.upstreamError ? `upstream error: ${out.detail.upstreamError}` : `stream timed out after ${out.totalMs}ms`);
23
36
  }
24
37
  return true;
25
38
  }
26
39
  evt("result", { reqId: handlerCtx.reqId, model, status: lastErr?.status ?? 502, via: "none", timing: null });
27
40
  evt("client-response", { requested, actual: model, via: "none", fallback: false, status: lastErr?.status ?? 502, reqId: handlerCtx.reqId });
28
41
  done({ via: "none", status: lastErr?.status ?? 502, error: lastErr?.message || "all auto models failed" });
29
- json(res, 502, { error: lastErr?.message || "all auto models failed" });
42
+ terminal(res, lastErr?.message);
30
43
  return true;
31
44
  }
32
45
 
@@ -41,16 +54,18 @@ export async function handleExhaustedAll({ res, body, lastErr, order, requested,
41
54
  onDownstreamAbort: () => evt("client-abort", { reqId: handlerCtx.reqId, model: lastErr.model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
42
55
  });
43
56
  evt("relay-done", { reqId: handlerCtx.reqId, model: lastErr.model, via: "local-final", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
57
+ // 同上:全链耗尽的最终转发也要留下问答原文
58
+ try { recordRelayTalk({ reqId: handlerCtx.reqId, model: lastErr.model, via: "local-final", body, out }); } catch {}
44
59
  evt("result", { reqId: handlerCtx.reqId, model: lastErr.model, status: out.status, via: "local", timing: lastErr.upstream._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null });
45
60
  evt("client-response", { requested, actual: lastErr.model, via: "local", fallback: false, status: out.status, reqId: handlerCtx.reqId });
46
- // 最后一站:relay 超时路径不写响应,这里没有上层 failover,必须自己收尾
47
- if (out.timedOut) {
48
- json(res, 502, { error: out.detail?.upstreamError ? `upstream error: ${out.detail.upstreamError}` : `stream timed out after ${out.totalMs}ms` });
61
+ // 最后一站:relay 超时/空轮留口路径不写响应,这里没有上层 failover,必须自己收尾
62
+ if (out.timedOut || out.heldOpen) {
63
+ terminal(res, out.detail?.upstreamError ? `upstream error: ${out.detail.upstreamError}` : `stream timed out after ${out.totalMs}ms`);
49
64
  }
50
65
  return true;
51
66
  }
52
67
  evt("result", { reqId: handlerCtx.reqId, model: lastErr?.model ?? requested, status: lastErr?.status ?? 502, via: "none", timing: null });
53
68
  evt("client-response", { requested, actual: lastErr?.model ?? requested, via: "none", fallback: false, status: lastErr?.status ?? 502, reqId: handlerCtx.reqId });
54
- json(res, 502, { error: lastErr?.message || "all auto models failed" });
69
+ terminal(res, lastErr?.message);
55
70
  return true;
56
71
  }
@@ -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
  }