mslxdff 0.1.116 → 0.1.118
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
CHANGED
|
@@ -4,6 +4,7 @@ import { appendRotationLog as defaultAppend } from "./rotation-log.js";
|
|
|
4
4
|
import { createTransport } from "../../transport/index.js";
|
|
5
5
|
import { reshapeWorkbuddySse } from "./reshape.js";
|
|
6
6
|
import { attemptOnceSdk } from "./sdk-chat.js";
|
|
7
|
+
import { sanitizeToolSequence } from "./sanitize-tools.js";
|
|
7
8
|
import { resolveEngineMode } from "../../upstream-engine/mode.js";
|
|
8
9
|
|
|
9
10
|
function buildAuthHeaders(key, auth) {
|
|
@@ -35,6 +36,19 @@ function withUidHeader(res, uid) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
// 400 判型需读 body 文本;读过的 Response body 不可再读(下游转发报 "Body is unusable"),
|
|
40
|
+
// 用文本重建一个等价响应(去掉 content-length 防长度不符)。
|
|
41
|
+
function rewrapBody(res, txt) {
|
|
42
|
+
try { console.error(`[workbuddy] upstream ${res.status} body: ${String(txt).slice(0, 300)}`); } catch {}
|
|
43
|
+
try {
|
|
44
|
+
const h = new Headers(res.headers);
|
|
45
|
+
h.delete("content-length");
|
|
46
|
+
const r = new Response(txt, { status: res.status, statusText: res.statusText, headers: h });
|
|
47
|
+
try { r._t = res._t; } catch {}
|
|
48
|
+
return r;
|
|
49
|
+
} catch { return res; }
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
function nowMs(clock) { return typeof performance !== "undefined" && performance.now ? performance.now() : clock(); }
|
|
39
53
|
|
|
40
54
|
function errRes(status, msg, reason, uid, t0, clock) {
|
|
@@ -109,7 +123,7 @@ export function createChatService({
|
|
|
109
123
|
if (res.status < 400) return reshapeWorkbuddySse(res);
|
|
110
124
|
let txt = "";
|
|
111
125
|
try { txt = await res.text(); } catch {}
|
|
112
|
-
if (!isAuthError(res.status, txt)) return res;
|
|
126
|
+
if (!isAuthError(res.status, txt)) return rewrapBody(res, txt);
|
|
113
127
|
const newKey = await authService?.refreshTokenFor?.(key, auth);
|
|
114
128
|
if (!newKey) throw failErr(`workbuddy refresh failed for ${auth?.uid || ""}: ${txt.slice(0, 120)}`, _t0(), clock);
|
|
115
129
|
const auth2 = authForKey(newKey);
|
|
@@ -118,6 +132,7 @@ export function createChatService({
|
|
|
118
132
|
catch (e2) { throw e2; }
|
|
119
133
|
let stillTxt = "";
|
|
120
134
|
if (res2.status >= 400) { try { stillTxt = await res2.text(); } catch {} }
|
|
135
|
+
if (stillTxt) res2 = rewrapBody(res2, stillTxt);
|
|
121
136
|
const stillAuth = isAuthError(res2.status, stillTxt);
|
|
122
137
|
const uid2 = auth2.uid || auth.uid;
|
|
123
138
|
res2 = withUidHeader(res2, uid2);
|
|
@@ -136,6 +151,11 @@ export function createChatService({
|
|
|
136
151
|
async function runChat(body, activeRing, opts = {}) {
|
|
137
152
|
const url = joinUrl(baseUrl, chatPath);
|
|
138
153
|
const t0 = nowMs(clock);
|
|
154
|
+
const clean = sanitizeToolSequence(body?.messages);
|
|
155
|
+
if (clean.droppedCalls || clean.droppedResults) {
|
|
156
|
+
try { console.error(`[workbuddy] tool-sequence sanitized: dropped ${clean.droppedCalls} call(s), ${clean.droppedResults} result(s), model=${body?.model || ""}`); } catch {}
|
|
157
|
+
body = { ...body, messages: clean.messages };
|
|
158
|
+
}
|
|
139
159
|
_t0Val = t0;
|
|
140
160
|
_modelVal = body?.model || "";
|
|
141
161
|
const preferredUid = opts?.workbuddyUid ? String(opts.workbuddyUid).trim() : "";
|
|
@@ -203,6 +223,7 @@ export function createChatService({
|
|
|
203
223
|
lastErr = failErr(`workbuddy insufficient for ${uid}: ${txt.slice(0, 120)}`, t0, clock);
|
|
204
224
|
continue;
|
|
205
225
|
}
|
|
226
|
+
res = rewrapBody(res, txt);
|
|
206
227
|
}
|
|
207
228
|
if (res.status === 401 || res.status === 403 || res.status === 429 || res.status >= 500) try { activeRing.onError(key); } catch {}
|
|
208
229
|
res = withUidHeader(res, uid);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// workbuddy 上游对 tool 序列严格校验(400 code 11148 tool_call_sequence_broken):
|
|
2
|
+
// assistant.tool_calls 与后续 tool 结果必须按 id 一一配对,否则整单拒绝并提示开新会话。
|
|
3
|
+
// 历史上下文经压缩/编辑/跨模型切换后可能断裂(有调用无结果、结果孤立、id 漂移),
|
|
4
|
+
// AI SDK 转换层对缺失字段用占位(toolName "tool" / toolCallId "")进一步错配。
|
|
5
|
+
// 这里在 workbuddy 出口做防御性清洗:仅保留能成对出现的调用与结果,避免整单 400。
|
|
6
|
+
// 其他上游(opencode 等)宽容,不受影响——只挂在 workbuddy 链路。
|
|
7
|
+
export function sanitizeToolSequence(messages) {
|
|
8
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
9
|
+
|
|
10
|
+
const callIds = new Set();
|
|
11
|
+
for (const m of list) {
|
|
12
|
+
if (m?.role === "assistant" && Array.isArray(m.tool_calls)) {
|
|
13
|
+
for (const tc of m.tool_calls) {
|
|
14
|
+
const id = String(tc?.id || "");
|
|
15
|
+
if (id) callIds.add(id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const resultIds = new Set();
|
|
21
|
+
for (const m of list) {
|
|
22
|
+
if (m?.role === "tool") {
|
|
23
|
+
const id = String(m.tool_call_id || "");
|
|
24
|
+
if (id) resultIds.add(id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const paired = new Set();
|
|
29
|
+
for (const id of callIds) if (resultIds.has(id)) paired.add(id);
|
|
30
|
+
|
|
31
|
+
let droppedCalls = 0;
|
|
32
|
+
let droppedResults = 0;
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const m of list) {
|
|
35
|
+
if (m?.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
36
|
+
const kept = m.tool_calls.filter((tc) => paired.has(String(tc?.id || "")));
|
|
37
|
+
droppedCalls += m.tool_calls.length - kept.length;
|
|
38
|
+
if (kept.length) {
|
|
39
|
+
out.push({ ...m, tool_calls: kept });
|
|
40
|
+
} else if (typeof m.content === "string" && m.content) {
|
|
41
|
+
const { tool_calls: _dropped, ...rest } = m;
|
|
42
|
+
out.push(rest);
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (m?.role === "tool") {
|
|
47
|
+
if (paired.has(String(m.tool_call_id || ""))) out.push(m);
|
|
48
|
+
else droppedResults++;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
out.push(m);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { messages: out, droppedCalls, droppedResults };
|
|
55
|
+
}
|