@dan-ai-studio/dshopencodego 0.1.19 → 0.1.21

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
@@ -162,7 +162,7 @@ The `engines.dsh` field is informational for readers and package managers; **DSH
162
162
  Things to keep in mind when maintaining this:
163
163
 
164
164
  - **Required versus optional peers**: six of the eighteen are marked `optional` (`dsh-api-remotes`, `dsh-client-locale`, `dsh-client-store`, `dsh-client-ui-model-selection`, `dsh-client-ui-settings`, `dsh-client-ui-slots`). The twelve that actually block loading are `cordis`, `dsh-attachment`, `dsh-brand`, `dsh-client-ui-conversation`, `dsh-client-ui-renderer`, `dsh-credentials`, `dsh-fs`, `dsh-launch-environment`, `dsh-llm`, `dsh-timeout`, `dsh-typert-protocol`, and `dsh-typert-registry`. `dsh-llm` is the heaviest coupling — more than twenty imports across the source.
165
- - **Declared range ≠ verified range**: `0.1.7-alpha.1` and `alpha.2` fall inside the declaration but were never verified here. What was verified is `0.1.7-rc.1`, `0.1.7-rc.2` (daily use), `0.2.0-rc.1` (the dev-dependency baseline: gate verdict + typecheck + full suite), and `0.2.0-rc.2` (dev-dependency baseline + `prepareCall` generation freeze + pi-ai delta-parse patch port).
165
+ - **Declared range ≠ verified range**: `0.1.7-alpha.1` and `alpha.2` fall inside the declaration but were never verified here. What was verified is `0.1.7-rc.1`, `0.1.7-rc.2` (daily use), `0.2.0-rc.1` (the dev-dependency baseline: gate verdict + typecheck + full suite), and `0.2.0-rc.2` (dev-dependency baseline + `prepareCall` generation freeze).
166
166
  - **The upper bound is a tracking line**: `<0.3.0` means that the moment DSH ships `0.3.0`, this plugin must ship a new release in the same window or every user loses the plugin. The current range spans both `0.1.7` and `0.2.0` (`0.1.8` and `0.1.9` were never published, hence two segments). After changing a range, `npm test` is the verification (local mock gateway, no network, no tokens) — ranges follow what the interfaces declare, and are never "measured" with live probes.
167
167
  - **Peer coverage is now guarded both ways**: `dsh-typert-registry`, `dsh-client-ui-conversation`, and `dsh-client-ui-renderer` were imported by the source but absent from `peerDependencies`, invisible to the gate; they are declared now, and `tests/peer-coverage.spec.ts` asserts both directions — anything the source imports must be declared, and anything declared but unreached must be removed (`dsh-settings` went that way: the settings form is actually provided by `dsh-client-ui-settings`).
168
168
  - **Exemptions are per profile**: only a profile that ran `dsh plugin allow-version` has one, and no record means the plugin must stay inside the declared range. `dsh plugin version-exemptions` lists a profile's exemptions.
@@ -179,6 +179,7 @@ Things to keep in mind when maintaining this:
179
179
  | "The settings write was rejected" | Usually a concurrent writer (another window or process) holding the same profile. The plugin retries once automatically; if it still fails the view has reloaded — click again. |
180
180
  | The usage button shows "unavailable" | An endpoint or credential change invalidated the previous reading. Fix the configuration and press Retry. It never shows `0` in place of a missing reading. |
181
181
  | npm publish fails with `ENEEDAUTH` | OIDC did not match: check the Trusted Publisher's **workflow filename** (must be exactly `publish-npm.yml`, case-sensitive) and the organization/repository fields; run on GitHub-hosted runners with `id-token: write`. A `repository.url` that does not match the GitHub repository is rejected as well. |
182
+ | The whole DSH process stalls during ultra-long tool calls (multi-MB arguments) | Inherent to pi-ai 0.87.1's per-delta re-parsing (issue #9265 / PR #9461). This plugin does not port DSH's consumer-side patch (ADR-0005); the fix arrives with an upstream release and a pi-ai upgrade. |
182
183
 
183
184
  ## Development
184
185
 
package/README.md CHANGED
@@ -162,7 +162,7 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
162
162
  维护时需要注意:
163
163
 
164
164
  - **必选与可选之分**:18 个 peer(17 个 `dsh-*` 加框架 `cordis`)中 6 个标了 `optional`(`dsh-api-remotes`、`dsh-client-locale`、`dsh-client-store`、`dsh-client-ui-model-selection`、`dsh-client-ui-settings`、`dsh-client-ui-slots`);真正卡住加载的是 12 个——`cordis`、`dsh-attachment`、`dsh-brand`、`dsh-client-ui-conversation`、`dsh-client-ui-renderer`、`dsh-credentials`、`dsh-fs`、`dsh-launch-environment`、`dsh-llm`、`dsh-timeout`、`dsh-typert-protocol`、`dsh-typert-registry`。其中 `dsh-llm` 是最重的一处耦合(源码 import 二十余处)。
165
- - **名义范围 ≠ 实测范围**:`0.1.7-alpha.1`、`alpha.2` 落在声明范围内,但本项目没有验证过;实际验证过的是 `0.1.7-rc.1`、`0.1.7-rc.2`(日常使用)、`0.2.0-rc.1`(开发依赖基线:门禁判定 + typecheck + 全量用例)与 `0.2.0-rc.2`(开发依赖基线 + `prepareCall` 代次冻结 + pi-ai 增量解析补丁移植)。
165
+ - **名义范围 ≠ 实测范围**:`0.1.7-alpha.1`、`alpha.2` 落在声明范围内,但本项目没有验证过;实际验证过的是 `0.1.7-rc.1`、`0.1.7-rc.2`(日常使用)、`0.2.0-rc.1`(开发依赖基线:门禁判定 + typecheck + 全量用例)与 `0.2.0-rc.2`(开发依赖基线 + `prepareCall` 代次冻结)。
166
166
  - **上界就是跟版线**:`<0.3.0` 意味着 DSH 一旦发布 `0.3.0`,本插件必须同批发新版,否则所有用户加载失败。当前范围跨 `0.1.7` 与 `0.2.0` 两代(`0.1.8`、`0.1.9` 从未发布,所以写成两段而不是一条)。调整范围后用 `npm test` 验证即可(本地 mock 网关,零网络零 token)——范围以接口声明为准,不要用线上探测来"测"出一个范围。
167
167
  - **peer 覆盖已双向守住**:`dsh-typert-registry`、`dsh-client-ui-conversation`、`dsh-client-ui-renderer` 曾被源码 import 却未声明、门禁管不到,现已补进 `peerDependencies`;`tests/peer-coverage.spec.ts` 双向断言——源码用到的必须声明,声明了却没人用的必须删(`dsh-settings` 就这样被移除:设置表单实际由 `dsh-client-ui-settings` 提供)。
168
168
  - **豁免是按 profile 记的**:只有显式执行过 `dsh plugin allow-version` 的 profile 才享有豁免;没有记录就代表必须落在声明范围内。用 `dsh plugin version-exemptions` 可查看当前 profile 的豁免。
@@ -179,6 +179,7 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
179
179
  | 设置页出现「设置写入被拒绝」 | 通常是与另一个窗口/进程并发写同一 profile;插件会自动重试一次,仍失败时界面已重载最新状态,再点一次即可。 |
180
180
  | 用量按钮显示「不可用」 | 端点或凭证变化导致旧读数作废;配置正确后点「重试」。数值不会以 0 冒充。 |
181
181
  | npm 发布报 `ENEEDAUTH` | OIDC 没匹配上:核对包设置里 Trusted Publisher 的 **workflow 文件名**(必须恰为 `publish-npm.yml`、大小写敏感)与组织/仓库字段;确认跑在 GitHub 托管 runner 上且工作流带 `id-token: write`。`package.json` 的 `repository.url` 与仓库不符也会被拒。 |
182
+ | 超长工具调用(多 MB 参数)期间整个 DSH 进程卡顿 | 上游 pi-ai 0.87.1 逐 delta 重解析的固有开销(issue #9265 / PR #9461)。本插件不移植 DSH 的消费者侧补丁(ADR-0005);修复随上游发布后升级 pi-ai 解决。 |
182
183
 
183
184
  ## 开发
184
185
 
package/icon.svg ADDED
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="OpenCode Go">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#1f6feb"/>
5
+ <stop offset="1" stop-color="#0d4196"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect x="1.5" y="1.5" width="61" height="61" rx="14" fill="url(#bg)" stroke="#0a2f6b" stroke-width="3"/>
9
+ <path d="M20 21.5 33.5 32 20 42.5" fill="none" stroke="#ffffff" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round"/>
10
+ <path d="M33 21.5 46.5 32 33 42.5" fill="none" stroke="#9ecbff" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round"/>
11
+ </svg>
package/lib/client.js CHANGED
@@ -462,7 +462,7 @@ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id) + "]")
462
462
  style.textContent = ".d7lz0W_root{color:var(--dsw-alias-label-primary);flex-direction:column;gap:1.25rem;display:flex}.d7lz0W_title{margin:0;font-size:1.05rem}.d7lz0W_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:.8rem}.d7lz0W_warn{color:var(--dsw-alias-state-warn-primary);margin:0;font-size:.8rem}.d7lz0W_ok{color:var(--dsw-alias-state-success-primary);font-size:.8rem}.d7lz0W_block{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:.6rem;flex-direction:column;gap:.5rem;padding:.85rem;display:flex}.d7lz0W_row{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.d7lz0W_row>strong{margin-right:auto}.d7lz0W_input{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);min-width:10rem;color:var(--dsw-alias-label-primary);font:inherit;border-radius:.35rem;flex:14rem;padding:.35rem .5rem;font-size:.85rem}.d7lz0W_button{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:.35rem;padding:.35rem .7rem;font-size:.8rem}.d7lz0W_button:disabled{color:var(--dsw-alias-state-idle-primary);cursor:default}.d7lz0W_number{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);width:5rem;color:var(--dsw-alias-label-primary);font:inherit;border-radius:.35rem;padding:.25rem .4rem;font-size:.8rem}.d7lz0W_select{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);width:auto;max-width:100%;color:var(--dsw-alias-label-primary);font:inherit;border-radius:.35rem;padding:.25rem .4rem;font-size:.8rem}.d7lz0W_list{flex-direction:column;display:flex}.d7lz0W_item{border-top:1px solid var(--dsw-alias-border-l1);align-items:flex-start;gap:.5rem;padding:.45rem .15rem;display:flex}.d7lz0W_item input[type=checkbox]{accent-color:var(--dsw-alias-brand-primary);margin-top:.15rem}.d7lz0W_itemBody{flex-direction:column;gap:.15rem;min-width:0;display:flex}.d7lz0W_itemMain{flex-wrap:wrap;align-items:baseline;gap:.6rem;display:flex}.d7lz0W_itemMeta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:.75rem;font-size:.75rem;display:flex}.d7lz0W_itemModalities{color:var(--dsw-alias-label-secondary);font-size:.75rem}.d7lz0W_name{color:var(--dsw-alias-label-primary)}.d7lz0W_mono{color:var(--dsw-alias-label-secondary);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.d7lz0W_badge{color:var(--dsw-alias-brand-primary)}";
463
463
  document.head.appendChild(style);
464
464
  }
465
- var section_default = { "row": "d7lz0W_row", "itemModalities": "d7lz0W_itemModalities", "input": "d7lz0W_input", "button": "d7lz0W_button", "list": "d7lz0W_list", "root": "d7lz0W_root", "item": "d7lz0W_item", "title": "d7lz0W_title", "itemMeta": "d7lz0W_itemMeta", "itemBody": "d7lz0W_itemBody", "itemMain": "d7lz0W_itemMain", "badge": "d7lz0W_badge", "ok": "d7lz0W_ok", "number": "d7lz0W_number", "name": "d7lz0W_name", "mono": "d7lz0W_mono", "select": "d7lz0W_select", "hint": "d7lz0W_hint", "block": "d7lz0W_block", "warn": "d7lz0W_warn" };
465
+ var section_default = { "itemBody": "d7lz0W_itemBody", "badge": "d7lz0W_badge", "item": "d7lz0W_item", "itemModalities": "d7lz0W_itemModalities", "row": "d7lz0W_row", "hint": "d7lz0W_hint", "root": "d7lz0W_root", "itemMeta": "d7lz0W_itemMeta", "block": "d7lz0W_block", "mono": "d7lz0W_mono", "button": "d7lz0W_button", "list": "d7lz0W_list", "warn": "d7lz0W_warn", "ok": "d7lz0W_ok", "select": "d7lz0W_select", "itemMain": "d7lz0W_itemMain", "number": "d7lz0W_number", "name": "d7lz0W_name", "title": "d7lz0W_title", "input": "d7lz0W_input" };
466
466
 
467
467
  // src/client/Section.tsx
468
468
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -963,7 +963,7 @@ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id2) + "]"
963
963
  style.textContent = ".mMNkkW_root{align-items:center;display:inline-flex;position:relative}.mMNkkW_trigger{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;white-space:nowrap;border-radius:999px;padding:.35rem .6rem;font-size:.8rem;line-height:1}.mMNkkW_trigger:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}.mMNkkW_panel{z-index:20;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-overlay);width:18rem;color:var(--dsw-alias-label-primary);border-radius:.6rem;flex-direction:column;gap:.6rem;padding:.75rem .85rem;display:flex;position:absolute;bottom:calc(100% + .5rem);right:0;box-shadow:0 8px 24px #0000002e}.mMNkkW_window{flex-direction:column;gap:.25rem;display:flex}.mMNkkW_row{justify-content:space-between;gap:.5rem;font-size:.85rem;display:flex}.mMNkkW_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:.75rem}.mMNkkW_progress{width:100%;height:.35rem;accent-color:var(--dsw-alias-brand-primary)}.mMNkkW_limited{color:var(--dsw-alias-state-warn-primary);font-size:.75rem}.mMNkkW_warning{border:1px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-label-primary);border-radius:.4rem;padding:.4rem .5rem;font-size:.75rem}.mMNkkW_retry{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:.35rem;align-self:flex-start;padding:.25rem .5rem;font-size:.75rem}.mMNkkW_local{border-top:1px solid var(--dsw-alias-border-l1);flex-direction:column;gap:.2rem;padding-top:.5rem;display:flex}";
964
964
  document.head.appendChild(style);
965
965
  }
966
- var pill_default = { "local": "mMNkkW_local", "row": "mMNkkW_row", "retry": "mMNkkW_retry", "panel": "mMNkkW_panel", "warning": "mMNkkW_warning", "window": "mMNkkW_window", "hint": "mMNkkW_hint", "root": "mMNkkW_root", "progress": "mMNkkW_progress", "limited": "mMNkkW_limited", "trigger": "mMNkkW_trigger" };
966
+ var pill_default = { "limited": "mMNkkW_limited", "retry": "mMNkkW_retry", "window": "mMNkkW_window", "row": "mMNkkW_row", "hint": "mMNkkW_hint", "panel": "mMNkkW_panel", "warning": "mMNkkW_warning", "local": "mMNkkW_local", "root": "mMNkkW_root", "progress": "mMNkkW_progress", "trigger": "mMNkkW_trigger" };
967
967
 
968
968
  // src/client/UsagePill.tsx
969
969
  var import_jsx_runtime2 = require("react/jsx-runtime");
package/lib/index.js CHANGED
@@ -1417,6 +1417,14 @@ function withModelLimit(model, limits) {
1417
1417
  maxTokens: limit.maxTokens ?? model.maxTokens
1418
1418
  };
1419
1419
  }
1420
+ function stopSequencesFor(model, stop) {
1421
+ if (stop === void 0) return void 0;
1422
+ if (model.api === "openai-completions") return stop;
1423
+ throw new LlmError8(
1424
+ `opencode-go model "${model.id}" speaks ${model.api}, which has no stop-sequence parameter this adapter can reach; drop GenerateOptions.stop for this model`,
1425
+ "UNSUPPORTED_OPTION"
1426
+ );
1427
+ }
1420
1428
  var OpencodeGoAdapter = class extends LlmAdapter {
1421
1429
  cache;
1422
1430
  options;
@@ -1538,12 +1546,10 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1538
1546
  async *streamWithSnapshot(options, config, snapshot) {
1539
1547
  var _stack = [];
1540
1548
  try {
1541
- if (options.stop !== void 0) {
1542
- throw new LlmError8("dshopencodego does not support GenerateOptions.stop", "UNSUPPORTED_OPTION");
1543
- }
1544
1549
  const facts = snapshot.facts.get(options.model);
1545
1550
  if (facts === void 0) throw new LlmError8(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
1546
1551
  const model = withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits);
1552
+ const stop = stopSequencesFor(model, options.stop);
1547
1553
  const ceiling = config.modelLimits[model.id]?.maxTokens;
1548
1554
  const maxTokens = options.maxTokens === void 0 || ceiling === null || ceiling === void 0 ? options.maxTokens : Math.min(options.maxTokens, ceiling);
1549
1555
  const apiKey = await this.options.resolveApiKey();
@@ -1579,6 +1585,10 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1579
1585
  ...reasoning === void 0 || reasoning === "off" ? {} : { reasoning },
1580
1586
  ...options.temperature === void 0 ? {} : { temperature: options.temperature },
1581
1587
  ...maxTokens === void 0 ? {} : { maxTokens },
1588
+ // `samplingParams` is merged into the request body as-is by the
1589
+ // OpenAI-compatible adapters, and `stop` is Chat Completions' own
1590
+ // stop-sequence field — the one place this control reaches the wire.
1591
+ ...stop === void 0 ? {} : { samplingParams: { stop: [...stop] } },
1582
1592
  ...options.sessionId === void 0 ? {} : { sessionId: String(options.sessionId) },
1583
1593
  signal: watchdog.signal,
1584
1594
  // The gateway refuses a request without `x-opencode-session`; the
@@ -1858,7 +1868,7 @@ import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protoc
1858
1868
  import { LlmError as LlmError9 } from "@deepseek-ai/dsh-llm";
1859
1869
 
1860
1870
  // src/catalog/reading.ts
1861
- function catalogReading(snapshot, visibility, listingFailure) {
1871
+ function catalogReading(snapshot, visibility, listingFailure, now) {
1862
1872
  const documentQuotas = snapshot.documentQuotas;
1863
1873
  const rows = [
1864
1874
  ...[...snapshot.facts.values()].map((fact) => {
@@ -1892,7 +1902,7 @@ function catalogReading(snapshot, visibility, listingFailure) {
1892
1902
  const models = rows.map((row) => ({ ...row, recommended: recommended.has(row.id) }));
1893
1903
  const enabled = models.filter((model) => isModelEnabled(model, visibility)).length;
1894
1904
  return {
1895
- models: sortModels(models),
1905
+ models: sortModels(models, now),
1896
1906
  stale: !snapshot.live,
1897
1907
  ...!snapshot.live && listingFailure !== void 0 ? { error: listingFailure } : {},
1898
1908
  fetchedAtMs: snapshot.fetchedAtMs,
@@ -90,6 +90,31 @@ function withModelLimit(model, limits) {
90
90
  maxTokens: limit.maxTokens ?? model.maxTokens,
91
91
  };
92
92
  }
93
+ /**
94
+ * Resolve one request's stop sequences against the model's protocol.
95
+ *
96
+ * The seam's stop control reaches a route only through the provider's own
97
+ * field, and pi-ai models none: its Anthropic options carry no stop sequences
98
+ * and that adapter ignores `samplingParams`, and its Responses options define
99
+ * no stop-sequence parameter at all. Only the OpenAI-compatible adapters merge
100
+ * arbitrary body fields, and `stop` is exactly the field Chat Completions
101
+ * defines, so a completions model can honor it. Every other protocol refuses:
102
+ * silently dropping the sequences would let a caller believe generation halts
103
+ * at a string the model never sees.
104
+ * @param model - the exact model this request will use.
105
+ * @param stop - the caller's stop sequences, when it named any.
106
+ * @returns the sequences to send, or undefined when the caller named none.
107
+ * @throws {LlmError} `UNSUPPORTED_OPTION` for a protocol with no reachable
108
+ * stop-sequence parameter.
109
+ */
110
+ function stopSequencesFor(model, stop) {
111
+ if (stop === undefined)
112
+ return undefined;
113
+ if (model.api === 'openai-completions')
114
+ return stop;
115
+ throw new LlmError(`opencode-go model "${model.id}" speaks ${model.api}, which has no stop-sequence parameter this adapter can reach;`
116
+ + ' drop GenerateOptions.stop for this model', 'UNSUPPORTED_OPTION');
117
+ }
93
118
  /**
94
119
  * The single `opencode-go` route's adapter.
95
120
  *
@@ -238,13 +263,13 @@ export class OpencodeGoAdapter extends LlmAdapter {
238
263
  async *streamWithSnapshot(options, config, snapshot) {
239
264
  const env_1 = { stack: [], error: void 0, hasError: false };
240
265
  try {
241
- if (options.stop !== undefined) {
242
- throw new LlmError('dshopencodego does not support GenerateOptions.stop', 'UNSUPPORTED_OPTION');
243
- }
244
266
  const facts = snapshot.facts.get(options.model);
245
267
  if (facts === undefined)
246
268
  throw new LlmError(`opencode-go has no model "${options.model}"`, 'UNKNOWN_MODEL');
247
269
  const model = withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits);
270
+ // Resolved before the first await so an unsupported protocol fails the
271
+ // request before any credential is read or any byte is sent.
272
+ const stop = stopSequencesFor(model, options.stop);
248
273
  // `defaultMaxTokens` already carries the route's ceiling for a caller that
249
274
  // names none, so only a caller's own cap travels, clamped by a configured
250
275
  // per-model ceiling when one exists.
@@ -287,6 +312,10 @@ export class OpencodeGoAdapter extends LlmAdapter {
287
312
  ...reasoning === undefined || reasoning === 'off' ? {} : { reasoning },
288
313
  ...options.temperature === undefined ? {} : { temperature: options.temperature },
289
314
  ...maxTokens === undefined ? {} : { maxTokens },
315
+ // `samplingParams` is merged into the request body as-is by the
316
+ // OpenAI-compatible adapters, and `stop` is Chat Completions' own
317
+ // stop-sequence field — the one place this control reaches the wire.
318
+ ...stop === undefined ? {} : { samplingParams: { stop: [...stop] } },
290
319
  ...options.sessionId === undefined ? {} : { sessionId: String(options.sessionId) },
291
320
  signal: watchdog.signal,
292
321
  // The gateway refuses a request without `x-opencode-session`; the
@@ -40,7 +40,9 @@ export interface CatalogReading {
40
40
  * @param snapshot - the catalog snapshot, live or retained.
41
41
  * @param visibility - per-model switches; absent entries keep the default.
42
42
  * @param listingFailure - the failure message when the listing failed.
43
+ * @param now - the clock the "newly shipped" ordering reads; injected so a
44
+ * reading is a pure function of its inputs rather than of the wall clock.
43
45
  * @returns the reading, with unconfigurable ids kept visible so the page can
44
46
  * explain them instead of hiding them.
45
47
  */
46
- export declare function catalogReading(snapshot: CatalogSnapshot, visibility: Readonly<Record<string, boolean>>, listingFailure?: string): CatalogReading;
48
+ export declare function catalogReading(snapshot: CatalogSnapshot, visibility: Readonly<Record<string, boolean>>, listingFailure?: string, now?: number): CatalogReading;
@@ -16,10 +16,12 @@ import { goQuotaFor } from "../go-limits.js";
16
16
  * @param snapshot - the catalog snapshot, live or retained.
17
17
  * @param visibility - per-model switches; absent entries keep the default.
18
18
  * @param listingFailure - the failure message when the listing failed.
19
+ * @param now - the clock the "newly shipped" ordering reads; injected so a
20
+ * reading is a pure function of its inputs rather than of the wall clock.
19
21
  * @returns the reading, with unconfigurable ids kept visible so the page can
20
22
  * explain them instead of hiding them.
21
23
  */
22
- export function catalogReading(snapshot, visibility, listingFailure) {
24
+ export function catalogReading(snapshot, visibility, listingFailure, now) {
23
25
  const documentQuotas = snapshot.documentQuotas;
24
26
  const rows = [
25
27
  ...[...snapshot.facts.values()].map(fact => {
@@ -55,7 +57,7 @@ export function catalogReading(snapshot, visibility, listingFailure) {
55
57
  const models = rows.map(row => ({ ...row, recommended: recommended.has(row.id) }));
56
58
  const enabled = models.filter(model => isModelEnabled(model, visibility)).length;
57
59
  return {
58
- models: sortModels(models),
60
+ models: sortModels(models, now),
59
61
  stale: !snapshot.live,
60
62
  ...!snapshot.live && listingFailure !== undefined ? { error: listingFailure } : {},
61
63
  fetchedAtMs: snapshot.fetchedAtMs,
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "OpenCode Go",
4
+ "description": "OpenCode Go provider for DeepSeek Harness: live gateway catalog, per-conversation session header, usage"
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "OpenCode Go",
4
+ "description": "DeepSeek Harness 的 OpenCode Go 提供方:实时网关目录、按对话的会话头、用量读数"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@dan-ai-studio/dshopencodego",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "OpenCode Go provider for DeepSeek Harness: live gateway catalog, per-conversation session header, usage",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "icon": "./icon.svg",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "git+https://github.com/dan-ai-studio/dshopencodego.git"
@@ -27,12 +28,14 @@
27
28
  "types": "./lib/types/client/index.d.ts",
28
29
  "default": "./lib/client.js"
29
30
  },
31
+ "./locale/*.json": "./locale/*.json",
30
32
  "./package.json": "./package.json"
31
33
  },
32
34
  "files": [
33
35
  "lib",
34
- "scripts/patch-pi-ai.mjs",
35
36
  "cordis.patch.yml",
37
+ "icon.svg",
38
+ "locale",
36
39
  "README.md",
37
40
  "README.en.md",
38
41
  "LICENSE"
@@ -60,16 +63,15 @@
60
63
  "test": "vitest run",
61
64
  "test:coverage": "vitest run --coverage",
62
65
  "test:watch": "vitest",
63
- "postinstall": "node scripts/patch-pi-ai.mjs",
64
66
  "prepare": "npm run build",
65
67
  "prepack": "npm run build"
66
68
  },
67
69
  "dependencies": {
68
- "@deepseek-ai/schemastery": "^3.18.3",
69
- "@earendil-works/pi-ai": "0.87.1"
70
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.1",
71
+ "@earendil-works/pi-ai": "~1.0.2"
70
72
  },
71
73
  "peerDependencies": {
72
- "@deepseek-ai/cordis": "4.0.2 || 4.0.3 || 4.0.4",
74
+ "@deepseek-ai/cordis": "4.0.2 || 4.0.3 || 4.0.4 || 4.0.5-alpha.1",
73
75
  "@deepseek-ai/dsh-api-remotes": ">=0.1.7-alpha.1 <0.2.0 || >=0.2.0-rc.1 <0.3.0",
74
76
  "@deepseek-ai/dsh-attachment": ">=0.1.7-alpha.1 <0.2.0 || >=0.2.0-rc.1 <0.3.0",
75
77
  "@deepseek-ai/dsh-brand": ">=0.1.7-alpha.1 <0.2.0 || >=0.2.0-rc.1 <0.3.0",
@@ -109,29 +111,28 @@
109
111
  }
110
112
  },
111
113
  "devDependencies": {
112
- "@deepseek-ai/cordis": "4.0.4",
113
- "@deepseek-ai/dsh-api-remotes": "0.2.0-rc.2",
114
- "@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
115
- "@deepseek-ai/dsh-brand": "0.2.0-rc.2",
116
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
117
- "@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
118
- "@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.2",
119
- "@deepseek-ai/dsh-client-ui-model-selection": "0.2.0-rc.2",
120
- "@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
121
- "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
122
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
123
- "@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
124
- "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
125
- "@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
126
- "@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
127
- "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
128
- "@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
129
- "@deepseek-ai/dsh-scope": "0.2.0-rc.2",
130
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
131
- "@deepseek-ai/dsh-settings": "0.2.0-rc.2",
132
- "@deepseek-ai/dsh-timeout": "0.2.0-rc.2",
133
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
134
- "@deepseek-ai/dsh-typert-registry": "^0.2.0-rc.2",
114
+ "@deepseek-ai/cordis": "4.0.5-alpha.1",
115
+ "@deepseek-ai/dsh-api-remotes": "0.2.1-alpha.2",
116
+ "@deepseek-ai/dsh-attachment": "0.2.1-alpha.2",
117
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
118
+ "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.2",
119
+ "@deepseek-ai/dsh-client-store": "0.2.1-alpha.2",
120
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.2.1-alpha.2",
121
+ "@deepseek-ai/dsh-client-ui-model-selection": "0.2.1-alpha.2",
122
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.2.1-alpha.2",
123
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.1-alpha.2",
124
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.2",
125
+ "@deepseek-ai/dsh-credentials": "0.2.1-alpha.2",
126
+ "@deepseek-ai/dsh-fs": "0.2.1-alpha.2",
127
+ "@deepseek-ai/dsh-launch-environment": "0.2.1-alpha.2",
128
+ "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
129
+ "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
130
+ "@deepseek-ai/dsh-scope": "0.2.1-alpha.2",
131
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
132
+ "@deepseek-ai/dsh-settings": "0.2.1-alpha.2",
133
+ "@deepseek-ai/dsh-timeout": "0.2.1-alpha.2",
134
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
135
+ "@deepseek-ai/dsh-typert-registry": "^0.2.1-alpha.2",
135
136
  "@testing-library/dom": "^10.4.1",
136
137
  "@testing-library/react": "^16.3.0",
137
138
  "@types/node": "^22.20.0",
@@ -1,115 +0,0 @@
1
- /**
2
- * Port of DSH's `patches/@earendil-works__pi-ai@0.87.1.patch` for npm installs.
3
- *
4
- * Upstream re-parses the whole accumulated tool-call argument JSON on every
5
- * stream delta (`O(n^2)` on multi-megabyte arguments and stalls the event
6
- * loop). DSH removes those per-delta parses via pnpm `patchedDependencies`;
7
- * this package is npm-installed, so the same removals are applied here,
8
- * idempotently, to the installed copy.
9
- *
10
- * Only the per-delta occurrences are removed — each is anchored on its
11
- * distinctive preceding accumulation line, exactly matching the upstream
12
- * hunks. The final parses (toolcall_end / done handlers, which strip the
13
- * scratch buffers so replay carries parsed arguments) are kept.
14
- *
15
- * The adapter only reads delta strings plus the finalized arguments (see
16
- * `src/conversion/stream.ts`), so dropping the intermediate `arguments`
17
- * assignments changes no wire behaviour — until `toolcall_end` the partials
18
- * stay `{}`, exactly like the patched DSH runtime.
19
- *
20
- * Best-effort: a missing tree or a newer pi-ai that already dropped the lines
21
- * only warns; it never fails the install.
22
- */
23
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
24
- import { dirname, resolve } from 'node:path'
25
- import { createRequire } from 'node:module'
26
- import { fileURLToPath } from 'node:url'
27
-
28
- const require = createRequire(import.meta.url)
29
-
30
- /** Locate the installed pi-ai root without crossing its `exports` map. */
31
- function findPiRoot() {
32
- try {
33
- const main = require.resolve('@earendil-works/pi-ai')
34
- if (basenameIsDist(dirname(main))) return dirname(dirname(main))
35
- } catch {
36
- // Fall through to the layout probe below.
37
- }
38
- const probe = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'node_modules', '@earendil-works', 'pi-ai')
39
- return existsSync(`${probe}/package.json`) ? probe : undefined
40
- }
41
-
42
- /** True when `dir` is the package's `dist` directory. */
43
- function basenameIsDist(dir) {
44
- return dir.endsWith('/dist') || dir.endsWith('\\dist')
45
- }
46
-
47
- /**
48
- * One hunk: `before` (kept) immediately followed by `removed`.
49
- * Whitespace between them is flexible; nothing else is touched.
50
- */
51
- const HUNKS = [
52
- ['dist/api/anthropic-messages.js',
53
- 'block.partialJson += event.delta.partial_json;',
54
- 'block.arguments = parseStreamingJson(block.partialJson);'],
55
- ['dist/api/bedrock-converse-stream.js',
56
- 'block.partialJson = (block.partialJson || "") + (delta.toolUse.input || "");',
57
- 'block.arguments = parseStreamingJson(block.partialJson);'],
58
- ['dist/api/mistral-conversations.js',
59
- 'block.partialArgs = (block.partialArgs || "") + argsDelta;',
60
- 'block.arguments = parseStreamingJson(block.partialArgs);'],
61
- ['dist/api/openai-completions.js',
62
- 'block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments;',
63
- 'block.arguments = parseStreamingJson(block.partialArgs);'],
64
- ['dist/api/openai-responses-shared.js',
65
- 'slot.block.partialJson += event.delta;',
66
- 'slot.block.arguments = parseStreamingJson(slot.block.partialJson);'],
67
- ]
68
-
69
- const escapeRegExp = text => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
70
-
71
- const piRoot = findPiRoot()
72
- if (piRoot === undefined) {
73
- console.warn('[dshopencodego] patch-pi-ai: pi-ai not installed, skipping')
74
- process.exit(0)
75
- }
76
-
77
- let version = 'unknown'
78
- try {
79
- version = JSON.parse(readFileSync(`${piRoot}/package.json`, 'utf8')).version ?? 'unknown'
80
- } catch (error) {
81
- console.warn(`[dshopencodego] patch-pi-ai: cannot read pi-ai version, skipping (${String(error)})`)
82
- process.exit(0)
83
- }
84
- if (version !== '0.87.1') {
85
- console.warn(`[dshopencodego] patch-pi-ai: pi-ai ${version} is not 0.87.1, skipping (re-check upstream pi#9265)`)
86
- process.exit(0)
87
- }
88
-
89
- let patched = 0
90
- let files = 0
91
- for (const [rel, before, removed] of HUNKS) {
92
- const path = `${piRoot}/${rel}`
93
- if (!existsSync(path)) continue
94
- files += 1
95
- const pattern = new RegExp(`(${escapeRegExp(before)}\\n)(\\s*${escapeRegExp(removed)}\\n)`)
96
- const text = readFileSync(path, 'utf8')
97
- if (!pattern.test(text)) continue
98
- writeFileSync(path, text.replace(pattern, '$1'))
99
- patched += 1
100
- }
101
-
102
- // pi-messages.js spans the delta assignment over two lines.
103
- {
104
- const path = `${piRoot}/dist/api/pi-messages.js`
105
- if (existsSync(path)) {
106
- files += 1
107
- const pattern = /partial\.content\[event\.contentIndex\]\.arguments =\s*\n?\s*parseStreamingJson\(json\);\n/
108
- const text = readFileSync(path, 'utf8')
109
- if (pattern.test(text)) {
110
- writeFileSync(path, text.replace(pattern, ''))
111
- patched += 1
112
- }
113
- }
114
- }
115
- console.log(`[dshopencodego] patch-pi-ai: ${patched} hunks applied across ${files} files (idempotent)`)