dsh-llm-workbuddy 0.1.7 → 0.1.11

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.
Files changed (3) hide show
  1. package/README.md +10 -5
  2. package/lib/index.js +68 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  1. **模型能力**:Web 界面的模型选择器(composer 模型菜单或 `/model` 命令)会多出
17
17
  一个 **WorkBuddy** 分组,模型(DeepSeek-V4、GLM-5.x、Kimi-K2.x、MiniMax-M3、
18
18
  Hy3、Hunyuan…)随账号可用列表实时同步,点一下即可切换;支持推理的模型还会
19
- 显示**推理等级**选择器(低 / / 高)。
19
+ 显示**推理等级**选择器(Low / Medium / High)。
20
20
  2. **Web 登录状态小组件**:在 Web GUI 右下角常驻一个状态胶囊,**实时显示登录/
21
21
  代理状态**,未登录时一键在新标签页打开 WorkBuddy 登录页,登录完成后自动变绿。
22
22
  无需再回到终端手动跑登录脚本。
@@ -280,8 +280,10 @@ DSH 的 `dsh.client` 机制只要求 `package.json` 里:
280
280
 
281
281
  模型列表默认取插件内置目录;代理可达时改为实时拉取 `/v1/models`(支持
282
282
  `{"models": [...]}` / `{"data": [...]}` 两种返回),未列出的模型 id 仍可原样
283
- 传递。每个模型的推理等级(`reasoningEffort`)默认来自内置目录,代理可达时优先
284
- 沿用目录值(代理 `/v1/models` 当前只上报 `High`,不区分模型)。
283
+ 传递。**内置目录充当白名单**:代理返回的已下架/不可用模型(如 glm-4.6v、
284
+ glm-5.0、minimax-m2.5 等,上游返回 `service info not found`)会被过滤,不显示
285
+ 在 UI 中。每个模型的推理等级(`reasoningEffort`)默认来自内置目录,代理可达时
286
+ 优先沿用目录值(代理 `/v1/models` 当前只上报 `High`,不区分模型)。
285
287
 
286
288
  ---
287
289
 
@@ -302,9 +304,12 @@ DSH 的 `dsh.client` 机制只要求 `package.json` 里:
302
304
 
303
305
  ## 限制
304
306
 
305
- - 当前为纯文本适配器:图片输入会以 `UNSUPPORTED_CONTENT` 拒绝(后续可加)。
307
+ - 图片输入:声明支持图片的模型(`inputModalities: ["text", "image"]`,如 deepseek-v4-pro、
308
+ glm-5.2、kimi-k2.x、hy3 等)可附带图片,适配器会通过 DSH 的附件服务把图片编码为
309
+ `data:<mime>;base64,<bytes>` 以 OpenAI `image_url` 格式透传给代理。若附件服务不可用
310
+ (headless 等无附件场景),图片输入会以 `UNSUPPORTED_CONTENT` 稳定报错。
306
311
  - 推理等级(reasoning effort):支持推理的模型(如 DeepSeek-V4、GLM、Kimi、MiniMax、
307
- Hy3 等)会显示推理等级下拉(低 / / 高),默认值取平台默认强度。`reasoning_effort`
312
+ Hy3 等)会显示推理等级下拉(Low / Medium / High),默认值取平台默认强度。`reasoning_effort`
308
313
  会透传给代理;若某模型平台侧只接受平台默认、忽略该参数,则退化为平台默认强度,不影响出字。
309
314
  - 代理未运行时,模型请求会以 `TRANSPORT` 错误快速失败(连接被拒绝);但状态
310
315
  小组件本身不依赖代理——代理挂了它仍能显示「代理未运行」并允许触发登录。
package/lib/index.js CHANGED
@@ -75,19 +75,15 @@ const DISCOVERY_TIMEOUT_MS = 2_000;
75
75
  const DEFAULT_MODELS = [
76
76
  { id: "deepseek-v4-pro", name: "Deepseek-V4-Pro", contextWindow: 1_000_000, maxTokens: 50_000, inputModalities: ["text", "image"], reasoningEffort: "high" },
77
77
  { id: "deepseek-v4-flash", name: "Deepseek-V4-Flash", contextWindow: 1_000_000, maxTokens: 50_000, inputModalities: ["text", "image"], reasoningEffort: "high" },
78
+ { id: "deepseek-v3-2-volc", name: "DeepSeek-V3.2", contextWindow: 96_000, maxTokens: 32_000, reasoningEffort: "medium" },
78
79
  { id: "glm-5.2", name: "GLM-5.2", contextWindow: 1_000_000, maxTokens: 48_000, inputModalities: ["text", "image"], reasoningEffort: "medium" },
79
80
  { id: "glm-5.1", name: "GLM-5.1", contextWindow: 200_000, maxTokens: 48_000, reasoningEffort: "medium" },
80
- { id: "glm-5.0", name: "GLM-5.0", contextWindow: 200_000, maxTokens: 48_000, reasoningEffort: "high" },
81
- { id: "glm-4.7", name: "GLM-4.7", contextWindow: 200_000, maxTokens: 48_000, reasoningEffort: "high" },
82
- { id: "glm-4.6", name: "GLM-4.6", contextWindow: 168_000, maxTokens: 32_000, reasoningEffort: "high" },
83
81
  { id: "glm-5v-turbo", name: "GLM-5v-Turbo", contextWindow: 200_000, maxTokens: 64_000, inputModalities: ["text", "image"], reasoningEffort: "medium" },
84
82
  { id: "kimi-k3-1", name: "Kimi-K3", contextWindow: 1_000_000, maxTokens: 32_000, inputModalities: ["text", "image"], reasoningEffort: "medium" },
85
83
  { id: "kimi-k2.7", name: "Kimi-K2.7-Code", contextWindow: 256_000, maxTokens: 32_000, inputModalities: ["text", "image"], reasoningEffort: "medium" },
86
84
  { id: "kimi-k2.6", name: "Kimi-K2.6", contextWindow: 256_000, maxTokens: 32_000, inputModalities: ["text", "image"], reasoningEffort: "medium" },
87
85
  { id: "kimi-k2.5", name: "Kimi-K2.5", contextWindow: 164_000, maxTokens: 32_000, inputModalities: ["text", "image"], reasoningEffort: "medium" },
88
- { id: "kimi-k2-thinking", name: "Kimi-K2-Thinking", contextWindow: 164_000, maxTokens: 32_000, reasoningEffort: "medium" },
89
86
  { id: "minimax-m3", name: "MiniMax-M3", contextWindow: 512_000, maxTokens: 128_000, inputModalities: ["text", "image"], reasoningEffort: "medium" },
90
- { id: "minimax-m2.5", name: "MiniMax-M2.5", contextWindow: 200_000, maxTokens: 48_000, reasoningEffort: "medium" },
91
87
  { id: "hy3", name: "Hy3", contextWindow: 192_000, maxTokens: 64_000, inputModalities: ["text", "image"], reasoningEffort: "high" },
92
88
  { id: "hunyuan-2.0-thinking", name: "Hunyuan-2.0-Thinking", contextWindow: 128_000, maxTokens: 24_000, reasoningEffort: "medium" },
93
89
  { id: "hunyuan-chat", name: "Hunyuan-Turbos", contextWindow: 200_000, maxTokens: 8_192 },
@@ -97,9 +93,9 @@ const DEFAULT_MODELS = [
97
93
 
98
94
  /** Selectable reasoning efforts exposed to the harness UI, in display order. */
99
95
  const REASONING_EFFORTS = [
100
- { id: "low", name: "" },
101
- { id: "medium", name: "" },
102
- { id: "high", name: "" },
96
+ { id: "low", name: "Low" },
97
+ { id: "medium", name: "Medium" },
98
+ { id: "high", name: "High" },
103
99
  ];
104
100
 
105
101
  /**
@@ -169,14 +165,32 @@ function serializeAssistant(message) {
169
165
  /**
170
166
  * Serialize the harness conversation into OpenAI chat-completions wire
171
167
  * messages. `tool-result` blocks become standalone `{role: 'tool'}` messages;
172
- * image content is rejected (the initial version is text-only).
168
+ * image blocks are read through the durable attachment service and emitted as
169
+ * OpenAI `image_url` parts (`data:<mime>;base64,<bytes>`), which the
170
+ * workbuddy2api proxy passes through to the upstream platform. When the
171
+ * attachment service is unavailable, image input degrades to the stable
172
+ * `UNSUPPORTED_CONTENT` error.
173
173
  */
174
- function serializeMessages(messages) {
174
+ async function serializeMessages(messages, attachments, signal) {
175
+ const refs = new Map();
176
+ for (const message of messages) collectImageRefs(message.content, refs);
177
+ const requestImages = new Map();
178
+ if (refs.size > 0) {
179
+ if (attachments === undefined) {
180
+ throw new LlmError("WorkBuddy image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
181
+ }
182
+ // Keep images small enough for the upstream platform: the proxy relays the
183
+ // base64 body verbatim, and oversized payloads are dropped/ignored upstream
184
+ // (models reply "no image attached"). Mirror the official adapter budgets.
185
+ const policy = { maxPixels: 2048 * 2048, maxBytes: 1024 * 1024 };
186
+ const ordered = [...refs.values()];
187
+ const prepared = await Promise.all(ordered.map((ref) => attachments.readImageRequest(ref, policy, signal)));
188
+ for (let index = 0; index < ordered.length; index += 1) {
189
+ requestImages.set(ordered[index].attachmentId, prepared[index]);
190
+ }
191
+ }
175
192
  const wire = [];
176
193
  for (const message of messages) {
177
- if (message.content.some((block) => block.type === "image")) {
178
- throw new LlmError("The WorkBuddy adapter does not support image content yet.", "UNSUPPORTED_CONTENT");
179
- }
180
194
  if (message.role === "system") {
181
195
  wire.push({ role: "system", content: flattenText(message.content) });
182
196
  continue;
@@ -187,7 +201,26 @@ function serializeMessages(messages) {
187
201
  }
188
202
  const toolResults = message.content.filter((block) => block.type === "tool-result");
189
203
  const text = flattenText(message.content);
190
- if (text.length > 0 || toolResults.length === 0) wire.push({ role: "user", content: text });
204
+ const images = message.content.filter((block) => block.type === "image");
205
+ if (text.length > 0 || toolResults.length === 0) {
206
+ if (images.length > 0) {
207
+ const parts = [];
208
+ if (text.length > 0) parts.push({ type: "text", text });
209
+ for (const block of images) {
210
+ const version = requestImages.get(block.attachment.attachmentId);
211
+ if (version === undefined) {
212
+ throw new LlmError("WorkBuddy image input missing attachment bytes", "UNSUPPORTED_CONTENT");
213
+ }
214
+ parts.push({
215
+ type: "image_url",
216
+ image_url: { url: `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}` },
217
+ });
218
+ }
219
+ wire.push({ role: "user", content: parts });
220
+ } else {
221
+ wire.push({ role: "user", content: text });
222
+ }
223
+ }
191
224
  for (const result of toolResults) {
192
225
  wire.push({ role: "tool", tool_call_id: result.toolCallId, content: flattenText(result.content) || "(no output)" });
193
226
  }
@@ -195,11 +228,19 @@ function serializeMessages(messages) {
195
228
  return wire;
196
229
  }
197
230
 
231
+ /** Collect image attachment refs from a content block list (recursing into tool results). */
232
+ function collectImageRefs(blocks, refs) {
233
+ for (const block of blocks) {
234
+ if (block.type === "image") refs.set(block.attachment.attachmentId, block.attachment);
235
+ else if (block.type === "tool-result") collectImageRefs(block.content, refs);
236
+ }
237
+ }
238
+
198
239
  /** Build the full wire request. Always streaming with usage reporting on. */
199
- function serializeRequest(options) {
240
+ async function serializeRequest(options, attachments, signal) {
200
241
  const messages = [];
201
242
  if (options.system !== undefined) messages.push({ role: "system", content: options.system });
202
- messages.push(...serializeMessages(options.messages));
243
+ messages.push(...await serializeMessages(options.messages, attachments, signal));
203
244
  const tools = options.tools?.map((tool) => ({
204
245
  type: "function",
205
246
  function: { name: tool.name, description: tool.description, parameters: tool.parameters },
@@ -511,6 +552,11 @@ export class WorkBuddyAdapter extends LlmAdapter {
511
552
  if (seen.has(entry.id)) continue;
512
553
  seen.add(entry.id);
513
554
  const catalog = byId.get(entry.id);
555
+ // Only keep models the static catalog declares. The proxy announces
556
+ // retired models (glm-4.6v, hunyuan-image-v3.0, ...) that upstream
557
+ // rejects with `service info not found`; the catalog doubles as the
558
+ // whitelist so those never reach the UI.
559
+ if (catalog === undefined) continue;
514
560
  const effort = liveReasoningEffort(entry, catalog?.reasoningEffort);
515
561
  const reasoning = modelReasoningInfo(effort);
516
562
  merged.push({
@@ -588,7 +634,8 @@ export class WorkBuddyAdapter extends LlmAdapter {
588
634
  }
589
635
 
590
636
  async *request(options, signal, connection, onComment) {
591
- const body = serializeRequest(options);
637
+ const attachments = this.config.resolveAttachments?.();
638
+ const body = await serializeRequest(options, attachments, signal);
592
639
  const headers = {
593
640
  "content-type": "application/json",
594
641
  "accept": "text/event-stream",
@@ -1053,7 +1100,10 @@ export function apply(ctx, config) {
1053
1100
  }
1054
1101
  };
1055
1102
  options();
1056
- const adapter = new WorkBuddyAdapter({ options });
1103
+ const adapter = new WorkBuddyAdapter({
1104
+ options,
1105
+ resolveAttachments: () => ctx.get("attachments"),
1106
+ });
1057
1107
  ctx.llm.registerConfigurableProviders([{
1058
1108
  provider: PROVIDER,
1059
1109
  displayName: "WorkBuddy",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-llm-workbuddy",
3
- "version": "0.1.7",
3
+ "version": "0.1.11",
4
4
  "description": "WorkBuddy (via the local workbuddy2api proxy) LLM provider adapter for DeepSeek Harness, with a Web login-status widget",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",