dsh-opencode-go 0.1.4 → 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
 
@@ -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 = { "intro": "BtKYva_intro", "head": "BtKYva_head", "label": "BtKYva_label", "invalid": "BtKYva_invalid", "hint": "BtKYva_hint", "chevron": "BtKYva_chevron", "chips": "BtKYva_chips", "failedNote": "BtKYva_failedNote", "advanced": "BtKYva_advanced", "input": "BtKYva_input", "reset": "BtKYva_reset", "badges": "BtKYva_badges", "actions": "BtKYva_actions", "inputInvalid": "BtKYva_inputInvalid", "disclosure": "BtKYva_disclosure", "chevronOpen": "BtKYva_chevronOpen", "field": "BtKYva_field" };
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 = { "panel": "dmPm3q_panel", "trigger": "dmPm3q_trigger", "hint": "dmPm3q_hint", "root": "dmPm3q_root", "window": "dmPm3q_window", "row": "dmPm3q_row" };
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) => ({
@@ -1006,7 +1036,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1006
1036
  const snapshot = await this.catalogOf(this.options.config()).forModel(model);
1007
1037
  const resolved = snapshot.models.get(model);
1008
1038
  if (resolved === void 0) {
1009
- throw new LlmError5(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
1039
+ throw new LlmError6(`opencode-go has no model "${model}"`, "UNKNOWN_MODEL");
1010
1040
  }
1011
1041
  return this.modelInfo(resolved);
1012
1042
  }
@@ -1036,7 +1066,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1036
1066
  if (effort === void 0) return void 0;
1037
1067
  const supported = getSupportedThinkingLevels(model);
1038
1068
  if (supported.some((level) => level === effort)) return effort;
1039
- throw new LlmError5(
1069
+ throw new LlmError6(
1040
1070
  `opencode-go model "${model.id}" does not support reasoning effort "${effort}"`,
1041
1071
  "UNSUPPORTED_REASONING_EFFORT"
1042
1072
  );
@@ -1045,17 +1075,17 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1045
1075
  var _stack = [];
1046
1076
  try {
1047
1077
  if (options.stop !== void 0) {
1048
- 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");
1049
1079
  }
1050
1080
  const config = this.options.config();
1051
1081
  const snapshot = await this.catalogOf(config).forModel(options.model);
1052
1082
  const model = snapshot.models.get(options.model);
1053
1083
  if (model === void 0) {
1054
- 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");
1055
1085
  }
1056
1086
  const apiKey = await this.options.resolveApiKey();
1057
1087
  if (apiKey === void 0 || apiKey.length === 0) {
1058
- 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");
1059
1089
  }
1060
1090
  const reasoning = this.resolveReasoningLevel(model, options.reasoningEffort);
1061
1091
  const consumer = new AbortController();
@@ -1064,14 +1094,14 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1064
1094
  try {
1065
1095
  const containsImage = options.messages.some((message) => contentHasImage2(message.content));
1066
1096
  if (containsImage && !model.input.includes("image")) {
1067
- 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");
1068
1098
  }
1069
1099
  let imageRequest;
1070
1100
  if (containsImage) {
1071
1101
  const access = this.options.imageAccess;
1072
1102
  const store = access?.resolveAttachments();
1073
1103
  if (access === void 0 || store === void 0) {
1074
- 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");
1075
1105
  }
1076
1106
  imageRequest = {
1077
1107
  attachments: store,
@@ -1108,7 +1138,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1108
1138
  while (true) {
1109
1139
  const result = await watchdog.next(iterator);
1110
1140
  if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
1111
- throw new LlmError5("opencode-go stream idle timeout", "TIMEOUT");
1141
+ throw new LlmError6("opencode-go stream idle timeout", "TIMEOUT");
1112
1142
  }
1113
1143
  if (result.done) {
1114
1144
  exhausted = true;
@@ -1127,10 +1157,10 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1127
1157
  }
1128
1158
  } catch (error) {
1129
1159
  if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) {
1130
- throw new LlmError5("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
1160
+ throw new LlmError6("opencode-go stream idle timeout", "TIMEOUT", { cause: error });
1131
1161
  }
1132
1162
  if (options.signal?.aborted) {
1133
- 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 });
1134
1164
  }
1135
1165
  throw error;
1136
1166
  }
@@ -1222,7 +1252,7 @@ function apply(ctx, raw) {
1222
1252
  const credentials = ctx.get("credentials");
1223
1253
  const hit = credentials !== void 0 ? (await credentials.resolve(credentialRef(ref)))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
1224
1254
  if (hit !== void 0 && hit.length > 0) return assertUsableApiKey(hit, name, ref);
1225
- throw new LlmError6(
1255
+ throw new LlmError7(
1226
1256
  `llm-opencode-go: no credential; the profile resolves ${ref}, which is not set \u2014 store ${ref} through the credentials service or export it`,
1227
1257
  "MISSING_CREDENTIAL"
1228
1258
  );
@@ -1284,7 +1314,7 @@ function apply(ctx, raw) {
1284
1314
  syncRoute();
1285
1315
  const undiscover = ctx.llm.registerModelDiscovery(name, async (request) => {
1286
1316
  if (request.provider !== PROVIDER_ID && !(request.baseURL ?? "").includes("opencode.ai")) {
1287
- throw new LlmError6(
1317
+ throw new LlmError7(
1288
1318
  "llm-opencode-go discovers only OpenCode zen/go endpoints; enter this provider's models by hand",
1289
1319
  "DISCOVERY_UNSUPPORTED"
1290
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.4",
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",