@dan-ai-studio/dshopencodego 0.1.9 → 0.1.10

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
@@ -76,10 +76,21 @@ Configuration lives in the profile's `cordis.patch.yml`:
76
76
  contextWindow: 262144
77
77
  modelProtocols: # last resort: per-model protocol override
78
78
  some-new-model: openai-responses
79
+ retryPolicy: # optional: this route's retry policy, executed by dsh-llm-retry
80
+ mode: normal
81
+ maxRetries: 2
82
+ backoff:
83
+ initialDelayMs: 500
79
84
  ```
80
85
 
81
86
  The API key comes from the Harness credential store (reference name `OPENCODE_GO_API_KEY`), or from `export OPENCODE_GO_API_KEY=...`.
82
87
 
88
+ **Output cap**: a caller's own `maxTokens` travels as given, still clamped by a `modelLimits` ceiling. When a caller names none, the host materializes the route's ceiling — the catalog's output capacity, or whatever `modelLimits` overrode it with.
89
+
90
+ **Tool declarations**: every request carries the complete current tool list. The session-folded history the host offers (`toolHistory`) is deliberately **not** projected: projecting it requires the route to declare a `toolUpdate` mode, and models.dev states no such mode for this gateway's models (only that tools are callable). Claiming one would silently change what the model sees.
91
+
92
+ **Retries**: `retryPolicy` is optional — omit it and the host's own default applies. When set, the optional `dsh-llm-retry` plugin executes it, and a malformed policy fails where it is written rather than at the first failure.
93
+
83
94
  ## How a model is resolved
84
95
 
85
96
  Four evidence levels decide how a model is called, first hit wins:
@@ -114,12 +125,31 @@ The "OpenCode Go" section shows the live gateway catalog (42+ models) with, per
114
125
 
115
126
  - all satisfied → the plugin loads;
116
127
  - any mismatch → **that plugin row is disabled with a printed reason** (`Plugin … is incompatible with dsh …`), leaving other plugins untouched;
117
- - to take the risk anyway, grant an **exact-version exemption**: `dsh plugin allow-version <pkg>@<version> --dsh-version <exact> --accept-risk` (applies only to that package and that exact runtime version).
128
+ - to take the risk anyway, grant an **exact-version exemption**: `dsh plugin --profile <profile> allow-version <pkg>@<version> --dsh-version <exact> --accept-risk` (applies only to that package and that exact runtime version); `dsh plugin --profile <profile> version-exemptions` lists what a profile holds.
118
129
 
119
130
  The `engines.dsh` field is informational for readers and package managers; **DSH's compatibility gate reads only `peerDependencies`**. `@deepseek-ai/cordis` is declared separately as `4.0.2 || 4.0.3 || 4.0.4`.
120
131
 
121
132
  **0.1.8+ or older releases**: the plugin is refused. If the seam is compatible, widen the peer range in your own build (never in DSH core), or wait for a plugin release.
122
133
 
134
+ ### Version-binding notes
135
+
136
+ | Binding layer | Declared | Enforced by |
137
+ | --- | --- | --- |
138
+ | `peerDependencies` (15 `@deepseek-ai/dsh-*` packages) | all `>=0.1.7-alpha.1 <0.1.8` | **DSH's gate, at plugin load time** |
139
+ | `@deepseek-ai/cordis` | `4.0.2 \|\| 4.0.3 \|\| 4.0.4` | the same gate |
140
+ | `engines.dsh` | `>=0.1.7-alpha.1 <0.1.8` | informational only; DSH never reads it |
141
+ | `engines.node` | `^22.19.0 \|\| >=24.0.0` | the package manager |
142
+ | bundled `@earendil-works/pi-ai` | pinned to exactly `0.87.1` | an independent coupling: a change in pi-ai's request construction changes this plugin's wire behaviour |
143
+ | bundled `@deepseek-ai/schemastery` | `^3.18.3` | ordinary semver |
144
+
145
+ Things to keep in mind when maintaining this:
146
+
147
+ - **Required versus optional peers**: six of the fifteen 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 nine that actually block loading are `dsh-llm`, `dsh-typert-protocol`, `dsh-attachment`, `dsh-brand`, `dsh-credentials`, `dsh-fs`, `dsh-launch-environment`, `dsh-settings`, and `dsh-timeout`. `dsh-llm` is the heaviest coupling — more than twenty imports across the source.
148
+ - **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` (the dev-dependency baseline) and `0.1.7-rc.2` (daily use).
149
+ - **The upper bound is a tracking line**: `<0.1.8` means that the moment DSH ships `0.1.8`, this plugin must ship a new release in the same window or every user loses the plugin. 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.
150
+ - **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`).
151
+ - **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.
152
+
123
153
  ## Troubleshooting
124
154
 
125
155
  | Symptom | Cause and fix |
package/README.md CHANGED
@@ -76,10 +76,21 @@ dsh plugin --profile web remove @dan-ai-studio/dshopencodego # 卸载
76
76
  contextWindow: 262144
77
77
  modelProtocols: # 最后兜底:逐模型协议覆盖
78
78
  some-new-model: openai-responses
79
+ retryPolicy: # 可选:本路由的重试策略(由 dsh-llm-retry 执行)
80
+ mode: normal
81
+ maxRetries: 2
82
+ backoff:
83
+ initialDelayMs: 500
79
84
  ```
80
85
 
81
86
  API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),也可以直接 `export OPENCODE_GO_API_KEY=...`。
82
87
 
88
+ **输出上限**:调用方给了 `maxTokens` 就用它(仍受 `modelLimits` 的逐模型上限约束);没给时由宿主按路由上限物化——该上限就是目录里的输出容量,或 `modelLimits` 覆盖后的值。
89
+
90
+ **工具声明**:每个请求都发送当前完整的工具列表。宿主提供的会话折叠历史(`toolHistory`)**故意不投影**:投影需要路由声明 `toolUpdate` 模式,而 models.dev 对本网关模型只声明"可调用工具"、没有声明该模式;凭空声明会让模型看到的东西悄悄改变。
91
+
92
+ **重试**:`retryPolicy` 是可选的,不写就用宿主默认。写了就由可选的 `dsh-llm-retry` 插件执行,配置错误在写入时报错而不是等到失败发生。
93
+
83
94
  ## 模型是怎么判定的
84
95
 
85
96
  一个模型"怎么调"由四级证据决定,先命中先赢:
@@ -114,12 +125,31 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
114
125
 
115
126
  - 全部满足 → 正常加载;
116
127
  - 任一不满足 → **该插件行被禁用并打印原因**(`Plugin … is incompatible with dsh …`),其他插件不受影响;
117
- - 需要冒险时可用**精确版本豁免**:`dsh plugin allow-version <包>@<版本> --dsh-version <精确版本> --accept-risk`(只对该包与该精确版本生效)。
128
+ - 需要冒险时可用**精确版本豁免**:`dsh plugin --profile <profile> allow-version <包>@<版本> --dsh-version <精确版本> --accept-risk`(只对该包与该精确版本生效);`dsh plugin --profile <profile> version-exemptions` 可列出当前 profile 的豁免。
118
129
 
119
130
  `package.json` 里的 `engines.dsh` 是本插件为读者/包管理器写的信息字段;**DSH 的兼容门禁只读 `peerDependencies`**,请以它为准。`@deepseek-ai/cordis` 单独声明为 `4.0.2 || 4.0.3 || 4.0.4`。
120
131
 
121
132
  **0.1.8+ 或更早版本**:会被拒绝加载。如果 DSH 侧接口兼容,可自行放宽该插件的 peer 范围并重新构建(不改 DSH 核心);否则请等插件跟进发版。
122
133
 
134
+ ### 版本绑定的注意事项
135
+
136
+ | 绑定层 | 声明 | 谁在把关 |
137
+ | --- | --- | --- |
138
+ | `peerDependencies`(15 个 `@deepseek-ai/dsh-*`) | 均为 `>=0.1.7-alpha.1 <0.1.8` | **DSH 加载时的真门禁** |
139
+ | `@deepseek-ai/cordis` | `4.0.2 \|\| 4.0.3 \|\| 4.0.4` | 同一门禁 |
140
+ | `engines.dsh` | `>=0.1.7-alpha.1 <0.1.8` | 仅信息字段,DSH 不读 |
141
+ | `engines.node` | `^22.19.0 \|\| >=24.0.0` | 包管理器 |
142
+ | 自带依赖 `@earendil-works/pi-ai` | 精确锁 `0.87.1` | 独立耦合:pi-ai 改请求构造即影响本插件的线上行为 |
143
+ | 自带依赖 `@deepseek-ai/schemastery` | `^3.18.3` | 常规 semver |
144
+
145
+ 维护时需要注意:
146
+
147
+ - **必选与可选之分**:15 个 peer 中 6 个标了 `optional`(`dsh-api-remotes`、`dsh-client-locale`、`dsh-client-store`、`dsh-client-ui-model-selection`、`dsh-client-ui-settings`、`dsh-client-ui-slots`);真正卡住加载的是 9 个——`dsh-llm`、`dsh-typert-protocol`、`dsh-attachment`、`dsh-brand`、`dsh-credentials`、`dsh-fs`、`dsh-launch-environment`、`dsh-settings`、`dsh-timeout`。其中 `dsh-llm` 是最重的一处耦合(源码 import 二十余处)。
148
+ - **名义范围 ≠ 实测范围**:`0.1.7-alpha.1`、`alpha.2` 落在声明范围内,但本项目没有验证过;实际验证过的是 `0.1.7-rc.1`(开发依赖基线)与 `0.1.7-rc.2`(日常使用)。
149
+ - **上界就是跟版线**:`<0.1.8` 意味着 DSH 一旦发布 `0.1.8`,本插件必须同批发新版,否则所有用户加载失败。调整范围后用 `npm test` 验证即可(本地 mock 网关,零网络零 token)——范围以接口声明为准,不要用线上探测来"测"出一个范围。
150
+ - **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` 提供)。
151
+ - **豁免是按 profile 记的**:只有显式执行过 `dsh plugin allow-version` 的 profile 才享有豁免;没有记录就代表必须落在声明范围内。用 `dsh plugin version-exemptions` 可查看当前 profile 的豁免。
152
+
123
153
  ## 故障排查
124
154
 
125
155
  | 现象 | 原因与处置 |
package/lib/client.js CHANGED
@@ -440,7 +440,7 @@ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id) + "]")
440
440
  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_segmented{border:1px solid var(--dsw-alias-border-l1);border-radius:.35rem;display:inline-flex;overflow:hidden}.d7lz0W_segment,.d7lz0W_segmentActive{background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;border:0;padding:.25rem .6rem;font-size:.8rem}.d7lz0W_segmentActive{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.d7lz0W_tableWrap{overflow-x:auto}.d7lz0W_table{border-collapse:collapse;width:100%;min-width:62rem;font-size:.78rem}.d7lz0W_table th{color:var(--dsw-alias-label-secondary);text-align:left;white-space:nowrap;padding:.25rem .45rem;font-weight:500}.d7lz0W_table td{border-top:1px solid var(--dsw-alias-border-l1);vertical-align:middle;white-space:nowrap;padding:.28rem .45rem}.d7lz0W_toggle{width:2rem}.d7lz0W_toggle input{accent-color:var(--dsw-alias-brand-primary)}.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_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)}";
441
441
  document.head.appendChild(style);
442
442
  }
443
- var section_default = { "tableWrap": "d7lz0W_tableWrap", "title": "d7lz0W_title", "itemBody": "d7lz0W_itemBody", "badge": "d7lz0W_badge", "segmented": "d7lz0W_segmented", "row": "d7lz0W_row", "select": "d7lz0W_select", "itemMeta": "d7lz0W_itemMeta", "input": "d7lz0W_input", "warn": "d7lz0W_warn", "number": "d7lz0W_number", "segmentActive": "d7lz0W_segmentActive", "table": "d7lz0W_table", "list": "d7lz0W_list", "name": "d7lz0W_name", "ok": "d7lz0W_ok", "segment": "d7lz0W_segment", "hint": "d7lz0W_hint", "item": "d7lz0W_item", "button": "d7lz0W_button", "root": "d7lz0W_root", "itemMain": "d7lz0W_itemMain", "block": "d7lz0W_block", "toggle": "d7lz0W_toggle", "mono": "d7lz0W_mono" };
443
+ var section_default = { "block": "d7lz0W_block", "table": "d7lz0W_table", "ok": "d7lz0W_ok", "row": "d7lz0W_row", "button": "d7lz0W_button", "itemBody": "d7lz0W_itemBody", "mono": "d7lz0W_mono", "title": "d7lz0W_title", "badge": "d7lz0W_badge", "list": "d7lz0W_list", "segment": "d7lz0W_segment", "warn": "d7lz0W_warn", "segmentActive": "d7lz0W_segmentActive", "segmented": "d7lz0W_segmented", "tableWrap": "d7lz0W_tableWrap", "toggle": "d7lz0W_toggle", "root": "d7lz0W_root", "input": "d7lz0W_input", "select": "d7lz0W_select", "item": "d7lz0W_item", "hint": "d7lz0W_hint", "itemMain": "d7lz0W_itemMain", "itemMeta": "d7lz0W_itemMeta", "name": "d7lz0W_name", "number": "d7lz0W_number" };
444
444
 
445
445
  // src/client/Section.tsx
446
446
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -996,7 +996,7 @@ if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(id2) + "]"
996
996
  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}";
997
997
  document.head.appendChild(style);
998
998
  }
999
- var pill_default = { "hint": "mMNkkW_hint", "panel": "mMNkkW_panel", "row": "mMNkkW_row", "window": "mMNkkW_window", "trigger": "mMNkkW_trigger", "retry": "mMNkkW_retry", "limited": "mMNkkW_limited", "warning": "mMNkkW_warning", "progress": "mMNkkW_progress", "root": "mMNkkW_root", "local": "mMNkkW_local" };
999
+ var pill_default = { "root": "mMNkkW_root", "window": "mMNkkW_window", "limited": "mMNkkW_limited", "progress": "mMNkkW_progress", "row": "mMNkkW_row", "retry": "mMNkkW_retry", "local": "mMNkkW_local", "hint": "mMNkkW_hint", "panel": "mMNkkW_panel", "warning": "mMNkkW_warning", "trigger": "mMNkkW_trigger" };
1000
1000
 
1001
1001
  // src/client/UsagePill.tsx
1002
1002
  var import_jsx_runtime2 = require("react/jsx-runtime");
package/lib/index.js CHANGED
@@ -57,7 +57,8 @@ import {
57
57
  contentHasImage as contentHasImage2,
58
58
  LlmAdapter,
59
59
  LlmError as LlmError7,
60
- ReasoningEffortId
60
+ ReasoningEffortId,
61
+ resolveRetryPolicy
61
62
  } from "@deepseek-ai/dsh-llm";
62
63
  import { idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
63
64
 
@@ -605,6 +606,7 @@ async function discoverCatalogModels(catalog) {
605
606
  }
606
607
 
607
608
  // src/config.ts
609
+ import { RetryPolicySchema } from "@deepseek-ai/dsh-llm";
608
610
  import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
609
611
  import z from "@deepseek-ai/schemastery";
610
612
  var DEFAULT_API_KEY_ENV = "OPENCODE_GO_API_KEY";
@@ -627,14 +629,18 @@ var fields = {
627
629
  contextWindow: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)]),
628
630
  maxTokens: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)])
629
631
  })])).default({}),
630
- modelProtocols: z.dict(z.string()).default({})
632
+ modelProtocols: z.dict(z.string()).default({}),
633
+ retryPolicy: RetryPolicySchema
631
634
  };
632
635
  var PlainConfig = z.object(fields);
633
636
  var Config = z.object(Object.fromEntries(
634
637
  Object.entries(fields).map(([key, schema]) => [key, schema.volatile()])
635
638
  ));
636
639
  function readConfig(config) {
637
- return Object.fromEntries(Object.keys(fields).map((key) => [key, config[key].get()]));
640
+ return Object.fromEntries(Object.keys(fields).flatMap((key) => {
641
+ const field = config[key];
642
+ return field?.get === void 0 ? [] : [[key, field.get()]];
643
+ }));
638
644
  }
639
645
  function assertBaseURL(raw) {
640
646
  let url;
@@ -1002,8 +1008,7 @@ import {
1002
1008
  EMPTY_RESPONSE_CODE,
1003
1009
  isContextWindowExceededError,
1004
1010
  isQuotaExceededError,
1005
- LlmError as LlmError6,
1006
- QUOTA_EXCEEDED_CODE
1011
+ LlmError as LlmError6
1007
1012
  } from "@deepseek-ai/dsh-llm";
1008
1013
  import { isContextOverflow } from "@earendil-works/pi-ai";
1009
1014
  function mapUsage(usage) {
@@ -1015,9 +1020,10 @@ function mapUsage(usage) {
1015
1020
  ...usage.cacheWrite > 0 ? { cacheWriteTokens: usage.cacheWrite } : {}
1016
1021
  };
1017
1022
  }
1023
+ var ACCOUNT_QUOTA_CODE = "ACCOUNT_QUOTA";
1018
1024
  function classifyPiAiError(message) {
1019
1025
  if (/\b(?:401|403)\b/.test(message)) return "AUTH";
1020
- if (isQuotaExceededError(message)) return QUOTA_EXCEEDED_CODE;
1026
+ if (isQuotaExceededError(message)) return ACCOUNT_QUOTA_CODE;
1021
1027
  if (/\b429\b|rate.?limit/i.test(message)) return "RATE_LIMIT";
1022
1028
  if (/\b413\b|payload too large|request body too large|length limit exceeded/i.test(message)) return "INVALID_REQUEST";
1023
1029
  if (/\b400\b|invalid.?request/i.test(message)) return "INVALID_REQUEST";
@@ -1332,9 +1338,23 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1332
1338
  name: model.name,
1333
1339
  inputModalities: [...model.input],
1334
1340
  context: { contextWindow: model.contextWindow },
1341
+ // The route's own output ceiling, so the host materializes it when a
1342
+ // caller names none and a caller that does name one still wins.
1343
+ defaultMaxTokens: model.maxTokens,
1335
1344
  ...reasoning
1336
1345
  };
1337
1346
  }
1347
+ /**
1348
+ * This route's retry policy, resolved from configuration on every read so a
1349
+ * profile edit reaches the next failed step. Absent configuration leaves the
1350
+ * host's own default in force.
1351
+ * @param _provider - unused; the mount owns exactly one route.
1352
+ * @returns the resolved policy, or undefined when none is configured.
1353
+ */
1354
+ providerRetryPolicy(_provider) {
1355
+ const configured = this.options.config().retryPolicy;
1356
+ return configured === void 0 ? void 0 : resolveRetryPolicy(configured, "dshopencodego: retryPolicy");
1357
+ }
1338
1358
  /** Validate an explicit effort against the model's own levels, without clamping. */
1339
1359
  resolveReasoningLevel(model, effort) {
1340
1360
  if (effort === void 0 || effort === "off") return void 0;
@@ -1356,8 +1376,8 @@ var OpencodeGoAdapter = class extends LlmAdapter {
1356
1376
  const facts = snapshot.facts.get(options.model);
1357
1377
  if (facts === void 0) throw new LlmError7(`opencode-go has no model "${options.model}"`, "UNKNOWN_MODEL");
1358
1378
  const model = withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits);
1359
- const outputLimit = config.modelLimits[model.id]?.maxTokens;
1360
- const maxTokens = outputLimit === null || outputLimit === void 0 ? options.maxTokens : Math.min(options.maxTokens ?? outputLimit, outputLimit);
1379
+ const ceiling = config.modelLimits[model.id]?.maxTokens;
1380
+ const maxTokens = options.maxTokens === void 0 || ceiling === null || ceiling === void 0 ? options.maxTokens : Math.min(options.maxTokens, ceiling);
1361
1381
  const apiKey = await this.options.resolveApiKey();
1362
1382
  if (apiKey === void 0 || apiKey.length === 0) {
1363
1383
  throw new LlmError7("dshopencodego: no credential resolved for the opencode-go route", "MISSING_CREDENTIAL");
@@ -13,7 +13,7 @@
13
13
  * @module @dan-ai-studio/dshopencodego/adapter
14
14
  */
15
15
  import { attributionHeaders, LlmAdapter } from '@deepseek-ai/dsh-llm';
16
- import type { GenerateOptions, ImageAttachmentAccess, LlmModelInfo, LlmResolvedModelInfo, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm';
16
+ import type { GenerateOptions, ImageAttachmentAccess, LlmModelInfo, LlmResolvedModelInfo, ResolvedRetryPolicy, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm';
17
17
  import type { AttachmentStore, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
18
18
  import { OpencodeGoCatalog } from './catalog/index.ts';
19
19
  import type { OpencodeGoConfig } from './config.ts';
@@ -76,6 +76,14 @@ export declare class OpencodeGoAdapter extends LlmAdapter {
76
76
  resolveModel(_provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
77
77
  /** Describe one model: capacities plus the reasoning levels it really offers. */
78
78
  private modelInfo;
79
+ /**
80
+ * This route's retry policy, resolved from configuration on every read so a
81
+ * profile edit reaches the next failed step. Absent configuration leaves the
82
+ * host's own default in force.
83
+ * @param _provider - unused; the mount owns exactly one route.
84
+ * @returns the resolved policy, or undefined when none is configured.
85
+ */
86
+ providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined;
79
87
  /** Validate an explicit effort against the model's own levels, without clamping. */
80
88
  private resolveReasoningLevel;
81
89
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
@@ -65,7 +65,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
65
65
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
66
66
  });
67
67
  import { getSupportedThinkingLevels, normalizeContext } from '@earendil-works/pi-ai';
68
- import { attributionHeaders, contentHasImage, LlmAdapter, LlmError, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
68
+ import { attributionHeaders, contentHasImage, LlmAdapter, LlmError, ReasoningEffortId, resolveRetryPolicy, } from '@deepseek-ai/dsh-llm';
69
69
  import { idleWatchdog, timeoutOf } from '@deepseek-ai/dsh-timeout';
70
70
  import { DISPLAY_NAME, PROVIDER_ID, OpencodeGoCatalog } from "./catalog/index.js";
71
71
  import { toPiModel } from "./catalog/metadata.js";
@@ -183,9 +183,25 @@ export class OpencodeGoAdapter extends LlmAdapter {
183
183
  name: model.name,
184
184
  inputModalities: [...model.input],
185
185
  context: { contextWindow: model.contextWindow },
186
+ // The route's own output ceiling, so the host materializes it when a
187
+ // caller names none and a caller that does name one still wins.
188
+ defaultMaxTokens: model.maxTokens,
186
189
  ...reasoning,
187
190
  };
188
191
  }
192
+ /**
193
+ * This route's retry policy, resolved from configuration on every read so a
194
+ * profile edit reaches the next failed step. Absent configuration leaves the
195
+ * host's own default in force.
196
+ * @param _provider - unused; the mount owns exactly one route.
197
+ * @returns the resolved policy, or undefined when none is configured.
198
+ */
199
+ providerRetryPolicy(_provider) {
200
+ const configured = this.options.config().retryPolicy;
201
+ return configured === undefined
202
+ ? undefined
203
+ : resolveRetryPolicy(configured, 'dshopencodego: retryPolicy');
204
+ }
189
205
  /** Validate an explicit effort against the model's own levels, without clamping. */
190
206
  resolveReasoningLevel(model, effort) {
191
207
  // "off" is not a level to validate: it is the absence of a request, exactly
@@ -209,10 +225,13 @@ export class OpencodeGoAdapter extends LlmAdapter {
209
225
  if (facts === undefined)
210
226
  throw new LlmError(`opencode-go has no model "${options.model}"`, 'UNKNOWN_MODEL');
211
227
  const model = withModelLimit(toPiModel(facts, config.baseURL), config.modelLimits);
212
- const outputLimit = config.modelLimits[model.id]?.maxTokens;
213
- const maxTokens = outputLimit === null || outputLimit === undefined
228
+ // `defaultMaxTokens` already carries the route's ceiling for a caller that
229
+ // names none, so only a caller's own cap travels, clamped by a configured
230
+ // per-model ceiling when one exists.
231
+ const ceiling = config.modelLimits[model.id]?.maxTokens;
232
+ const maxTokens = options.maxTokens === undefined || ceiling === null || ceiling === undefined
214
233
  ? options.maxTokens
215
- : Math.min(options.maxTokens ?? outputLimit, outputLimit);
234
+ : Math.min(options.maxTokens, ceiling);
216
235
  const apiKey = await this.options.resolveApiKey();
217
236
  if (apiKey === undefined || apiKey.length === 0) {
218
237
  throw new LlmError('dshopencodego: no credential resolved for the opencode-go route', 'MISSING_CREDENTIAL');
@@ -11,6 +11,7 @@
11
11
  *
12
12
  * @module @dan-ai-studio/dshopencodego/config
13
13
  */
14
+ import type { RetryPolicyConfig } from '@deepseek-ai/dsh-llm';
14
15
  import z from '@deepseek-ai/schemastery';
15
16
  /** Credential reference resolving the OpenCode Go API key. */
16
17
  export declare const DEFAULT_API_KEY_ENV = "OPENCODE_GO_API_KEY";
@@ -70,6 +71,11 @@ export interface OpencodeGoConfig {
70
71
  * The escape hatch for a model whose family rule guessed wrong.
71
72
  */
72
73
  modelProtocols: Record<string, string>;
74
+ /**
75
+ * Retry policy for this route, owned here and executed by the optional
76
+ * `dsh-llm-retry` plugin. Absent leaves the host's own default in force.
77
+ */
78
+ retryPolicy?: RetryPolicyConfig;
73
79
  }
74
80
  /** Plain resolved values used by the adapter. */
75
81
  export declare const PlainConfig: z<OpencodeGoConfig>;
@@ -81,7 +87,12 @@ export type LiveConfig = {
81
87
  };
82
88
  /** Runtime schema for {@link OpencodeGoConfig}; every field stays live. */
83
89
  export declare const Config: z<Partial<OpencodeGoConfig>, LiveConfig>;
84
- /** Keep the Loader's references: reparsing them would detach live updates. */
90
+ /**
91
+ * Keep the Loader's references: reparsing them would detach live updates.
92
+ *
93
+ * An optional field a profile never wrote has no live reference to read, so it
94
+ * stays absent instead of failing the operation that asked for the config.
95
+ */
85
96
  export declare function readConfig(config: LiveConfig): OpencodeGoConfig;
86
97
  /**
87
98
  * Accept only an http(s) base without a query or fragment. Runs at load for the
@@ -11,6 +11,7 @@
11
11
  *
12
12
  * @module @dan-ai-studio/dshopencodego/config
13
13
  */
14
+ import { RetryPolicySchema } from '@deepseek-ai/dsh-llm';
14
15
  import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout';
15
16
  import z from '@deepseek-ai/schemastery';
16
17
  import { DEFAULT_BASE_URL } from "./catalog/constants.js";
@@ -41,15 +42,24 @@ const fields = {
41
42
  maxTokens: z.union([z.const(null), z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER)]),
42
43
  })])).default({}),
43
44
  modelProtocols: z.dict(z.string()).default({}),
45
+ retryPolicy: RetryPolicySchema,
44
46
  };
45
47
  /** Plain resolved values used by the adapter. */
46
48
  export const PlainConfig = z.object(fields);
47
49
  /** Runtime schema for {@link OpencodeGoConfig}; every field stays live. */
48
50
  export const Config = z.object(Object.fromEntries(Object.entries(fields).map(([key, schema]) => [key, schema.volatile()])));
49
- /** Keep the Loader's references: reparsing them would detach live updates. */
51
+ /**
52
+ * Keep the Loader's references: reparsing them would detach live updates.
53
+ *
54
+ * An optional field a profile never wrote has no live reference to read, so it
55
+ * stays absent instead of failing the operation that asked for the config.
56
+ */
50
57
  export function readConfig(config) {
51
58
  return Object.fromEntries(Object.keys(fields)
52
- .map(key => [key, config[key].get()]));
59
+ .flatMap(key => {
60
+ const field = config[key];
61
+ return field?.get === undefined ? [] : [[key, field.get()]];
62
+ }));
53
63
  }
54
64
  /**
55
65
  * Accept only an http(s) base without a query or fragment. Runs at load for the
@@ -42,6 +42,16 @@ function assertSupportedImageRoles(messages) {
42
42
  }
43
43
  }
44
44
  }
45
+ /**
46
+ * Every tool declaration this request carries.
47
+ *
48
+ * `GenerateOptions.toolHistory` is deliberately not projected: projecting it
49
+ * requires the route to declare a `toolUpdate` mode, and no source states one
50
+ * for this gateway's models (models.dev describes tools only as callable). A
51
+ * route that claims a mode the provider does not implement would silently
52
+ * change what the model sees, so every request sends the complete current tool
53
+ * list — the documented behavior when a route declares no mode.
54
+ */
45
55
  function toolsOf(options) {
46
56
  return options.tools?.map(tool => ({
47
57
  name: tool.name,
@@ -23,6 +23,11 @@ export declare function mapUsage(usage: PiUsage): TokenUsage;
23
23
  * so the actionable detail is only available as text. The patterns are ordered
24
24
  * from most specific to least, and anything unrecognized stays `PI_AI_ERROR`
25
25
  * rather than being guessed into a retryable class.
26
+ *
27
+ * Exhausted quota is reported as `ACCOUNT_QUOTA` rather than the generic
28
+ * `QUOTA`: this gateway meters a prepaid balance (the usage window the plugin
29
+ * shows is the same meter), so the remedy is topping up, not retrying — the
30
+ * same upgrade `llm-deepseek-account` applies to its own prepaid route.
26
31
  */
27
32
  export declare function classifyPiAiError(message: string): string;
28
33
  /**
@@ -8,7 +8,7 @@
8
8
  * @module @dan-ai-studio/dshopencodego/conversion/stream
9
9
  */
10
10
  import { brandString } from '@deepseek-ai/dsh-brand';
11
- import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, QUOTA_EXCEEDED_CODE, } from '@deepseek-ai/dsh-llm';
11
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, } from '@deepseek-ai/dsh-llm';
12
12
  import { isContextOverflow } from '@earendil-works/pi-ai';
13
13
  import { toPiReplayState } from "./replay.js";
14
14
  /**
@@ -26,6 +26,13 @@ export function mapUsage(usage) {
26
26
  ...usage.cacheWrite > 0 ? { cacheWriteTokens: usage.cacheWrite } : {},
27
27
  };
28
28
  }
29
+ /**
30
+ * The account-quota failure class, written out rather than imported:
31
+ * `dsh-llm` exports it as `ACCOUNT_QUOTA_EXCEEDED_CODE` from `0.1.7-rc.2`, and
32
+ * the spelling is the stable protocol value the host routes on, so spelling it
33
+ * here keeps the route correct on every host this plugin supports.
34
+ */
35
+ const ACCOUNT_QUOTA_CODE = 'ACCOUNT_QUOTA';
29
36
  /**
30
37
  * Classify a provider error string into a Harness error code.
31
38
  *
@@ -33,12 +40,17 @@ export function mapUsage(usage) {
33
40
  * so the actionable detail is only available as text. The patterns are ordered
34
41
  * from most specific to least, and anything unrecognized stays `PI_AI_ERROR`
35
42
  * rather than being guessed into a retryable class.
43
+ *
44
+ * Exhausted quota is reported as `ACCOUNT_QUOTA` rather than the generic
45
+ * `QUOTA`: this gateway meters a prepaid balance (the usage window the plugin
46
+ * shows is the same meter), so the remedy is topping up, not retrying — the
47
+ * same upgrade `llm-deepseek-account` applies to its own prepaid route.
36
48
  */
37
49
  export function classifyPiAiError(message) {
38
50
  if (/\b(?:401|403)\b/.test(message))
39
51
  return 'AUTH';
40
52
  if (isQuotaExceededError(message))
41
- return QUOTA_EXCEEDED_CODE;
53
+ return ACCOUNT_QUOTA_CODE;
42
54
  if (/\b429\b|rate.?limit/i.test(message))
43
55
  return 'RATE_LIMIT';
44
56
  if (/\b413\b|payload too large|request body too large|length limit exceeded/i.test(message))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dan-ai-studio/dshopencodego",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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",
@@ -72,16 +72,18 @@
72
72
  "@deepseek-ai/dsh-brand": ">=0.1.7-alpha.1 <0.1.8",
73
73
  "@deepseek-ai/dsh-client-locale": ">=0.1.7-alpha.1 <0.1.8",
74
74
  "@deepseek-ai/dsh-client-store": ">=0.1.7-alpha.1 <0.1.8",
75
+ "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.7-alpha.1 <0.1.8",
75
76
  "@deepseek-ai/dsh-client-ui-model-selection": ">=0.1.7-alpha.1 <0.1.8",
77
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-alpha.1 <0.1.8",
76
78
  "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-alpha.1 <0.1.8",
77
79
  "@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-alpha.1 <0.1.8",
78
80
  "@deepseek-ai/dsh-credentials": ">=0.1.7-alpha.1 <0.1.8",
79
81
  "@deepseek-ai/dsh-fs": ">=0.1.7-alpha.1 <0.1.8",
80
82
  "@deepseek-ai/dsh-launch-environment": ">=0.1.7-alpha.1 <0.1.8",
81
83
  "@deepseek-ai/dsh-llm": ">=0.1.7-alpha.1 <0.1.8",
82
- "@deepseek-ai/dsh-settings": ">=0.1.7-alpha.1 <0.1.8",
83
84
  "@deepseek-ai/dsh-timeout": ">=0.1.7-alpha.1 <0.1.8",
84
- "@deepseek-ai/dsh-typert-protocol": ">=0.1.7-alpha.1 <0.1.8"
85
+ "@deepseek-ai/dsh-typert-protocol": ">=0.1.7-alpha.1 <0.1.8",
86
+ "@deepseek-ai/dsh-typert-registry": ">=0.1.7-alpha.1 <0.1.8"
85
87
  },
86
88
  "peerDependenciesMeta": {
87
89
  "@deepseek-ai/dsh-api-remotes": {