mslxdff 0.1.118 → 0.1.119

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.118",
3
+ "version": "0.1.119",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,17 @@ 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
- const engineMode = resolveEngineMode(process.env, "MSLXDFF_WORKBUDDY_SDK");
103
+ // workbuddy 缺省 native transport(legacy):上游对 DeepSeek 有非标字段要求
104
+ // (thinking 注入 / reasoning_content 回填,见 payload.js),AI SDK 序列化层 providerOptions
105
+ // 白名单无法承载这些字段——v0.1.111~118 的 SDK 缺省即 tool 校验 400 的温床。
106
+ // 显式 MSLXDFF_WORKBUDDY_SDK=sdk(或全局 MSLXDFF_UPSTREAM_ENGINE=sdk)仍可切回 SDK 通道。
107
+ const engineMode = resolveEngineMode(process.env, "MSLXDFF_WORKBUDDY_SDK", "legacy");
103
108
  let sdkFallbackLogged = false;
104
109
  async function fetchOnce(url, body, key, auth) {
110
+ const payload = rewriteWorkbuddyPayload(body);
105
111
  if (engineMode === "sdk") {
106
112
  try {
107
- return await attemptOnceSdk({ url, body, key, auth, buildHeaders: buildAuthHeaders });
113
+ return await attemptOnceSdk({ url, body: payload, key, auth, buildHeaders: buildAuthHeaders });
108
114
  } catch (e) {
109
115
  if (!e || !e._sdkLoadFailed) throw e;
110
116
  if (!sdkFallbackLogged) {
@@ -113,7 +119,7 @@ export function createChatService({
113
119
  }
114
120
  }
115
121
  }
116
- return transport.request({ url, headers: buildAuthHeaders(key, auth), body: { ...body, stream: true }, stream: true });
122
+ return transport.request({ url, headers: buildAuthHeaders(key, auth), body: { ...payload, stream: true }, stream: true });
117
123
  }
118
124
 
119
125
  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,16 @@
1
1
  // 引擎模式解析(纯函数,零依赖):缺省 sdk,legacy/关闭词回退原实现。
2
2
  // 供应商级开关未设置时继承全局总闸,使 MSLXDFF_UPSTREAM_ENGINE=legacy 成为一键熔断。
3
+ // defaultMode:调用方可自定缺省(workbuddy 因上游非标字段 thinking/reasoning_content
4
+ // 无法经 AI SDK 序列化层注入,缺省 legacy);显式 env(供应商级或全局)始终优先。
3
5
  // 供 upstream-engine/index.js(opencode 上游)与 providers/workbuddy/chat.js 共用。
4
6
  // 见 docs/adr/0017。
5
7
  const ENGINE_OFF_WORDS = new Set(["0", "off", "false", "no", "disable", "disabled"]);
6
8
 
7
- export function resolveEngineMode(env = process.env, varName = "MSLXDFF_UPSTREAM_ENGINE") {
9
+ export function resolveEngineMode(env = process.env, varName = "MSLXDFF_UPSTREAM_ENGINE", defaultMode = "sdk") {
8
10
  const pick = (name) => String(env?.[name] ?? "").trim().toLowerCase();
9
11
  let raw = pick(varName);
10
12
  if (!raw && varName !== "MSLXDFF_UPSTREAM_ENGINE") raw = pick("MSLXDFF_UPSTREAM_ENGINE");
11
13
  if (raw === "legacy" || ENGINE_OFF_WORDS.has(raw)) return "legacy";
12
- return "sdk";
14
+ if (raw) return "sdk";
15
+ return defaultMode === "legacy" ? "legacy" : "sdk";
13
16
  }