mslxdff 0.1.103 → 0.1.105
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/serial-trial.js +15 -5
- package/src/cli/commands/use-group.js +56 -0
- package/src/cli/help.js +2 -0
- package/src/cli/index.js +2 -0
- package/src/state/facade.js +1 -0
- package/src/state/schemas/use-group.js +56 -0
- package/src/upstream-responses.js +110 -44
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import { handlePeerRelay } from "../routes/chat/peer-handler.js";
|
|
|
9
9
|
import { handleBroadbandRelay } from "../routes/chat/broadband-handler.js";
|
|
10
10
|
import { handleViaRoute } from "../routes/chat/via-route-handler.js";
|
|
11
11
|
import { handleExhaustedLocal, handleExhaustedAll } from "../routes/chat/exhausted-handler.js";
|
|
12
|
+
import { shouldUseGroupForModel } from "../state/schemas/use-group.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* 串行 trial — 从 engine.js 抽出的第二段:via-route 单路径 → 串行 trial →
|
|
@@ -112,7 +113,8 @@ export async function runSerialTrial(ctx, deps = {}) {
|
|
|
112
113
|
const isStream = Boolean(body.stream);
|
|
113
114
|
const d = hedgeDelayMs();
|
|
114
115
|
const hasPeers = Boolean(peers) && peers.ordered().length > 0;
|
|
115
|
-
const
|
|
116
|
+
const canUseGroup = shouldUseGroupForModel(model);
|
|
117
|
+
const doHedge = canUseGroup && shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers, model }) && upRes.status === 200 && upRes.body;
|
|
116
118
|
if (doHedge) {
|
|
117
119
|
const hr = await hedge({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res, hedgeDelayMs: d });
|
|
118
120
|
if (hr.handled) return { done: true };
|
|
@@ -128,12 +130,20 @@ export async function runSerialTrial(ctx, deps = {}) {
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
if (canForwardPeers) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
if (!shouldUseGroupForModel(model)) {
|
|
134
|
+
evt("group-skip", { reqId, model, reason: "useGroup=off for opencode (peer)" });
|
|
135
|
+
} else {
|
|
136
|
+
const pr = await peerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, stages, startedAt, plugins, res });
|
|
137
|
+
if (pr.handled) return { done: true };
|
|
138
|
+
}
|
|
133
139
|
}
|
|
134
140
|
if (groups) {
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
if (!shouldUseGroupForModel(model)) {
|
|
142
|
+
evt("group-skip", { reqId, model, reason: "useGroup=off for opencode (broadband)" });
|
|
143
|
+
} else {
|
|
144
|
+
const br = await broadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, stages, res, startedAt, plugins });
|
|
145
|
+
if (br.handled) return { done: true };
|
|
146
|
+
}
|
|
137
147
|
}
|
|
138
148
|
if (canFallback) { evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: lastErr?.message || `upstream ${lastErr?.status ?? 502}` }); continue; }
|
|
139
149
|
await exhaustedLocal({ res, body, lastErr, order, handlerCtx: { ...handlerCtx, model, reqId }, evt, logCall, mark, perf0, stages, done, requested, useAuto });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { loadUseGroup, saveUseGroup, getEffectiveUseGroup, getUseGroupEnv } from "../../state/schemas/use-group.js";
|
|
2
|
+
import { argValue } from "../policy.js";
|
|
3
|
+
|
|
4
|
+
function parseInput(v) {
|
|
5
|
+
if (v === undefined || v === null || v === "") return null;
|
|
6
|
+
const s = String(v).trim().toLowerCase();
|
|
7
|
+
if (["1", "true", "on", "yes", "enable", "enabled", "open"].includes(s)) return true;
|
|
8
|
+
if (["0", "false", "off", "no", "disable", "disabled", "close", "closed"].includes(s)) return false;
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function handleUseGroup(args) {
|
|
13
|
+
const hasFlag = args.some((a) => ["-use-group", "--use-group", "-use_group", "--use_group", "-usegroup", "--usegroup"].includes(a));
|
|
14
|
+
if (!hasFlag) return false;
|
|
15
|
+
|
|
16
|
+
// 取值:支持 -use-group true / -use-group=true / --use-group=off
|
|
17
|
+
let raw = argValue(args, "-use-group", "--use-group", "-use_group", "--use_group", "-usegroup", "--usegroup");
|
|
18
|
+
// 兼容 -use-group=true 这种等号形态
|
|
19
|
+
if (raw === null || raw === undefined) {
|
|
20
|
+
const eq = args.find((a) => a.startsWith("-use-group=") || a.startsWith("--use-group=") || a.startsWith("-use_group=") || a.startsWith("--use_group="));
|
|
21
|
+
if (eq) raw = eq.split("=")[1];
|
|
22
|
+
}
|
|
23
|
+
// 如果 flag 后面紧跟的不是另一个 flag,则视为值;否则视为查询
|
|
24
|
+
if (raw !== null && raw !== undefined && String(raw).startsWith("-")) raw = null;
|
|
25
|
+
|
|
26
|
+
const envVal = getUseGroupEnv();
|
|
27
|
+
const effective = getEffectiveUseGroup();
|
|
28
|
+
const stored = loadUseGroup();
|
|
29
|
+
|
|
30
|
+
if (raw === null || raw === undefined || raw === "") {
|
|
31
|
+
// 查询模式
|
|
32
|
+
console.log(`use-group: ${effective ? "on" : "off"} (effective)`);
|
|
33
|
+
console.log(` stored: ${stored ? "on" : "off"} (state.json useGroup)`);
|
|
34
|
+
if (envVal !== null) console.log(` env MSLXDFF_USE_GROUP=${envVal ? "on" : "off"} (overrides stored)`);
|
|
35
|
+
console.log(` default: on`);
|
|
36
|
+
console.log(` usage: mslxdff -use-group on|off (opencode 供应商本机失败时是否走组员网络,默认 on)`);
|
|
37
|
+
console.log(` env: MSLXDFF_USE_GROUP=0|1 (优先级高于 state)`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const parsed = parseInput(raw);
|
|
42
|
+
if (parsed === null) {
|
|
43
|
+
console.error(`invalid value for -use-group: ${raw} (expected on/off/true/false/1/0)`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (envVal !== null) {
|
|
48
|
+
console.log(`note: env MSLXDFF_USE_GROUP=${envVal ? "on" : "off"} is set and overrides stored value; unset env to use stored value`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
saveUseGroup(parsed);
|
|
52
|
+
console.log(`use-group set to ${parsed ? "on" : "off"} (stored in state.json)`);
|
|
53
|
+
console.log(` opencode 供应商:本机失败时 ${parsed ? "允许" : "不再"} 通过组员网络请求上游`);
|
|
54
|
+
if (!parsed) console.log(` 提示:opencode 请求将仅在本机重试,不再走 peer/broadband 组员中继`);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
package/src/cli/help.js
CHANGED
|
@@ -43,6 +43,7 @@ Usage:
|
|
|
43
43
|
mslxdff -autostart status 查看自启状态
|
|
44
44
|
mslxdff -chat ["prompt"] chat REPL(mimo-v2.5-free 优先/big-pickle 兜底,自然语言转命令,模糊匹配由模型完成,历史持久化,超长自动压缩,仅拦 -uninstall,daemon 重启不影响)
|
|
45
45
|
mslxdff -resetban [ip] clear join-failure bans (all, or one ip)
|
|
46
|
+
mslxdff -use-group [on|off] opencode 供应商本机失败时是否走组员网络(默认 on;off 则仅本机,MSLXDFF_USE_GROUP 环境变量可覆盖)
|
|
46
47
|
mslxdff -help show this help
|
|
47
48
|
|
|
48
49
|
Environment:
|
|
@@ -62,6 +63,7 @@ Environment:
|
|
|
62
63
|
MSLXDFF_BAN_THRESHOLD failed joins before an ip is banned (default 5)
|
|
63
64
|
MSLXDFF_BAN_WINDOW_MS ban duration after too many failures (default 48h)
|
|
64
65
|
MSLXDFF_HEDGE_DELAY_MS hedge peer race when local stream first chunk slow (default 1000, 0/off to disable)
|
|
66
|
+
MSLXDFF_USE_GROUP opencode 组员中继开关(默认 on;0/off/false 关闭后 opencode 仅本机,不走 peer/broadband)
|
|
65
67
|
MSLXDFF_AUTO_UPDATE auto-update: hourly by default, 0/off/false to disable, 1/true or ms
|
|
66
68
|
MSLXDFF_AUTO_UPDATE_MS same as above, explicit ms (overrides AUTO_UPDATE)
|
|
67
69
|
`);
|
package/src/cli/index.js
CHANGED
|
@@ -49,6 +49,8 @@ export async function run(args = process.argv.slice(2)) {
|
|
|
49
49
|
if (await handleLeaveGroup(args)) return;
|
|
50
50
|
if (await handleDelGroup(args)) return;
|
|
51
51
|
|
|
52
|
+
const { handleUseGroup } = await import("./commands/use-group.js");
|
|
53
|
+
if (await handleUseGroup(args)) return;
|
|
52
54
|
const { handlePort, handleDaemonFlag, handleBareRun } = await import("./commands/daemon.js");
|
|
53
55
|
if (await handlePort(args)) return;
|
|
54
56
|
if (await handleDaemonFlag(args, VERSION)) return;
|
package/src/state/facade.js
CHANGED
|
@@ -57,3 +57,4 @@ export {
|
|
|
57
57
|
export { loadPeers, savePeers, loadPeerErrors, savePeerErrors, loadPeerStats, savePeerStats } from "./schemas/peer.js";
|
|
58
58
|
export { loadGroups, loadGroupsJoined, saveGroupsJoined, loadBans, saveBans, saveGroups } from "./schemas/group.js";
|
|
59
59
|
export { loadTimezone, loadTimezoneState, saveTimezone, clearTimezone, getTimezoneEnv, DEFAULT_TZ, isValidTimezone } from "./schemas/timezone.js";
|
|
60
|
+
export { loadUseGroup, saveUseGroup, getEffectiveUseGroup, shouldUseGroupForModel, isUseGroupEnabled, getUseGroupEnv } from "./schemas/use-group.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defaultStateFile, readState, writeStateImmediate } from "../store.js";
|
|
2
|
+
|
|
3
|
+
function parseBool(v) {
|
|
4
|
+
if (typeof v === "boolean") return v;
|
|
5
|
+
if (typeof v === "number") return v !== 0;
|
|
6
|
+
if (typeof v === "string") {
|
|
7
|
+
const s = v.trim().toLowerCase();
|
|
8
|
+
if (["0", "false", "off", "no", "disable", "disabled", "close", "closed"].includes(s)) return false;
|
|
9
|
+
if (["1", "true", "on", "yes", "enable", "enabled", "open"].includes(s)) return true;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function loadUseGroup({ file = defaultStateFile() } = {}) {
|
|
15
|
+
const raw = readState(file).useGroup;
|
|
16
|
+
const parsed = parseBool(raw);
|
|
17
|
+
return parsed === null ? true : parsed; // 默认开
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function saveUseGroup(value, { file = defaultStateFile() } = {}) {
|
|
21
|
+
const b = Boolean(value);
|
|
22
|
+
writeStateImmediate(file, { useGroup: b });
|
|
23
|
+
return b;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getUseGroupEnv() {
|
|
27
|
+
const raw = process.env.MSLXDFF_USE_GROUP;
|
|
28
|
+
if (raw === undefined || raw === null || raw === "") return null;
|
|
29
|
+
return parseBool(raw);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getEffectiveUseGroup({ file = defaultStateFile() } = {}) {
|
|
33
|
+
const env = getUseGroupEnv();
|
|
34
|
+
if (env !== null) return env;
|
|
35
|
+
return loadUseGroup({ file });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 仅对 opencode 供应商生效:opencode 的模型为裸 id 或 opencode/ 前缀
|
|
39
|
+
export function shouldUseGroupForModel(model, { file = defaultStateFile() } = {}) {
|
|
40
|
+
const m = String(model || "").trim();
|
|
41
|
+
if (!m) return getEffectiveUseGroup({ file });
|
|
42
|
+
// 带前缀:判断是否为 opencode
|
|
43
|
+
if (m.includes("/")) {
|
|
44
|
+
const head = m.split("/")[0].trim().toLowerCase();
|
|
45
|
+
if (head === "opencode" || head === "oc") {
|
|
46
|
+
return getEffectiveUseGroup({ file });
|
|
47
|
+
}
|
|
48
|
+
return true; // 其他供应商不受此开关限制
|
|
49
|
+
}
|
|
50
|
+
// 裸 id 视为 opencode
|
|
51
|
+
return getEffectiveUseGroup({ file });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isUseGroupEnabled({ file } = {}) {
|
|
55
|
+
return getEffectiveUseGroup({ file });
|
|
56
|
+
}
|
|
@@ -12,9 +12,17 @@ export function chatToResponsesBody(chatBody) {
|
|
|
12
12
|
const nonSystem = msgs.filter((m) => m.role !== "system");
|
|
13
13
|
const inputParts = nonSystem.map((m) => {
|
|
14
14
|
const c = m.content;
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
15
|
+
let base;
|
|
16
|
+
if (typeof c === "string") base = `${m.role}: ${c}`;
|
|
17
|
+
else if (Array.isArray(c)) base = `${m.role}: ${c.map((x) => x.text || x.content || "").join("")}`;
|
|
18
|
+
else base = `${m.role}: ${String(c || "")}`;
|
|
19
|
+
// 保留 tool_calls / tool 结果,避免多轮丢失
|
|
20
|
+
if (Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
21
|
+
const tcStr = m.tool_calls.map((tc) => `${tc.function?.name || "tool"}(${tc.function?.arguments || ""})`).join("; ");
|
|
22
|
+
base += ` [tool_calls: ${tcStr}]`;
|
|
23
|
+
}
|
|
24
|
+
if (m.role === "tool" && m.tool_call_id) base += ` (call_id=${m.tool_call_id})`;
|
|
25
|
+
return base;
|
|
18
26
|
});
|
|
19
27
|
const input = inputParts.join("\n\n") || "hi";
|
|
20
28
|
const out = { model: chatBody.model, input, stream: false };
|
|
@@ -25,25 +33,31 @@ export function chatToResponsesBody(chatBody) {
|
|
|
25
33
|
if (!t || typeof t !== "object") return null;
|
|
26
34
|
if (t.type === "function" && t.function && typeof t.function === "object") {
|
|
27
35
|
const fn = t.function;
|
|
36
|
+
// 去掉 responses 不支持的 strict 等字段,parameters 原样透传
|
|
28
37
|
const nt = { type: "function", name: fn.name, description: fn.description || undefined, parameters: fn.parameters || undefined };
|
|
29
38
|
// 清理 undefined
|
|
30
39
|
Object.keys(nt).forEach((k) => nt[k] === undefined && delete nt[k]);
|
|
31
40
|
return nt.name ? nt : null;
|
|
32
41
|
}
|
|
33
|
-
// 已是平铺形态或未知形态,透传但确保 name
|
|
34
|
-
if (t.name)
|
|
42
|
+
// 已是平铺形态或未知形态,透传但确保 name 存在,清理 strict
|
|
43
|
+
if (t.name) {
|
|
44
|
+
const { strict, ...rest } = t;
|
|
45
|
+
return rest;
|
|
46
|
+
}
|
|
35
47
|
return null;
|
|
36
48
|
}).filter(Boolean);
|
|
37
49
|
if (mapped.length) out.tools = mapped;
|
|
38
50
|
}
|
|
39
51
|
if (chatBody.tool_choice) {
|
|
40
52
|
const tc = chatBody.tool_choice;
|
|
41
|
-
//
|
|
42
|
-
if (typeof tc === "string")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
else out.tool_choice =
|
|
53
|
+
// responses 仅支持 "auto"(实测 required/named 均 400),一律归一为 auto
|
|
54
|
+
if (typeof tc === "string") {
|
|
55
|
+
out.tool_choice = tc === "auto" ? "auto" : "auto";
|
|
56
|
+
} else if (tc && typeof tc === "object") {
|
|
57
|
+
if (tc.type === "auto" || tc.type === "required") out.tool_choice = "auto";
|
|
58
|
+
else if (tc.type === "function") out.tool_choice = "auto";
|
|
59
|
+
else if (tc.type) out.tool_choice = "auto";
|
|
60
|
+
else out.tool_choice = "auto";
|
|
47
61
|
}
|
|
48
62
|
}
|
|
49
63
|
if (chatBody.temperature != null) out.temperature = chatBody.temperature;
|
|
@@ -53,15 +67,22 @@ export function chatToResponsesBody(chatBody) {
|
|
|
53
67
|
|
|
54
68
|
export function responsesToChatJson(respJson) {
|
|
55
69
|
let text = "";
|
|
70
|
+
const toolCalls = [];
|
|
56
71
|
for (const item of respJson.output || []) {
|
|
57
72
|
if (item.type === "message" && item.role === "assistant") {
|
|
58
73
|
for (const c of item.content || []) {
|
|
59
74
|
if (c.type === "output_text") text += c.text || "";
|
|
60
75
|
else if (c.type === "text") text += c.text || "";
|
|
61
76
|
}
|
|
77
|
+
} else if (item.type === "function_call") {
|
|
78
|
+
toolCalls.push({
|
|
79
|
+
id: item.call_id || item.id || `call_${toolCalls.length}`,
|
|
80
|
+
type: "function",
|
|
81
|
+
function: { name: item.name || "", arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments || "") },
|
|
82
|
+
});
|
|
62
83
|
}
|
|
63
84
|
}
|
|
64
|
-
if (!text) {
|
|
85
|
+
if (!text && toolCalls.length === 0) {
|
|
65
86
|
for (const item of respJson.output || []) {
|
|
66
87
|
if (item.type === "message") {
|
|
67
88
|
const t = item.content?.[0]?.text;
|
|
@@ -69,12 +90,18 @@ export function responsesToChatJson(respJson) {
|
|
|
69
90
|
}
|
|
70
91
|
}
|
|
71
92
|
}
|
|
93
|
+
const message = { role: "assistant", content: text };
|
|
94
|
+
if (toolCalls.length) {
|
|
95
|
+
message.tool_calls = toolCalls;
|
|
96
|
+
// 有 tool_calls 时 content 可为 "",finish_reason 应为 tool_calls
|
|
97
|
+
}
|
|
98
|
+
const finish = toolCalls.length ? "tool_calls" : (respJson.status === "completed" ? "stop" : "length");
|
|
72
99
|
const chatJson = {
|
|
73
100
|
id: respJson.id || `resp_${Date.now()}`,
|
|
74
101
|
object: "chat.completion",
|
|
75
102
|
created: Math.floor((respJson.created_at || Date.now() / 1000)),
|
|
76
103
|
model: respJson.model,
|
|
77
|
-
choices: [{ index: 0, finish_reason:
|
|
104
|
+
choices: [{ index: 0, finish_reason: finish, message }],
|
|
78
105
|
usage: respJson.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
79
106
|
};
|
|
80
107
|
return chatJson;
|
|
@@ -102,6 +129,7 @@ export function reshapeResponsesSse(res, fallbackModel) {
|
|
|
102
129
|
let respModel = fallbackModel || "";
|
|
103
130
|
let created = Math.floor(Date.now() / 1000);
|
|
104
131
|
let hasSentRole = false;
|
|
132
|
+
const toolMap = new Map(); // output_index -> {idx, id, name}
|
|
105
133
|
|
|
106
134
|
function chatChunk(delta, finish) {
|
|
107
135
|
const id = respId || `resp_${Date.now()}`;
|
|
@@ -155,37 +183,75 @@ export function reshapeResponsesSse(res, fallbackModel) {
|
|
|
155
183
|
if (data.response?.created_at) created = Math.floor(data.response.created_at);
|
|
156
184
|
if (data.response?.id && !respId) respId = data.response.id;
|
|
157
185
|
// 关注 output_text.delta
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
if (curEvent === "response.output_text.delta" || data.type === "response.output_text.delta") {
|
|
187
|
+
const deltaText = data.delta || "";
|
|
188
|
+
if (deltaText) {
|
|
189
|
+
if (!hasSentRole) {
|
|
190
|
+
hasSentRole = true;
|
|
191
|
+
out += chatChunk({ role: "assistant" }, null);
|
|
192
|
+
}
|
|
193
|
+
out += chatChunk({ content: deltaText }, null);
|
|
194
|
+
}
|
|
195
|
+
} else if (curEvent === "response.completed" || data.type === "response.completed") {
|
|
196
|
+
const usage = data.response?.usage || null;
|
|
197
|
+
// 若有 tool_calls,finish 应为 tool_calls
|
|
198
|
+
const hasTools = toolMap.size > 0;
|
|
199
|
+
const finish = hasTools ? "tool_calls" : (data.response?.status === "completed" ? "stop" : null);
|
|
200
|
+
// 末帧带 usage
|
|
201
|
+
const id = respId || `resp_${Date.now()}`;
|
|
202
|
+
const payload = {
|
|
203
|
+
id,
|
|
204
|
+
object: "chat.completion.chunk",
|
|
205
|
+
created,
|
|
206
|
+
model: respModel,
|
|
207
|
+
choices: [{ index: 0, delta: {}, finish_reason: finish }],
|
|
208
|
+
usage: usage || undefined,
|
|
209
|
+
};
|
|
210
|
+
out += `data: ${JSON.stringify(payload)}\n\n`;
|
|
211
|
+
} else if (data.type === "response.output_item.added" && data.item?.type === "message") {
|
|
212
|
+
// message 开始,可发送 role
|
|
213
|
+
if (!hasSentRole) {
|
|
214
|
+
hasSentRole = true;
|
|
215
|
+
out += chatChunk({ role: "assistant" }, null);
|
|
216
|
+
}
|
|
217
|
+
} else if (data.type === "response.output_item.added" && data.item?.type === "function_call") {
|
|
218
|
+
const outIdx = Number(data.output_index ?? 1);
|
|
219
|
+
const toolIdx = Math.max(0, outIdx - 1);
|
|
220
|
+
const callId = data.item?.call_id || data.item?.id || "";
|
|
221
|
+
const name = data.item?.name || "";
|
|
222
|
+
toolMap.set(outIdx, { idx: toolIdx, id: callId, name });
|
|
223
|
+
if (!hasSentRole) {
|
|
224
|
+
hasSentRole = true;
|
|
225
|
+
out += chatChunk({ role: "assistant" }, null);
|
|
226
|
+
}
|
|
227
|
+
const tc = { index: toolIdx, id: callId, type: "function", function: { name, arguments: "" } };
|
|
228
|
+
// 清理空字符串,避免 undefined
|
|
229
|
+
if (!callId) delete tc.id;
|
|
230
|
+
if (!name) delete tc.function.name;
|
|
231
|
+
out += chatChunk({ tool_calls: [tc] }, null);
|
|
232
|
+
} else if (data.type === "response.function_call_arguments.delta") {
|
|
233
|
+
const outIdx = Number(data.output_index ?? 1);
|
|
234
|
+
const entry = toolMap.get(outIdx) || { idx: Math.max(0, outIdx - 1) };
|
|
235
|
+
const deltaArgs = data.delta || "";
|
|
236
|
+
if (deltaArgs) {
|
|
237
|
+
if (!hasSentRole) {
|
|
238
|
+
hasSentRole = true;
|
|
239
|
+
out += chatChunk({ role: "assistant" }, null);
|
|
240
|
+
}
|
|
241
|
+
out += chatChunk({ tool_calls: [{ index: entry.idx, function: { arguments: deltaArgs } }] }, null);
|
|
242
|
+
}
|
|
243
|
+
} else if (data.type === "response.function_call_arguments.done") {
|
|
244
|
+
const outIdx = Number(data.output_index ?? 1);
|
|
245
|
+
const entry = toolMap.get(outIdx) || { idx: Math.max(0, outIdx - 1) };
|
|
246
|
+
const args = data.arguments || "";
|
|
247
|
+
if (args && !toolMap.get(outIdx)?._done) {
|
|
248
|
+
// done 可能带全量,若未通过 delta 发送过,补发
|
|
249
|
+
// 已通过 delta 流式发送则忽略,避免重复
|
|
250
|
+
}
|
|
251
|
+
} else if (data.type === "response.output_item.done" && data.item?.type === "function_call") {
|
|
252
|
+
// 可忽略,已通过 added+delta 完整
|
|
253
|
+
}
|
|
254
|
+
// reasoning 加密块忽略
|
|
189
255
|
}
|
|
190
256
|
if (out) controller.enqueue(encoder.encode(out));
|
|
191
257
|
} catch {
|