dsh-opencode-go 0.1.3 → 0.1.5
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 +30 -1
- package/docs/verification.md +15 -1
- package/lib/client.js +2 -2
- package/lib/index.js +88 -62
- package/lib/types/conversion/context.d.ts +3 -2
- package/lib/types/conversion/image-offload.d.ts +12 -0
- package/package.json +24 -21
package/README.md
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
## 安装与使用
|
|
11
11
|
|
|
12
|
+
插件 `0.1.5` 起支持 DSH `0.1.5-rc.1`、`0.1.5-rc.2` 和 `0.1.6-alpha.1`。使用 DSH `0.1.5` 的用户可直接升级插件,无需升级 DSH。
|
|
13
|
+
|
|
14
|
+
npm 包通过 `engines.dsh` 明确声明宿主版本要求,并保留 DSH 组件的 peerDependencies。dshmarket 中本插件的安装来源也是该 npm 包,市场会将其安装到选定的 DSH profile。
|
|
15
|
+
|
|
12
16
|
### Web
|
|
13
17
|
|
|
14
18
|
```sh
|
|
@@ -78,6 +82,29 @@ Web 用户可直接在 **设置 → OpenCode Go** 中修改配置。启用开关
|
|
|
78
82
|
|
|
79
83
|
## 常见问题
|
|
80
84
|
|
|
85
|
+
### DSH 0.1.5 安装插件后无法启动
|
|
86
|
+
|
|
87
|
+
插件 `0.1.0`–`0.1.4` 使用了 DSH `0.1.6` 新增的图片接口,在旧版 DSH 上可能出现 `IMAGE_OFFLOAD_REQUIRED_CODE` 导出不存在的启动错误。升级插件到 `0.1.5` 或更新版本后重启即可:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
dsh plugin --profile web add dsh-opencode-go@0.1.5
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Headless 用户将 `web` 换成 `headless`。修复保留了两版宿主的图片处理方式:DSH `0.1.5` 在请求超过图片预算时将最旧图片转成占位文本,DSH `0.1.6` 继续由宿主记录并处理图片卸载。
|
|
94
|
+
|
|
95
|
+
### 新版本是否必须等待一天才能安装
|
|
96
|
+
|
|
97
|
+
npm 本身没有统一的 24 小时安装等待期。DSH 插件命令底层使用 pnpm;pnpm 11 起默认设置 `minimumReleaseAge: 1440`,按包名或 `latest` 安装、升级时可能继续选中发布满一天的旧版本。默认的非严格策略允许上面显式指定 `@0.1.5` 的安装;如果 profile 显式配置了严格的发布时间限制,则仍会被阻止。
|
|
98
|
+
|
|
99
|
+
需要立即安装该修复时,可在对应 profile 的 `pnpm-workspace.yaml`(默认位于 `~/.dsh/profiles/web/`)中,将此版本合并到已有的例外列表,再运行上面的安装命令:
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
minimumReleaseAgeExclude:
|
|
103
|
+
- dsh-opencode-go@0.1.5
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
保留文件中的其他设置和已有例外。此项仅豁免本插件的 `0.1.5`,不改变其他依赖的等待策略。参见 [pnpm 发布时间限制文档](https://pnpm.io/settings/dependency-resolution#minimumreleaseage)。市场目录、兼容性缓存或 npm 镜像也可能晚于官方 registry 更新;页面仍显示旧版本不代表新版尚未发布。
|
|
107
|
+
|
|
81
108
|
|
|
82
109
|
### 提示 `opencode-go` 路由已被占用
|
|
83
110
|
|
|
@@ -89,7 +116,9 @@ Web 用户可直接在 **设置 → OpenCode Go** 中修改配置。启用开关
|
|
|
89
116
|
|
|
90
117
|
网关和在线配置已收录、且使用 Anthropic Messages、OpenAI Chat Completions 或 OpenAI Responses 协议的新模型,在下次读取或刷新列表时即可使用。刷新会绕过已有会话的目录缓存;直接请求尚未缓存的新模型也会立即重新同步。设置页显示完整模型列表。
|
|
91
118
|
|
|
92
|
-
网关已公布 ID
|
|
119
|
+
网关已公布 ID 但尚无有效协议/能力配置的模型会在设置页的发现结果中标注配置暂不可用,暂不进入对话模型选择器,避免一个未配置的模型阻断整个列表;直接调用时会说明原因。配置补齐后,刷新列表即可使用。仅凭模型 ID 无法可靠推断调用方式。上游新增全新协议或协议特例时,仍可能需要适配。
|
|
120
|
+
|
|
121
|
+
模型具有推理能力但没有可调节的推理档位时(如 `union-alpha`),仍可正常选择和使用,只是不显示推理强度选项。
|
|
93
122
|
|
|
94
123
|
在线配置暂时不可达时,优先复用本次运行中成功获取的配置,以 pi-ai 内置配置作为备用。网关目录不可达时,已有请求可以使用上次目录;设置页刷新会显示失败,避免把旧目录误认为最新结果。`refreshMinutes` 只控制已有模型请求的缓存时长,不阻止主动读取列表获取新模型。
|
|
95
124
|
|
package/docs/verification.md
CHANGED
|
@@ -59,4 +59,18 @@ A read-only check against the live OpenCode Go listing and models.dev resolved 3
|
|
|
59
59
|
|
|
60
60
|
## Remaining limits
|
|
61
61
|
|
|
62
|
-
Real paid OpenCode Go completions, Desktop, non-macOS platforms, and
|
|
62
|
+
Real paid OpenCode Go completions, Desktop, non-macOS platforms, and DSH releases outside the versions documented here are not verified. The automated gateway tests preserve the adapter's request and replay semantics but cannot establish account validity or live provider availability.
|
|
63
|
+
|
|
64
|
+
## DSH 0.1.5 compatibility (plugin 0.1.5)
|
|
65
|
+
|
|
66
|
+
Issue #1 reproduced as an ESM import failure before plugin activation: DSH 0.1.5 lacks `IMAGE_OFFLOAD_REQUIRED_CODE`, `requiredImageOffload`, and `projectOffloadedImages`. The regression test failed against both actual published `dsh-llm` 0.1.5 release candidates before the fix.
|
|
67
|
+
|
|
68
|
+
`npm test` builds the distributed artifact and runs `tests/host-compatibility.spec.ts` in fresh Node processes against the published LLM packages `0.1.5-rc.1`, `0.1.5-rc.2`, and `0.1.6-alpha.1`. The two older versions are pinned npm aliases in development dependencies. A resolution hook selects the LLM package for both the plugin and Cordis Loader; it does not mock that package's exports. This focused matrix does not replace full CLI/profile checks.
|
|
69
|
+
|
|
70
|
+
The fixture verifies native ESM loading, Loader activation, model listing, streamed text, image transport, repeated-image byte accounting, nested oldest-image offloading, immutable history, and a second check against encoded image sizes. All external model metadata is replaced by an offline fixture, and completions go to a loopback gateway with a fake credential.
|
|
71
|
+
|
|
72
|
+
The compatibility bridge uses the host's own functions. On 0.1.5 it preserves the stock adapter's two-pass transient projection (estimated bytes before reading images, exact encoded bytes afterward). On 0.1.6 it preserves durable offload marks and the `IMAGE_OFFLOAD_REQUIRED` retry signal; it never silently substitutes legacy offloading on a modern host. The existing conversion suite additionally checks surface-marked images, path descriptions, and unsupported image roles.
|
|
73
|
+
|
|
74
|
+
Validation: 146 tests passed, Host and Client type checks passed, and the package built successfully on Node.js 24.14.1.
|
|
75
|
+
|
|
76
|
+
Full installed checks also passed against separate npm CLI installations whose entire DSH dependency closures were pinned to `0.1.5-rc.1` and `0.1.5-rc.2` respectively (not just the CLI version). The `0.1.5` plugin tarball was installed through `dsh plugin` into each installation's Web and Headless profiles under isolated `DSH_HOME` directories. Both versions passed `verify:installed` for package resolution, catalog, streaming, request headers, and unload; both passed `verify:headless` through the official launcher and loopback gateway. Browser checks confirmed the Web shell and OpenCode Go settings section load, including the API-key field, advanced settings, and enable switch, with no page errors.
|
package/lib/client.js
CHANGED
|
@@ -40,7 +40,7 @@ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id) + "]")
|
|
|
40
40
|
style.textContent = ".BtKYva_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.BtKYva_field+.BtKYva_field{border-top:.5px solid var(--dsw-alias-border-l2)}.BtKYva_head{align-items:center;gap:8px;display:flex}.BtKYva_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.BtKYva_badges{align-items:center;gap:8px;display:inline-flex}.BtKYva_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.BtKYva_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.BtKYva_reset:disabled{cursor:default}.BtKYva_input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.BtKYva_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.BtKYva_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.BtKYva_inputInvalid{border-color:var(--dsw-alias-label-error);}.BtKYva_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.BtKYva_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.BtKYva_intro{color:var(--dsw-alias-label-secondary);margin:0 0 8px;font-size:13px;line-height:1.5}.BtKYva_disclosure{font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;margin:0;padding:0;display:flex}.BtKYva_disclosure:hover .BtKYva_label{color:var(--dsw-alias-brand-primary)}.BtKYva_chevron,.BtKYva_chevronOpen{color:var(--dsw-alias-label-tertiary);flex:none}.BtKYva_chevron{transform:rotate(-90deg)}.BtKYva_advanced{flex-direction:column;gap:4px;padding:4px 0 0 22px;display:flex}.BtKYva_advanced .BtKYva_field+.BtKYva_field{border-top:none}.BtKYva_chips{flex-wrap:wrap;gap:6px;display:flex}.BtKYva_actions{align-items:center;gap:8px;padding-top:12px;display:flex}.BtKYva_failedNote{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}";
|
|
41
41
|
document.head.appendChild(style);
|
|
42
42
|
}
|
|
43
|
-
var Section_default = { "
|
|
43
|
+
var Section_default = { "chevronOpen": "BtKYva_chevronOpen", "actions": "BtKYva_actions", "input": "BtKYva_input", "reset": "BtKYva_reset", "hint": "BtKYva_hint", "head": "BtKYva_head", "advanced": "BtKYva_advanced", "inputInvalid": "BtKYva_inputInvalid", "disclosure": "BtKYva_disclosure", "field": "BtKYva_field", "label": "BtKYva_label", "chevron": "BtKYva_chevron", "invalid": "BtKYva_invalid", "badges": "BtKYva_badges", "intro": "BtKYva_intro", "chips": "BtKYva_chips", "failedNote": "BtKYva_failedNote" };
|
|
44
44
|
|
|
45
45
|
// src/client/Section.tsx
|
|
46
46
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -885,7 +885,7 @@ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id2) + "]"
|
|
|
885
885
|
style.textContent = ".dmPm3q_root{min-width:0;display:inline-flex;position:relative}.dmPm3q_trigger{color:inherit;opacity:.75;font:inherit;cursor:pointer;white-space:nowrap;background:0 0;border:0;border-radius:6px;padding:4px 6px;font-size:12px}.dmPm3q_trigger:hover,.dmPm3q_trigger:focus-visible{opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.dmPm3q_panel{z-index:100;border:1px solid color-mix(in srgb, currentColor 18%, transparent);width:280px;max-width:calc(100vw - 32px);color:var(--dsw-alias-label-primary,CanvasText);background:var(--dsw-specific-menu,Canvas);border-radius:12px;padding:16px;font-size:12px;position:absolute;bottom:calc(100% + 12px);right:0;box-shadow:0 8px 30px #0002}.dmPm3q_hint{opacity:.65;font-size:11px;line-height:1.6}.dmPm3q_window{margin-top:14px}.dmPm3q_row{justify-content:space-between;margin-bottom:5px;display:flex}.dmPm3q_window progress{accent-color:#39a878;width:100%;height:5px;margin-bottom:5px;display:block}.dmPm3q_window progress::-webkit-progress-bar{background:color-mix(in srgb, currentColor 15%, transparent);border-radius:4px}.dmPm3q_window progress::-webkit-progress-value{background:#39a878;border-radius:4px}.dmPm3q_window progress::-moz-progress-bar{background:#39a878;border-radius:4px}";
|
|
886
886
|
document.head.appendChild(style);
|
|
887
887
|
}
|
|
888
|
-
var UsagePill_default = { "
|
|
888
|
+
var UsagePill_default = { "window": "dmPm3q_window", "panel": "dmPm3q_panel", "hint": "dmPm3q_hint", "root": "dmPm3q_root", "row": "dmPm3q_row", "trigger": "dmPm3q_trigger" };
|
|
889
889
|
|
|
890
890
|
// src/client/UsagePill.tsx
|
|
891
891
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
package/lib/index.js
CHANGED
|
@@ -47,14 +47,14 @@ var __callDispose = (stack, error, hasError) => {
|
|
|
47
47
|
// src/index.ts
|
|
48
48
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
49
49
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
50
|
-
import { LlmError as
|
|
50
|
+
import { LlmError as LlmError7, assertUsableApiKey, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
|
|
51
51
|
|
|
52
52
|
// src/adapter.ts
|
|
53
53
|
import { randomUUID } from "node:crypto";
|
|
54
54
|
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
55
55
|
import {
|
|
56
56
|
LlmAdapter,
|
|
57
|
-
LlmError as
|
|
57
|
+
LlmError as LlmError6,
|
|
58
58
|
ReasoningEffortId,
|
|
59
59
|
attributionHeaders as attributionHeaders2,
|
|
60
60
|
contentHasImage as contentHasImage2
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
|
|
63
63
|
// src/conversion/context.ts
|
|
64
64
|
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
65
|
-
import { contentHasImage,
|
|
65
|
+
import { contentHasImage, LlmError as LlmError3, offloadedImageText, requestImageHandleText } from "@deepseek-ai/dsh-llm";
|
|
66
66
|
|
|
67
67
|
// src/conversion/replay.ts
|
|
68
68
|
import { LlmError } from "@deepseek-ai/dsh-llm";
|
|
@@ -267,6 +267,40 @@ var DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
|
267
267
|
var DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
|
|
268
268
|
var DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
|
|
269
269
|
|
|
270
|
+
// src/conversion/image-offload.ts
|
|
271
|
+
import * as llm from "@deepseek-ai/dsh-llm";
|
|
272
|
+
var api = llm;
|
|
273
|
+
function projectRequestImages(messages, policy) {
|
|
274
|
+
if (api.requiredImageOffload !== void 0 && api.projectOffloadedImages !== void 0) {
|
|
275
|
+
if (!policy.exact) return messages;
|
|
276
|
+
if (policy.maxBytes !== void 0) {
|
|
277
|
+
const offloadImages = api.requiredImageOffload(
|
|
278
|
+
messages,
|
|
279
|
+
{ representation: "base64", maxBytes: policy.maxBytes },
|
|
280
|
+
(block) => policy.byteLength(block.attachment)
|
|
281
|
+
);
|
|
282
|
+
if (offloadImages > 0) {
|
|
283
|
+
throw new llm.LlmError(
|
|
284
|
+
`pi-ai request images exceed the ${policy.maxBytes}-byte base64 bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`,
|
|
285
|
+
api.IMAGE_OFFLOAD_REQUIRED_CODE ?? "IMAGE_OFFLOAD_REQUIRED",
|
|
286
|
+
{ offloadImages }
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return api.projectOffloadedImages(messages, policy.placeholder);
|
|
291
|
+
}
|
|
292
|
+
if (api.offloadRequestImagesWithPolicy === void 0) {
|
|
293
|
+
throw new llm.LlmError("The DSH host has no supported image offload API", "UNSUPPORTED_CONTENT");
|
|
294
|
+
}
|
|
295
|
+
return api.offloadRequestImagesWithPolicy(messages, {
|
|
296
|
+
representation: "base64",
|
|
297
|
+
byteQuantum: 1,
|
|
298
|
+
maxBytes: policy.maxBytes,
|
|
299
|
+
byteLength: policy.byteLength,
|
|
300
|
+
placeholder: policy.placeholder
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
270
304
|
// src/conversion/context.ts
|
|
271
305
|
function flattenText(message) {
|
|
272
306
|
return message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
@@ -277,7 +311,7 @@ function toolResultText(blocks) {
|
|
|
277
311
|
function assertSupportedImageRoles(messages) {
|
|
278
312
|
for (const message of messages) {
|
|
279
313
|
if (message.role !== "user" && contentHasImage(message.content)) {
|
|
280
|
-
throw new
|
|
314
|
+
throw new LlmError3(
|
|
281
315
|
`pi-ai cannot represent an image in an in-history ${message.role} message`,
|
|
282
316
|
"UNSUPPORTED_CONTENT"
|
|
283
317
|
);
|
|
@@ -381,7 +415,7 @@ function textOnlyContext(options, onReplayDegrade) {
|
|
|
381
415
|
const messages = [];
|
|
382
416
|
for (const message of split.messages) {
|
|
383
417
|
if (contentHasImage(message.content)) {
|
|
384
|
-
throw new
|
|
418
|
+
throw new LlmError3("pi-ai image conversion requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
385
419
|
}
|
|
386
420
|
if (message.role === "system") {
|
|
387
421
|
messages.push({ role: "user", content: flattenText(message), timestamp: 0 });
|
|
@@ -424,25 +458,21 @@ async function toPiContextWithImages(options, images, onReplayDegrade) {
|
|
|
424
458
|
};
|
|
425
459
|
assertSupportedImageRoles(options.messages);
|
|
426
460
|
const split = splitSystemPrompt(options);
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
const exactMessages = projectOffloadedImages(
|
|
443
|
-
split.messages,
|
|
444
|
-
(ref) => offloadedImageText(ref, resolveImageAccess(ref))
|
|
445
|
-
);
|
|
461
|
+
const projection = {
|
|
462
|
+
maxBytes: maxRequestImageBytes,
|
|
463
|
+
placeholder: (ref) => offloadedImageText(ref, resolveImageAccess(ref))
|
|
464
|
+
};
|
|
465
|
+
const requestMessages = projectRequestImages(split.messages, {
|
|
466
|
+
...projection,
|
|
467
|
+
exact: false,
|
|
468
|
+
byteLength: (ref) => Math.min(ref.bytes, requestImagePolicy.maxBytes)
|
|
469
|
+
});
|
|
470
|
+
const requestImages = await prepareRequestImages(requestMessages, attachments, requestImagePolicy, options.signal);
|
|
471
|
+
const exactMessages = projectRequestImages(requestMessages, {
|
|
472
|
+
...projection,
|
|
473
|
+
exact: true,
|
|
474
|
+
byteLength: (ref) => requestImages.get(ref.attachmentId).bytes
|
|
475
|
+
});
|
|
446
476
|
const toolNames = /* @__PURE__ */ new Map();
|
|
447
477
|
const messages = [];
|
|
448
478
|
for (const message of exactMessages) {
|
|
@@ -477,7 +507,7 @@ async function toPiContextWithImages(options, images, onReplayDegrade) {
|
|
|
477
507
|
|
|
478
508
|
// src/conversion/stream.ts
|
|
479
509
|
import { brandString as brandString2 } from "@deepseek-ai/dsh-brand";
|
|
480
|
-
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError as
|
|
510
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError as LlmError4, QUOTA_EXCEEDED_CODE } from "@deepseek-ai/dsh-llm";
|
|
481
511
|
import { isContextOverflow } from "@earendil-works/pi-ai";
|
|
482
512
|
function mapUsage(usage) {
|
|
483
513
|
return {
|
|
@@ -628,7 +658,7 @@ async function* toStreamChunks(events, contextWindow, callerSignal, requestedMod
|
|
|
628
658
|
return;
|
|
629
659
|
}
|
|
630
660
|
}
|
|
631
|
-
throw new
|
|
661
|
+
throw new LlmError4("pi-ai event stream ended without done/error", "STREAM_CLOSED");
|
|
632
662
|
}
|
|
633
663
|
|
|
634
664
|
// src/adapter.ts
|
|
@@ -640,7 +670,7 @@ import { getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
|
|
|
640
670
|
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
641
671
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
642
672
|
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
643
|
-
import { attributionHeaders, LlmError as
|
|
673
|
+
import { attributionHeaders, LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
|
|
644
674
|
|
|
645
675
|
// src/model-metadata.ts
|
|
646
676
|
var MODEL_METADATA_URL = "https://models.dev/api.json";
|
|
@@ -680,9 +710,9 @@ function thinkingLevels(metadata, known) {
|
|
|
680
710
|
if (known?.reasoning && known.thinkingLevelMap?.off !== null) map.off ??= known.thinkingLevelMap?.off ?? "off";
|
|
681
711
|
return map;
|
|
682
712
|
}
|
|
683
|
-
function modelBaseURL(
|
|
713
|
+
function modelBaseURL(api2, baseURL) {
|
|
684
714
|
const base = baseURL.replace(/\/+$/, "");
|
|
685
|
-
return
|
|
715
|
+
return api2 === "anthropic-messages" ? base.replace(/\/v1$/, "") : base;
|
|
686
716
|
}
|
|
687
717
|
function readModelMetadata(body, baseURL, builtin) {
|
|
688
718
|
const provider = record(record(body)["opencode-go"]);
|
|
@@ -696,22 +726,22 @@ function readModelMetadata(body, baseURL, builtin) {
|
|
|
696
726
|
try {
|
|
697
727
|
const metadata = record(value);
|
|
698
728
|
const npm = record(metadata.provider).npm ?? provider.npm;
|
|
699
|
-
const
|
|
700
|
-
if (
|
|
729
|
+
const api2 = npm === "@ai-sdk/anthropic" ? "anthropic-messages" : npm === "@ai-sdk/openai" ? "openai-responses" : npm === "@ai-sdk/openai-compatible" ? "openai-completions" : void 0;
|
|
730
|
+
if (api2 === void 0) throw new Error(`unsupported model protocol ${String(npm)}`);
|
|
701
731
|
const limit = record(metadata.limit);
|
|
702
732
|
const input = record(metadata.modalities).input;
|
|
703
733
|
if (!Array.isArray(input) || !input.includes("text")) throw new Error("missing text input modality");
|
|
704
734
|
if (typeof metadata.reasoning !== "boolean") throw new Error("missing reasoning capability");
|
|
705
735
|
const exact = builtin.get(id);
|
|
706
|
-
const family = typeof metadata.family === "string" ? Object.keys(entries).find((key) => record(entries[key]).family === metadata.family && builtin.get(key)?.api ===
|
|
707
|
-
const known = exact?.api ===
|
|
708
|
-
const compat =
|
|
736
|
+
const family = typeof metadata.family === "string" ? Object.keys(entries).find((key) => record(entries[key]).family === metadata.family && builtin.get(key)?.api === api2) : void 0;
|
|
737
|
+
const known = exact?.api === api2 ? exact : family === void 0 ? void 0 : builtin.get(family);
|
|
738
|
+
const compat = api2 === "openai-completions" ? {
|
|
709
739
|
supportsStore: false,
|
|
710
740
|
supportsDeveloperRole: false,
|
|
711
741
|
maxTokensField: "max_tokens",
|
|
712
742
|
...record(metadata.interleaved).field === "reasoning_content" ? { requiresReasoningContentOnAssistantMessages: true } : {},
|
|
713
743
|
...known?.compat
|
|
714
|
-
} :
|
|
744
|
+
} : api2 === "openai-responses" ? { sessionAffinityFormat: "openai-nosession", ...known?.compat } : { ...known?.compat };
|
|
715
745
|
const cost = rates(metadata.cost);
|
|
716
746
|
const tiers = record(metadata.cost).tiers;
|
|
717
747
|
if (Array.isArray(tiers)) {
|
|
@@ -724,8 +754,8 @@ function readModelMetadata(body, baseURL, builtin) {
|
|
|
724
754
|
id,
|
|
725
755
|
name: typeof metadata.name === "string" && metadata.name.length > 0 ? metadata.name : id,
|
|
726
756
|
provider: "opencode-go",
|
|
727
|
-
api,
|
|
728
|
-
baseUrl: modelBaseURL(
|
|
757
|
+
api: api2,
|
|
758
|
+
baseUrl: modelBaseURL(api2, baseURL),
|
|
729
759
|
reasoning: metadata.reasoning,
|
|
730
760
|
thinkingLevelMap: thinkingLevels(metadata, known),
|
|
731
761
|
input: input.includes("image") ? ["text", "image"] : ["text"],
|
|
@@ -774,9 +804,9 @@ async function fetchLiveModelIds(baseURL) {
|
|
|
774
804
|
signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS)
|
|
775
805
|
});
|
|
776
806
|
} catch (error) {
|
|
777
|
-
throw new
|
|
807
|
+
throw new LlmError5(`could not reach ${url}`, "DISCOVERY_FAILED", { cause: error });
|
|
778
808
|
}
|
|
779
|
-
if (!response.ok) throw new
|
|
809
|
+
if (!response.ok) throw new LlmError5(`${url} answered ${response.status}`, "DISCOVERY_FAILED");
|
|
780
810
|
return readLiveModelIds(await response.json());
|
|
781
811
|
}
|
|
782
812
|
function harnessApiKeyAuth() {
|
|
@@ -889,7 +919,7 @@ var OpencodeGoCatalog = class {
|
|
|
889
919
|
let snapshot = await this.snapshot();
|
|
890
920
|
if (!snapshot.models.has(id) && snapshot === cached) snapshot = await this.snapshot(true);
|
|
891
921
|
if (snapshot.unavailable.has(id)) {
|
|
892
|
-
throw new
|
|
922
|
+
throw new LlmError5(
|
|
893
923
|
`opencode-go model "${id}" is advertised but cannot be configured: ${snapshot.unavailable.get(id)}; refresh the model list to retry`,
|
|
894
924
|
"MODEL_METADATA_UNAVAILABLE"
|
|
895
925
|
);
|
|
@@ -900,7 +930,7 @@ var OpencodeGoCatalog = class {
|
|
|
900
930
|
async function discoverCatalogModels(catalog) {
|
|
901
931
|
const snapshot = await catalog.snapshot(true);
|
|
902
932
|
if (!snapshot.live) {
|
|
903
|
-
throw new
|
|
933
|
+
throw new LlmError5("llm-opencode-go: the live model listing is unreachable; try again later", "DISCOVERY_FAILED");
|
|
904
934
|
}
|
|
905
935
|
return [
|
|
906
936
|
...[...snapshot.models.values()].map((model) => ({
|
|
@@ -995,32 +1025,28 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
995
1025
|
}
|
|
996
1026
|
async listModels(_provider) {
|
|
997
1027
|
const snapshot = await this.catalogOf(this.options.config()).snapshot(true);
|
|
998
|
-
return [...
|
|
1028
|
+
return [...snapshot.models.values()].map((model) => ({
|
|
999
1029
|
provider: PROVIDER_ID,
|
|
1000
1030
|
id: model.id,
|
|
1001
1031
|
name: model.name,
|
|
1002
1032
|
inputModalities: [...model.input]
|
|
1003
|
-
}))
|
|
1004
|
-
provider: PROVIDER_ID,
|
|
1005
|
-
id,
|
|
1006
|
-
name: id,
|
|
1007
|
-
description: `Metadata unavailable: ${reason}`
|
|
1008
|
-
}))];
|
|
1033
|
+
}));
|
|
1009
1034
|
}
|
|
1010
1035
|
async resolveModel(_provider, model, _signal) {
|
|
1011
1036
|
const snapshot = await this.catalogOf(this.options.config()).forModel(model);
|
|
1012
1037
|
const resolved = snapshot.models.get(model);
|
|
1013
1038
|
if (resolved === void 0) {
|
|
1014
|
-
throw new
|
|
1039
|
+
throw new LlmError6(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
|
|
1015
1040
|
}
|
|
1016
1041
|
return this.modelInfo(resolved);
|
|
1017
1042
|
}
|
|
1018
1043
|
/** Describe one model: capacities plus the reasoning levels it actually offers. */
|
|
1019
1044
|
modelInfo(model) {
|
|
1020
1045
|
const reasoning = {};
|
|
1021
|
-
|
|
1046
|
+
const levels = model.reasoning ? getSupportedThinkingLevels(model) : [];
|
|
1047
|
+
if (levels.length > 0) {
|
|
1022
1048
|
reasoning.reasoning = {
|
|
1023
|
-
efforts:
|
|
1049
|
+
efforts: levels.map((level) => ({
|
|
1024
1050
|
id: ReasoningEffortId(level),
|
|
1025
1051
|
name: `${level.charAt(0).toUpperCase()}${level.slice(1)}`
|
|
1026
1052
|
}))
|
|
@@ -1040,7 +1066,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1040
1066
|
if (effort === void 0) return void 0;
|
|
1041
1067
|
const supported = getSupportedThinkingLevels(model);
|
|
1042
1068
|
if (supported.some((level) => level === effort)) return effort;
|
|
1043
|
-
throw new
|
|
1069
|
+
throw new LlmError6(
|
|
1044
1070
|
`opencode-go model "${model.id}" does not support reasoning effort "${effort}"`,
|
|
1045
1071
|
"UNSUPPORTED_REASONING_EFFORT"
|
|
1046
1072
|
);
|
|
@@ -1049,17 +1075,17 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1049
1075
|
var _stack = [];
|
|
1050
1076
|
try {
|
|
1051
1077
|
if (options.stop !== void 0) {
|
|
1052
|
-
throw new
|
|
1078
|
+
throw new LlmError6("llm-opencode-go does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
|
|
1053
1079
|
}
|
|
1054
1080
|
const config = this.options.config();
|
|
1055
1081
|
const snapshot = await this.catalogOf(config).forModel(options.model);
|
|
1056
1082
|
const model = snapshot.models.get(options.model);
|
|
1057
1083
|
if (model === void 0) {
|
|
1058
|
-
throw new
|
|
1084
|
+
throw new LlmError6(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
|
|
1059
1085
|
}
|
|
1060
1086
|
const apiKey = await this.options.resolveApiKey();
|
|
1061
1087
|
if (apiKey === void 0 || apiKey.length === 0) {
|
|
1062
|
-
throw new
|
|
1088
|
+
throw new LlmError6("llm-opencode-go: no credential resolved for the route", "MISSING_CREDENTIAL");
|
|
1063
1089
|
}
|
|
1064
1090
|
const reasoning = this.resolveReasoningLevel(model, options.reasoningEffort);
|
|
1065
1091
|
const consumer = new AbortController();
|
|
@@ -1068,14 +1094,14 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1068
1094
|
try {
|
|
1069
1095
|
const containsImage = options.messages.some((message) => contentHasImage2(message.content));
|
|
1070
1096
|
if (containsImage && !model.input.includes("image")) {
|
|
1071
|
-
throw new
|
|
1097
|
+
throw new LlmError6(`opencode-go model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
|
|
1072
1098
|
}
|
|
1073
1099
|
let imageRequest;
|
|
1074
1100
|
if (containsImage) {
|
|
1075
1101
|
const access = this.options.imageAccess;
|
|
1076
1102
|
const store = access?.resolveAttachments();
|
|
1077
1103
|
if (access === void 0 || store === void 0) {
|
|
1078
|
-
throw new
|
|
1104
|
+
throw new LlmError6("llm-opencode-go image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1079
1105
|
}
|
|
1080
1106
|
imageRequest = {
|
|
1081
1107
|
attachments: store,
|
|
@@ -1112,7 +1138,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1112
1138
|
while (true) {
|
|
1113
1139
|
const result = await watchdog.next(iterator);
|
|
1114
1140
|
if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
|
|
1115
|
-
throw new
|
|
1141
|
+
throw new LlmError6("opencode-go stream idle timeout", "TIMEOUT");
|
|
1116
1142
|
}
|
|
1117
1143
|
if (result.done) {
|
|
1118
1144
|
exhausted = true;
|
|
@@ -1131,10 +1157,10 @@ var OpencodeGoAdapter = class extends LlmAdapter {
|
|
|
1131
1157
|
}
|
|
1132
1158
|
} catch (error) {
|
|
1133
1159
|
if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
|
|
1134
|
-
throw new
|
|
1160
|
+
throw new LlmError6("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
|
|
1135
1161
|
}
|
|
1136
1162
|
if (options.signal?.aborted) {
|
|
1137
|
-
throw new
|
|
1163
|
+
throw new LlmError6("opencode-go request aborted by caller", "ABORTED", { cause: error });
|
|
1138
1164
|
}
|
|
1139
1165
|
throw error;
|
|
1140
1166
|
}
|
|
@@ -1226,7 +1252,7 @@ function apply(ctx, raw) {
|
|
|
1226
1252
|
const credentials = ctx.get("credentials");
|
|
1227
1253
|
const hit = credentials !== void 0 ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
|
|
1228
1254
|
if (hit !== void 0 && hit.length > 0) return assertUsableApiKey(hit, name, ref);
|
|
1229
|
-
throw new
|
|
1255
|
+
throw new LlmError7(
|
|
1230
1256
|
`llm-opencode-go: no credential; the profile resolves ${ref}, which is not set \u2014 store ${ref} through the credentials service or export it`,
|
|
1231
1257
|
"MISSING_CREDENTIAL"
|
|
1232
1258
|
);
|
|
@@ -1288,7 +1314,7 @@ function apply(ctx, raw) {
|
|
|
1288
1314
|
syncRoute();
|
|
1289
1315
|
const undiscover = ctx.llm.registerModelDiscovery(name, async (request) => {
|
|
1290
1316
|
if (request.provider !== PROVIDER_ID && !(request.baseURL ?? "").includes("opencode.ai")) {
|
|
1291
|
-
throw new
|
|
1317
|
+
throw new LlmError7(
|
|
1292
1318
|
"llm-opencode-go discovers only OpenCode zen/go endpoints; enter this provider's models by hand",
|
|
1293
1319
|
"DISCOVERY_UNSUPPORTED"
|
|
1294
1320
|
);
|
|
@@ -36,8 +36,9 @@ export interface PiImageRequestBudget {
|
|
|
36
36
|
export declare function toPiContext(options: GenerateOptions, images?: undefined, onReplayDegrade?: (reason: string) => void): PiContext;
|
|
37
37
|
/**
|
|
38
38
|
* Convert harness history to a pi-ai Context while resolving durable images.
|
|
39
|
-
* Tool result names are recovered from preceding assistant tool calls.
|
|
40
|
-
*
|
|
39
|
+
* Tool result names are recovered from preceding assistant tool calls. On DSH
|
|
40
|
+
* 0.1.5, oldest images over the request budget become transient placeholders.
|
|
41
|
+
* On newer hosts, occurrences the surface marks offloaded become placeholders; when the
|
|
41
42
|
* retained occurrences' exact base64 payload still exceeds
|
|
42
43
|
* `maxRequestImageBytes`, the call fails with `IMAGE_OFFLOAD_REQUIRED` naming
|
|
43
44
|
* how many more oldest occurrences must be offloaded.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Message } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
3
|
+
interface ImageProjectionPolicy {
|
|
4
|
+
maxBytes?: number;
|
|
5
|
+
byteLength: (ref: ImageAttachmentRef) => number;
|
|
6
|
+
placeholder: (ref: ImageAttachmentRef) => string;
|
|
7
|
+
/** False before preparing image bytes, true once encoded sizes are known. */
|
|
8
|
+
exact: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Preserve the host generation's image policy, using estimates first and exact sizes second. */
|
|
11
|
+
export declare function projectRequestImages(messages: readonly Message[], policy: ImageProjectionPolicy): readonly Message[];
|
|
12
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-opencode-go",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "OpenCode Go model provider and settings UI for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"url": "https://github.com/Duskriver/dsh-opencode-go/issues"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
16
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
17
|
+
"dsh": ">=0.1.5-rc.1 <0.1.6-0 || 0.1.6-alpha.1"
|
|
17
18
|
},
|
|
18
19
|
"main": "./lib/index.js",
|
|
19
20
|
"types": "./lib/types/index.d.ts",
|
|
@@ -60,25 +61,25 @@
|
|
|
60
61
|
},
|
|
61
62
|
"peerDependencies": {
|
|
62
63
|
"@deepseek-ai/cordis": "4.0.2",
|
|
63
|
-
"@deepseek-ai/dsh-api-gateway": "0.1.6-alpha.1",
|
|
64
|
-
"@deepseek-ai/dsh-api-remotes": "0.1.6-alpha.1",
|
|
65
|
-
"@deepseek-ai/dsh-api-settings-controller": "0.1.6-alpha.1",
|
|
66
|
-
"@deepseek-ai/dsh-attachment": "0.1.6-alpha.1",
|
|
67
|
-
"@deepseek-ai/dsh-client-locale": "0.1.6-alpha.1",
|
|
68
|
-
"@deepseek-ai/dsh-client-store": "0.1.6-alpha.1",
|
|
69
|
-
"@deepseek-ai/dsh-client-ui-
|
|
70
|
-
"@deepseek-ai/dsh-client-ui-
|
|
71
|
-
"@deepseek-ai/dsh-client-ui-
|
|
72
|
-
"@deepseek-ai/dsh-client-ui-
|
|
73
|
-
"@deepseek-ai/dsh-
|
|
74
|
-
"@deepseek-ai/dsh-
|
|
75
|
-
"@deepseek-ai/dsh-
|
|
76
|
-
"@deepseek-ai/dsh-
|
|
77
|
-
"@deepseek-ai/dsh-
|
|
78
|
-
"@deepseek-ai/dsh-
|
|
79
|
-
"@deepseek-ai/dsh-
|
|
80
|
-
"@deepseek-ai/dsh-
|
|
81
|
-
"@deepseek-ai/dsh-typert-registry": "0.1.6-alpha.1"
|
|
64
|
+
"@deepseek-ai/dsh-api-gateway": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
65
|
+
"@deepseek-ai/dsh-api-remotes": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
66
|
+
"@deepseek-ai/dsh-api-settings-controller": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
67
|
+
"@deepseek-ai/dsh-attachment": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
68
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
69
|
+
"@deepseek-ai/dsh-client-store": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-model-selection": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
75
|
+
"@deepseek-ai/dsh-credentials": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
76
|
+
"@deepseek-ai/dsh-fs": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
77
|
+
"@deepseek-ai/dsh-launch-environment": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
78
|
+
"@deepseek-ai/dsh-llm": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
79
|
+
"@deepseek-ai/dsh-settings": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
80
|
+
"@deepseek-ai/dsh-timeout": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
81
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1",
|
|
82
|
+
"@deepseek-ai/dsh-typert-registry": ">=0.1.5-rc.1 <0.1.6 || 0.1.6-alpha.1"
|
|
82
83
|
},
|
|
83
84
|
"dependencies": {
|
|
84
85
|
"@deepseek-ai/dsh-brand": "0.1.6-alpha.1",
|
|
@@ -121,6 +122,8 @@
|
|
|
121
122
|
"anser": "^2.3.5",
|
|
122
123
|
"clsx": "^2.0.0",
|
|
123
124
|
"diff": "^9.0.0",
|
|
125
|
+
"dsh-llm-v015-rc1": "npm:@deepseek-ai/dsh-llm@0.1.5-rc.1",
|
|
126
|
+
"dsh-llm-v015-rc2": "npm:@deepseek-ai/dsh-llm@0.1.5-rc.2",
|
|
124
127
|
"esbuild": "^0.25.0",
|
|
125
128
|
"immer": "^10.1.1",
|
|
126
129
|
"jsdom": "29.1.1",
|