dsh-opencode-go 0.1.16 → 0.1.17

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.en.md CHANGED
@@ -19,7 +19,7 @@ The plugin automatically adds the session headers required by OpenCode Go, reads
19
19
 
20
20
  Supports DSH `0.1.5-rc.1` and later, including alpha, rc, and stable releases. Compatibility will be maintained as new host versions are released.
21
21
 
22
- Verified versions: `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.6-alpha.1`, `0.1.6-alpha.2`, `0.1.7-alpha.1`, `0.1.7-alpha.2`, `0.1.7-rc.1`, and `0.1.7-rc.2`.
22
+ Verified versions: `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.6-alpha.1`, `0.1.6-alpha.2`, `0.1.7-alpha.1`, `0.1.7-alpha.2`, `0.1.7-rc.1`, `0.1.7-rc.2`, and `0.2.0-rc.2`.
23
23
 
24
24
  ### Install from DSH (recommended)
25
25
 
@@ -44,7 +44,7 @@ If your DSH version does not have an **Add plugin** entry, use the command-line
44
44
  ### Command-line installation (alternative)
45
45
 
46
46
  ```sh
47
- dsh plugin --profile web add dsh-opencode-go@0.1.15
47
+ dsh plugin --profile web add dsh-opencode-go
48
48
  ```
49
49
 
50
50
  Start or restart `dsh web`, then:
@@ -58,7 +58,7 @@ Start or restart `dsh web`, then:
58
58
  Install the plugin into the Headless profile:
59
59
 
60
60
  ```sh
61
- dsh plugin --profile headless add dsh-opencode-go@0.1.15
61
+ dsh plugin --profile headless add dsh-opencode-go
62
62
  ```
63
63
 
64
64
  Save the following as `headless.patch.yml` to select a default model:
@@ -66,7 +66,7 @@ Save the following as `headless.patch.yml` to select a default model:
66
66
  ```yaml
67
67
  - id: agent-default-model
68
68
  config:
69
- provider: opencode-go
69
+ provider: dsh-opencode-go
70
70
  model: deepseek-v4.1-flash
71
71
  ```
72
72
 
@@ -86,7 +86,7 @@ To build from source and install a local package:
86
86
  npm ci
87
87
  npm run compile
88
88
  npm pack
89
- dsh plugin --profile web add ./dsh-opencode-go-0.1.15.tgz
89
+ dsh plugin --profile web add ./dsh-opencode-go-0.1.17.tgz
90
90
  ```
91
91
 
92
92
  Source development builds the plugin explicitly with `npm run compile`; `--legacy-peer-deps` is not required. Multi-version DSH compatibility tests use independent environments and are not installed during ordinary installation or builds. For Headless, replace `web` with `headless`.
@@ -152,9 +152,11 @@ DSH 0.1.5 offloads only for the current request. DSH 0.1.6 and later record offl
152
152
 
153
153
  ## FAQ
154
154
 
155
- ### The `opencode-go` route is already in use
155
+ ### Coexistence with the host's OpenCode Go and upgrade migration
156
156
 
157
- Only one adapter in a profile can provide the `opencode-go` route. If another plugin or a generic pi-ai configuration already connects OpenCode Go, disable that configuration first. Other providers can continue to run.
157
+ Version 0.1.17 and later use the independent `dsh-opencode-go` provider, shown as **DSH OpenCode Go** in the model picker. The host pi-ai's `opencode-go` provider can run alongside it. Configure the API key in this plugin's settings, then choose a model under **DSH OpenCode Go**. Versions 0.1.16 and earlier use `opencode-go`; their headless configuration should also use that older identity.
158
+
159
+ Sessions, Agent presets and headless defaults previously saved with `provider: opencode-go` need to select **DSH OpenCode Go** again, or change the provider to `dsh-opencode-go`. API keys, model settings and metadata caches are retained. Existing session content remains available; when switching providers, DSH removes the previous adapter's private replay metadata according to its ownership rules.
158
160
 
159
161
  ### An expected model is missing
160
162
 
@@ -178,7 +180,7 @@ Model configuration is saved to `$DSH_HOME/cache/dsh-opencode-go/models.dev.api.
178
180
 
179
181
  `refreshMinutes` controls the cache lifetime after a successful refresh. Failed refreshes become eligible for retry on the next read after 5, 10, 20, 40, then at most 60 seconds; there is no background polling when nothing reads the catalog. Explicit Settings refreshes and previously unknown model requests bypass this delay. Cancelling model resolution or generation immediately ends that caller's catalog wait while other callers can continue sharing the same refresh.
180
182
 
181
- Verified model configurations can serve generation immediately for five minutes after their cache lifetime expires, while triggering one shared background refresh. This window is measured from each source's last successful check; failed retries do not extend it. Beyond that window, requests wait for the next due refresh, retaining the existing fallback behavior if it fails. Initial loads without a disk cache, unknown models, and manual refreshes wait for online results. Listing requests have a 10-second deadline; the larger model metadata download has its own 30-second deadline. JSON requests still use uncompressed transfer for compatibility with hosts affected by the HTTP/2 decompression issue.
183
+ Verified model configurations can serve generation immediately for five minutes after their cache lifetime expires, while triggering one shared background refresh. This window is measured from each source's last successful check; failed retries do not extend it. Beyond that window, requests wait for the next due refresh, retaining the existing fallback behavior if it fails. Initial loads without a disk cache, unknown models, and manual refreshes wait for online results. Listing requests have a 10-second deadline; the larger model metadata download has its own 30-second deadline and negotiates gzip to reduce transfer size on slow connections. If an HTTP/2 host compatibility issue delivers raw gzip bytes, the plugin asynchronously decompresses them within the same deadline, limiting both the delivered bytes and decoded result to 16 MiB. Model listing and usage requests continue to use uncompressed transfer.
182
184
 
183
185
  Continuation, retries, and restored sessions reuse the Host's durable session ID. Forks and subagent sessions use their own IDs, separate from the parent. Standalone requests without a session ID receive a fresh random identifier each time.
184
186
 
package/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  支持 DSH `0.1.5-rc.1` 及以上版本,包括 alpha、rc 和正式版,并持续跟随宿主版本更新维护兼容性。
21
21
 
22
- 已验证版本:`0.1.5-rc.1`、`0.1.5-rc.2`、`0.1.6-alpha.1`、`0.1.6-alpha.2`、`0.1.7-alpha.1`、`0.1.7-alpha.2`、`0.1.7-rc.1` 和 `0.1.7-rc.2`。
22
+ 已验证版本:`0.1.5-rc.1`、`0.1.5-rc.2`、`0.1.6-alpha.1`、`0.1.6-alpha.2`、`0.1.7-alpha.1`、`0.1.7-alpha.2`、`0.1.7-rc.1`、`0.1.7-rc.2` 和 `0.2.0-rc.2`。
23
23
 
24
24
  ### 在 DSH 中安装(推荐)
25
25
 
@@ -44,7 +44,7 @@ https://github.com/Duskriver/dsh-opencode-go
44
44
  ### 命令行安装(备选)
45
45
 
46
46
  ```sh
47
- dsh plugin --profile web add dsh-opencode-go@0.1.15
47
+ dsh plugin --profile web add dsh-opencode-go
48
48
  ```
49
49
 
50
50
  安装后启动或重启 `dsh web`,然后:
@@ -58,7 +58,7 @@ dsh plugin --profile web add dsh-opencode-go@0.1.15
58
58
  安装到 Headless profile:
59
59
 
60
60
  ```sh
61
- dsh plugin --profile headless add dsh-opencode-go@0.1.15
61
+ dsh plugin --profile headless add dsh-opencode-go
62
62
  ```
63
63
 
64
64
  将以下内容保存为 `headless.patch.yml`,选择默认模型:
@@ -66,7 +66,7 @@ dsh plugin --profile headless add dsh-opencode-go@0.1.15
66
66
  ```yaml
67
67
  - id: agent-default-model
68
68
  config:
69
- provider: opencode-go
69
+ provider: dsh-opencode-go
70
70
  model: deepseek-v4.1-flash
71
71
  ```
72
72
 
@@ -86,7 +86,7 @@ dsh --profile headless --patch ./headless.patch.yml "你好"
86
86
  npm ci
87
87
  npm run compile
88
88
  npm pack
89
- dsh plugin --profile web add ./dsh-opencode-go-0.1.15.tgz
89
+ dsh plugin --profile web add ./dsh-opencode-go-0.1.17.tgz
90
90
  ```
91
91
 
92
92
  源码开发时显式运行 `npm run compile`;普通用户直接安装预编译产物,无需 `--legacy-peer-deps`。多版本 DSH 兼容性测试使用独立环境,不参与普通安装或构建。Headless 用户将 `web` 换成 `headless`。
@@ -152,9 +152,11 @@ DSH 0.1.5 的卸载只影响本次请求;DSH 0.1.6 及以上版本通过宿主
152
152
 
153
153
  ## 常见问题
154
154
 
155
- ### 提示 `opencode-go` 路由已被占用
155
+ ### 与宿主的 OpenCode Go 共存及升级迁移
156
156
 
157
- 同一 profile 中只能有一个适配器提供 `opencode-go` 路由。如果已经通过其他插件或通用 pi-ai 配置接入 OpenCode Go,请先停用那一项配置。其他提供方可以继续使用。
157
+ 从 0.1.17 起使用独立的 `dsh-opencode-go` provider,模型选择器中显示为 **DSH OpenCode Go**;宿主 pi-ai 的 `opencode-go` 可以同时使用。请在插件设置页配置 API Key,然后选择 **DSH OpenCode Go** 下的模型。0.1.16 及以前版本使用 `opencode-go`,这些版本的 headless 配置也应使用旧标识。
158
+
159
+ 升级前保存为 `provider: opencode-go` 的会话、Agent 预设和 headless 默认模型,需要重新选择 **DSH OpenCode Go**,或将 provider 改为 `dsh-opencode-go`。API Key、模型设置和元数据缓存继续沿用。旧会话内容保留;跨 provider 切换时,DSH 会按其规则去除旧适配器的专用回放元数据。
158
160
 
159
161
  ### 没有出现预期的模型
160
162
 
@@ -178,7 +180,7 @@ DSH 0.1.5 的卸载只影响本次请求;DSH 0.1.6 及以上版本通过宿主
178
180
 
179
181
  `refreshMinutes` 控制成功刷新后的目录缓存时长。刷新失败后,下一次读取会按 5、10、20、40、60 秒的退避间隔尝试恢复,最长等待间隔为 60 秒;没有读取时不会后台轮询。设置页主动刷新和未知模型的即时发现不受这个间隔限制。取消模型解析或生成时,会立即结束当前调用的目录等待,其他调用仍可继续使用同一次共享刷新。
180
182
 
181
- 已验证的模型配置在缓存到期后的 5 分钟内可直接用于生成,同时触发一次共享后台刷新。这个窗口按两种来源各自上次成功检查的时间计算,失败重试不会延长它;超过窗口后,请求等待下一次到期刷新,刷新仍失败时保留已有回退行为。没有磁盘缓存的首次加载、未知模型和手动刷新会等待在线结果。模型名单请求限时 10 秒,体积更大的模型配置下载单独限时 30 秒。JSON 请求仍使用非压缩传输,以兼容存在 HTTP/2 解压问题的宿主。
183
+ 已验证的模型配置在缓存到期后的 5 分钟内可直接用于生成,同时触发一次共享后台刷新。这个窗口按两种来源各自上次成功检查的时间计算,失败重试不会延长它;超过窗口后,请求等待下一次到期刷新,刷新仍失败时保留已有回退行为。没有磁盘缓存的首次加载、未知模型和手动刷新会等待在线结果。模型名单请求限时 10 秒,体积更大的模型配置下载单独限时 30 秒,并协商 gzip 压缩以减少慢速连接的传输量。若宿主因 HTTP/2 兼容问题返回未解压的 gzip 数据,插件会在同一超时预算内异步解压,原始数据和解压结果均受 16 MiB 限制。模型名单和用量接口继续使用非压缩传输。
182
184
 
183
185
  同一会话继续、重试以及恢复后的请求沿用宿主保存的会话 ID。分叉和子会话使用各自的 ID,不复用父会话标识;没有会话 ID 的独立请求每次生成随机标识。
184
186
 
@@ -1,5 +1,5 @@
1
1
  # Third-party notices
2
2
 
3
- The adapter, settings UI, and `src/conversion/{context,stream,replay}.ts` derive from DeepSeek Harness, copyright (c) 2026 DeepSeek, under the MIT license included in LICENSE. The conversion modules are maintained in this package because the published DSH pi-ai package does not export its conversion helpers. Wire protocol implementations remain supplied by `@earendil-works/pi-ai`.
3
+ The adapter, settings UI, and `src/conversion/{context,stream,replay}.ts` derive from DeepSeek Harness, copyright (c) 2026 DeepSeek, under the MIT license included in LICENSE. The conversion modules are maintained in this package because the published DSH pi-ai package does not export its conversion helpers. Wire protocol implementations remain supplied by `@earendil-works/pi-ai`, installed under the plugin's `opencode-go-pi-ai` npm alias.
4
4
 
5
5
  The GitHub icon in the settings footer comes from Simple Icons, distributed under CC0 1.0 Universal.
@@ -12,7 +12,7 @@ The root package contains one coherent DSH 0.1.6-alpha.1 development environment
12
12
  | `npm run test:ci` | Verify shipped artifacts match source, then run the core suite |
13
13
  | `npm test` | Rebuild, then run the core Vitest suite |
14
14
  | `npm run test:compat` | Pack once and test the installed artifact across every supported host fixture |
15
- | `npm run test:compat -- v015-rc1 v017-rc2` | Test only the named fixtures; unknown names fail |
15
+ | `npm run test:compat -- v015-rc1 v017-rc2 v020-rc2` | Test only the named fixtures; unknown names fail |
16
16
  | `npm run test:install` | Test npm and pnpm 11.7.0 Git installs of prebuilt artifacts without plugin build approval |
17
17
  | `npm run test:install -- npm` / `-- pnpm` | Run just one package manager; unknown names fail |
18
18
  | `npm run verify` | Run core, compatibility, and Git-installation checks |
@@ -29,7 +29,7 @@ The Git-installation regression creates a fresh repository containing the shippe
29
29
 
30
30
  ## Installed-package matrix
31
31
 
32
- `scripts/compatibility-hosts.mjs` lists the eight host generations. Each `tests/hosts/<id>` has a private manifest and its own lockfile. DSH packages and their required DSH peer/dependency closure are pinned to that host generation, together with compatible Cordis packages. This prevents a broad upstream peer range from silently selecting a later host generation.
32
+ `scripts/compatibility-hosts.mjs` lists nine host generations, including DSH `0.2.0-rc.2`. Each `tests/hosts/<id>` has a private manifest and its own lockfile. DSH packages and their required DSH peer/dependency closure are pinned to that host generation, together with compatible Cordis packages. This prevents a broad upstream peer range from silently selecting a later host generation. The 0.2 fixture also installs its real `dsh-llm-pi-ai` adapter and public pi-ai dependency alongside this plugin.
33
33
 
34
34
  The runner performs these steps for each host:
35
35
 
@@ -41,7 +41,15 @@ The runner performs these steps for each host:
41
41
 
42
42
  Local compatibility runs are serial by default. Set `DSH_COMPAT_CONCURRENCY` to a positive integer to run that many isolated hosts at once; CI uses four workers and prefers its restored npm download cache, fetching missing packages as needed. Each host keeps its own dependency installation and fixture processes, and all workers consume the same tarball. A failed host does not skip queued hosts or delete files still used by running checks: the runner collects every result before cleanup. Logs are grouped by host, and the console and GitHub job summary report each result and elapsed time.
43
43
 
44
- All host JavaScript imports use ordinary Node resolution. The fixtures do not redirect DSH imports to alternate packages or load the plugin from the source checkout. The client fixture uses the consumer's React, store, and UI primitives, with a CSS loader for published host styles. It verifies the installed browser factory, settings registration, catalog injection, rendering, CSS, and cleanup. Host fixtures retain text streaming, real image processing, history/offload, limits, reasoning, live settings, and profile-bundle checks.
44
+ All host JavaScript imports use ordinary Node resolution. The fixtures do not redirect DSH imports to alternate packages or load the plugin from the source checkout. The client fixture uses the consumer's React, store, and UI primitives, with a CSS loader for published host styles. It verifies the installed browser factory, settings registration, catalog injection, rendering, CSS, and cleanup. Host fixtures retain text streaming, real image processing, history/offload, limits, reasoning, live settings, and profile-bundle checks. Each host also runs all three wire protocols through prompt and tool declaration checks, a streamed tool call, JSON-restored assistant replay, tool-result continuation, and a one-shot request with no tools.
45
+
46
+ ## Isolated pi-ai dependency
47
+
48
+ The runtime dependency `opencode-go-pi-ai` is an npm alias for `@earendil-works/pi-ai@0.87.1`. All plugin imports, including declaration types and lazy API factories, use the alias. It keeps this plugin's SDK requirement separate from the host's public `@earendil-works/pi-ai` name, so an older plugin that declares pi-ai only as a peer is not redirected by our dependency. The npm and pnpm Git-installation checks include a public pi-ai 0.85.1 and a peer-only probe, and verify that the probe retains 0.85.1 while this plugin resolves its own 0.87.1.
49
+
50
+ DSH requests still convert through `toPiContext`, preserving the existing host input and durable replay contracts. Immediately before calling a provider directly, the adapter applies pi-ai's `normalizeContext`. In 0.87.1, direct providers require `TranscriptContext`; only `Models` streaming entry points normalize legacy Context automatically. Normalizing at this call site preserves the system prompt and tool declarations on both text and image paths without tying the SDK format to the DSH version.
51
+
52
+ This upgrade does not enable developer messages, tool-change blocks, or deferred tool loading. Their existing `UNSUPPORTED_CONTENT` errors remain explicit. The minimum DSH version and peer ranges stay unchanged; new host fixtures verify runtime compatibility rather than expanding the supported content vocabulary.
45
53
 
46
54
  There is one explicit npm prerelease exception: DSH evaluates plugin ranges with prereleases included, whereas npm does not generally accept a future prerelease under `>=0.1.5-rc.1`. Only the tarball-install step uses `--legacy-peer-deps` after the host has passed strict installation. This is not a project `.npmrc` policy and does not affect root installs. The post-install version assertions ensure npm did not replace pinned host packages. Changing the plugin's published version policy is a separate decision.
47
55
 
@@ -57,6 +65,8 @@ npm install --package-lock-only --ignore-scripts --strict-peer-deps
57
65
 
58
66
  Copy the resulting lockfile back to its fixture directory. Generating it outside the checkout prevents an existing root `node_modules` from influencing resolution. Then run the selected compatibility check and the full verification before release. Root development dependency changes require the same strict resolution discipline.
59
67
 
68
+ After changing the root lockfile, verify a clean `npm ci --strict-peer-deps` and `npm run test:ci` with each supported Node version's bundled npm. Node 22.19.0 ships npm 10.9.3; running Node 22 with a newer npm does not check that install contract. In this dependency tree, npm 10 requires the optional esbuild peer used by Vite under Vitest to be recorded, even when npm 11 can install without that record. Keep its platform packages in the lockfile as well.
69
+
60
70
  The GitHub workflow runs core and Git-installation checks on Linux and Windows with Node 22.19.0 and 24. Core tests, npm installs, and pnpm installs run as independent jobs, so one slow installation does not consume another check's time allowance. Installation jobs need no root `npm ci`: their runner uses only Node built-ins, and the fresh Git fixture supplies committed artifacts and must not need build dependencies or plugin build approval. The complete installed-host matrix runs separately on Linux/Node 24.
61
71
 
62
72
  Installation jobs reuse the npm download cache and a separate pnpm store, preferring cached packages while still fetching missing data. They never restore the checkout's `node_modules` or `lib/`. CI keeps the npm cache, isolated consumers, and the cached pnpm store under `RUNNER_TEMP`, on the runner's work volume. `DSH_PNPM_STORE_DIR` selects the pnpm store; local runs use the system temporary directory and the default package caches unless overridden.
@@ -4,7 +4,7 @@ The plugin owns its Host adapter, Web settings entry, build, package manifest, a
4
4
 
5
5
  The Host depends on DSH services through peer dependencies so Cordis service classes and LLM error identities remain shared with the application. The Client resolves React, the store, and UI primitives through the DSH module table. UI type packages are optional peers because headless installations do not mount those services; this project's development dependencies supply the types and test runtime libraries.
6
6
 
7
- The package's bundle inserts `id: opencode-go`. The original fork's `llm-opencode-go` row can be disabled by a profile patch. Plugin registration does not select the user's default model or replace another provider automatically.
7
+ The package's bundle keeps the configuration entry `id: opencode-go` so existing settings remain attached. Its LLM provider route is independently named `dsh-opencode-go`, shown as `DSH OpenCode Go`, and can coexist with the generic adapter's `opencode-go` route. The SDK retains its native `opencode-go` identity for transport behavior; replay metadata records the DSH route and an optional `sdkProvider` to restore the native identity. The original fork's `llm-opencode-go` row can be disabled by a profile patch. Plugin registration does not select the user's default model or replace another provider automatically. Saved session/preset provider selections must be updated explicitly.
8
8
 
9
9
  Published `@deepseek-ai/dsh-llm-pi-ai` versions `0.1.5-rc.2` and `0.1.6-alpha.1` do not export `./conversion`. This package therefore carries the request-history, stream, and replay conversion modules under `src/conversion`, with the upstream MIT license. It does not import unpublished files or bundle the whole adapter. Protocol transport remains implemented by pi-ai. Conversion maintenance is now this project's responsibility; a future public conversion library can replace these modules once published and tested.
10
10
 
@@ -1,5 +1,49 @@
1
1
  # Verification
2
2
 
3
+ ## Release 0.1.17 validation (2026-09-30)
4
+
5
+ After integrating the npm 10 lockfile correction, the 0.1.17 manifest passed Host/Client type checks, all **367 tests in 26 files**, and freshness checks for **32 shipped artifacts** on macOS / Node 24.14.1. Both npm and pnpm 11.7.0 Git installs passed without plugin build approval; the installed package uses private pi-ai 0.87.1 while the independent peer probe keeps public pi-ai 0.85.1. The nine-generation runtime compatibility results below remain applicable: the subsequent integration changes only lockfile metadata, documentation and the package version.
6
+
7
+ ## Independent DSH provider route (2026-09-30)
8
+
9
+ The public LLM route is now `dsh-opencode-go`, shown as **DSH OpenCode Go**, and does not register an `opencode-go` alias. This permits the generic host adapter and the plugin to register in either order. The SDK's native provider remains `opencode-go`, preserving its OpenCode-specific reasoning serialization and models.dev lookup. Host and Client share the route constant; settings discovery and the usage badge follow the new route. The profile entry `opencode-go`, settings namespace, credentials and metadata cache retain their existing identities.
10
+
11
+ Durable replay still uses envelope version 2. Its `provider` matches the DSH assistant source; the optional `sdkProvider` restores the SDK's native identity. JSON-restored requests retain reasoning signatures and tool replay. A saved session or Agent/headless preset selecting the old route must explicitly select `dsh-opencode-go`; DSH's existing cross-adapter ownership rules strip old private replay metadata while keeping durable session content. The plugin does not rewrite user profiles or historical route selections.
12
+
13
+ Before the change, `npx vitest run tests/provider-identity.spec.ts` failed all three regressions: plugin-first registration prevented the host route from mounting, host-first registration left the plugin without its own route, and the independent route had no replay state. These now pass, including real loopback streams for both routes, JSON-restored reasoning continuation, and old-session content after switching. The usage badge also verifies that selecting the host's `opencode-go` starts no plugin polling.
14
+
15
+ Host/Client type checks, production compilation, all **367 tests in 26 files**, and freshness checks for all **32 shipped build files** passed on macOS / Node 24.14.1. `DSH_COMPAT_CONCURRENCY=3 npm run test:compat` passed all **nine isolated installed-host generations**, from 0.1.5-rc.1 through 0.2.0-rc.2, including three-protocol replay, reasoning, images, Client rendering, live settings and applicable Desktop/Web/Headless bundle gates. The run took 884 seconds, dominated by npm installation in the first three consumers. No live inference, installed user profile, GitHub issue or npm publication was changed. This addresses route collisions; the stale protocol dependency reported in Issue #26 remains a separate investigation.
16
+
17
+ ## Issue #22: gzip metadata download on slow connections (2026-09-30)
18
+
19
+ Only the models.dev request now negotiates `accept-encoding: gzip`; listing and usage requests retain `identity`. Requests still use the host's global Fetch and dispatcher. Normally Fetch decodes gzip. If the Issue #7 HTTP/2 mismatch instead delivers raw gzip bytes, the metadata reader recognizes the gzip signature and asynchronously decompresses that one format. Both delivered input and decoded output remain bounded to 16 MiB. Download, connection retry, and recovery decompression share the original 30-second abort signal; corrupt gzip is not retried. No cache format, persistence policy, or proxy configuration changed.
20
+
21
+ Before the change, `vitest run tests/metadata-transport.spec.ts` failed both regressions: a cold catalog timed out transferring approximately 5.23 MB over a local bandwidth-limited HTTP connection, and raw gzip metadata with missing encoding headers failed JSON parsing. Both pass after the change. The bandwidth fixture gives both representations the same 16 KiB per 10 ms transfer budget and scales the deadline to 500 ms. Further regressions cover Fetch-decoded gzip, raw gzip with present or missing headers, input/output limits, corrupt data, cancellation, listing negotiation, and persisted ETag verification.
22
+
23
+ A separate real TLS/HTTP/2 fixture used Node 24.14.1's built-in Fetch with an Undici 8.11.0 global ProxyAgent through a local CONNECT tunnel. The control lost encoding headers and returned raw gzip that native JSON parsing rejected. The shared reader and an actual cold catalog both succeeded through the same proxy; all four reads used HTTP/2. The fixture trusted its own certificate and did not disable TLS verification. This recovers gzip bodies without changing the host's dispatcher; it does not repair the host's underlying header loss.
24
+
25
+ Host/Client type checks, production compilation, all **363 tests in 25 files**, and freshness checks for all **31 shipped build files** passed on macOS / Node 24.14.1. A public read-only probe through the updated reader received HTTP 200 with gzip and parsed 5,264,592 bytes of JSON, including 33 OpenCode Go models, in **6.83 seconds**. This measures decoded data, not wire transfer size; the response supplied no Content-Length. Existing upstream missing-source-map warnings remain. The local fixture and public probe do not guarantee connectivity on the reporter's network.
26
+
27
+ The updated package also passed `npm run test:compat -- v020-rc2` against isolated, real DSH **0.2.0-rc.2** dependencies: Host activation/RPC/streaming/images, all three transcript protocols, reasoning, Client settings/rendering/CSS, live profile settings, and bundle admission. The bundle fixture now explicitly checks **Desktop, Web and Headless** profile directories. The installed macOS Desktop app's manifest reports 0.2.0-rc.2, and a separate gzip-reader probe passed using its actual bundled **Node 24.21.0** executable. These are plugin compatibility and runtime checks, not an end-to-end Desktop GUI session; no installed user profile was updated. The npm latest version remains 0.1.16, which does not yet contain the repository's 0.2 adaptation or this gzip change.
28
+
29
+ ## Node 22 bundled-npm clean installs (2026-09-30)
30
+
31
+ The [Issue #24 main-branch workflow](https://github.com/Duskriver/dsh-opencode-go/actions/runs/36696633511) passed 11 of 13 jobs. Both Linux and Windows core jobs on Node 22.19.0 failed during `npm ci --strict-peer-deps`, before running tests, with `Missing: esbuild@0.28.2 from lock file`. Their bundled npm is 10.9.3. The previous local Node 22 checks used npm 11, so they did not cover that clean-install contract.
32
+
33
+ A fresh fixture containing only the manifest and lockfile reproduced the same error with Node 22.19.0 / npm 10.9.3 in offline dry-run mode, while npm 11 accepted the same files. Vite under Vitest declares an optional esbuild peer whose range excludes the root build tool's esbuild 0.25.12. npm 10 resolves a nested 0.28.2 and checks its lock entries; npm 11 can omit them. The fix records that optional peer and all 26 platform packages. All existing lock entries, package versions, and platform metadata are preserved.
34
+
35
+ The same isolated-fixture command rejects the original lockfile and accepts the corrected one. Clean strict installs and `npm run test:ci` passed on macOS with Node 22.19.0 / npm 10.9.3 and Node 24.14.1 / npm 11.11.0: **354 tests in 24 files** and all **31** shipped artifacts matching source in each environment. The existing Linux/Windows core jobs retain their clean-install check as the regression guard. Development guidance now requires each supported Node version's bundled npm when verifying a changed lockfile.
36
+
37
+ ## Issue #24: DSH 0.2.0 and pi-ai transcript compatibility (2026-09-30)
38
+
39
+ The plugin now uses pi-ai **0.87.1** through the `opencode-go-pi-ai` npm alias. All runtime imports, lazy protocol factories, and declaration imports use that alias, leaving the host's public pi-ai dependency to resolve independently. Before a direct provider call, the adapter applies the official `normalizeContext` to the existing text or image conversion result. This preserves system prompts and tool declarations under the new transcript contract while retaining DSH `0.1.5-rc.1` as the minimum supported host. Tool argument replay uses the SDK's JSON object type; the persisted replay envelope remains version 2.
40
+
41
+ The installed-package matrix now includes **DSH 0.2.0-rc.2**, with all 50 DSH dependency/peer package entries pinned to that generation and its real `dsh-llm-pi-ai` adapter and public pi-ai 0.87.1 installed. Every host exercises all three protocols through leading system prompts, tool declarations and schemas, streamed tool calls, JSON-restored assistant replay, tool-result continuation, and one-shot prompts with no tool leakage. Existing real image preparation, offloading, reasoning, settings, client rendering, and Web/Headless admission checks remain enabled. A separate payload-only control confirmed that each direct API loses the prompt and tools without normalization and preserves both with it; the capture hook stopped execution before any network request.
42
+
43
+ Both npm and pnpm Git-installation fixtures now contain public pi-ai **0.85.1** and a plugin that declares only a broad pi-ai peer. Normal module resolution confirmed that the peer still receives 0.85.1 while this plugin receives its aliased 0.87.1. These checks use prebuilt Git artifacts without source files or plugin build approval. The updated SDK's built-in catalog now includes DeepSeek V4.1 and no longer offers its off effort; tests that need an unknown model use a deliberately fictional ID, and the off-effort transport test uses DeepSeek V4, which still declares that capability.
44
+
45
+ Validation passed on macOS: Host/Client type checks, all **31** shipped build artifacts matching source, **354 tests in 24 files** on Node **24.14.1** and minimum-version **22.19.0**, all **nine installed-host environments** on Node 24, npm and pnpm **11.7.0** Git installs on Node 24, and an additional npm Git install on Node 22.19.0. Existing upstream missing-source-map and peer warnings remain non-failing. Tests use isolated consumers and local fixture endpoints; no user profile, live inference, npm publication, or GitHub issue was changed. Developer messages, tool-change blocks, and deferred loading retain their explicit unsupported-content errors.
46
+
3
47
  ## Release 0.1.16 integration (2026-09-29)
4
48
 
5
49
  The metadata-cache fix was rebased onto the merged settings-page changes from PR #21. The combined release passed Host/Client type checks and the production build, **347 tests in 24 files**, artifact freshness checks, all **eight installed-host generations**, and **npm/pnpm Git installation without plugin build approval**. Validation ran on macOS / Node 24.14.1 using isolated consumers and fixture endpoints. The earlier 336-test result below describes the cache change before integrating the latest main branch.
@@ -1,4 +1,4 @@
1
1
  - id: agent-default-model
2
2
  config:
3
- provider: opencode-go
3
+ provider: dsh-opencode-go
4
4
  model: deepseek-v4.1-flash
package/lib/client.js CHANGED
@@ -145,10 +145,10 @@ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id) + "]")
145
145
  const style = document.createElement("style");
146
146
  style.dataset.plugin = "dsh-opencode-go";
147
147
  style.dataset.pluginCss = id;
148
- style.textContent = ".N807zW_page{flex-direction:column;min-width:0;height:100%;min-height:0;display:flex;container-type:size}.N807zW_pageBody{scrollbar-gutter:stable;flex-direction:column;flex:1;min-height:0;padding-right:4px;display:flex;overflow-y:auto}.N807zW_pageBody,.N807zW_modelList,.N807zW_modelDetails{scrollbar-width:thin;scrollbar-color:var(--dsw-alias-border-l3) transparent}.N807zW_pageBody::-webkit-scrollbar,.N807zW_modelList::-webkit-scrollbar,.N807zW_modelDetails::-webkit-scrollbar{width:8px;height:8px}.N807zW_pageBody::-webkit-scrollbar-track,.N807zW_modelList::-webkit-scrollbar-track,.N807zW_modelDetails::-webkit-scrollbar-track{background:0 0}.N807zW_pageBody::-webkit-scrollbar-thumb,.N807zW_modelList::-webkit-scrollbar-thumb,.N807zW_modelDetails::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);background-clip:content-box;border:2px solid #0000;border-radius:999px}.N807zW_pageBody:hover::-webkit-scrollbar-thumb,.N807zW_modelList:hover::-webkit-scrollbar-thumb,.N807zW_modelDetails:hover::-webkit-scrollbar-thumb{background:var(--dsw-alias-label-tertiary);background-clip:content-box}.N807zW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.N807zW_field+.N807zW_field{border-top:.5px solid var(--dsw-alias-border-l2)}.N807zW_head{align-items:center;gap:8px;display:flex}.N807zW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.N807zW_badges{align-items:center;gap:8px;display:inline-flex}.N807zW_trailing{flex:none;align-items:center;gap:8px;display:inline-flex}.N807zW_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}.N807zW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.N807zW_reset:disabled{cursor:default}.N807zW_reset:focus-visible,.N807zW_cardTrigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.N807zW_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}.N807zW_input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px;border-color:var(--dsw-alias-brand-primary)}.N807zW_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.N807zW_inputInvalid{border-color:var(--dsw-alias-label-error);}.N807zW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.N807zW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.N807zW_pageTop{flex-direction:column;gap:3px;padding:2px 0 12px;display:flex}.N807zW_title{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:600;line-height:1.4}.N807zW_intro{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:1.5}.N807zW_card,.N807zW_cardModels{border:.5px solid var(--dsw-alias-border-l2);background:color-mix(in srgb, var(--dsw-alias-bg-layer-3) 40%, transparent);border-radius:12px;padding:8px 0 10px}.N807zW_card{flex:none}.N807zW_card+.N807zW_card,.N807zW_card+.N807zW_cardModels,.N807zW_cardModels+.N807zW_card{margin-top:8px}.N807zW_cardModels{flex-direction:column;flex:auto;max-height:720px;display:flex}.N807zW_cardHead{align-items:center;gap:8px;min-height:24px;padding:0 14px;display:flex}.N807zW_cardTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13.5px;font-weight:600;line-height:1.5}.N807zW_cardSub{color:var(--dsw-alias-label-tertiary);flex:none;font-size:11.5px;line-height:1.5}.N807zW_cardBody{flex-direction:column;gap:8px;padding:4px 14px 0;display:flex}.N807zW_cardBody>.N807zW_input{box-sizing:border-box;width:100%}.N807zW_cardModels>.N807zW_cardBody{flex:auto}.N807zW_cardDivider{border-top:.5px solid var(--dsw-alias-border-l2);margin:10px 14px}.N807zW_cardTrigger{width:100%;min-height:24px;font:inherit;text-align:left;color:inherit;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;margin:0;padding:0 14px;display:flex}.N807zW_cardTrigger:hover .N807zW_cardTitle{color:var(--dsw-alias-brand-primary)}.N807zW_advancedGrid{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px 18px;display:grid}.N807zW_advancedGrid .N807zW_field{padding:0}.N807zW_advancedGrid .N807zW_field+.N807zW_field{border-top:none}.N807zW_limitsEditor{flex-direction:column;flex:auto;gap:10px;padding-top:4px;display:flex}.N807zW_limitsSummary{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-bottom:8px;font-size:12px;line-height:1.5}.N807zW_modelMeta{align-items:baseline;gap:10px;display:flex}.N807zW_modelMeta .N807zW_limitsModelId{flex:1;min-width:0}.N807zW_releaseDate{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:11px;line-height:1.4}.N807zW_limitsModelId{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:1.4;display:block;overflow:hidden}.N807zW_visuallyHidden{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.N807zW_actions{border-top:.5px solid var(--dsw-alias-border-l2);flex-wrap:wrap;flex:none;align-items:center;gap:8px;padding:12px 0;display:flex}.N807zW_starLink{max-width:100%;color:var(--dsw-alias-label-secondary);border-radius:4px;align-items:center;gap:7px;margin-left:auto;padding:6px 0;font-size:12px;line-height:1.5;text-decoration:none;display:inline-flex}.N807zW_starLink svg{flex:none}.N807zW_starLink:hover{color:var(--dsw-alias-brand-primary)}.N807zW_starLink:hover span{text-underline-offset:3px;text-decoration:underline}.N807zW_starLink:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.N807zW_failedNote{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.N807zW_slot{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.N807zW_slot svg{width:15px;height:15px}.N807zW_chevron,.N807zW_chevronOpen{width:14px;height:14px;color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .15s}.N807zW_chevron{transform:rotate(-90deg)}.N807zW_filters{flex-wrap:wrap;gap:6px;display:flex}.N807zW_filter{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:6px;padding:6px 9px;font-size:12px}.N807zW_filter[aria-pressed=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3)}.N807zW_filter span{color:var(--dsw-alias-label-tertiary);margin-left:4px}.N807zW_modelLayout{border:.5px solid var(--dsw-alias-border-l4);border-radius:10px;flex:auto;grid-template-columns:minmax(0,45fr) minmax(0,55fr);min-height:260px;max-height:560px;display:grid;overflow:hidden}.N807zW_modelList{border-right:.5px solid var(--dsw-alias-border-l2);min-height:0;padding:4px 6px;overflow-y:auto}.N807zW_modelRow{align-items:center;gap:8px;padding-right:6px;display:flex}.N807zW_modelRow+.N807zW_modelRow{border-top:.5px solid var(--dsw-alias-border-l2)}.N807zW_modelRowSelected{background:var(--dsw-alias-bg-layer-3);border-top-color:#0000;border-radius:6px}.N807zW_modelRow>[role=switch]{flex-shrink:0}.N807zW_modelChoice{text-align:left;min-width:0;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:0;border-radius:6px;flex:1;align-items:baseline;padding:9px 3px 9px 6px;display:flex}.N807zW_modelRow:hover:not(.N807zW_modelRowSelected){background:color-mix(in srgb, var(--dsw-alias-bg-layer-3) 92%, var(--dsw-alias-label-primary));border-top-color:#0000;border-radius:6px}.N807zW_modelName{overflow-wrap:anywhere;flex-wrap:wrap;flex:0 auto;align-items:center;gap:5px;min-width:0;font-size:13px;display:flex}.N807zW_overrideDot{background:var(--dsw-alias-brand-primary);border-radius:50%;flex:none;width:6px;height:6px}.N807zW_modelDetails{flex-direction:column;gap:10px;min-height:0;padding:14px;display:flex;overflow-y:auto}.N807zW_modelDetails h3{overflow-wrap:anywhere;margin:0;font-size:14px}.N807zW_modelHeading{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.N807zW_stats{flex-direction:column;gap:10px;display:flex}.N807zW_stat{flex-direction:column;gap:4px;display:flex}.N807zW_statLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5}.N807zW_modelDetails .N807zW_input{box-sizing:border-box;width:100%;min-width:0;height:30px;padding:0 9px;font-size:12px}.N807zW_modalities{flex-wrap:wrap;gap:4px;display:flex}.N807zW_modality,.N807zW_modalityOff{border-radius:6px;padding:2px 7px;font-size:11px;line-height:1.5}.N807zW_modality{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.N807zW_modalityOff{border:.5px dashed var(--dsw-alias-border-l4);color:var(--dsw-alias-label-tertiary)}.N807zW_modelDetails .N807zW_reset{align-self:flex-start}.N807zW_paneFoot{color:var(--dsw-alias-label-tertiary);margin:auto 0 0;font-size:11px;line-height:1.5}.N807zW_newBadge{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l4);border-radius:4px;padding:2px 4px;font-size:9px}.N807zW_modelChoice:focus-visible,.N807zW_filter:focus-visible,.N807zW_cardTrigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}@container (width<=440px){.N807zW_advancedGrid{grid-template-columns:minmax(0,1fr)}.N807zW_cardModels{max-height:none}.N807zW_modelLayout{flex:none;grid-template-columns:minmax(0,1fr);max-height:none}.N807zW_modelList{border-right:0;border-bottom:.5px solid var(--dsw-alias-border-l2);max-height:240px}.N807zW_modelDetails{max-height:460px}}@media (prefers-reduced-motion:reduce){.N807zW_chevron,.N807zW_chevronOpen{transition:none}}";
148
+ style.textContent = ".N807zW_page{flex-direction:column;min-width:0;height:100%;min-height:0;display:flex;container-type:size}.N807zW_pageBody{scrollbar-gutter:stable;flex-direction:column;flex:1;min-height:0;padding:0 4px 16px 0;display:flex;overflow-y:auto}.N807zW_pageBody,.N807zW_modelList,.N807zW_modelDetails{scrollbar-width:thin;scrollbar-color:var(--dsw-alias-border-l3) transparent}.N807zW_pageBody::-webkit-scrollbar,.N807zW_modelList::-webkit-scrollbar,.N807zW_modelDetails::-webkit-scrollbar{width:8px;height:8px}.N807zW_pageBody::-webkit-scrollbar-track,.N807zW_modelList::-webkit-scrollbar-track,.N807zW_modelDetails::-webkit-scrollbar-track{background:0 0}.N807zW_pageBody::-webkit-scrollbar-thumb,.N807zW_modelList::-webkit-scrollbar-thumb,.N807zW_modelDetails::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);background-clip:content-box;border:2px solid #0000;border-radius:999px}.N807zW_pageBody:hover::-webkit-scrollbar-thumb,.N807zW_modelList:hover::-webkit-scrollbar-thumb,.N807zW_modelDetails:hover::-webkit-scrollbar-thumb{background:var(--dsw-alias-label-tertiary);background-clip:content-box}.N807zW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.N807zW_field+.N807zW_field{border-top:.5px solid var(--dsw-alias-border-l2)}.N807zW_head{align-items:center;gap:8px;display:flex}.N807zW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.N807zW_badges{align-items:center;gap:8px;display:inline-flex}.N807zW_trailing{flex:none;align-items:center;gap:8px;display:inline-flex}.N807zW_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}.N807zW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.N807zW_reset:disabled{cursor:default}.N807zW_reset:focus-visible,.N807zW_cardTrigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.N807zW_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}.N807zW_input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px;border-color:var(--dsw-alias-brand-primary)}.N807zW_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.N807zW_inputInvalid{border-color:var(--dsw-alias-label-error);}.N807zW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.N807zW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.N807zW_pageTop{flex-direction:column;gap:3px;padding:2px 0 12px;display:flex}.N807zW_title{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:600;line-height:1.4}.N807zW_intro{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:1.5}.N807zW_card,.N807zW_cardModels{border:.5px solid var(--dsw-alias-border-l2);background:color-mix(in srgb, var(--dsw-alias-bg-layer-3) 40%, transparent);border-radius:12px;padding:8px 0 10px}.N807zW_card{flex:none}.N807zW_card+.N807zW_card,.N807zW_card+.N807zW_cardModels,.N807zW_cardModels+.N807zW_card{margin-top:8px}.N807zW_cardModels{flex-direction:column;flex:1 0 auto;display:flex}.N807zW_cardFolded{flex:none}.N807zW_cardHead{align-items:center;gap:8px;min-height:24px;padding:0 14px;display:flex}.N807zW_cardTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13.5px;font-weight:600;line-height:1.5}.N807zW_cardSub{color:var(--dsw-alias-label-tertiary);flex:none;font-size:11.5px;line-height:1.5}.N807zW_cardBody{flex-direction:column;gap:8px;padding:4px 14px 0;display:flex}.N807zW_cardBody>.N807zW_input{box-sizing:border-box;width:100%}.N807zW_cardModels>.N807zW_cardBody{flex:1 0 auto}.N807zW_cardNotes{flex-direction:column;gap:8px;padding:6px 14px 2px;display:flex}.N807zW_cardNotes:empty{display:none}.N807zW_cardDivider{border-top:.5px solid var(--dsw-alias-border-l2);margin:10px 14px}.N807zW_cardTrigger{width:100%;min-height:24px;font:inherit;text-align:left;color:inherit;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;margin:0;padding:0 14px;display:flex}.N807zW_cardTrigger:hover .N807zW_cardTitle{color:var(--dsw-alias-brand-primary)}.N807zW_advancedGrid{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px 18px;display:grid}.N807zW_advancedGrid .N807zW_field{padding:0}.N807zW_advancedGrid .N807zW_field+.N807zW_field{border-top:none}.N807zW_limitsEditor{flex-direction:column;flex:1 0 auto;gap:10px;padding-top:4px;display:flex}.N807zW_limitsFoot{border-top:.5px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;margin:2px -14px 0;padding:10px 14px 0;display:flex}.N807zW_limitsSummary{min-width:0;color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:1;font-size:12px;line-height:1.5}.N807zW_modelMeta{align-items:baseline;gap:10px;display:flex}.N807zW_modelMeta .N807zW_limitsModelId{flex:1;min-width:0}.N807zW_releaseDate{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:11px;line-height:1.4}.N807zW_limitsModelId{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:1.4;display:block;overflow:hidden}.N807zW_visuallyHidden{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.N807zW_actions{border-top:.5px solid var(--dsw-alias-border-l2);flex-wrap:wrap;flex:none;align-items:center;gap:8px;padding:12px 0;display:flex}.N807zW_starLink{max-width:100%;color:var(--dsw-alias-label-secondary);border-radius:4px;align-items:center;gap:7px;margin-left:auto;padding:6px 0;font-size:12px;line-height:1.5;text-decoration:none;display:inline-flex}.N807zW_starLink svg{flex:none}.N807zW_starLink:hover{color:var(--dsw-alias-brand-primary)}.N807zW_starLink:hover span{text-underline-offset:3px;text-decoration:underline}.N807zW_starLink:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.N807zW_failedNote{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.N807zW_slot{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.N807zW_slot svg{width:15px;height:15px}.N807zW_chevron,.N807zW_chevronOpen{width:14px;height:14px;color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .15s}.N807zW_chevron{transform:rotate(-90deg)}.N807zW_filters{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.N807zW_filterAction{align-items:center;margin-left:auto;display:inline-flex}.N807zW_filter{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:6px;padding:6px 9px;font-size:12px}.N807zW_filter[aria-pressed=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3)}.N807zW_filter span{color:var(--dsw-alias-label-tertiary);margin-left:4px}.N807zW_modelLayout{border:.5px solid var(--dsw-alias-border-l4);border-radius:10px;flex:auto;grid-template-columns:minmax(0,45fr) minmax(0,55fr);min-height:260px;max-height:560px;display:grid;overflow:hidden}.N807zW_modelList{border-right:.5px solid var(--dsw-alias-border-l2);min-height:0;padding:4px 6px;overflow-y:auto}.N807zW_modelRow{align-items:center;gap:8px;padding-right:6px;display:flex}.N807zW_modelRow+.N807zW_modelRow{border-top:.5px solid var(--dsw-alias-border-l2)}.N807zW_modelRowSelected{background:var(--dsw-alias-bg-layer-3);border-top-color:#0000;border-radius:6px}.N807zW_modelRow>[role=switch]{flex-shrink:0}.N807zW_modelChoice{text-align:left;min-width:0;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:0;border-radius:6px;flex:1;align-items:baseline;padding:9px 3px 9px 6px;display:flex}.N807zW_modelRow:hover:not(.N807zW_modelRowSelected){background:color-mix(in srgb, var(--dsw-alias-bg-layer-3) 92%, var(--dsw-alias-label-primary));border-top-color:#0000;border-radius:6px}.N807zW_modelName{overflow-wrap:anywhere;flex-wrap:wrap;flex:0 auto;align-items:center;gap:5px;min-width:0;font-size:13px;display:flex}.N807zW_overrideDot{background:var(--dsw-alias-brand-primary);border-radius:50%;flex:none;width:6px;height:6px}.N807zW_modelDetails{flex-direction:column;gap:10px;min-height:0;padding:14px;display:flex;overflow-y:auto}.N807zW_modelDetails h3{overflow-wrap:anywhere;margin:0;font-size:14px}.N807zW_modelHeading{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.N807zW_stats{flex-direction:column;gap:10px;display:flex}.N807zW_stat{flex-direction:column;gap:4px;display:flex}.N807zW_statLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5}.N807zW_modelDetails .N807zW_input{box-sizing:border-box;width:100%;min-width:0;height:30px;padding:0 9px;font-size:12px}.N807zW_modalities{flex-wrap:wrap;gap:4px;display:flex}.N807zW_modality,.N807zW_modalityOff{border-radius:6px;padding:2px 7px;font-size:11px;line-height:1.5}.N807zW_modality{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.N807zW_modalityOff{border:.5px dashed var(--dsw-alias-border-l4);color:var(--dsw-alias-label-tertiary)}.N807zW_modelDetails .N807zW_reset{align-self:flex-start}.N807zW_paneFoot{color:var(--dsw-alias-label-tertiary);margin:auto 0 0;font-size:11px;line-height:1.5}.N807zW_newBadge{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l4);border-radius:4px;padding:2px 4px;font-size:9px}.N807zW_modelChoice:focus-visible,.N807zW_filter:focus-visible,.N807zW_cardTrigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}@container (width<=440px){.N807zW_advancedGrid{grid-template-columns:minmax(0,1fr)}.N807zW_modelLayout{flex:none;grid-template-columns:minmax(0,1fr);max-height:none}.N807zW_modelList{border-right:0;border-bottom:.5px solid var(--dsw-alias-border-l2);max-height:240px}.N807zW_modelDetails{max-height:460px}}@media (prefers-reduced-motion:reduce){.N807zW_chevron,.N807zW_chevronOpen{transition:none}}";
149
149
  document.head.appendChild(style);
150
150
  }
151
- var Section_default = { "actions": "N807zW_actions", "advancedGrid": "N807zW_advancedGrid", "badges": "N807zW_badges", "card": "N807zW_card", "cardBody": "N807zW_cardBody", "cardDivider": "N807zW_cardDivider", "cardHead": "N807zW_cardHead", "cardModels": "N807zW_cardModels", "cardSub": "N807zW_cardSub", "cardTitle": "N807zW_cardTitle", "cardTrigger": "N807zW_cardTrigger", "chevron": "N807zW_chevron", "chevronOpen": "N807zW_chevronOpen", "failedNote": "N807zW_failedNote", "field": "N807zW_field", "filter": "N807zW_filter", "filters": "N807zW_filters", "head": "N807zW_head", "hint": "N807zW_hint", "input": "N807zW_input", "inputInvalid": "N807zW_inputInvalid N807zW_input", "intro": "N807zW_intro", "invalid": "N807zW_invalid", "label": "N807zW_label", "limitsEditor": "N807zW_limitsEditor", "limitsModelId": "N807zW_limitsModelId", "limitsSummary": "N807zW_limitsSummary", "modalities": "N807zW_modalities", "modality": "N807zW_modality", "modalityOff": "N807zW_modalityOff", "modelChoice": "N807zW_modelChoice", "modelDetails": "N807zW_modelDetails", "modelHeading": "N807zW_modelHeading", "modelLayout": "N807zW_modelLayout", "modelList": "N807zW_modelList", "modelMeta": "N807zW_modelMeta", "modelName": "N807zW_modelName", "modelRow": "N807zW_modelRow", "modelRowSelected": "N807zW_modelRowSelected", "newBadge": "N807zW_newBadge", "overrideDot": "N807zW_overrideDot", "page": "N807zW_page", "pageBody": "N807zW_pageBody", "pageTop": "N807zW_pageTop", "paneFoot": "N807zW_paneFoot", "releaseDate": "N807zW_releaseDate", "reset": "N807zW_reset", "slot": "N807zW_slot", "starLink": "N807zW_starLink", "stat": "N807zW_stat", "statLabel": "N807zW_statLabel", "stats": "N807zW_stats", "title": "N807zW_title", "trailing": "N807zW_trailing", "visuallyHidden": "N807zW_visuallyHidden" };
151
+ var Section_default = { "actions": "N807zW_actions", "advancedGrid": "N807zW_advancedGrid", "badges": "N807zW_badges", "card": "N807zW_card", "cardBody": "N807zW_cardBody", "cardDivider": "N807zW_cardDivider", "cardFolded": "N807zW_cardFolded", "cardHead": "N807zW_cardHead", "cardModels": "N807zW_cardModels", "cardNotes": "N807zW_cardNotes", "cardSub": "N807zW_cardSub", "cardTitle": "N807zW_cardTitle", "cardTrigger": "N807zW_cardTrigger", "chevron": "N807zW_chevron", "chevronOpen": "N807zW_chevronOpen", "failedNote": "N807zW_failedNote", "field": "N807zW_field", "filter": "N807zW_filter", "filterAction": "N807zW_filterAction", "filters": "N807zW_filters", "head": "N807zW_head", "hint": "N807zW_hint", "input": "N807zW_input", "inputInvalid": "N807zW_inputInvalid N807zW_input", "intro": "N807zW_intro", "invalid": "N807zW_invalid", "label": "N807zW_label", "limitsEditor": "N807zW_limitsEditor", "limitsFoot": "N807zW_limitsFoot", "limitsModelId": "N807zW_limitsModelId", "limitsSummary": "N807zW_limitsSummary", "modalities": "N807zW_modalities", "modality": "N807zW_modality", "modalityOff": "N807zW_modalityOff", "modelChoice": "N807zW_modelChoice", "modelDetails": "N807zW_modelDetails", "modelHeading": "N807zW_modelHeading", "modelLayout": "N807zW_modelLayout", "modelList": "N807zW_modelList", "modelMeta": "N807zW_modelMeta", "modelName": "N807zW_modelName", "modelRow": "N807zW_modelRow", "modelRowSelected": "N807zW_modelRowSelected", "newBadge": "N807zW_newBadge", "overrideDot": "N807zW_overrideDot", "page": "N807zW_page", "pageBody": "N807zW_pageBody", "pageTop": "N807zW_pageTop", "paneFoot": "N807zW_paneFoot", "releaseDate": "N807zW_releaseDate", "reset": "N807zW_reset", "slot": "N807zW_slot", "starLink": "N807zW_starLink", "stat": "N807zW_stat", "statLabel": "N807zW_statLabel", "stats": "N807zW_stats", "title": "N807zW_title", "trailing": "N807zW_trailing", "visuallyHidden": "N807zW_visuallyHidden" };
152
152
 
153
153
  // src/client/ModelEditor.tsx
154
154
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -162,7 +162,7 @@ var MODALITY_COPY = {
162
162
  function hasCapacityOverride(limit) {
163
163
  return limit?.contextWindow != null || limit?.maxTokens != null;
164
164
  }
165
- function ModelEditor({ models, draft, modelVisibility, t, locale, disabled, visibilitySaving, onEdit, onModelEnabled }) {
165
+ function ModelEditor({ models, draft, modelVisibility, t, locale, disabled, visibilitySaving, filterAction, onEdit, onModelEnabled }) {
166
166
  const [query, setQuery] = (0, import_react.useState)("");
167
167
  const [filter, setFilter] = (0, import_react.useState)("all");
168
168
  const [selected, setSelected] = (0, import_react.useState)();
@@ -214,23 +214,26 @@ function ModelEditor({ models, draft, modelVisibility, t, locale, disabled, visi
214
214
  }
215
215
  }
216
216
  ),
217
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: Section_default.filters, role: "group", "aria-label": t("filterLabel"), children: filters.map(([key, label, count]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
218
- "button",
219
- {
220
- type: "button",
221
- className: Section_default.filter,
222
- "aria-pressed": filter === key,
223
- onClick: () => {
224
- setFilter(key);
217
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: Section_default.filters, role: "group", "aria-label": t("filterLabel"), children: [
218
+ filters.map(([key, label, count]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
219
+ "button",
220
+ {
221
+ type: "button",
222
+ className: Section_default.filter,
223
+ "aria-pressed": filter === key,
224
+ onClick: () => {
225
+ setFilter(key);
226
+ },
227
+ children: [
228
+ t(label),
229
+ " ",
230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: count })
231
+ ]
225
232
  },
226
- children: [
227
- t(label),
228
- " ",
229
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: count })
230
- ]
231
- },
232
- key
233
- )) }),
233
+ key
234
+ )),
235
+ filterAction ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: Section_default.filterAction, children: filterAction }) : null
236
+ ] }),
234
237
  model ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: Section_default.modelLayout, children: [
235
238
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { className: Section_default.modelList, "aria-label": t("modelsLabel"), children: entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
236
239
  "div",
@@ -309,13 +312,13 @@ function ModelEditor({ models, draft, modelVisibility, t, locale, disabled, visi
309
312
  ] })
310
313
  ] })
311
314
  ] }) : models.status === "ready" && all.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: Section_default.hint, children: t("limitsNoMatches", { query }) }) : null,
312
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: Section_default.head, children: [
315
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: Section_default.limitsFoot, children: [
313
316
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: Section_default.limitsSummary, children: t("limitsSummary", { count: customized }) }),
314
317
  Object.values(draft).some(hasCapacityOverride) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
315
- "button",
318
+ import_dsh_client_ui_primitives.Button,
316
319
  {
317
- type: "button",
318
- className: Section_default.reset,
320
+ variant: "outline",
321
+ size: "sm",
319
322
  disabled,
320
323
  onClick: () => {
321
324
  onEdit(Object.fromEntries(Object.keys(draft).map((id3) => [id3, null])));
@@ -471,6 +474,11 @@ function OpencodeGoSection(props) {
471
474
  function Loaded(props) {
472
475
  const { t, state, loadModels } = props;
473
476
  const [advanced, setAdvanced] = (0, import_react2.useState)(false);
477
+ const [modelsOpen, setModelsOpen] = (0, import_react2.useState)(false);
478
+ const modelNotices = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
479
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ModelsBody, { models: state.models, t, locale: props.locale }),
480
+ state.pickerFailed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: Section_default.failedNote, role: "alert", children: t("pickerFailed") }) : null
481
+ ] });
474
482
  (0, import_react2.useEffect)(() => {
475
483
  if (state.available && state.models.status === "idle") loadModels();
476
484
  }, [state.available, state.models.status, loadModels]);
@@ -534,26 +542,25 @@ function Loaded(props) {
534
542
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: "opencode-go-key-hint", className: Section_default.hint, children: state.apiKeyWritable ? t("keyHint") : t("keyNotWritable") })
535
543
  ] })
536
544
  ] }),
537
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: Section_default.cardModels, children: [
538
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: Section_default.cardHead, children: [
539
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: Section_default.cardTitle, children: t("modelsLabel") }),
540
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: Section_default.trailing, children: [
541
- state.models.status === "ready" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.Tag, { tone: "neutral", children: t("modelsCount", { count: state.models.count }) }) : null,
542
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
543
- "button",
544
- {
545
- type: "button",
546
- className: Section_default.reset,
547
- disabled: state.models.status === "loading" || state.models.status === "ready" && state.models.refreshing,
548
- onClick: loadModels,
549
- children: t("modelsRefresh")
550
- }
551
- )
552
- ] })
553
- ] }),
554
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: Section_default.cardBody, children: [
555
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ModelsBody, { models: state.models, t, locale: props.locale }),
556
- state.pickerFailed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: Section_default.failedNote, role: "alert", children: t("pickerFailed") }) : null,
545
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: modelsOpen ? Section_default.cardModels : Section_default.cardModels + " " + Section_default.cardFolded, children: [
546
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
547
+ "button",
548
+ {
549
+ type: "button",
550
+ className: Section_default.cardTrigger,
551
+ "aria-expanded": modelsOpen,
552
+ "aria-controls": "opencode-go-models",
553
+ onClick: () => {
554
+ setModelsOpen(!modelsOpen);
555
+ },
556
+ children: [
557
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: Section_default.cardTitle, children: t("modelsLabel") }),
558
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChevronDown, { className: modelsOpen ? Section_default.chevronOpen : Section_default.chevron })
559
+ ]
560
+ }
561
+ ),
562
+ modelsOpen ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { id: "opencode-go-models", className: Section_default.cardBody, children: [
563
+ modelNotices,
557
564
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
558
565
  ModelEditor,
559
566
  {
@@ -564,13 +571,23 @@ function Loaded(props) {
564
571
  disabled: disabled || state.saving,
565
572
  modelVisibility: state.modelVisibility,
566
573
  visibilitySaving: state.pickerSaving,
574
+ filterAction: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
575
+ import_dsh_client_ui_primitives2.Button,
576
+ {
577
+ variant: "outline",
578
+ size: "sm",
579
+ disabled: state.models.status === "loading" || state.models.status === "ready" && state.models.refreshing,
580
+ onClick: loadModels,
581
+ children: t("modelsRefresh")
582
+ }
583
+ ),
567
584
  onModelEnabled: props.setModelEnabled,
568
585
  onEdit: (next) => {
569
586
  props.edit("modelLimits", JSON.stringify(next));
570
587
  }
571
588
  }
572
589
  )
573
- ] })
590
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: Section_default.cardNotes, children: modelNotices })
574
591
  ] }),
575
592
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: Section_default.card, children: [
576
593
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
@@ -755,6 +772,9 @@ function Loaded(props) {
755
772
  ] });
756
773
  }
757
774
 
775
+ // src/provider-identity.ts
776
+ var PROVIDER_ID = "dsh-opencode-go";
777
+
758
778
  // src/client/staged-form.ts
759
779
  var import_dsh_client_store = require("@deepseek-ai/dsh-client-store");
760
780
  function numberField(field, validate = () => true) {
@@ -1011,7 +1031,6 @@ function sameJsonValue(left, right) {
1011
1031
  var OPENCODE_GO_NS = "llm-opencode-go";
1012
1032
  var DEFAULT_API_KEY_REF = "OPENCODE_API_KEY";
1013
1033
  var API_KEY_FIELD = "apiKey";
1014
- var OPENCODE_GO_PROVIDER = "opencode-go";
1015
1034
  var OpencodeGoSectionController = class {
1016
1035
  /**
1017
1036
  * @param scope - the bound settings scope for the `llm-opencode-go` namespace.
@@ -1019,7 +1038,7 @@ var OpencodeGoSectionController = class {
1019
1038
  * answers for the credential the section references.
1020
1039
  */
1021
1040
  constructor(scope, ctx, readModels = async () => {
1022
- const result = await ctx.remote.llm.discoverModels(OPENCODE_GO_NS, { provider: OPENCODE_GO_PROVIDER });
1041
+ const result = await ctx.remote.llm.discoverModels(OPENCODE_GO_NS, { provider: PROVIDER_ID });
1023
1042
  return result.ok ? { ok: true, value: { models: result.value, stale: false } } : result;
1024
1043
  }) {
1025
1044
  this.scope = scope;
@@ -1349,7 +1368,6 @@ var en = {
1349
1368
  modelsSourceUpdated: "Last successful check: {time}",
1350
1369
  modelsRefreshing: "Refreshing\u2026 Keeping the last fetched models visible.",
1351
1370
  modelsEmpty: "The gateway currently serves no model this adapter can route.",
1352
- modelsCount: "{count} models",
1353
1371
  limitsLabel: "Model capacities",
1354
1372
  limitsHint: "Capacities come from the online configuration; blank keeps the default and Save applies your overrides.",
1355
1373
  limitsFilterLabel: "Find a model",
@@ -1469,7 +1487,6 @@ var zh = {
1469
1487
  modelsSourceUpdated: "\u4E0A\u6B21\u6210\u529F\u68C0\u67E5\uFF1A{time}",
1470
1488
  modelsRefreshing: "\u6B63\u5728\u5237\u65B0\uFF0C\u6682\u65F6\u4FDD\u7559\u4E0A\u6B21\u83B7\u53D6\u7684\u6A21\u578B\u3002",
1471
1489
  modelsEmpty: "\u7F51\u5173\u5F53\u524D\u6CA1\u6709\u672C\u9002\u914D\u5668\u53EF\u8DEF\u7531\u7684\u6A21\u578B\u3002",
1472
- modelsCount: "{count} \u4E2A\u6A21\u578B",
1473
1490
  limitsLabel: "\u6A21\u578B\u5BB9\u91CF",
1474
1491
  limitsHint: "\u5BB9\u91CF\u6765\u81EA\u5728\u7EBF\u6A21\u578B\u914D\u7F6E\uFF1B\u7559\u7A7A\u7528\u9ED8\u8BA4\u503C\uFF0C\u4FDD\u5B58\u540E\u751F\u6548\u3002",
1475
1492
  limitsFilterLabel: "\u67E5\u627E\u6A21\u578B",
@@ -1621,7 +1638,7 @@ function usageLevel(window) {
1621
1638
  }
1622
1639
  function UsagePill({ directory, ...props }) {
1623
1640
  const state = (0, import_react3.useSyncExternalStore)(directory.subscribe, directory.getSnapshot, directory.getSnapshot);
1624
- return state.current?.provider === "opencode-go" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ActiveUsage, { ...props }) : null;
1641
+ return state.current?.provider === PROVIDER_ID ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ActiveUsage, { ...props }) : null;
1625
1642
  }
1626
1643
  function ActiveUsage({ readUsage, t, getLocale }) {
1627
1644
  const [snapshot, setSnapshot] = (0, import_react3.useState)(null);
package/lib/index.js CHANGED
@@ -51,7 +51,7 @@ import { LlmError as LlmError7, assertUsableApiKey, resolveImageAttachmentAccess
51
51
 
52
52
  // src/adapter.ts
53
53
  import { randomUUID as randomUUID2 } from "node:crypto";
54
- import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
54
+ import { getSupportedThinkingLevels, normalizeContext } from "opencode-go-pi-ai";
55
55
  import {
56
56
  LlmAdapter,
57
57
  LlmError as LlmError6,
@@ -86,13 +86,14 @@ function emptyPiUsage() {
86
86
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
87
87
  };
88
88
  }
89
- function toPiReplayState(message, requestedModel = message.model) {
89
+ function toPiReplayState(message, requestedModel = message.model, requestedProvider = message.provider) {
90
90
  const responseModel = message.api === "anthropic-messages" && message.model !== requestedModel ? message.model : message.responseModel;
91
91
  const response = {
92
92
  kind: "pi-ai",
93
93
  version: 2,
94
94
  api: message.api,
95
- provider: message.provider,
95
+ provider: requestedProvider,
96
+ ...requestedProvider === message.provider ? {} : { sdkProvider: message.provider },
96
97
  model: requestedModel,
97
98
  ...responseModel === void 0 ? {} : { responseModel },
98
99
  ...message.responseId === void 0 ? {} : { responseId: message.responseId },
@@ -137,6 +138,9 @@ function readReplayState(value) {
137
138
  for (const key of ["api", "provider", "model"]) {
138
139
  if (typeof response[key] !== "string" || response[key].length === 0) return invalidReplay(`${key} must be a non-empty string`);
139
140
  }
141
+ if (response["sdkProvider"] !== void 0 && (typeof response["sdkProvider"] !== "string" || response["sdkProvider"].length === 0)) {
142
+ return invalidReplay("sdkProvider must be a non-empty string");
143
+ }
140
144
  if (!["stop", "length", "toolUse", "error", "aborted"].includes(String(response["stopReason"]))) {
141
145
  return invalidReplay("unknown stopReason");
142
146
  }
@@ -236,7 +240,7 @@ function replayedAssistant(message, source, rawState) {
236
240
  role: "assistant",
237
241
  content,
238
242
  api: state.response.api,
239
- provider: state.response.provider,
243
+ provider: state.response.sdkProvider ?? state.response.provider,
240
244
  // Anthropic reports aliases and fallbacks as model, unlike Completions' informational responseModel.
241
245
  model: state.response.api === "anthropic-messages" ? state.response.responseModel ?? state.response.model : state.response.model,
242
246
  ...state.response.responseModel === void 0 ? {} : { responseModel: state.response.responseModel },
@@ -551,7 +555,7 @@ async function toPiContextWithImages(options, images, onReplayDegrade) {
551
555
  // src/conversion/stream.ts
552
556
  import { brandString as brandString2 } from "@deepseek-ai/dsh-brand";
553
557
  import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError as LlmError4, QUOTA_EXCEEDED_CODE } from "@deepseek-ai/dsh-llm";
554
- import { isContextOverflow } from "@earendil-works/pi-ai";
558
+ import { isContextOverflow } from "opencode-go-pi-ai";
555
559
  function mapUsage(usage) {
556
560
  return {
557
561
  inputTokens: usage.input,
@@ -624,7 +628,7 @@ function mapStopReason(message, contextWindow) {
624
628
  }
625
629
  }
626
630
  }
627
- async function* toStreamChunks(events, contextWindow, callerSignal, requestedModel) {
631
+ async function* toStreamChunks(events, contextWindow, callerSignal, requestedModel, requestedProvider) {
628
632
  const toolIds = /* @__PURE__ */ new Map();
629
633
  for await (const event of events) {
630
634
  switch (event.type) {
@@ -686,7 +690,7 @@ async function* toStreamChunks(events, contextWindow, callerSignal, requestedMod
686
690
  yield {
687
691
  type: "finish",
688
692
  reason: mapStopReason(event.message, contextWindow),
689
- replayState: toPiReplayState(event.message, requestedModel)
693
+ replayState: toPiReplayState(event.message, requestedModel, requestedProvider)
690
694
  };
691
695
  return;
692
696
  case "error":
@@ -708,11 +712,11 @@ async function* toStreamChunks(events, contextWindow, callerSignal, requestedMod
708
712
  import { idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
709
713
 
710
714
  // src/catalog.ts
711
- import { createProvider } from "@earendil-works/pi-ai";
712
- import { getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
713
- import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
714
- import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
715
- import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
715
+ import { createProvider } from "opencode-go-pi-ai";
716
+ import { getBuiltinModels } from "opencode-go-pi-ai/providers/all";
717
+ import { anthropicMessagesApi } from "opencode-go-pi-ai/api/anthropic-messages.lazy";
718
+ import { openAICompletionsApi } from "opencode-go-pi-ai/api/openai-completions.lazy";
719
+ import { openAIResponsesApi } from "opencode-go-pi-ai/api/openai-responses.lazy";
716
720
  import { attributionHeaders, LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
717
721
 
718
722
  // src/models-contract.ts
@@ -917,6 +921,9 @@ function readModelMetadata(body, baseURL, builtin) {
917
921
 
918
922
  // src/json-response.ts
919
923
  import { setTimeout as delay } from "node:timers/promises";
924
+ import { Readable, Writable } from "node:stream";
925
+ import { pipeline } from "node:stream/promises";
926
+ import { createGunzip } from "node:zlib";
920
927
  var RETRYABLE_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "EPIPE", "UND_ERR_SOCKET", "EAI_AGAIN"]);
921
928
  function errorCauses(error) {
922
929
  const causes = [];
@@ -954,10 +961,10 @@ function diagnosticURL(raw) {
954
961
  url.hash = "";
955
962
  return url.href;
956
963
  }
957
- async function fetchJsonResponse(url, init, maxBytes) {
964
+ async function fetchJsonResponse(url, init, maxBytes, encoding = "identity") {
958
965
  if ((init.method ?? "GET").toUpperCase() !== "GET") throw new TypeError("JSON requests must use GET");
959
966
  const headers = new Headers(init.headers);
960
- headers.set("accept-encoding", "identity");
967
+ headers.set("accept-encoding", encoding);
961
968
  const options = { ...init, method: "GET", headers };
962
969
  for (let attempt = 0; ; attempt += 1) {
963
970
  init.signal?.throwIfAborted();
@@ -968,7 +975,7 @@ async function fetchJsonResponse(url, init, maxBytes) {
968
975
  });
969
976
  return { response, body: void 0 };
970
977
  }
971
- return { response, body: await readJsonResponse(response, maxBytes) };
978
+ return { response, body: await readJsonResponse(response, maxBytes, { gzip: encoding === "gzip", signal: init.signal }) };
972
979
  } catch (error) {
973
980
  init.signal?.throwIfAborted();
974
981
  if (attempt > 0 || !retryableTransportFailure(error)) throw error;
@@ -998,8 +1005,33 @@ async function readBoundedBody(response, maxBytes) {
998
1005
  reader.releaseLock();
999
1006
  }
1000
1007
  }
1001
- async function readJsonResponse(response, maxBytes) {
1002
- const bytes = await readBoundedBody(response, maxBytes);
1008
+ async function readGzipBody(bytes, maxBytes, signal) {
1009
+ const chunks = [];
1010
+ let size = 0;
1011
+ await pipeline(Readable.from([bytes]), createGunzip(), new Writable({
1012
+ write(chunk, _encoding, done) {
1013
+ if (chunk.length > maxBytes - size) {
1014
+ done(new RangeError(`Response body exceeds ${maxBytes} byte limit`));
1015
+ return;
1016
+ }
1017
+ size += chunk.length;
1018
+ chunks.push(chunk);
1019
+ done();
1020
+ }
1021
+ }), { signal: signal ?? void 0 });
1022
+ return Buffer.concat(chunks, size);
1023
+ }
1024
+ async function readJsonResponse(response, maxBytes, options = {}) {
1025
+ let bytes = await readBoundedBody(response, maxBytes);
1026
+ if (options.gzip && bytes[0] === 31 && bytes[1] === 139) {
1027
+ try {
1028
+ bytes = await readGzipBody(bytes, maxBytes, options.signal);
1029
+ } catch (error) {
1030
+ options.signal?.throwIfAborted();
1031
+ if (error instanceof RangeError) throw error;
1032
+ throw new SyntaxError("Response is not valid gzip", { cause: error });
1033
+ }
1034
+ }
1003
1035
  try {
1004
1036
  return JSON.parse(new TextDecoder().decode(bytes));
1005
1037
  } catch (error) {
@@ -1056,9 +1088,12 @@ async function writeMetadataCache(path, value) {
1056
1088
  }
1057
1089
  }
1058
1090
 
1091
+ // src/provider-identity.ts
1092
+ var PROVIDER_ID = "dsh-opencode-go";
1093
+ var DISPLAY_NAME = "DSH OpenCode Go";
1094
+ var SDK_PROVIDER_ID = "opencode-go";
1095
+
1059
1096
  // src/catalog.ts
1060
- var PROVIDER_ID = "opencode-go";
1061
- var DISPLAY_NAME = "OpenCode Go";
1062
1097
  var DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
1063
1098
  var MODELS_FETCH_TIMEOUT_MS = 1e4;
1064
1099
  var METADATA_FETCH_TIMEOUT_MS = 3e4;
@@ -1089,9 +1124,9 @@ function waitForSnapshot(pending, signal) {
1089
1124
  });
1090
1125
  }
1091
1126
  function builtinModels(baseURL) {
1092
- return new Map(getBuiltinModels("opencode-go").map((model) => [model.id, {
1127
+ return new Map(getBuiltinModels(SDK_PROVIDER_ID).map((model) => [model.id, {
1093
1128
  ...model,
1094
- provider: PROVIDER_ID,
1129
+ provider: SDK_PROVIDER_ID,
1095
1130
  baseUrl: modelBaseURL(model.api, baseURL)
1096
1131
  }]));
1097
1132
  }
@@ -1137,7 +1172,7 @@ function harnessApiKeyAuth() {
1137
1172
  }
1138
1173
  function buildProvider(baseURL, models) {
1139
1174
  return createProvider({
1140
- id: PROVIDER_ID,
1175
+ id: SDK_PROVIDER_ID,
1141
1176
  name: DISPLAY_NAME,
1142
1177
  baseUrl: baseURL,
1143
1178
  auth: harnessApiKeyAuth(),
@@ -1242,7 +1277,7 @@ var OpencodeGoCatalog = class {
1242
1277
  },
1243
1278
  cache: "no-cache",
1244
1279
  signal: AbortSignal.timeout(METADATA_FETCH_TIMEOUT_MS)
1245
- }, MODEL_METADATA_MAX_BYTES);
1280
+ }, MODEL_METADATA_MAX_BYTES, "gzip");
1246
1281
  if (response.status === 304 && this.metadata !== void 0) {
1247
1282
  this.metadataUpdatedAtMs = Date.now();
1248
1283
  this.metadataETag = metadataETag(response.headers.get("etag")) ?? this.metadataETag;
@@ -1608,7 +1643,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1608
1643
  };
1609
1644
  }
1610
1645
  const context = imageRequest === void 0 ? toPiContext(options, void 0, this.options.onReplayDegrade) : await toPiContext({ ...options, signal: watchdog.signal }, imageRequest, this.options.onReplayDegrade);
1611
- const events = snapshot.provider.streamSimple(model, context, {
1646
+ const events = snapshot.provider.streamSimple(model, normalizeContext(context), {
1612
1647
  apiKey,
1613
1648
  ...reasoning === void 0 || reasoning === "off" ? {} : { reasoning },
1614
1649
  ...options.temperature === void 0 ? {} : { temperature: options.temperature },
@@ -1626,7 +1661,7 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1626
1661
  // one SDK attempt.
1627
1662
  maxRetries: 0
1628
1663
  });
1629
- const iterator = toStreamChunks(events, model.contextWindow, options.signal, model.id)[Symbol.asyncIterator]();
1664
+ const iterator = toStreamChunks(events, model.contextWindow, options.signal, model.id, options.provider)[Symbol.asyncIterator]();
1630
1665
  let exhausted = false;
1631
1666
  try {
1632
1667
  while (true) {
@@ -1878,6 +1913,7 @@ function apply(ctx, raw) {
1878
1913
  if (configured && current().enabled && registration === void 0) {
1879
1914
  try {
1880
1915
  registration = ctx.llm.registerAdapter([PROVIDER_ID], adapter);
1916
+ ctx.logger.info(`llm-opencode-go: route "${PROVIDER_ID}" registered as ${DISPLAY_NAME}`);
1881
1917
  } catch (error) {
1882
1918
  ctx.logger.error(`llm-opencode-go: not registering the "${PROVIDER_ID}" route (${String(error)})`);
1883
1919
  }
@@ -1951,7 +1987,6 @@ function apply(ctx, raw) {
1951
1987
  });
1952
1988
  syncRoute();
1953
1989
  });
1954
- ctx.logger.info(`llm-opencode-go: route "${PROVIDER_ID}" registered as ${DISPLAY_NAME}`);
1955
1990
  }
1956
1991
  export {
1957
1992
  Config,
@@ -1,9 +1,8 @@
1
- import type { Api, Model, Provider } from '@earendil-works/pi-ai';
1
+ import type { Api, Model, Provider } from 'opencode-go-pi-ai';
2
2
  import type { LlmDiscoveredModel } from '@deepseek-ai/dsh-llm';
3
3
  import { type GoModelCatalog } from './models-contract.ts';
4
4
  import type { ModelMetadata } from './model-metadata.ts';
5
- export declare const PROVIDER_ID = "opencode-go";
6
- export declare const DISPLAY_NAME = "OpenCode Go";
5
+ export { PROVIDER_ID, DISPLAY_NAME } from './provider-identity.ts';
7
6
  export declare const DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
8
7
  export interface CatalogSnapshot {
9
8
  readonly details: ModelMetadata['details'];
@@ -1,9 +1,10 @@
1
+ import { type ReactNode } from 'react';
1
2
  import type { OpencodeGoModelLimit, OpencodeGoModelLimits, OpencodeGoModels } from './section-controller.ts';
2
3
  import type { en } from './locales.ts';
3
4
  type Translate = (key: keyof typeof en, params?: Record<string, unknown>) => string;
4
5
  export declare function hasCapacityOverride(limit: OpencodeGoModelLimit | null | undefined): boolean;
5
6
  /** Per-model switches apply immediately; capacity edits stay in the staged form. */
6
- export declare function ModelEditor({ models, draft, modelVisibility, t, locale, disabled, visibilitySaving, onEdit, onModelEnabled }: {
7
+ export declare function ModelEditor({ models, draft, modelVisibility, t, locale, disabled, visibilitySaving, filterAction, onEdit, onModelEnabled }: {
7
8
  models: OpencodeGoModels;
8
9
  draft: OpencodeGoModelLimits;
9
10
  modelVisibility: Readonly<Record<string, boolean>>;
@@ -11,6 +12,8 @@ export declare function ModelEditor({ models, draft, modelVisibility, t, locale,
11
12
  locale?: string;
12
13
  disabled: boolean;
13
14
  visibilitySaving: boolean;
15
+ /** Optional control pinned to the right of the filter row, such as a refresh. */
16
+ filterAction?: ReactNode;
14
17
  onEdit: (next: OpencodeGoModelLimits) => void;
15
18
  onModelEnabled: (id: string, enabled: boolean) => void;
16
19
  }): import("react").JSX.Element;
@@ -46,7 +46,6 @@ export declare const en: {
46
46
  modelsSourceUpdated: string;
47
47
  modelsRefreshing: string;
48
48
  modelsEmpty: string;
49
- modelsCount: string;
50
49
  limitsLabel: string;
51
50
  limitsHint: string;
52
51
  limitsFilterLabel: string;
@@ -32,7 +32,7 @@ export type OpencodeGoModelLimits = Record<string, OpencodeGoModelLimit | null>;
32
32
  export interface OpencodeGoConfig {
33
33
  /**
34
34
  * Whether this adapter serves its route at all. False withdraws the
35
- * `opencode-go` route and its models from every picker without unloading the
35
+ * `dsh-opencode-go` route and its models from every picker without unloading the
36
36
  * plugin, so the settings page that owns this switch stays reachable to turn
37
37
  * it back on. Independent of the credential: a key present while this is
38
38
  * false registers nothing.
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import type { GenerateOptions, ImageAttachmentAccessResolver } from '@deepseek-ai/dsh-llm';
7
7
  import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
8
- import type { Context as PiContext } from '@earendil-works/pi-ai';
8
+ import type { Context as PiContext } from 'opencode-go-pi-ai';
9
9
  /** Inputs that bind deterministic request images to one current tool execution world. */
10
10
  export interface PiImageRequestContext {
11
11
  /** Durable provider that resolves request-image bytes and provider-owned host objects. */
@@ -8,7 +8,7 @@
8
8
  * @module dsh-opencode-go/conversion/replay
9
9
  */
10
10
  import type { Message, ReplayEnvelope } from '@deepseek-ai/dsh-llm';
11
- import type { Api, AssistantMessage } from '@earendil-works/pi-ai';
11
+ import type { Api, AssistantMessage } from 'opencode-go-pi-ai';
12
12
  /** Per-block half of the pi-ai replay envelope, one entry per content block. */
13
13
  export type PiAiReplayBlock = {
14
14
  type: 'text';
@@ -26,7 +26,10 @@ export interface PiAiReplayResponse {
26
26
  kind: 'pi-ai';
27
27
  version: 2;
28
28
  api: Api;
29
+ /** Requested DSH provider identity, matching the durable assistant source. */
29
30
  provider: string;
31
+ /** Native SDK provider when it differs from the DSH route. */
32
+ sdkProvider?: string;
30
33
  /** Requested model identity, matching the durable assistant source. */
31
34
  model: string;
32
35
  /** Provider-reported model; only Anthropic replays it as the native model (reported in `message.model`, not `message.responseModel`). */
@@ -43,9 +46,10 @@ export interface PiAiReplayResponse {
43
46
  * removes one.
44
47
  * @param message - completed native pi-ai assistant response.
45
48
  * @param requestedModel - request identity stored in the assistant source; defaults to the native model.
49
+ * @param requestedProvider - DSH route stored in the assistant source; defaults to the native provider.
46
50
  * @returns the versioned lossless-JSON replay projection.
47
51
  */
48
- export declare function toPiReplayState(message: AssistantMessage, requestedModel?: string): ReplayEnvelope;
52
+ export declare function toPiReplayState(message: AssistantMessage, requestedModel?: string, requestedProvider?: string): ReplayEnvelope;
49
53
  /**
50
54
  * Convert one durable Harness assistant message into pi-ai history.
51
55
  *
@@ -8,7 +8,7 @@
8
8
  * @module dsh-opencode-go/conversion/stream
9
9
  */
10
10
  import type { FinishReason, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm';
11
- import type { AssistantMessage, AssistantMessageEvent, Usage as PiUsage } from '@earendil-works/pi-ai';
11
+ import type { AssistantMessage, AssistantMessageEvent, Usage as PiUsage } from 'opencode-go-pi-ai';
12
12
  /**
13
13
  * Map pi-ai usage (reasoning folded into output by pi-ai).
14
14
  * @param usage - cumulative usage from the terminal pi-ai event.
@@ -36,7 +36,8 @@ export declare function mapStopReason(message: AssistantMessage, contextWindow?:
36
36
  * @param callerSignal - caller cancellation state; an aborted caller makes any
37
37
  * in-band terminal error an aborted finish.
38
38
  * @param requestedModel - request model identity recorded for durable replay.
39
+ * @param requestedProvider - DSH provider identity recorded for durable replay.
39
40
  * @returns the harness chunks, ending with `usage` then `finish`; throws
40
41
  * `LlmError` (`STREAM_CLOSED`) if the source ends without a terminal event.
41
42
  */
42
- export declare function toStreamChunks(events: AsyncIterable<AssistantMessageEvent>, contextWindow?: number, callerSignal?: AbortSignal, requestedModel?: string): AsyncGenerator<StreamChunk>;
43
+ export declare function toStreamChunks(events: AsyncIterable<AssistantMessageEvent>, contextWindow?: number, callerSignal?: AbortSignal, requestedModel?: string, requestedProvider?: string): AsyncGenerator<StreamChunk>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Dedicated OpenCode Go adapter plugin. Registers one `opencode-go` route
2
+ * Dedicated OpenCode Go adapter plugin. Registers one `dsh-opencode-go` route
3
3
  * whose catalog follows the gateway's live model listing and models.dev
4
4
  * metadata, and installs the `llm-opencode-go` settings section: the Web UI
5
5
  * renders it as its own settings page where the API key and every knob are
@@ -24,7 +24,7 @@
24
24
  * The credential resolves per request through the credentials seam, falling
25
25
  * back to the process environment — the same reference semantics the generic
26
26
  * pi-ai adapter uses. The route registers atomically: if another adapter
27
- * family already owns `opencode-go` (a profile in `llm-pi-ai`, for example),
27
+ * mount already owns `dsh-opencode-go`,
28
28
  * the refusal is logged with the reason and everything else this plugin does
29
29
  * still works.
30
30
  *
@@ -3,9 +3,12 @@ export declare function transportFailure(error: unknown): string;
3
3
  /** Keep the endpoint useful in diagnostics without user information or query secrets. */
4
4
  export declare function diagnosticURL(raw: string): string;
5
5
  /** The caller's signal covers both attempts, the retry delay, and all response reads. */
6
- export declare function fetchJsonResponse(url: string, init: RequestInit, maxBytes: number): Promise<{
6
+ export declare function fetchJsonResponse(url: string, init: RequestInit, maxBytes: number, encoding?: 'identity' | 'gzip'): Promise<{
7
7
  response: Response;
8
8
  body: unknown;
9
9
  }>;
10
- /** Fetch handles HTTP decoding; this reader bounds the bytes it delivers. */
11
- export declare function readJsonResponse(response: Response, maxBytes: number): Promise<unknown>;
10
+ /** Fetch normally decodes HTTP; only negotiated gzip may recover a raw gzip body (#7). */
11
+ export declare function readJsonResponse(response: Response, maxBytes: number, options?: {
12
+ gzip?: boolean;
13
+ signal?: AbortSignal | null;
14
+ }): Promise<unknown>;
@@ -1,5 +1,5 @@
1
1
  /** Convert OpenCode's online models.dev metadata into the SDK's three wire protocols. */
2
- import type { Api, Model } from '@earendil-works/pi-ai';
2
+ import type { Api, Model } from 'opencode-go-pi-ai';
3
3
  import { type GoModel } from './models-contract.ts';
4
4
  export declare const MODEL_METADATA_URL = "https://models.dev/api.json";
5
5
  /** Lifecycle and capability data the online catalog adds to a gateway listing. */
@@ -0,0 +1,4 @@
1
+ /** Public DSH route and the private SDK transport have separate identities. */
2
+ export declare const PROVIDER_ID = "dsh-opencode-go";
3
+ export declare const DISPLAY_NAME = "DSH OpenCode Go";
4
+ export declare const SDK_PROVIDER_ID = "opencode-go";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-opencode-go",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "OpenCode Go model provider and settings UI for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -89,7 +89,7 @@
89
89
  "dependencies": {
90
90
  "@deepseek-ai/dsh-brand": "0.1.6-alpha.1",
91
91
  "@deepseek-ai/schemastery": "3.18.3",
92
- "@earendil-works/pi-ai": "0.85.1"
92
+ "opencode-go-pi-ai": "npm:@earendil-works/pi-ai@0.87.1"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@deepseek-ai/cordis": "4.0.2",