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 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 但尚无有效协议/能力配置的模型仍会显示,并标注配置暂不可用;调用时会说明原因。仅凭模型 ID 无法可靠推断调用方式。上游新增全新协议或协议特例时,仍可能需要适配。
119
+ 网关已公布 ID 但尚无有效协议/能力配置的模型会在设置页的发现结果中标注配置暂不可用,暂不进入对话模型选择器,避免一个未配置的模型阻断整个列表;直接调用时会说明原因。配置补齐后,刷新列表即可使用。仅凭模型 ID 无法可靠推断调用方式。上游新增全新协议或协议特例时,仍可能需要适配。
120
+
121
+ 模型具有推理能力但没有可调节的推理档位时(如 `union-alpha`),仍可正常选择和使用,只是不显示推理强度选项。
93
122
 
94
123
  在线配置暂时不可达时,优先复用本次运行中成功获取的配置,以 pi-ai 内置配置作为备用。网关目录不可达时,已有请求可以使用上次目录;设置页刷新会显示失败,避免把旧目录误认为最新结果。`refreshMinutes` 只控制已有模型请求的缓存时长,不阻止主动读取列表获取新模型。
95
124
 
@@ -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 other DSH releases are not verified. The automated gateway tests preserve the adapter's request and replay semantics but cannot establish account validity or live provider availability.
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 = { "badges": "BtKYva_badges", "field": "BtKYva_field", "inputInvalid": "BtKYva_inputInvalid", "input": "BtKYva_input", "hint": "BtKYva_hint", "actions": "BtKYva_actions", "label": "BtKYva_label", "invalid": "BtKYva_invalid", "reset": "BtKYva_reset", "intro": "BtKYva_intro", "disclosure": "BtKYva_disclosure", "head": "BtKYva_head", "chevronOpen": "BtKYva_chevronOpen", "chips": "BtKYva_chips", "failedNote": "BtKYva_failedNote", "chevron": "BtKYva_chevron", "advanced": "BtKYva_advanced" };
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 = { "hint": "dmPm3q_hint", "row": "dmPm3q_row", "window": "dmPm3q_window", "root": "dmPm3q_root", "trigger": "dmPm3q_trigger", "panel": "dmPm3q_panel" };
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 LlmError6, assertUsableApiKey, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
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 LlmError5,
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, IMAGE_OFFLOAD_REQUIRED_CODE, LlmError as LlmError2, offloadedImageText, projectOffloadedImages, requestImageHandleText, requiredImageOffload } from "@deepseek-ai/dsh-llm";
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 LlmError2(
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 LlmError2("pi-ai image conversion requires the durable attachment service", "UNSUPPORTED_CONTENT");
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 requestImages = await prepareRequestImages(split.messages, attachments, requestImagePolicy, options.signal);
428
- if (maxRequestImageBytes !== void 0) {
429
- const offloadImages = requiredImageOffload(
430
- split.messages,
431
- { representation: "base64", maxBytes: maxRequestImageBytes },
432
- (block) => requestImages.get(block.attachment.attachmentId).bytes
433
- );
434
- if (offloadImages > 0) {
435
- throw new LlmError2(
436
- `pi-ai request images exceed the ${maxRequestImageBytes}-byte base64 bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`,
437
- IMAGE_OFFLOAD_REQUIRED_CODE,
438
- { offloadImages }
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 LlmError3, QUOTA_EXCEEDED_CODE } from "@deepseek-ai/dsh-llm";
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 LlmError3("pi-ai event stream ended without done/error", "STREAM_CLOSED");
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 LlmError4 } from "@deepseek-ai/dsh-llm";
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(api, baseURL) {
713
+ function modelBaseURL(api2, baseURL) {
684
714
  const base = baseURL.replace(/\/+$/, "");
685
- return api === "anthropic-messages" ? base.replace(/\/v1$/, "") : base;
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 api = npm === "@ai-sdk/anthropic" ? "anthropic-messages" : npm === "@ai-sdk/openai" ? "openai-responses" : npm === "@ai-sdk/openai-compatible" ? "openai-completions" : void 0;
700
- if (api === void 0) throw new Error(`unsupported model protocol ${String(npm)}`);
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 === api) : void 0;
707
- const known = exact?.api === api ? exact : family === void 0 ? void 0 : builtin.get(family);
708
- const compat = api === "openai-completions" ? {
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
- } : api === "openai-responses" ? { sessionAffinityFormat: "openai-nosession", ...known?.compat } : { ...known?.compat };
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(api, baseURL),
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 LlmError4(`could not reach ${url}`, "DISCOVERY_FAILED", { cause: error });
807
+ throw new LlmError5(`could not reach ${url}`, "DISCOVERY_FAILED", { cause: error });
778
808
  }
779
- if (!response.ok) throw new LlmError4(`${url} answered ${response.status}`, "DISCOVERY_FAILED");
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 LlmError4(
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 LlmError4("llm-opencode-go: the live model listing is unreachable; try again later", "DISCOVERY_FAILED");
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 [...[...snapshot.models.values()].map((model) => ({
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
- })), ...[...snapshot.unavailable].map(([id, reason]) => ({
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 LlmError5(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
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
- if (model.reasoning) {
1046
+ const levels = model.reasoning ? getSupportedThinkingLevels(model) : [];
1047
+ if (levels.length > 0) {
1022
1048
  reasoning.reasoning = {
1023
- efforts: getSupportedThinkingLevels(model).map((level) => ({
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 LlmError5(
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 LlmError5("llm-opencode-go does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
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 LlmError5(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
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 LlmError5("llm-opencode-go: no credential resolved for the route", "MISSING_CREDENTIAL");
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 LlmError5(`opencode-go model "${model.id}" does not support image input`, "UNSUPPORTED_CONTENT");
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 LlmError5("llm-opencode-go image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
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 LlmError5("opencode-go stream idle timeout", "TIMEOUT");
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 LlmError5("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
1160
+ throw new LlmError6("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
1135
1161
  }
1136
1162
  if (options.signal?.aborted) {
1137
- throw new LlmError5("opencode-go request aborted by caller", "ABORTED", { cause: error });
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 LlmError6(
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 LlmError6(
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. Image
40
- * occurrences the surface marks offloaded become text placeholders; when the
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",
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-model-selection": "0.1.6-alpha.1",
70
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.1",
71
- "@deepseek-ai/dsh-client-ui-settings": "0.1.6-alpha.1",
72
- "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.1",
73
- "@deepseek-ai/dsh-credentials": "0.1.6-alpha.1",
74
- "@deepseek-ai/dsh-fs": "0.1.6-alpha.1",
75
- "@deepseek-ai/dsh-launch-environment": "0.1.6-alpha.1",
76
- "@deepseek-ai/dsh-llm": "0.1.6-alpha.1",
77
- "@deepseek-ai/dsh-settings": "0.1.6-alpha.1",
78
- "@deepseek-ai/dsh-timeout": "0.1.6-alpha.1",
79
- "@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.1",
80
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.6-alpha.1",
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",