mslxdff 0.1.101 → 0.1.102
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/package.json +1 -1
- package/src/chat-pipeline/index.js +5 -1
- package/src/providers/workbuddy/chat.js +3 -2
- package/src/providers/workbuddy/reshape.js +142 -0
- package/src/routes/hedge.js +2 -0
- package/src/routes/stream.js +2 -1
- package/src/upstream-responses.js +118 -1
- package/src/upstream.js +24 -11
package/package.json
CHANGED
|
@@ -30,10 +30,14 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// order 推导 + plugin model:select 可改
|
|
33
|
+
// 语义:指定模型 = 死锁单模型(本机→组员同款,挂了就报挂,不兜其他 picks);只有 auto 才轮 picks
|
|
33
34
|
let order;
|
|
34
35
|
if (lockModel) order = [requested];
|
|
35
36
|
else if (useAuto) order = auto ? await auto.candidates() : [""];
|
|
36
|
-
else
|
|
37
|
+
else {
|
|
38
|
+
if (auto && requested) { try { await auto.candidatesFor(requested); } catch {} }
|
|
39
|
+
order = [requested];
|
|
40
|
+
}
|
|
37
41
|
if (!order.length) order = [""];
|
|
38
42
|
const canFallback = order.length > 1;
|
|
39
43
|
const canForwardPeers = Boolean(peers) && hops < (maxHops ?? 3);
|
|
@@ -2,6 +2,7 @@ import { joinUrl } from "../base.js";
|
|
|
2
2
|
import { isAuthError, isInsufficientStatus } from "./auth.js";
|
|
3
3
|
import { appendRotationLog as defaultAppend } from "./rotation-log.js";
|
|
4
4
|
import { createTransport } from "../../transport/index.js";
|
|
5
|
+
import { reshapeWorkbuddySse } from "./reshape.js";
|
|
5
6
|
|
|
6
7
|
function buildAuthHeaders(key, auth) {
|
|
7
8
|
const h = {
|
|
@@ -86,7 +87,7 @@ export function createChatService({
|
|
|
86
87
|
let res;
|
|
87
88
|
try { res = await fetchOnce(url, body, key, auth); }
|
|
88
89
|
catch (e) { throw e; }
|
|
89
|
-
if (res.status < 400) return res;
|
|
90
|
+
if (res.status < 400) return reshapeWorkbuddySse(res);
|
|
90
91
|
let txt = "";
|
|
91
92
|
try { txt = await res.text(); } catch {}
|
|
92
93
|
if (!isAuthError(res.status, txt)) return res;
|
|
@@ -105,7 +106,7 @@ export function createChatService({
|
|
|
105
106
|
if (stillAuth || res2.status === 429 || res2.status >= 500) try { ring.onError(newKey); } catch {}
|
|
106
107
|
doLog({ uid: uid2, model: _modelForLog(), totalMs: Math.round(nowMs(clock) - _t0()), balanceHit: false, error: stillAuth ? `still auth ${res2.status}` : undefined });
|
|
107
108
|
if (stillAuth) throw failErr(`workbuddy auth still failing after refresh for ${uid2}: ${stillTxt.slice(0, 120)}`, _t0(), clock);
|
|
108
|
-
return res2;
|
|
109
|
+
return reshapeWorkbuddySse(res2);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
let _t0Val = 0;
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
const { done, value } = await reader.read();
|
|
111
|
+
if (done) {
|
|
112
|
+
closed = true;
|
|
113
|
+
const out = [];
|
|
114
|
+
if (buf) { processInto(buf + "\n", out); buf = ""; }
|
|
115
|
+
flushReasoningInto(out);
|
|
116
|
+
if (out.length) controller.enqueue(encoder.encode(out.join("")));
|
|
117
|
+
controller.close();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
buf += decoder.decode(value, { stream: true });
|
|
121
|
+
const idx = buf.lastIndexOf("\n");
|
|
122
|
+
if (idx < 0) return;
|
|
123
|
+
const complete = buf.slice(0, idx + 1);
|
|
124
|
+
buf = buf.slice(idx + 1);
|
|
125
|
+
const out = [];
|
|
126
|
+
processInto(complete, out);
|
|
127
|
+
if (out.length) controller.enqueue(encoder.encode(out.join("")));
|
|
128
|
+
} catch {
|
|
129
|
+
closed = true;
|
|
130
|
+
try { controller.close(); } catch {}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
cancel() {
|
|
134
|
+
closed = true;
|
|
135
|
+
try { reader.cancel(); } catch {}
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const out = new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
140
|
+
try { out._t = res._t; } catch {}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
package/src/routes/hedge.js
CHANGED
|
@@ -20,6 +20,8 @@ function shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers, mod
|
|
|
20
20
|
if (!d || d <= 0) return false;
|
|
21
21
|
// deepseek = 本机私有凭据供应商(组员节点没有凭据),对冲必败且 both-fail 会误杀本地慢首块流(reasoner 思考 3-30s),不走组员
|
|
22
22
|
if (String(model || "").startsWith("deepseek/")) return false;
|
|
23
|
+
// muse-spark 走 /responses 流式(event: 包装 + 加密 reasoning),组员旧版无此整形且聚合 JSON 带错 header,必抢赢本地慢首块,需本地直出
|
|
24
|
+
if (String(model || "").toLowerCase().startsWith("muse-spark")) return false;
|
|
23
25
|
return true;
|
|
24
26
|
}
|
|
25
27
|
|
package/src/routes/stream.js
CHANGED
|
@@ -31,7 +31,8 @@ export const MAX_STREAM_MS = (() => {
|
|
|
31
31
|
export async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort, streamTimeoutMs = STREAM_TIMEOUT_MS, fallback } = {}) {
|
|
32
32
|
const t0 = performance.now();
|
|
33
33
|
const contentType = upRes.headers.get("content-type") || "";
|
|
34
|
-
|
|
34
|
+
// 需同时满足:客户端要流 + 上游真的是 SSE;避免 muse-spark 聚合 JSON 被误判为流式,或 workbuddy SSE 被聚合
|
|
35
|
+
const isStream = Boolean(body?.stream) && contentType.includes("text/event-stream");
|
|
35
36
|
res.statusCode = upRes.status;
|
|
36
37
|
// propagate workbuddy uid / allowlist headers
|
|
37
38
|
try {
|
|
@@ -17,7 +17,7 @@ export function chatToResponsesBody(chatBody) {
|
|
|
17
17
|
return `${m.role}: ${String(c || "")}`;
|
|
18
18
|
});
|
|
19
19
|
const input = inputParts.join("\n\n") || "hi";
|
|
20
|
-
const out = { model: chatBody.model, input, stream: false };
|
|
20
|
+
const out = { model: chatBody.model, input, stream: chatBody.stream !== false };
|
|
21
21
|
if (system) out.instructions = system;
|
|
22
22
|
if (chatBody.tools) out.tools = chatBody.tools;
|
|
23
23
|
if (chatBody.tool_choice) out.tool_choice = chatBody.tool_choice;
|
|
@@ -62,3 +62,120 @@ export function toChatResponse(res, respJson) {
|
|
|
62
62
|
headers.set("content-type", "application/json");
|
|
63
63
|
return new Response(JSON.stringify(chatJson), { status: res.status, headers });
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
export function reshapeResponsesSse(res, fallbackModel) {
|
|
67
|
+
try {
|
|
68
|
+
const ct = res.headers?.get?.("content-type") || "";
|
|
69
|
+
if (res.status !== 200 || !ct.includes("text/event-stream") || !res.body) return res;
|
|
70
|
+
} catch { return res; }
|
|
71
|
+
const reader = res.body.getReader();
|
|
72
|
+
const decoder = new TextDecoder();
|
|
73
|
+
const encoder = new TextEncoder();
|
|
74
|
+
let buf = "";
|
|
75
|
+
let evtType = "";
|
|
76
|
+
let respId = "";
|
|
77
|
+
let respModel = fallbackModel || "";
|
|
78
|
+
let created = Math.floor(Date.now() / 1000);
|
|
79
|
+
let hasSentRole = false;
|
|
80
|
+
|
|
81
|
+
function chatChunk(delta, finish) {
|
|
82
|
+
const id = respId || `resp_${Date.now()}`;
|
|
83
|
+
const payload = {
|
|
84
|
+
id,
|
|
85
|
+
object: "chat.completion.chunk",
|
|
86
|
+
created,
|
|
87
|
+
model: respModel,
|
|
88
|
+
choices: [{ index: 0, delta: delta || {}, finish_reason: finish || null }],
|
|
89
|
+
};
|
|
90
|
+
return `data: ${JSON.stringify(payload)}\n\n`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let closed = false;
|
|
94
|
+
const body = new ReadableStream({
|
|
95
|
+
async pull(controller) {
|
|
96
|
+
if (closed) { try { controller.close(); } catch {} return; }
|
|
97
|
+
try {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done) {
|
|
100
|
+
closed = true;
|
|
101
|
+
if (buf.trim()) {
|
|
102
|
+
// 残余缓冲尝试处理
|
|
103
|
+
}
|
|
104
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
105
|
+
controller.close();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
buf += decoder.decode(value, { stream: true });
|
|
109
|
+
let out = "";
|
|
110
|
+
// 按 \n\n 分事件
|
|
111
|
+
while (true) {
|
|
112
|
+
const sep = buf.indexOf("\n\n");
|
|
113
|
+
if (sep < 0) break;
|
|
114
|
+
const raw = buf.slice(0, sep);
|
|
115
|
+
buf = buf.slice(sep + 2);
|
|
116
|
+
const lines = raw.split("\n");
|
|
117
|
+
let curEvent = evtType;
|
|
118
|
+
let dataStr = "";
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
if (line.startsWith("event:")) curEvent = line.slice(6).trim();
|
|
121
|
+
else if (line.startsWith("data:")) dataStr += line.slice(5).trim();
|
|
122
|
+
}
|
|
123
|
+
if (!dataStr) { evtType = ""; continue; }
|
|
124
|
+
evtType = "";
|
|
125
|
+
let data;
|
|
126
|
+
try { data = JSON.parse(dataStr); } catch { continue; }
|
|
127
|
+
// 记录 id/model/created
|
|
128
|
+
if (data.response?.id) respId = data.response.id;
|
|
129
|
+
if (data.response?.model) respModel = data.response.model;
|
|
130
|
+
if (data.response?.created_at) created = Math.floor(data.response.created_at);
|
|
131
|
+
if (data.response?.id && !respId) respId = data.response.id;
|
|
132
|
+
// 关注 output_text.delta
|
|
133
|
+
if (curEvent === "response.output_text.delta" || data.type === "response.output_text.delta") {
|
|
134
|
+
const deltaText = data.delta || "";
|
|
135
|
+
if (deltaText) {
|
|
136
|
+
if (!hasSentRole) {
|
|
137
|
+
hasSentRole = true;
|
|
138
|
+
out += chatChunk({ role: "assistant" }, null);
|
|
139
|
+
}
|
|
140
|
+
out += chatChunk({ content: deltaText }, null);
|
|
141
|
+
}
|
|
142
|
+
} else if (curEvent === "response.completed" || data.type === "response.completed") {
|
|
143
|
+
const usage = data.response?.usage || null;
|
|
144
|
+
const finish = data.response?.status === "completed" ? "stop" : null;
|
|
145
|
+
// 末帧带 usage
|
|
146
|
+
const id = respId || `resp_${Date.now()}`;
|
|
147
|
+
const payload = {
|
|
148
|
+
id,
|
|
149
|
+
object: "chat.completion.chunk",
|
|
150
|
+
created,
|
|
151
|
+
model: respModel,
|
|
152
|
+
choices: [{ index: 0, delta: {}, finish_reason: finish }],
|
|
153
|
+
usage: usage || undefined,
|
|
154
|
+
};
|
|
155
|
+
out += `data: ${JSON.stringify(payload)}\n\n`;
|
|
156
|
+
} else if (data.type === "response.output_item.added" && data.item?.type === "message") {
|
|
157
|
+
// message 开始,可发送 role
|
|
158
|
+
if (!hasSentRole) {
|
|
159
|
+
hasSentRole = true;
|
|
160
|
+
out += chatChunk({ role: "assistant" }, null);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// reasoning 加密块忽略
|
|
164
|
+
}
|
|
165
|
+
if (out) controller.enqueue(encoder.encode(out));
|
|
166
|
+
} catch {
|
|
167
|
+
closed = true;
|
|
168
|
+
try { controller.close(); } catch {}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
cancel() {
|
|
172
|
+
closed = true;
|
|
173
|
+
try { reader.cancel(); } catch {}
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const headers = new Headers(res.headers);
|
|
177
|
+
headers.set("content-type", "text/event-stream");
|
|
178
|
+
const out = new Response(body, { status: res.status, statusText: res.statusText, headers });
|
|
179
|
+
try { out._t = res._t; } catch {}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
package/src/upstream.js
CHANGED
|
@@ -6,7 +6,7 @@ import { performance } from "node:perf_hooks";
|
|
|
6
6
|
import { isFreeModel } from "./models.js";
|
|
7
7
|
import { fmtShanghaiYMDHMS } from "./time.js";
|
|
8
8
|
import { createTransport } from "./transport/index.js";
|
|
9
|
-
import { isResponsesModel, chatToResponsesBody, toChatResponse } from "./upstream-responses.js";
|
|
9
|
+
import { isResponsesModel, chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "./upstream-responses.js";
|
|
10
10
|
import { uuid } from "./compat.js";
|
|
11
11
|
|
|
12
12
|
function genId(prefix) {
|
|
@@ -110,7 +110,7 @@ export function createUpstreamClient({
|
|
|
110
110
|
const reqBody = isResp ? chatToResponsesBody(body) : body;
|
|
111
111
|
const t0 = performance.now();
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
// 首发请求(transport 已处理 network/429 等重试)
|
|
114
114
|
let res;
|
|
115
115
|
try {
|
|
116
116
|
res = await transport.request({
|
|
@@ -151,15 +151,21 @@ export function createUpstreamClient({
|
|
|
151
151
|
// responses 模型需转回 chat 形状(复用 upstream-responses)
|
|
152
152
|
let outAnon = anonRes;
|
|
153
153
|
if (isResp && anonRes.ok) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
154
|
+
const ctAnon = anonRes.headers.get("content-type") || "";
|
|
155
|
+
const isStreamAnon = body?.stream !== false && ctAnon.includes("text/event-stream");
|
|
156
|
+
if (isStreamAnon) {
|
|
157
|
+
outAnon = reshapeResponsesSse(anonRes, body.model);
|
|
158
|
+
} else {
|
|
159
|
+
try {
|
|
160
|
+
const txt = await anonRes.text();
|
|
161
|
+
const j = JSON.parse(txt);
|
|
162
|
+
if (j && Array.isArray(j.output)) {
|
|
163
|
+
outAnon = toChatResponse(anonRes, j);
|
|
164
|
+
} else {
|
|
165
|
+
outAnon = new Response(txt, { status: anonRes.status, headers: anonRes.headers });
|
|
166
|
+
}
|
|
167
|
+
} catch { outAnon = anonRes; }
|
|
168
|
+
}
|
|
163
169
|
}
|
|
164
170
|
outAnon._t = { ...(outAnon._t || {}), anonTried: true, anonAttempts: i + 1, totalMs: Math.round(performance.now() - t0) };
|
|
165
171
|
consecutiveHits += 1;
|
|
@@ -180,6 +186,13 @@ export function createUpstreamClient({
|
|
|
180
186
|
|
|
181
187
|
// responses 模型成功态转 chat(复用 upstream-responses)
|
|
182
188
|
if (isResp && res.ok) {
|
|
189
|
+
const ct = res.headers.get("content-type") || "";
|
|
190
|
+
const isStream = body?.stream !== false && ct.includes("text/event-stream");
|
|
191
|
+
if (isStream) {
|
|
192
|
+
const transformed = reshapeResponsesSse(res, body.model);
|
|
193
|
+
transformed._t = { ...(res._t || {}), totalMs: Math.round(performance.now() - t0) };
|
|
194
|
+
return transformed;
|
|
195
|
+
}
|
|
183
196
|
try {
|
|
184
197
|
const txt = await res.text();
|
|
185
198
|
const j = JSON.parse(txt);
|