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.
- package/README.md +10 -5
- package/lib/index.js +68 -18
- 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
|
-
|
|
284
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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({
|
|
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