mslxdff 0.1.118 → 0.1.120
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
|
@@ -5,6 +5,7 @@ import { createTransport } from "../../transport/index.js";
|
|
|
5
5
|
import { reshapeWorkbuddySse } from "./reshape.js";
|
|
6
6
|
import { attemptOnceSdk } from "./sdk-chat.js";
|
|
7
7
|
import { sanitizeToolSequence } from "./sanitize-tools.js";
|
|
8
|
+
import { rewriteWorkbuddyPayload } from "./payload.js";
|
|
8
9
|
import { resolveEngineMode } from "../../upstream-engine/mode.js";
|
|
9
10
|
|
|
10
11
|
function buildAuthHeaders(key, auth) {
|
|
@@ -99,12 +100,16 @@ export function createChatService({
|
|
|
99
100
|
// (局部 MSLXDFF_WORKBUDDY_SDK,未设则继承全局 MSLXDFF_UPSTREAM_ENGINE)回退原生 transport;
|
|
100
101
|
// 不可用(Node16/未安装)自动回退并告警一次。上层轮换/刷新/reshape 全链复用。
|
|
101
102
|
// Note: 为什么默认 SDK、翻译层代价与回退语义 — 见 .agents/notes/implemented/feature/2026-09-12-workbuddy-sdk-channel.md
|
|
102
|
-
|
|
103
|
+
// 上游非标字段(thinking / reasoning_effort / reasoning_content / tool_choice 字符串)不依赖 SDK 白名单:
|
|
104
|
+
// payload.js 前置语义改写 + AI SDK providerOptions 透传(非白名单 key 原样 spread 进 body,2.0.41 实测)。
|
|
105
|
+
// 序列化/流式/错误映射仍全部交 AI SDK;原生 transport 仅作 SDK 不可用时的回退。v0.1.120 实测见 .scratch/probe-sdk-body.mjs。
|
|
106
|
+
const engineMode = resolveEngineMode(process.env, "MSLXDFF_WORKBUDDY_SDK", "sdk");
|
|
103
107
|
let sdkFallbackLogged = false;
|
|
104
108
|
async function fetchOnce(url, body, key, auth) {
|
|
109
|
+
const payload = rewriteWorkbuddyPayload(body);
|
|
105
110
|
if (engineMode === "sdk") {
|
|
106
111
|
try {
|
|
107
|
-
return await attemptOnceSdk({ url, body, key, auth, buildHeaders: buildAuthHeaders });
|
|
112
|
+
return await attemptOnceSdk({ url, body: payload, key, auth, buildHeaders: buildAuthHeaders });
|
|
108
113
|
} catch (e) {
|
|
109
114
|
if (!e || !e._sdkLoadFailed) throw e;
|
|
110
115
|
if (!sdkFallbackLogged) {
|
|
@@ -113,7 +118,7 @@ export function createChatService({
|
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
|
-
return transport.request({ url, headers: buildAuthHeaders(key, auth), body: { ...
|
|
121
|
+
return transport.request({ url, headers: buildAuthHeaders(key, auth), body: { ...payload, stream: true }, stream: true });
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
async function withRefresh(url, body, key, auth) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// workbuddy 出站 payload 改写(逆向官方客户端行为,参考 Sliverkiss/workbuddy2api 的 payload.go/thinking.go):
|
|
2
|
+
// 1. developer → system:上游 role 白名单不含 developer(命中 400 code=11128)
|
|
3
|
+
// 2. tool_choice 归一:上游该字段是 string 类型,对象形式 400 code=11101(none 连带删 tools/functions)
|
|
4
|
+
// 3. DeepSeek thinking 注入:必须显式 thinking.type=enabled + 有 effort 档位,否则上游按不思考应答;
|
|
5
|
+
// 显式 disabled 尊重并删 effort;缺档补默认 "high"
|
|
6
|
+
// 4. DeepSeek reasoning_content 回填:会话内任一 assistant 带 reasoning 痕迹时,
|
|
7
|
+
// 所有 assistant 必须带 reasoning_content(string,可空串)——requiresReasoningContentOnAssistantMessages
|
|
8
|
+
const DEEPSEEK_PREFIX = "deepseek";
|
|
9
|
+
const DEFAULT_DEEPSEEK_EFFORT = "high";
|
|
10
|
+
|
|
11
|
+
export function isDeepSeekModel(model) {
|
|
12
|
+
return String(model || "").trim().toLowerCase().startsWith(DEEPSEEK_PREFIX);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeRoles(obj) {
|
|
16
|
+
const msgs = obj?.messages;
|
|
17
|
+
if (!Array.isArray(msgs)) return obj;
|
|
18
|
+
for (const m of msgs) {
|
|
19
|
+
if (m && typeof m === "object" && typeof m.role === "string" && m.role.trim().toLowerCase() === "developer") {
|
|
20
|
+
m.role = "system";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return obj;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizeToolChoice(obj) {
|
|
27
|
+
const tc = obj?.tool_choice;
|
|
28
|
+
if (tc === undefined || tc === null) return obj;
|
|
29
|
+
const dropTools = () => { delete obj.tools; delete obj.functions; };
|
|
30
|
+
if (typeof tc === "string") {
|
|
31
|
+
if (tc.trim().toLowerCase() === "none") { delete obj.tool_choice; dropTools(); }
|
|
32
|
+
return obj;
|
|
33
|
+
}
|
|
34
|
+
if (typeof tc === "object") {
|
|
35
|
+
const type = String(tc.type || "").trim().toLowerCase();
|
|
36
|
+
if (type === "none") { delete obj.tool_choice; dropTools(); }
|
|
37
|
+
else if (type === "auto" || type === "required") obj.tool_choice = type;
|
|
38
|
+
else if (type === "function") {
|
|
39
|
+
const name = String(tc?.function?.name || tc?.name || "").trim();
|
|
40
|
+
obj.tool_choice = name || "auto";
|
|
41
|
+
} else delete obj.tool_choice;
|
|
42
|
+
return obj;
|
|
43
|
+
}
|
|
44
|
+
delete obj.tool_choice;
|
|
45
|
+
return obj;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function injectThinking(obj) {
|
|
49
|
+
if (!isDeepSeekModel(obj?.model)) return obj;
|
|
50
|
+
const th = obj.thinking && typeof obj.thinking === "object" ? obj.thinking : null;
|
|
51
|
+
const type = th ? String(th.type || "").trim() : "";
|
|
52
|
+
if (type && type.toLowerCase() === "disabled") {
|
|
53
|
+
delete obj.reasoning_effort;
|
|
54
|
+
delete obj.reasoningEffort;
|
|
55
|
+
return obj;
|
|
56
|
+
}
|
|
57
|
+
if (!type) {
|
|
58
|
+
if (th) th.type = "enabled";
|
|
59
|
+
else obj.thinking = { type: "enabled" };
|
|
60
|
+
}
|
|
61
|
+
if (!("reasoning_effort" in obj) && !("reasoningEffort" in obj)) obj.reasoning_effort = DEFAULT_DEEPSEEK_EFFORT;
|
|
62
|
+
return obj;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function backfillReasoningContent(obj) {
|
|
66
|
+
if (!isDeepSeekModel(obj?.model)) return obj;
|
|
67
|
+
const msgs = obj.messages;
|
|
68
|
+
if (!Array.isArray(msgs) || !msgs.length) return obj;
|
|
69
|
+
let hasTrace = false;
|
|
70
|
+
for (const m of msgs) {
|
|
71
|
+
if (!m || typeof m !== "object") continue;
|
|
72
|
+
if (typeof m.reasoning === "string" && m.reasoning) { hasTrace = true; break; }
|
|
73
|
+
if ("reasoning_content" in m) { hasTrace = true; break; }
|
|
74
|
+
}
|
|
75
|
+
if (!hasTrace) return obj;
|
|
76
|
+
for (const m of msgs) {
|
|
77
|
+
if (!m || m.role !== "assistant") continue;
|
|
78
|
+
if ("reasoning_content" in m) continue;
|
|
79
|
+
m.reasoning_content = typeof m.reasoning === "string" ? m.reasoning : "";
|
|
80
|
+
}
|
|
81
|
+
return obj;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 入口:输入 chat body(OpenAI 格式),返回改写后的新对象(messages 浅拷贝,避免污染调用方)。
|
|
85
|
+
export function rewriteWorkbuddyPayload(body) {
|
|
86
|
+
if (!body || typeof body !== "object") return body;
|
|
87
|
+
const obj = { ...body };
|
|
88
|
+
obj.messages = Array.isArray(body.messages)
|
|
89
|
+
? body.messages.map((m) => (m && typeof m === "object" ? { ...m } : m))
|
|
90
|
+
: body.messages;
|
|
91
|
+
normalizeRoles(obj);
|
|
92
|
+
normalizeToolChoice(obj);
|
|
93
|
+
if (isDeepSeekModel(obj.model)) {
|
|
94
|
+
injectThinking(obj);
|
|
95
|
+
backfillReasoningContent(obj);
|
|
96
|
+
}
|
|
97
|
+
return obj;
|
|
98
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// 引擎模式解析(纯函数,零依赖):缺省 sdk,legacy/关闭词回退原实现。
|
|
2
2
|
// 供应商级开关未设置时继承全局总闸,使 MSLXDFF_UPSTREAM_ENGINE=legacy 成为一键熔断。
|
|
3
|
+
// defaultMode:调用方可自定缺省(一般无需传,保持 sdk);显式 env(供应商级或全局)始终优先。
|
|
3
4
|
// 供 upstream-engine/index.js(opencode 上游)与 providers/workbuddy/chat.js 共用。
|
|
4
5
|
// 见 docs/adr/0017。
|
|
5
6
|
const ENGINE_OFF_WORDS = new Set(["0", "off", "false", "no", "disable", "disabled"]);
|
|
6
7
|
|
|
7
|
-
export function resolveEngineMode(env = process.env, varName = "MSLXDFF_UPSTREAM_ENGINE") {
|
|
8
|
+
export function resolveEngineMode(env = process.env, varName = "MSLXDFF_UPSTREAM_ENGINE", defaultMode = "sdk") {
|
|
8
9
|
const pick = (name) => String(env?.[name] ?? "").trim().toLowerCase();
|
|
9
10
|
let raw = pick(varName);
|
|
10
11
|
if (!raw && varName !== "MSLXDFF_UPSTREAM_ENGINE") raw = pick("MSLXDFF_UPSTREAM_ENGINE");
|
|
11
12
|
if (raw === "legacy" || ENGINE_OFF_WORDS.has(raw)) return "legacy";
|
|
12
|
-
return "sdk";
|
|
13
|
+
if (raw) return "sdk";
|
|
14
|
+
return defaultMode === "legacy" ? "legacy" : "sdk";
|
|
13
15
|
}
|
|
@@ -52,8 +52,9 @@ export function toModelPrompt(messages) {
|
|
|
52
52
|
const parts = [];
|
|
53
53
|
const text = textOf(m.content);
|
|
54
54
|
if (text) parts.push({ type: "text", text });
|
|
55
|
-
// 带加密态的 reasoning items(responses 通道思考跨轮):AI SDK 会转成上游要的 encrypted reasoning item。
|
|
56
55
|
// 无加密态时才退回纯文本 reasoning(chat 通道的 reasoning_content)。
|
|
56
|
+
// 空串是 payload 回填的有意标记(requiresReasoningContentOnAssistantMessages):所有 assistant
|
|
57
|
+
// 必须带该字段——AI SDK 以 length>0 判定是否输出,故用 " " 占位而非 ""。
|
|
57
58
|
const items = Array.isArray(m.reasoning_items) ? m.reasoning_items : [];
|
|
58
59
|
let pushedEncrypted = false;
|
|
59
60
|
for (const r of items) {
|
|
@@ -66,7 +67,9 @@ export function toModelPrompt(messages) {
|
|
|
66
67
|
});
|
|
67
68
|
pushedEncrypted = true;
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
+
const rc = typeof m.reasoning_content === "string" ? m.reasoning_content
|
|
71
|
+
: (typeof m.reasoning === "string" ? m.reasoning : null);
|
|
72
|
+
if (!pushedEncrypted && rc !== null) parts.push({ type: "reasoning", text: rc || " " });
|
|
70
73
|
for (const tc of Array.isArray(m.tool_calls) ? m.tool_calls : []) {
|
|
71
74
|
if (!tc || typeof tc !== "object") continue;
|
|
72
75
|
parts.push({
|