mslxdff 0.1.155 → 0.1.158
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mslxdff.js +4 -4
- package/docs/ARCHITECTURE.md +426 -0
- package/docs/FEATURE_TREE.md +164 -0
- package/docs/MOBILE.md +82 -0
- package/docs/adr/0001-reasoning-content-injection.md +14 -0
- package/docs/adr/0002-models-free-filter.md +12 -0
- package/docs/adr/0003-zero-state-no-auth.md +10 -0
- package/docs/adr/0004-bearer-token.md +18 -0
- package/docs/adr/0005-peer-mesh.md +53 -0
- package/docs/adr/0006-broadband-member.md +103 -0
- package/docs/adr/0007-multi-provider-prefix.md +25 -0
- package/docs/adr/0008-share-keys-to-peers.md +52 -0
- package/docs/adr/0009-chat-repl.md +37 -0
- package/docs/adr/0010-allowlist.md +36 -0
- package/docs/adr/0011-broadband-stream.md +30 -0
- package/docs/adr/0012-responses-endpoint-codex-sync.md +48 -0
- package/docs/adr/0013-node16-compat.md +41 -0
- package/docs/adr/0014-deepseek-provider.md +52 -0
- package/docs/adr/0015-upstream-probe-routing.md +50 -0
- package/docs/adr/0016-model-capabilities.md +27 -0
- package/docs/adr/0017-ai-sdk-upstream-engine.md +59 -0
- package/docs/adr/0018-zen-client-identity.md +52 -0
- package/docs/adr/0019-share-keys-always-lend.md +59 -0
- package/docs/adr/0020-zen-free-lane-agent-shape.md +52 -0
- package/docs/adr/0021-usage-report-jsonl.md +47 -0
- package/docs/adr/0022-models-capability-merge.md +61 -0
- package/docs/adr/0023-key-provider-default-direct.md +58 -0
- package/docs/adr/0024-node18-baseline.md +63 -0
- package/docs/adr/0025-workbuddy-authdir-follows-state.md +71 -0
- package/docs/adr/0026-cline-provider-id-unify.md +67 -0
- package/docs/adr/0027-codearts-provider.md +48 -0
- package/docs/adr/0028-traework-provider.md +34 -0
- package/docs/adr/0029-qoder-native-provider.md +60 -0
- package/docs/adr/0030-models-list-scoped-by-picks.md +53 -0
- package/docs/adr/0031-qoder-true-streaming.md +50 -0
- package/docs/adr/0032-generic-responses-channel.md +72 -0
- package/docs/adr/0033-cline-allowlist-auto-sync.md +75 -0
- package/docs/adr/0034-request-level-human-readable-observability.md +60 -0
- package/docs/adr/0035-sdk-channel-headers-timeout.md +49 -0
- package/docs/adr/0036-qoder-per-request-sticky-account.md +82 -0
- package/docs/adr/0037-qwenwork-independent-provider.md +82 -0
- package/docs/adr/0038-zcode-provider.md +140 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/cli_help.md +1391 -0
- package/docs/cli_help_mini.md +133 -0
- package/docs/plans/bench-via-latency-2026-09-01.md +215 -0
- package/docs/plugins.md +187 -0
- package/package.json +1 -1
- package/src/auto.js +254 -254
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +123 -123
- package/src/chat-pipeline/policy.js +76 -76
- package/src/chat-pipeline/serial-trial.js +210 -210
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/list-providers.js +4 -3
- package/src/cli/commands/model/list-render.js +7 -9
- package/src/cli/commands/model/list-sort.js +52 -0
- package/src/cli/commands/model/list-tty.js +76 -0
- package/src/cli/commands/model/list.js +5 -62
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +124 -116
- package/src/cli/commands/provider/models.js +139 -128
- package/src/cli/commands/provider/qwenwork-login.js +119 -0
- package/src/cli/commands/provider/zcode-login.js +77 -0
- package/src/cli/commands/provider/zcode-quota.js +55 -0
- package/src/cli/commands/sync.js +232 -232
- package/src/cli/provider-row.js +2 -2
- package/src/cli/status.js +279 -279
- package/src/daemon.js +96 -96
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/model-trace.js +1 -0
- package/src/models.js +225 -225
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/keyring.js +60 -56
- package/src/providers/qoder/chat.js +183 -100
- package/src/providers/qoder/index.js +230 -184
- package/src/providers/qoder/sse.js +103 -46
- package/src/providers/qoder/sticky.js +1 -0
- package/src/providers/qoder/stream.js +32 -9
- package/src/providers/qwenwork/account-store.js +133 -0
- package/src/providers/qwenwork/constants.js +67 -0
- package/src/providers/qwenwork/cosy.js +120 -0
- package/src/providers/qwenwork/crypto.js +218 -0
- package/src/providers/qwenwork/http.js +20 -0
- package/src/providers/qwenwork/index.js +327 -0
- package/src/providers/qwenwork/payload.js +142 -0
- package/src/providers/qwenwork/rsa.js +54 -0
- package/src/providers/qwenwork/sse.js +268 -0
- package/src/providers/qwenwork/stream.js +130 -0
- package/src/providers/qwenwork/upstream.js +120 -0
- package/src/providers/qwenwork.js +1 -0
- package/src/providers/registry.js +66 -56
- package/src/providers/share-keys.js +2 -2
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/account-store.js +129 -0
- package/src/providers/zcode/auth.js +28 -0
- package/src/providers/zcode/chat.js +171 -0
- package/src/providers/zcode/const.js +54 -0
- package/src/providers/zcode/headers.js +55 -0
- package/src/providers/zcode/index.js +124 -0
- package/src/providers/zcode/models.js +65 -0
- package/src/providers/zcode/oauth.js +120 -0
- package/src/providers/zcode/quota.js +176 -0
- package/src/providers/zcode/sse.js +179 -0
- package/src/reasoning.js +32 -32
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +250 -246
- package/src/routes/chat/via-route-handler.js +144 -144
- package/src/routes/hedge.js +255 -255
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream.js +438 -399
- package/src/runtime/bootstrap.js +45 -45
- package/src/runtime/provider-gate.js +33 -30
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +64 -64
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +289 -289
|
@@ -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";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// zcode 账号落盘单一源:auths/zcode-<uid>.json(0600 tmp+rename)+ state.json providerConfigs.zcode.keys 同步。
|
|
2
|
+
// deviceMid(per-account 持久 UUID,对齐 zcode-switch 实证)随账号文档走,不进 providerConfigs,
|
|
3
|
+
// 规避 providerConfig 重建路径静默丢字段的既有故障类。
|
|
4
|
+
import { writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync, readdirSync, readFileSync, chmodSync } from "node:fs";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { uuid } from "../../compat.js";
|
|
8
|
+
import { isTestEnv, defaultStateFile } from "../../state/store.js";
|
|
9
|
+
|
|
10
|
+
export function zcodeAuthDir({ explicit = "", testEnv = false, stateFile } = {}) {
|
|
11
|
+
if (explicit) return explicit;
|
|
12
|
+
if (testEnv) return join(tmpdir(), "mslxdff-test-auths");
|
|
13
|
+
return join(dirname(stateFile), "auths");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveZcodeAuthDir() {
|
|
17
|
+
return zcodeAuthDir({
|
|
18
|
+
explicit: process.env.MSLXDFF_ZCODE_AUTH_DIR || "",
|
|
19
|
+
testEnv: isTestEnv(),
|
|
20
|
+
stateFile: defaultStateFile(),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveZcodeAuthDirs() {
|
|
25
|
+
const primary = resolveZcodeAuthDir();
|
|
26
|
+
const cwdDir = join(process.cwd(), "auths");
|
|
27
|
+
return primary === cwdDir ? [primary] : [primary, cwdDir];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function listZcodeAccountDocs({ dirs } = {}) {
|
|
31
|
+
const out = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
for (const dir of dirs || resolveZcodeAuthDirs()) {
|
|
34
|
+
let files = [];
|
|
35
|
+
try {
|
|
36
|
+
if (!existsSync(dir)) continue;
|
|
37
|
+
files = readdirSync(dir).filter((f) => f.startsWith("zcode-") && f.endsWith(".json"));
|
|
38
|
+
} catch { continue; }
|
|
39
|
+
for (const f of files) {
|
|
40
|
+
try {
|
|
41
|
+
const doc = JSON.parse(readFileSync(join(dir, f), "utf8"));
|
|
42
|
+
const uid = doc?.account?.uid;
|
|
43
|
+
if (!uid || !doc?.auth?.jwt) continue;
|
|
44
|
+
if (seen.has(String(uid))) continue;
|
|
45
|
+
seen.add(String(uid));
|
|
46
|
+
out.push({
|
|
47
|
+
uid: String(uid),
|
|
48
|
+
jwt: String(doc.auth.jwt),
|
|
49
|
+
deviceMid: String(doc.auth.deviceMid || ""),
|
|
50
|
+
name: String(doc.account.name || ""),
|
|
51
|
+
email: String(doc.account.email || ""),
|
|
52
|
+
provider: String(doc.auth.provider || ""),
|
|
53
|
+
file: join(dir, f),
|
|
54
|
+
dir,
|
|
55
|
+
doc,
|
|
56
|
+
});
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function accountFilePath(uid, dir) {
|
|
64
|
+
return join(dir, `zcode-${uid}.json`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function writeZcodeAccountFile({ uid, jwt, name = "", email = "", provider = "zai", deviceMid = "" }, { dir = resolveZcodeAuthDir() } = {}) {
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
const doc = { account: { uid, name, email }, auth: { jwt, provider, deviceMid } };
|
|
70
|
+
const fp = accountFilePath(uid, dir);
|
|
71
|
+
const tmp = `${fp}.tmp`;
|
|
72
|
+
writeFileSync(tmp, JSON.stringify(doc, null, 2), { mode: 0o600 });
|
|
73
|
+
try {
|
|
74
|
+
if (existsSync(fp)) { try { chmodSync(fp, 0o600); } catch {} unlinkSync(fp); }
|
|
75
|
+
renameSync(tmp, fp);
|
|
76
|
+
} catch {
|
|
77
|
+
writeFileSync(fp, JSON.stringify(doc, null, 2), { mode: 0o600 });
|
|
78
|
+
try { unlinkSync(tmp); } catch {}
|
|
79
|
+
}
|
|
80
|
+
try { chmodSync(fp, 0o600); } catch {}
|
|
81
|
+
return fp;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readZcodeAccountDoc(uid, { dir = resolveZcodeAuthDir() } = {}) {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(accountFilePath(uid, dir), "utf8"));
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function saveZcodeAccount({ uid, jwt, name = "", email = "", provider = "zai", deviceMid = "", dir, file } = {}) {
|
|
93
|
+
if (!uid || !jwt) throw new Error("saveZcodeAccount: 缺少 uid/jwt");
|
|
94
|
+
const targetDir = dir || resolveZcodeAuthDir();
|
|
95
|
+
const existing = readZcodeAccountDoc(uid, { dir: targetDir });
|
|
96
|
+
const mid = deviceMid || existing?.auth?.deviceMid || uuid();
|
|
97
|
+
const fp = writeZcodeAccountFile({ uid, jwt, name, email, provider, deviceMid: mid }, { dir: targetDir });
|
|
98
|
+
|
|
99
|
+
const { loadProviderConfig, saveProviderConfig } = await import("../../state.js");
|
|
100
|
+
const cur = loadProviderConfig("zcode", file ? { file } : {}) || {};
|
|
101
|
+
const keys = [...new Set([...(Array.isArray(cur.keys) ? cur.keys : []), jwt])];
|
|
102
|
+
// spread cur:保留 allowlist/allowAnyModels/baseUrl 等既有字段,不做 `{keys}` 重建
|
|
103
|
+
saveProviderConfig("zcode", { ...cur, keys }, file ? { file } : {});
|
|
104
|
+
return { file: fp, keys: keys.length, updated: Boolean(existing) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// deviceMid:账号文档内持久复用;缺失时生成 UUID 并写回(文档不存在或尚无 jwt 时只返回不落盘)。
|
|
108
|
+
export function ensureZcodeDeviceMid({ uid, dir } = {}) {
|
|
109
|
+
if (!uid) return "";
|
|
110
|
+
const targetDir = dir || resolveZcodeAuthDir();
|
|
111
|
+
const doc = readZcodeAccountDoc(uid, { dir: targetDir });
|
|
112
|
+
const cur = String(doc?.auth?.deviceMid || "").trim();
|
|
113
|
+
if (cur) return cur;
|
|
114
|
+
const mid = uuid();
|
|
115
|
+
if (doc?.auth?.jwt) {
|
|
116
|
+
writeZcodeAccountFile(
|
|
117
|
+
{
|
|
118
|
+
uid,
|
|
119
|
+
jwt: doc.auth.jwt,
|
|
120
|
+
name: doc.account?.name || "",
|
|
121
|
+
email: doc.account?.email || "",
|
|
122
|
+
provider: doc.auth?.provider || "zai",
|
|
123
|
+
deviceMid: mid,
|
|
124
|
+
},
|
|
125
|
+
{ dir: targetDir },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return mid;
|
|
129
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// zcode JWT 工具:解析、过期判断、指纹(日志只准出指纹,全文绝不落日志)。
|
|
2
|
+
export function decodeJwtPayload(token) {
|
|
3
|
+
const s = String(token || "");
|
|
4
|
+
const parts = s.split(".");
|
|
5
|
+
if (parts.length < 3) return null;
|
|
6
|
+
try {
|
|
7
|
+
const json = Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
|
|
8
|
+
const payload = JSON.parse(json);
|
|
9
|
+
return payload && typeof payload === "object" ? payload : null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function jwtExpiresAt(token) {
|
|
16
|
+
const exp = Number(decodeJwtPayload(token)?.exp);
|
|
17
|
+
return Number.isFinite(exp) && exp > 0 ? exp * 1000 : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isJwtExpired(token, now = Date.now()) {
|
|
21
|
+
const at = jwtExpiresAt(token);
|
|
22
|
+
return at != null && at <= now;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function tokenFingerprint(token) {
|
|
26
|
+
const s = String(token || "");
|
|
27
|
+
return s ? s.slice(0, 8) : "";
|
|
28
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// zcode 模型转发:OpenAI 请求 → Anthropic messages(网关出站恒流式;非流式本地聚合回 OpenAI JSON)。
|
|
2
|
+
// 失败响应带 x-mslxdff-zcode-kind 标记,供 provider 工厂决定冷却策略。
|
|
3
|
+
import { timeoutSignal } from "../../compat.js";
|
|
4
|
+
import { ZCODE_MESSAGES_URL, canonicalZcodeModel, zcodeErrorKind } from "./const.js";
|
|
5
|
+
import { buildZcodeHeaders } from "./headers.js";
|
|
6
|
+
import { aggregateAnthropicToOpenAi, anthropicToOpenAiStream } from "./sse.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_TOKENS = 4096;
|
|
9
|
+
|
|
10
|
+
function textOf(content) {
|
|
11
|
+
if (typeof content === "string") return content;
|
|
12
|
+
if (Array.isArray(content)) return content.filter((p) => p?.type === "text").map((p) => p.text || "").join("");
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mapToolChoice(tc) {
|
|
17
|
+
if (!tc) return undefined;
|
|
18
|
+
if (tc === "auto") return { type: "auto" };
|
|
19
|
+
if (tc === "required") return { type: "any" };
|
|
20
|
+
if (tc === "none") return { type: "none" };
|
|
21
|
+
const name = tc?.function?.name;
|
|
22
|
+
return name ? { type: "tool", name: String(name) } : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function toAnthropicRequest(body = {}) {
|
|
26
|
+
const messages = [];
|
|
27
|
+
let system = "";
|
|
28
|
+
for (const m of Array.isArray(body.messages) ? body.messages : []) {
|
|
29
|
+
const role = String(m?.role || "");
|
|
30
|
+
if (role === "system") {
|
|
31
|
+
const part = textOf(m.content);
|
|
32
|
+
if (part) system += (system ? "\n\n" : "") + part;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (role === "tool") {
|
|
36
|
+
messages.push({
|
|
37
|
+
role: "user",
|
|
38
|
+
content: [{ type: "tool_result", tool_use_id: String(m.tool_call_id || ""), content: typeof m.content === "string" ? m.content : textOf(m.content) }],
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (role === "assistant") {
|
|
43
|
+
const blocks = [];
|
|
44
|
+
const text = textOf(m.content);
|
|
45
|
+
if (text) blocks.push({ type: "text", text });
|
|
46
|
+
for (const tc of Array.isArray(m.tool_calls) ? m.tool_calls : []) {
|
|
47
|
+
let input = {};
|
|
48
|
+
try {
|
|
49
|
+
input = JSON.parse(tc?.function?.arguments || "{}");
|
|
50
|
+
} catch {}
|
|
51
|
+
blocks.push({ type: "tool_use", id: String(tc?.id || ""), name: String(tc?.function?.name || ""), input });
|
|
52
|
+
}
|
|
53
|
+
if (blocks.length) messages.push({ role: "assistant", content: blocks });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
messages.push({ role: "user", content: textOf(m.content) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const req = {
|
|
60
|
+
model: canonicalZcodeModel(body.model),
|
|
61
|
+
max_tokens: Number(body.max_tokens ?? body.max_completion_tokens) || DEFAULT_MAX_TOKENS,
|
|
62
|
+
messages,
|
|
63
|
+
stream: true, // 上游恒流式;非流式在本地聚合
|
|
64
|
+
};
|
|
65
|
+
if (system) req.system = system;
|
|
66
|
+
if (Number.isFinite(Number(body.temperature))) req.temperature = Number(body.temperature);
|
|
67
|
+
if (Array.isArray(body.tools) && body.tools.length) {
|
|
68
|
+
req.tools = body.tools
|
|
69
|
+
.map((t) => ({
|
|
70
|
+
name: String(t?.function?.name || t?.name || ""),
|
|
71
|
+
description: String(t?.function?.description || t?.description || ""),
|
|
72
|
+
input_schema: t?.function?.parameters || t?.parameters || { type: "object", properties: {} },
|
|
73
|
+
}))
|
|
74
|
+
.filter((t) => t.name);
|
|
75
|
+
}
|
|
76
|
+
// tool_choice 独立映射(不依赖 tools 是否在场,客户端显式给定即按 Anthropic 形状透出)
|
|
77
|
+
const choice = mapToolChoice(body.tool_choice);
|
|
78
|
+
if (choice) req.tool_choice = choice;
|
|
79
|
+
if (body.stop !== undefined && body.stop !== null) {
|
|
80
|
+
req.stop_sequences = Array.isArray(body.stop) ? body.stop : [String(body.stop)];
|
|
81
|
+
}
|
|
82
|
+
return req;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function classifyZcodeFailure(status, payload) {
|
|
86
|
+
const code = Number(payload?.code) || 0;
|
|
87
|
+
let kind = zcodeErrorKind(code);
|
|
88
|
+
if (kind === "unknown") {
|
|
89
|
+
if (status === 401) kind = "auth";
|
|
90
|
+
else if (status === 429) kind = "rate_limit";
|
|
91
|
+
else if (status >= 500) kind = "server";
|
|
92
|
+
}
|
|
93
|
+
return { kind, code, status, message: String(payload?.msg || payload?.message || "").trim() };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ERROR_MAP = {
|
|
97
|
+
auth: { status: 401, type: "auth_error", text: (f) => `zcode: 登录已失效(code ${f.code || 1006})。请运行 mslxdff -provider zcode login 重新登录` },
|
|
98
|
+
quota: { status: 429, type: "quota_exhausted", text: () => "zcode: 免费额度已用完(按日重置),请明日再试或添加账号" },
|
|
99
|
+
rate_limit: { status: 429, type: "all_cooling", text: () => "zcode: 上游限流(账号冷却中),请稍后重试" },
|
|
100
|
+
security: { status: 403, type: "security_reject", text: (f) => `zcode: 安全校验拒绝(code ${f.code || 3007})${f.message ? `:${f.message}` : ""} · 该额度走 Start Plan 验证码通道(每条请求需一次性 x-aliyun-captcha-verify-param;未启用验证码农场时必现),见 docs/adr/0038` },
|
|
101
|
+
param: { status: 400, type: "invalid_request", text: (f) => `zcode: 请求参数被上游拒绝(code ${f.code})${f.message ? `:${f.message}` : ""}` },
|
|
102
|
+
server: { status: 502, type: "upstream_error", text: (f) => `zcode: 上游服务异常${f.code ? `(code ${f.code})` : ""}${f.message ? `:${f.message}` : ""}` },
|
|
103
|
+
network: { status: 502, type: "upstream_error", text: (f) => `zcode: 网络错误 ${f.message || ""}`.trim() },
|
|
104
|
+
unknown: { status: 502, type: "upstream_error", text: (f) => `zcode: 上游错误(HTTP ${f.status || "?"}${f.code ? ` code ${f.code}` : ""})${f.message ? `:${f.message}` : ""}` },
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export function zcodeErrorResponse(fail = {}) {
|
|
108
|
+
const kind = ERROR_MAP[fail.kind] ? fail.kind : "unknown";
|
|
109
|
+
const entry = ERROR_MAP[kind];
|
|
110
|
+
const headers = { "Content-Type": "application/json" };
|
|
111
|
+
if (fail.kind) headers["x-mslxdff-zcode-kind"] = fail.kind;
|
|
112
|
+
return new Response(JSON.stringify({ error: { message: entry.text(fail), type: entry.type, code: fail.code || 0 } }), {
|
|
113
|
+
status: entry.status,
|
|
114
|
+
headers,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function forwardZcodeChat({ body, token, deviceMid, fetchImpl, version, timeoutMs = 120_000 } = {}) {
|
|
119
|
+
const anthropicBody = toAnthropicRequest(body);
|
|
120
|
+
const modelId = anthropicBody.model;
|
|
121
|
+
const headers = {
|
|
122
|
+
...buildZcodeHeaders({ token, deviceMid, version }),
|
|
123
|
+
"Content-Type": "application/json",
|
|
124
|
+
Accept: "text/event-stream",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
let res;
|
|
128
|
+
try {
|
|
129
|
+
res = await fetchImpl(ZCODE_MESSAGES_URL, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers,
|
|
132
|
+
body: JSON.stringify(anthropicBody),
|
|
133
|
+
signal: timeoutSignal(timeoutMs),
|
|
134
|
+
});
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return zcodeErrorResponse({ kind: "network", message: String(e?.message || e).slice(0, 160) });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const payload = await res.json().catch(() => null);
|
|
141
|
+
return zcodeErrorResponse({ ...classifyZcodeFailure(res.status, payload), model: modelId });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 上游业务错误惯例:HTTP 200 + JSON body(code 非 0/200)→ 同样按错误分类
|
|
145
|
+
if (!String(res.headers.get("content-type") || "").includes("text/event-stream")) {
|
|
146
|
+
const payload = await res.json().catch(() => null);
|
|
147
|
+
const code = Number(payload?.code);
|
|
148
|
+
const bizOk = payload?.success !== false && (!Number.isFinite(code) || code === 0 || code === 200);
|
|
149
|
+
if (payload && !bizOk) {
|
|
150
|
+
return zcodeErrorResponse({ ...classifyZcodeFailure(res.status, payload), model: modelId });
|
|
151
|
+
}
|
|
152
|
+
if (payload && body?.stream === false) {
|
|
153
|
+
return new Response(JSON.stringify(payload), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (body?.stream !== false) {
|
|
158
|
+
const stream = anthropicToOpenAiStream(res.body, { model: modelId });
|
|
159
|
+
return new Response(stream, {
|
|
160
|
+
status: 200,
|
|
161
|
+
headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const text = await res.text();
|
|
166
|
+
const out = await aggregateAnthropicToOpenAi(text, { model: modelId });
|
|
167
|
+
if (out.error) {
|
|
168
|
+
return zcodeErrorResponse({ kind: out.error.kind || "server", code: out.error.code || 0, status: 200, message: out.error.message });
|
|
169
|
+
}
|
|
170
|
+
return new Response(JSON.stringify(out.openAi), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
171
|
+
}
|