mslxdff 0.1.120 → 0.1.121
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
|
@@ -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 { dispatcherFetch } from "../../upstream-engine/sdk/attempt.js";
|
|
5
6
|
import { reshapeWorkbuddySse } from "./reshape.js";
|
|
6
7
|
import { attemptOnceSdk } from "./sdk-chat.js";
|
|
7
8
|
import { sanitizeToolSequence } from "./sanitize-tools.js";
|
|
@@ -88,6 +89,9 @@ export function createChatService({
|
|
|
88
89
|
try { defaultAppend(opts); } catch {}
|
|
89
90
|
};
|
|
90
91
|
const transport = createTransport({ fetchImpl, dispatcher, keepAlive: !!dispatcher, timeoutMs: connectTimeoutMs, retry: {} });
|
|
92
|
+
// SDK 通道与 legacy 同享 keep-alive 连接池(dispatcher → fetch 注入);同时该 fetch 被 attempt 层
|
|
93
|
+
// 包装用于错误路径捕获请求体(上游 4xx 时打印 tool 序列断裂诊断,见 sdk/attempt.js)。
|
|
94
|
+
const sdkFetch = dispatcher ? dispatcherFetch(dispatcher) : fetchImpl;
|
|
91
95
|
|
|
92
96
|
function authForKey(key) {
|
|
93
97
|
const idx = keys.indexOf(key);
|
|
@@ -109,7 +113,7 @@ export function createChatService({
|
|
|
109
113
|
const payload = rewriteWorkbuddyPayload(body);
|
|
110
114
|
if (engineMode === "sdk") {
|
|
111
115
|
try {
|
|
112
|
-
return await attemptOnceSdk({ url, body: payload, key, auth, buildHeaders: buildAuthHeaders });
|
|
116
|
+
return await attemptOnceSdk({ url, body: payload, key, auth, buildHeaders: buildAuthHeaders, ...(sdkFetch ? { fetchImpl: sdkFetch } : {}) });
|
|
113
117
|
} catch (e) {
|
|
114
118
|
if (!e || !e._sdkLoadFailed) throw e;
|
|
115
119
|
if (!sdkFallbackLogged) {
|
|
@@ -157,8 +161,8 @@ export function createChatService({
|
|
|
157
161
|
const url = joinUrl(baseUrl, chatPath);
|
|
158
162
|
const t0 = nowMs(clock);
|
|
159
163
|
const clean = sanitizeToolSequence(body?.messages);
|
|
160
|
-
if (clean.droppedCalls || clean.droppedResults) {
|
|
161
|
-
try { console.error(`[workbuddy] tool-sequence sanitized: dropped ${clean.droppedCalls} call(s), ${clean.droppedResults} result(s), model=${body?.model || ""}`); } catch {}
|
|
164
|
+
if (clean.droppedCalls || clean.droppedResults || clean.movedResults || clean.injectedHead) {
|
|
165
|
+
try { console.error(`[workbuddy] tool-sequence sanitized: dropped ${clean.droppedCalls} call(s), ${clean.droppedResults} result(s), moved ${clean.movedResults} result(s), head+${clean.injectedHead}, model=${body?.model || ""}`); } catch {}
|
|
162
166
|
body = { ...body, messages: clean.messages };
|
|
163
167
|
}
|
|
164
168
|
_t0Val = t0;
|
|
@@ -1,55 +1,61 @@
|
|
|
1
|
-
// workbuddy 上游对 tool 序列严格校验(400 code 11148 tool_call_sequence_broken
|
|
2
|
-
// assistant.tool_calls
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// workbuddy 上游对 tool 序列严格校验(400 code 11148 tool_call_sequence_broken),实测三条规则:
|
|
2
|
+
// ① assistant.tool_calls 与结果必须按 id 一一配对(孤立调用/结果均拒);
|
|
3
|
+
// ② 结果必须紧跟对应 assistant(配对不能被 user/assistant 消息打断);
|
|
4
|
+
// ③ 序列不能以"带 tool_calls 的 assistant"开头(tool call 无前置上下文即拒;compaction 裁剪点
|
|
5
|
+
// 落在工具链中间时最易触发)。
|
|
6
|
+
// 这里在 workbuddy 出口做防御性清洗:结果前移到对应调用之后(组内按 calls 顺序)、剔无法配对的
|
|
7
|
+
// 调用/结果、首条为 assistant/tool 时注入占位 user("continue")。其他上游宽容不受影响——只挂 workbuddy。
|
|
7
8
|
export function sanitizeToolSequence(messages) {
|
|
8
9
|
const list = Array.isArray(messages) ? messages : [];
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
if (m?.role
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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);
|
|
11
|
+
const resultById = new Map();
|
|
12
|
+
list.forEach((m, idx) => {
|
|
13
|
+
if (m?.role !== "tool") return;
|
|
14
|
+
const id = String(m.tool_call_id || "");
|
|
15
|
+
if (id && !resultById.has(id)) resultById.set(id, { msg: m, idx, used: false });
|
|
16
|
+
});
|
|
30
17
|
|
|
31
18
|
let droppedCalls = 0;
|
|
32
19
|
let droppedResults = 0;
|
|
20
|
+
let movedResults = 0;
|
|
33
21
|
const out = [];
|
|
34
|
-
|
|
22
|
+
list.forEach((m, idx) => {
|
|
35
23
|
if (m?.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
36
|
-
const kept =
|
|
37
|
-
|
|
24
|
+
const kept = [];
|
|
25
|
+
for (const tc of m.tool_calls) {
|
|
26
|
+
const r = resultById.get(String(tc?.id || ""));
|
|
27
|
+
if (r && !r.used) kept.push(tc);
|
|
28
|
+
else droppedCalls++;
|
|
29
|
+
}
|
|
38
30
|
if (kept.length) {
|
|
39
31
|
out.push({ ...m, tool_calls: kept });
|
|
32
|
+
for (const tc of kept) {
|
|
33
|
+
const r = resultById.get(String(tc.id));
|
|
34
|
+
r.used = true;
|
|
35
|
+
if (!(r.idx >= idx + 1 && r.idx <= idx + kept.length)) movedResults++;
|
|
36
|
+
out.push(r.msg);
|
|
37
|
+
}
|
|
40
38
|
} else if (typeof m.content === "string" && m.content) {
|
|
41
39
|
const { tool_calls: _dropped, ...rest } = m;
|
|
42
40
|
out.push(rest);
|
|
43
41
|
}
|
|
44
|
-
|
|
42
|
+
return;
|
|
45
43
|
}
|
|
46
44
|
if (m?.role === "tool") {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const id = String(m.tool_call_id || "");
|
|
46
|
+
const r = resultById.get(id);
|
|
47
|
+
if (r && r.idx === idx && r.used) return; // 已前移消费,跳过原位置
|
|
48
|
+
droppedResults++; // 孤儿或重复
|
|
49
|
+
return;
|
|
50
50
|
}
|
|
51
51
|
out.push(m);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let injectedHead = 0;
|
|
55
|
+
if (out.length && out[0]?.role !== "user" && out[0]?.role !== "system" && out[0]?.role !== "developer") {
|
|
56
|
+
out.unshift({ role: "user", content: "continue" });
|
|
57
|
+
injectedHead = 1;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
return { messages: out, droppedCalls, droppedResults };
|
|
60
|
+
return { messages: out, droppedCalls, droppedResults, movedResults, injectedHead };
|
|
55
61
|
}
|
|
@@ -8,7 +8,7 @@ export { sdkBaseFromUrl };
|
|
|
8
8
|
|
|
9
9
|
let channelLogged = false;
|
|
10
10
|
|
|
11
|
-
export async function attemptOnceSdk({ url, body, key, auth, buildHeaders, clock = Date.now } = {}) {
|
|
11
|
+
export async function attemptOnceSdk({ url, body, key, auth, buildHeaders, clock = Date.now, fetchImpl } = {}) {
|
|
12
12
|
const out = await attemptGeneric({
|
|
13
13
|
url,
|
|
14
14
|
body,
|
|
@@ -16,6 +16,7 @@ export async function attemptOnceSdk({ url, body, key, auth, buildHeaders, clock
|
|
|
16
16
|
providerName: "workbuddy",
|
|
17
17
|
marker: { name: "x-mslxdff-workbuddy-channel", value: "sdk" },
|
|
18
18
|
clock,
|
|
19
|
+
...(fetchImpl ? { fetchImpl } : {}),
|
|
19
20
|
});
|
|
20
21
|
if (!channelLogged) {
|
|
21
22
|
channelLogged = true;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// 见 .scratch/ai-sdk-upstream/{SPEC.md,SPEC-p3-responses.md} 与 docs/adr/0017。
|
|
7
7
|
import { toModelPrompt, toModelTools, toModelToolChoice, toModelParams } from "./convert.js";
|
|
8
8
|
import { createSseSerializer } from "./sse.js";
|
|
9
|
+
import { diagnoseToolSequence, compactSequence } from "./diagnose.js";
|
|
9
10
|
import { getUndici } from "../../compat.js";
|
|
10
11
|
|
|
11
12
|
let sdkPromise = null;
|
|
@@ -44,6 +45,20 @@ export function markerHeaders(marker) {
|
|
|
44
45
|
return marker && marker.name ? { [marker.name]: marker.value } : {};
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
// 错误路径诊断:AI SDK 的 APICallError 对 HTTP 4xx 的 requestBodyValues 是空对象(provider-utils 实测),
|
|
49
|
+
// 故用包装 fetch 捕获真实发出的 body,在上游非 2xx 时打印 tool 序列断裂点与尾部序列。
|
|
50
|
+
function logRequestDiagnosis(bodyText, status) {
|
|
51
|
+
if (!bodyText || typeof bodyText !== "string") return;
|
|
52
|
+
let parsed = null;
|
|
53
|
+
try { parsed = JSON.parse(bodyText); } catch { return; }
|
|
54
|
+
const msgs = parsed?.messages;
|
|
55
|
+
if (!Array.isArray(msgs)) return;
|
|
56
|
+
const { issues, summary } = diagnoseToolSequence(msgs);
|
|
57
|
+
console.error(`[sdk-upstream] ${status} ${summary} issues=${issues.length ? issues.join(" | ") : "none"}`);
|
|
58
|
+
console.error(`[sdk-upstream] head: ${compactSequence(msgs, 0, 6)}`);
|
|
59
|
+
console.error(`[sdk-upstream] tail: ${compactSequence(msgs, Math.max(0, msgs.length - 40))}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
// SDK 抛出的 APICallError → 带状态码的 Response;非 HTTP 错误返回 null 由调用方 rethrow。
|
|
48
63
|
export function errorResponseFromSdkError(e, { marker = null } = {}) {
|
|
49
64
|
const status = Number(e?.statusCode ?? e?.status);
|
|
@@ -125,11 +140,19 @@ export async function attemptOnceSdk({
|
|
|
125
140
|
err._sdkLoadFailed = true;
|
|
126
141
|
throw err;
|
|
127
142
|
}
|
|
143
|
+
// 包装注入的 fetch 以捕获真实请求体(诊断用):不改变调用语义,仅在非 2xx 时被读取。
|
|
144
|
+
let lastBodyText = null;
|
|
145
|
+
const capturedFetch = fetchImpl
|
|
146
|
+
? async (u, init) => {
|
|
147
|
+
try { if (init && typeof init.body === "string") lastBodyText = init.body; } catch {}
|
|
148
|
+
return fetchImpl(u, init);
|
|
149
|
+
}
|
|
150
|
+
: undefined;
|
|
128
151
|
const provider = createOpenAICompatible({
|
|
129
152
|
name: providerName,
|
|
130
153
|
baseURL,
|
|
131
154
|
headers: sanitizeHeaders(headers),
|
|
132
|
-
...(
|
|
155
|
+
...(capturedFetch ? { fetch: capturedFetch } : {}),
|
|
133
156
|
});
|
|
134
157
|
const model = provider.chatModel(String(body?.model || ""));
|
|
135
158
|
let res;
|
|
@@ -142,7 +165,10 @@ export async function attemptOnceSdk({
|
|
|
142
165
|
});
|
|
143
166
|
} catch (e) {
|
|
144
167
|
const mapped = errorResponseFromSdkError(e, { marker });
|
|
145
|
-
if (mapped)
|
|
168
|
+
if (mapped) {
|
|
169
|
+
try { logRequestDiagnosis(lastBodyText, mapped.status); } catch {}
|
|
170
|
+
return mapped;
|
|
171
|
+
}
|
|
146
172
|
throw e;
|
|
147
173
|
}
|
|
148
174
|
return streamResponseFromParts(res.stream, { marker, clock, t0 });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// 请求消息序列诊断(纯函数):上游 11148 tool_call_sequence_broken 时定位断裂点。
|
|
2
|
+
// 上游校验规则:assistant.tool_calls 必须与紧随其后的 tool 结果按 id 严格配对,
|
|
3
|
+
// 且配对过程不能被其他消息(user/assistant/system)打断——仅集合级 id 配对(sanitize-tools)不够。
|
|
4
|
+
// 在 SDK 错误路径对 AI SDK 实际发出的请求体做本诊断,异常位置用消息下标指认。
|
|
5
|
+
export function compactSequence(messages, from = 0, limit = Infinity) {
|
|
6
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
7
|
+
const end = Number.isFinite(limit) ? Math.min(list.length, from + limit) : list.length;
|
|
8
|
+
const out = [];
|
|
9
|
+
for (let i = Math.max(0, from); i < end; i++) {
|
|
10
|
+
const m = list[i] || {};
|
|
11
|
+
const role = String(m.role || "?");
|
|
12
|
+
if (role === "assistant") {
|
|
13
|
+
const calls = Array.isArray(m.tool_calls) ? m.tool_calls : [];
|
|
14
|
+
out.push(calls.length ? `A{${calls.map((t) => String(t?.id || "empty")).join(",")}}` : "A");
|
|
15
|
+
} else if (role === "tool") out.push(`T{${String(m.tool_call_id || "empty")}}`);
|
|
16
|
+
else if (role === "user") out.push("U");
|
|
17
|
+
else if (role === "system" || role === "developer") out.push("S");
|
|
18
|
+
else out.push(String(role)[0] || "?");
|
|
19
|
+
}
|
|
20
|
+
return out.join(" ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function diagnoseToolSequence(messages) {
|
|
24
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
25
|
+
const issues = [];
|
|
26
|
+
const open = new Map();
|
|
27
|
+
let calls = 0;
|
|
28
|
+
let results = 0;
|
|
29
|
+
const first = list[0];
|
|
30
|
+
if (first && first.role !== "user" && first.role !== "system" && first.role !== "developer") {
|
|
31
|
+
issues.push(`#0 首条为 ${String(first.role || "?")}(上游拒带工具链的 assistant/tool 开头)`);
|
|
32
|
+
}
|
|
33
|
+
list.forEach((m, i) => {
|
|
34
|
+
const role = m?.role;
|
|
35
|
+
if (role === "assistant") {
|
|
36
|
+
const tcs = Array.isArray(m.tool_calls) ? m.tool_calls : [];
|
|
37
|
+
if (tcs.length) {
|
|
38
|
+
if (open.size) issues.push(`#${i} assistant 打断未闭合{${[...open.keys()].join(",")}}`);
|
|
39
|
+
for (const tc of tcs) {
|
|
40
|
+
const id = String(tc?.id || "");
|
|
41
|
+
calls++;
|
|
42
|
+
if (!id) issues.push(`#${i} tool_call 空 id`);
|
|
43
|
+
else if (open.has(id)) issues.push(`#${i} call id 重复:${id}`);
|
|
44
|
+
else open.set(id, i);
|
|
45
|
+
}
|
|
46
|
+
} else if (open.size) {
|
|
47
|
+
issues.push(`#${i} assistant(无调用) 打断{${[...open.keys()].join(",")}}`);
|
|
48
|
+
}
|
|
49
|
+
} else if (role === "tool") {
|
|
50
|
+
results++;
|
|
51
|
+
const id = String(m.tool_call_id || "");
|
|
52
|
+
if (!id) issues.push(`#${i} tool 结果空 id`);
|
|
53
|
+
else if (!open.has(id)) issues.push(`#${i} 孤立结果:${id}`);
|
|
54
|
+
else open.delete(id);
|
|
55
|
+
} else if (open.size) {
|
|
56
|
+
issues.push(`#${i} ${role} 打断{${[...open.keys()].join(",")}}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
for (const [id, at] of open) issues.push(`#${at} 悬空调用:${id}`);
|
|
60
|
+
return { issues, summary: `msgs=${list.length} calls=${calls} results=${results}` };
|
|
61
|
+
}
|