@dan-ai-studio/dshopencodego 0.1.9 → 0.1.11
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 +31 -1
- package/README.md +31 -1
- package/lib/client.js +2 -2
- package/lib/index.js +28 -8
- package/lib/types/adapter.d.ts +9 -1
- package/lib/types/adapter.js +23 -4
- package/lib/types/config.d.ts +12 -1
- package/lib/types/config.js +12 -2
- package/lib/types/conversion/context.js +10 -0
- package/lib/types/conversion/stream.d.ts +5 -0
- package/lib/types/conversion/stream.js +14 -2
- package/package.json +5 -3
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 = { "
|
|
443
|
+
var section_default = { "list": "d7lz0W_list", "badge": "d7lz0W_badge", "segment": "d7lz0W_segment", "mono": "d7lz0W_mono", "itemMain": "d7lz0W_itemMain", "root": "d7lz0W_root", "block": "d7lz0W_block", "segmentActive": "d7lz0W_segmentActive", "tableWrap": "d7lz0W_tableWrap", "title": "d7lz0W_title", "ok": "d7lz0W_ok", "button": "d7lz0W_button", "item": "d7lz0W_item", "itemBody": "d7lz0W_itemBody", "table": "d7lz0W_table", "segmented": "d7lz0W_segmented", "row": "d7lz0W_row", "input": "d7lz0W_input", "hint": "d7lz0W_hint", "number": "d7lz0W_number", "select": "d7lz0W_select", "itemMeta": "d7lz0W_itemMeta", "name": "d7lz0W_name", "toggle": "d7lz0W_toggle", "warn": "d7lz0W_warn" };
|
|
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 = { "
|
|
999
|
+
var pill_default = { "root": "mMNkkW_root", "progress": "mMNkkW_progress", "hint": "mMNkkW_hint", "window": "mMNkkW_window", "retry": "mMNkkW_retry", "warning": "mMNkkW_warning", "local": "mMNkkW_local", "panel": "mMNkkW_panel", "row": "mMNkkW_row", "limited": "mMNkkW_limited", "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).
|
|
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
|
|
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
|
|
1360
|
-
const maxTokens =
|
|
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");
|
package/lib/types/adapter.d.ts
CHANGED
|
@@ -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>;
|
package/lib/types/adapter.js
CHANGED
|
@@ -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
|
-
|
|
213
|
-
|
|
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
|
|
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');
|
package/lib/types/config.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
package/lib/types/config.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
.
|
|
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,
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.1.11",
|
|
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": {
|