@goodandready/dsh-opencode-provider 0.5.17 → 0.5.19
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/CHANGELOG.md +13 -0
- package/README.md +3 -3
- package/README.ru.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +155 -1
- package/lib/index.d.ts +13 -0
- package/lib/index.js +112 -13
- package/package.json +18 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.19
|
|
4
|
+
|
|
5
|
+
- **Per-model soft cooldown**: quota/free-tier/rate-limit rejections now cool only that key+model (`quota` 5m, `rate-limit` 60s); other models on the same account remain usable. Invalid credentials still cool the account. If every key is cooling, the request degrades to the normal candidate list instead of failing before dispatch.
|
|
6
|
+
- **Service status**: `/state` exposes the usage check time, latency and error, plus catalog source (`live`/`snapshot`), count and fetch time. Settings shows a compact Service status card.
|
|
7
|
+
- 105 tests; engine matrix/install matrix remain green.
|
|
8
|
+
|
|
9
|
+
## 0.5.18
|
|
10
|
+
|
|
11
|
+
- **Compatibility range**: `engines.dsh`, DSH peer ranges and `dsh.compatibility.dsh` now use `>=0.2.1-rc.2 <0.2.2-0`; `dshReleases` keeps the tested rc.2/rc.3/rc.4 records. Future `0.2.1` RCs are admitted automatically; the next minor remains excluded.
|
|
12
|
+
- **Engine matrix**: added `test:engine` and `test:install`, which install the packed plugin into isolated web profiles for every `dshReleases` version, then boot the host and assert provider activation and a non-empty live catalog.
|
|
13
|
+
- **Key status**: `/state` now reports `keyStatus` per account (`ok`, `expired`, `invalid`, `missing`, `unavailable`, `unchecked`); usage 401/403 bodies are classified as expired/invalid. Settings shows the badge and adds a per-account **Check key** action backed by the new `/dsh-opencode/accounts/check` route. A check only reads the usage endpoint and does not send a model request.
|
|
14
|
+
- 104 tests.
|
|
15
|
+
|
|
3
16
|
## 0.5.17
|
|
4
17
|
|
|
5
18
|
- **DSH 0.2.1-rc.4 compatibility**: added rc.4 to `engines.dsh`, peer ranges and `dsh.compatibility.dshReleases`; rc.2 and rc.3 remain supported. Development/typecheck dependencies moved to `@deepseek-ai/*@0.2.1-rc.4`.
|
package/README.md
CHANGED
|
@@ -130,12 +130,12 @@ npm pack
|
|
|
130
130
|
Install the **packed tarball** into a profile:
|
|
131
131
|
|
|
132
132
|
```bash
|
|
133
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
133
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.19.tgz
|
|
134
134
|
# DSH Desktop:
|
|
135
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
135
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.19.tgz
|
|
136
136
|
|
|
137
137
|
# Or install the published package:
|
|
138
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
138
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.19
|
|
139
139
|
```
|
|
140
140
|
|
|
141
141
|
Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
|
package/README.ru.md
CHANGED
|
@@ -129,12 +129,12 @@ npm pack
|
|
|
129
129
|
Установка из **упакованного tarball**:
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
132
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.19.tgz
|
|
133
133
|
# DSH Desktop:
|
|
134
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
134
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.19.tgz
|
|
135
135
|
|
|
136
136
|
# Либо установка опубликованного пакета из npm:
|
|
137
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
137
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.19
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
|
package/README.zh.md
CHANGED
|
@@ -124,12 +124,12 @@ npm pack
|
|
|
124
124
|
通过**打包文件 (.tgz)**安装:
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
127
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.19.tgz
|
|
128
128
|
# DSH 桌面端:
|
|
129
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
129
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.19.tgz
|
|
130
130
|
|
|
131
131
|
# 或从 npm 安装已发布版本:
|
|
132
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
132
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.19
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
|
package/lib/client.js
CHANGED
|
@@ -17,6 +17,25 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
keyHint: "Paste a key from the OpenCode console, or import the key OpenCode already stored.",
|
|
18
18
|
keyConfigured: "Configured",
|
|
19
19
|
keyMissing: "Not configured",
|
|
20
|
+
keyStatusOk: "Key OK",
|
|
21
|
+
keyStatusExpired: "Key expired",
|
|
22
|
+
keyStatusInvalid: "Key rejected",
|
|
23
|
+
keyStatusMissing: "Key missing",
|
|
24
|
+
keyStatusUnavailable: "Key check failed",
|
|
25
|
+
keyStatusUnchecked: "Key unchecked",
|
|
26
|
+
checkKey: "Check key",
|
|
27
|
+
keyStatusHint: "A key check reads the usage endpoint; it does not send a model request.",
|
|
28
|
+
serviceStatus: "Service status",
|
|
29
|
+
serviceOk: "Reachable",
|
|
30
|
+
serviceDegraded: "Degraded",
|
|
31
|
+
usageEndpoint: "Usage endpoint",
|
|
32
|
+
catalogStatus: "Model catalog",
|
|
33
|
+
catalogLive: "live",
|
|
34
|
+
catalogSnapshot: "snapshot",
|
|
35
|
+
catalogAge: "updated {age}",
|
|
36
|
+
checkedAt: "checked {time}",
|
|
37
|
+
latencyValue: "{ms} ms",
|
|
38
|
+
notChecked: "not checked",
|
|
20
39
|
keySource: "Source",
|
|
21
40
|
keyWritable: "Writable",
|
|
22
41
|
pasteKey: "Paste a key",
|
|
@@ -202,6 +221,25 @@ window.__ModuleLoader__.load({
|
|
|
202
221
|
keyHint: "粘贴 OpenCode 控制台的密钥,或导入 OpenCode 已保存的密钥。",
|
|
203
222
|
keyConfigured: "已配置",
|
|
204
223
|
keyMissing: "未配置",
|
|
224
|
+
keyStatusOk: "密钥正常",
|
|
225
|
+
keyStatusExpired: "密钥已过期",
|
|
226
|
+
keyStatusInvalid: "密钥被拒绝",
|
|
227
|
+
keyStatusMissing: "缺少密钥",
|
|
228
|
+
keyStatusUnavailable: "检查失败",
|
|
229
|
+
keyStatusUnchecked: "未检查",
|
|
230
|
+
checkKey: "检查密钥",
|
|
231
|
+
keyStatusHint: "检查仅读取用量端点,不会发送模型请求。",
|
|
232
|
+
serviceStatus: "服务状态",
|
|
233
|
+
serviceOk: "可访问",
|
|
234
|
+
serviceDegraded: "异常",
|
|
235
|
+
usageEndpoint: "用量端点",
|
|
236
|
+
catalogStatus: "模型目录",
|
|
237
|
+
catalogLive: "实时",
|
|
238
|
+
catalogSnapshot: "快照",
|
|
239
|
+
catalogAge: "更新于 {age}",
|
|
240
|
+
checkedAt: "检查于 {time}",
|
|
241
|
+
latencyValue: "{ms} ms",
|
|
242
|
+
notChecked: "未检查",
|
|
205
243
|
keySource: "来源",
|
|
206
244
|
keyWritable: "可写",
|
|
207
245
|
pasteKey: "粘贴密钥",
|
|
@@ -412,6 +450,12 @@ window.__ModuleLoader__.load({
|
|
|
412
450
|
body: "{}"
|
|
413
451
|
});
|
|
414
452
|
}
|
|
453
|
+
function checkAccount(label) {
|
|
454
|
+
return call("/dsh-opencode/accounts/check", {
|
|
455
|
+
method: "POST",
|
|
456
|
+
body: JSON.stringify({ label })
|
|
457
|
+
});
|
|
458
|
+
}
|
|
415
459
|
function saveConfig(patch) {
|
|
416
460
|
return call("/dsh-opencode/config", {
|
|
417
461
|
method: "POST",
|
|
@@ -1819,6 +1863,26 @@ window.__ModuleLoader__.load({
|
|
|
1819
1863
|
}
|
|
1820
1864
|
//#endregion
|
|
1821
1865
|
//#region src/client/settings-page.tsx
|
|
1866
|
+
function keyStatusLabel(status, t) {
|
|
1867
|
+
if (status === "ok") return t("keyStatusOk");
|
|
1868
|
+
if (status === "expired") return t("keyStatusExpired");
|
|
1869
|
+
if (status === "invalid") return t("keyStatusInvalid");
|
|
1870
|
+
if (status === "missing") return t("keyStatusMissing");
|
|
1871
|
+
if (status === "unavailable") return t("keyStatusUnavailable");
|
|
1872
|
+
return t("keyStatusUnchecked");
|
|
1873
|
+
}
|
|
1874
|
+
function keyStatusClass(status) {
|
|
1875
|
+
if (status === "ok") return "oc-badge-ok";
|
|
1876
|
+
if (status === "expired" || status === "invalid") return "oc-badge-bad";
|
|
1877
|
+
return "oc-badge-warn";
|
|
1878
|
+
}
|
|
1879
|
+
function ageText(at) {
|
|
1880
|
+
if (at === void 0) return "";
|
|
1881
|
+
const minutes = Math.max(0, Math.round((Date.now() - at) / 6e4));
|
|
1882
|
+
if (minutes < 1) return "now";
|
|
1883
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1884
|
+
return `${Math.round(minutes / 60)}h`;
|
|
1885
|
+
}
|
|
1822
1886
|
function OpenCodeSettingsPage(props) {
|
|
1823
1887
|
const { t } = props;
|
|
1824
1888
|
const [state, setState] = (0, react.useState)(null);
|
|
@@ -1834,6 +1898,7 @@ window.__ModuleLoader__.load({
|
|
|
1834
1898
|
const [testing, setTesting] = (0, react.useState)(false);
|
|
1835
1899
|
const [testResult, setTestResult] = (0, react.useState)(null);
|
|
1836
1900
|
const [importing, setImporting] = (0, react.useState)(false);
|
|
1901
|
+
const [checking, setChecking] = (0, react.useState)(null);
|
|
1837
1902
|
const load = (0, react.useCallback)(async () => {
|
|
1838
1903
|
setLoading(true);
|
|
1839
1904
|
try {
|
|
@@ -1875,6 +1940,18 @@ window.__ModuleLoader__.load({
|
|
|
1875
1940
|
setSaving(false);
|
|
1876
1941
|
}
|
|
1877
1942
|
};
|
|
1943
|
+
const checkKey = async (label) => {
|
|
1944
|
+
setChecking(label);
|
|
1945
|
+
setError(null);
|
|
1946
|
+
try {
|
|
1947
|
+
await checkAccount(label);
|
|
1948
|
+
await load();
|
|
1949
|
+
} catch (caught) {
|
|
1950
|
+
setError(caught instanceof Error ? caught.message : t("failed"));
|
|
1951
|
+
} finally {
|
|
1952
|
+
setChecking(null);
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1878
1955
|
const refresh = async () => {
|
|
1879
1956
|
try {
|
|
1880
1957
|
const next = await refreshState();
|
|
@@ -2125,7 +2202,11 @@ window.__ModuleLoader__.load({
|
|
|
2125
2202
|
className: "oc-badge oc-badge-bad",
|
|
2126
2203
|
children: mark
|
|
2127
2204
|
});
|
|
2128
|
-
})()
|
|
2205
|
+
})(),
|
|
2206
|
+
defaultAccount === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2207
|
+
className: `oc-badge ${keyStatusClass(defaultAccount.keyStatus)}`,
|
|
2208
|
+
children: keyStatusLabel(defaultAccount.keyStatus, t)
|
|
2209
|
+
})
|
|
2129
2210
|
]
|
|
2130
2211
|
}),
|
|
2131
2212
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -2248,6 +2329,62 @@ window.__ModuleLoader__.load({
|
|
|
2248
2329
|
})
|
|
2249
2330
|
]
|
|
2250
2331
|
}),
|
|
2332
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2333
|
+
className: "oc-card",
|
|
2334
|
+
children: [
|
|
2335
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
2336
|
+
className: "oc-card-title",
|
|
2337
|
+
children: [t("serviceStatus"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2338
|
+
className: `oc-badge ${state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-warn"}`,
|
|
2339
|
+
children: state.service.usageError === void 0 ? t("serviceOk") : t("serviceDegraded")
|
|
2340
|
+
})]
|
|
2341
|
+
}),
|
|
2342
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2343
|
+
className: "oc-row",
|
|
2344
|
+
children: [
|
|
2345
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2346
|
+
className: "oc-hint",
|
|
2347
|
+
children: t("usageEndpoint")
|
|
2348
|
+
}),
|
|
2349
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2350
|
+
className: `oc-badge ${state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2351
|
+
children: state.service.usageCheckedAt === void 0 ? t("notChecked") : t("checkedAt").replace("{time}", new Date(state.service.usageCheckedAt).toLocaleTimeString())
|
|
2352
|
+
}),
|
|
2353
|
+
state.service.usageLatencyMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2354
|
+
className: "oc-badge",
|
|
2355
|
+
children: t("latencyValue").replace("{ms}", String(state.service.usageLatencyMs))
|
|
2356
|
+
})
|
|
2357
|
+
]
|
|
2358
|
+
}),
|
|
2359
|
+
state.service.usageError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2360
|
+
className: "oc-hint",
|
|
2361
|
+
children: state.service.usageError
|
|
2362
|
+
}),
|
|
2363
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2364
|
+
className: "oc-row",
|
|
2365
|
+
children: [
|
|
2366
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2367
|
+
className: "oc-hint",
|
|
2368
|
+
children: t("catalogStatus")
|
|
2369
|
+
}),
|
|
2370
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2371
|
+
className: `oc-badge ${state.catalog.source === "live" ? "oc-badge-ok" : "oc-badge-warn"}`,
|
|
2372
|
+
children: [
|
|
2373
|
+
state.catalog.source === "live" ? t("catalogLive") : t("catalogSnapshot"),
|
|
2374
|
+
" · ",
|
|
2375
|
+
state.catalog.count,
|
|
2376
|
+
" ",
|
|
2377
|
+
t("modelCount")
|
|
2378
|
+
]
|
|
2379
|
+
}),
|
|
2380
|
+
state.catalog.fetchedAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2381
|
+
className: "oc-badge",
|
|
2382
|
+
children: t("catalogAge").replace("{age}", ageText(state.catalog.fetchedAt))
|
|
2383
|
+
})
|
|
2384
|
+
]
|
|
2385
|
+
})
|
|
2386
|
+
]
|
|
2387
|
+
}),
|
|
2251
2388
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2252
2389
|
className: "oc-card",
|
|
2253
2390
|
children: [
|
|
@@ -2310,6 +2447,10 @@ window.__ModuleLoader__.load({
|
|
|
2310
2447
|
className: "oc-hint",
|
|
2311
2448
|
children: t("accountsHint")
|
|
2312
2449
|
}),
|
|
2450
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2451
|
+
className: "oc-hint",
|
|
2452
|
+
children: t("keyStatusHint")
|
|
2453
|
+
}),
|
|
2313
2454
|
draft.accounts.map((account, index) => {
|
|
2314
2455
|
const known = state.accounts.find((row) => account.apiKeyEnv !== "" && row.ref === account.apiKeyEnv || account.label !== "" && row.label === account.label);
|
|
2315
2456
|
const configured = known?.configured === true;
|
|
@@ -2348,6 +2489,10 @@ window.__ModuleLoader__.load({
|
|
|
2348
2489
|
className: `oc-badge ${configured ? "oc-badge-ok" : "oc-badge-warn"}`,
|
|
2349
2490
|
children: configured ? t("keyConfigured") : t("keyMissing")
|
|
2350
2491
|
}),
|
|
2492
|
+
known === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2493
|
+
className: `oc-badge ${keyStatusClass(known.keyStatus)}`,
|
|
2494
|
+
children: keyStatusLabel(known.keyStatus, t)
|
|
2495
|
+
}),
|
|
2351
2496
|
known === void 0 || known.mark === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2352
2497
|
className: "oc-badge oc-badge-bad",
|
|
2353
2498
|
children: accountMark(known, t)
|
|
@@ -2401,6 +2546,15 @@ window.__ModuleLoader__.load({
|
|
|
2401
2546
|
},
|
|
2402
2547
|
children: t("saveKey")
|
|
2403
2548
|
}),
|
|
2549
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2550
|
+
type: "button",
|
|
2551
|
+
className: "oc-button",
|
|
2552
|
+
disabled: known === void 0 || checking === known.label,
|
|
2553
|
+
onClick: () => {
|
|
2554
|
+
if (known !== void 0) checkKey(known.label);
|
|
2555
|
+
},
|
|
2556
|
+
children: checking === known?.label ? t("loading") : t("checkKey")
|
|
2557
|
+
}),
|
|
2404
2558
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2405
2559
|
type: "button",
|
|
2406
2560
|
className: "oc-button",
|
package/lib/index.d.ts
CHANGED
|
@@ -190,6 +190,8 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
190
190
|
private stickyWrites;
|
|
191
191
|
/** key fingerprint → cooldown deadline and the rejection that caused it. */
|
|
192
192
|
private readonly cooldowns;
|
|
193
|
+
/** key + model → short soft rejection; other models stay usable. */
|
|
194
|
+
private readonly modelCooldowns;
|
|
193
195
|
private readonly sticky;
|
|
194
196
|
private readonly routing;
|
|
195
197
|
private warnedTools;
|
|
@@ -215,6 +217,11 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
215
217
|
private send;
|
|
216
218
|
private keyId;
|
|
217
219
|
private coolingDown;
|
|
220
|
+
/** Current model-scoped soft cooldown for one key, if any. */
|
|
221
|
+
modelHealth(key: string, model: string): {
|
|
222
|
+
coolingUntil: number;
|
|
223
|
+
reason: string;
|
|
224
|
+
} | undefined;
|
|
218
225
|
/** Current cooldown for one resolved key, if any. */
|
|
219
226
|
accountHealth(key: string): {
|
|
220
227
|
coolingUntil: number;
|
|
@@ -225,6 +232,9 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
225
232
|
private loadCooldowns;
|
|
226
233
|
private pruneCooldowns;
|
|
227
234
|
private persistCooldowns;
|
|
235
|
+
private modelCoolingDown;
|
|
236
|
+
private markRejection;
|
|
237
|
+
private markModelCooldown;
|
|
228
238
|
private markCooldown;
|
|
229
239
|
private isTransient;
|
|
230
240
|
}
|
|
@@ -242,9 +252,12 @@ interface UsageReport {
|
|
|
242
252
|
readonly monthly?: UsageWindow;
|
|
243
253
|
};
|
|
244
254
|
}
|
|
255
|
+
type UsageKeyStatus = 'ok' | 'invalid' | 'expired' | 'missing' | 'unavailable';
|
|
245
256
|
interface UsageResult {
|
|
246
257
|
readonly report?: UsageReport;
|
|
247
258
|
readonly error?: string;
|
|
259
|
+
readonly keyStatus?: UsageKeyStatus;
|
|
260
|
+
readonly latencyMs?: number;
|
|
248
261
|
readonly checkedAt: number;
|
|
249
262
|
}
|
|
250
263
|
interface UsageWindowPayload {
|
package/lib/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
|
|
|
18
18
|
const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
|
|
19
19
|
const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
20
20
|
const DEFAULT_AUTH_ENTRY = "opencode-go";
|
|
21
|
-
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.
|
|
21
|
+
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.19";
|
|
22
22
|
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
23
23
|
const DEFAULT_MAX_TOKENS = 32768;
|
|
24
24
|
const DEFAULT_GENERATE_MAX_TOKENS = 32768;
|
|
@@ -1090,6 +1090,17 @@ function describeModel(meta, provider) {
|
|
|
1090
1090
|
}
|
|
1091
1091
|
return parts.join(" · ");
|
|
1092
1092
|
}
|
|
1093
|
+
function catalogStatus(provider, config, count) {
|
|
1094
|
+
const cached = catalogCache.get(`${provider}|${modelsUrlFor(provider, config)}`);
|
|
1095
|
+
return cached === void 0 ? {
|
|
1096
|
+
source: "snapshot",
|
|
1097
|
+
count
|
|
1098
|
+
} : {
|
|
1099
|
+
source: "live",
|
|
1100
|
+
count,
|
|
1101
|
+
fetchedAt: cached.at
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1093
1104
|
function clearCatalogCache() {
|
|
1094
1105
|
catalogCache.clear();
|
|
1095
1106
|
}
|
|
@@ -3100,6 +3111,8 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3100
3111
|
stickyWrites = Promise.resolve();
|
|
3101
3112
|
/** key fingerprint → cooldown deadline and the rejection that caused it. */
|
|
3102
3113
|
cooldowns = /* @__PURE__ */ new Map();
|
|
3114
|
+
/** key + model → short soft rejection; other models stay usable. */
|
|
3115
|
+
modelCooldowns = /* @__PURE__ */ new Map();
|
|
3103
3116
|
sticky = /* @__PURE__ */ new Map();
|
|
3104
3117
|
routing = [];
|
|
3105
3118
|
warnedTools = false;
|
|
@@ -3202,7 +3215,10 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3202
3215
|
}
|
|
3203
3216
|
}
|
|
3204
3217
|
}
|
|
3205
|
-
const
|
|
3218
|
+
const eligible = fallback ? keys : keys.filter((entry) => !this.coolingDown(entry.key) && !this.modelCoolingDown(entry.key, options.model));
|
|
3219
|
+
const ordered = eligible.length > 0 ? eligible : keys;
|
|
3220
|
+
const forced = !fallback && eligible.length === 0;
|
|
3221
|
+
const initial = ordered[0];
|
|
3206
3222
|
const url = endpointFor(options.provider, model.transport, options.model, config);
|
|
3207
3223
|
let images;
|
|
3208
3224
|
if (hasImages(options)) {
|
|
@@ -3226,8 +3242,8 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3226
3242
|
let lastError;
|
|
3227
3243
|
let lastAccount = initial?.label ?? "default";
|
|
3228
3244
|
let loggedAny = false;
|
|
3229
|
-
for (const [index, entry] of
|
|
3230
|
-
if (!fallback && this.coolingDown(entry.key)) continue;
|
|
3245
|
+
for (const [index, entry] of ordered.entries()) {
|
|
3246
|
+
if (!fallback && !forced && (this.coolingDown(entry.key) || this.modelCoolingDown(entry.key, options.model))) continue;
|
|
3231
3247
|
lastAccount = entry.label ?? "default";
|
|
3232
3248
|
const attemptReason = index === 0 ? reason : "rotation";
|
|
3233
3249
|
const attemptStartedAt = Date.now();
|
|
@@ -3254,9 +3270,9 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3254
3270
|
const error = classifyResponseError(response.status, text, response.headers);
|
|
3255
3271
|
lastError = error;
|
|
3256
3272
|
logAttempt(false, error.code);
|
|
3257
|
-
const canRotate = index <
|
|
3273
|
+
const canRotate = index < ordered.length - 1;
|
|
3258
3274
|
if (error.code === "INVALID_CREDENTIAL" || error.code === "RATE_LIMIT" || error.code === "QUOTA") {
|
|
3259
|
-
if (!fallback) this.
|
|
3275
|
+
if (!fallback) this.markRejection(entry.key, options.model, response.status, response.headers, error.code);
|
|
3260
3276
|
responseFailure = true;
|
|
3261
3277
|
if (canRotate) continue;
|
|
3262
3278
|
}
|
|
@@ -3297,8 +3313,8 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3297
3313
|
}
|
|
3298
3314
|
lastError = classified;
|
|
3299
3315
|
const transient = this.isTransient(classified);
|
|
3300
|
-
if (transient && !responseFailure && !fallback) this.
|
|
3301
|
-
if (!(index <
|
|
3316
|
+
if (transient && !responseFailure && !fallback) this.markRejection(entry.key, options.model, 0, void 0, classified.code);
|
|
3317
|
+
if (!(index < ordered.length - 1 && transient)) {
|
|
3302
3318
|
if (!fallback && config.quotaFallbackModel !== "" && isGoProvider(options.provider) && classified.code === "QUOTA") break;
|
|
3303
3319
|
throw classified;
|
|
3304
3320
|
}
|
|
@@ -3453,6 +3469,20 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3453
3469
|
}
|
|
3454
3470
|
return true;
|
|
3455
3471
|
}
|
|
3472
|
+
/** Current model-scoped soft cooldown for one key, if any. */
|
|
3473
|
+
modelHealth(key, model) {
|
|
3474
|
+
const id = `${this.keyId(key)}:${model}`;
|
|
3475
|
+
const entry = this.modelCooldowns.get(id);
|
|
3476
|
+
if (entry === void 0) return void 0;
|
|
3477
|
+
if (entry.until <= Date.now()) {
|
|
3478
|
+
this.modelCooldowns.delete(id);
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
return {
|
|
3482
|
+
coolingUntil: entry.until,
|
|
3483
|
+
reason: entry.reason
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3456
3486
|
/** Current cooldown for one resolved key, if any. */
|
|
3457
3487
|
accountHealth(key) {
|
|
3458
3488
|
const id = this.keyId(key);
|
|
@@ -3505,6 +3535,30 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3505
3535
|
await writeFile(this.cooldownPath, payload, "utf8");
|
|
3506
3536
|
}).catch(() => {});
|
|
3507
3537
|
}
|
|
3538
|
+
modelCoolingDown(key, model) {
|
|
3539
|
+
const id = `${this.keyId(key)}:${model}`;
|
|
3540
|
+
const entry = this.modelCooldowns.get(id);
|
|
3541
|
+
if (entry === void 0) return false;
|
|
3542
|
+
if (entry.until <= Date.now()) {
|
|
3543
|
+
this.modelCooldowns.delete(id);
|
|
3544
|
+
return false;
|
|
3545
|
+
}
|
|
3546
|
+
return true;
|
|
3547
|
+
}
|
|
3548
|
+
markRejection(key, model, status, headers, code) {
|
|
3549
|
+
if (code === "QUOTA" || code === "RATE_LIMIT") {
|
|
3550
|
+
this.markModelCooldown(key, model, code);
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
this.markCooldown(key, status, headers, code);
|
|
3554
|
+
}
|
|
3555
|
+
markModelCooldown(key, model, code) {
|
|
3556
|
+
const delayMs = code === "QUOTA" ? 3e5 : 6e4;
|
|
3557
|
+
this.modelCooldowns.set(`${this.keyId(key)}:${model}`, {
|
|
3558
|
+
until: Date.now() + delayMs,
|
|
3559
|
+
reason: code === "QUOTA" ? "quota" : "rate-limit"
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3508
3562
|
markCooldown(key, status, headers, code) {
|
|
3509
3563
|
let delayMs = 3e4;
|
|
3510
3564
|
let reason = "transport";
|
|
@@ -4737,6 +4791,8 @@ async function buildState(ctx, deps) {
|
|
|
4737
4791
|
source = hit.source;
|
|
4738
4792
|
}
|
|
4739
4793
|
const health = hit === void 0 ? void 0 : deps.adapter.accountHealth(hit.key);
|
|
4794
|
+
const usage = hit === void 0 ? null : usageByLabel.get(slot.label) ?? null;
|
|
4795
|
+
const keyStatus = !configured ? "missing" : health?.reason === "invalid-credential" ? "invalid" : usage?.keyStatus ?? "unchecked";
|
|
4740
4796
|
accounts.push({
|
|
4741
4797
|
...slot,
|
|
4742
4798
|
configured,
|
|
@@ -4744,7 +4800,8 @@ async function buildState(ctx, deps) {
|
|
|
4744
4800
|
active: activeLabel === slot.label,
|
|
4745
4801
|
coolingUntil: health?.coolingUntil ?? 0,
|
|
4746
4802
|
mark: health?.reason ?? "",
|
|
4747
|
-
|
|
4803
|
+
keyStatus,
|
|
4804
|
+
usage
|
|
4748
4805
|
});
|
|
4749
4806
|
}
|
|
4750
4807
|
let models = [];
|
|
@@ -4776,7 +4833,13 @@ async function buildState(ctx, deps) {
|
|
|
4776
4833
|
key: keyInfo,
|
|
4777
4834
|
accounts,
|
|
4778
4835
|
models,
|
|
4836
|
+
catalog: catalogStatus(GO_PROVIDER, config, models.length),
|
|
4779
4837
|
usage: usage ?? null,
|
|
4838
|
+
service: {
|
|
4839
|
+
...usage?.checkedAt === void 0 ? {} : { usageCheckedAt: usage.checkedAt },
|
|
4840
|
+
...usage?.latencyMs === void 0 ? {} : { usageLatencyMs: usage.latencyMs },
|
|
4841
|
+
...usage?.error === void 0 ? {} : { usageError: usage.error }
|
|
4842
|
+
},
|
|
4780
4843
|
routing: deps.adapter.routingTrace(),
|
|
4781
4844
|
auth: {
|
|
4782
4845
|
entry: config.openCodeAuthEntry,
|
|
@@ -4972,6 +5035,28 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4972
5035
|
json(res, 500, { error: error instanceof Error ? error.message : "refresh failed" });
|
|
4973
5036
|
}
|
|
4974
5037
|
});
|
|
5038
|
+
route("/dsh-opencode/accounts/check", async (req, res) => {
|
|
5039
|
+
if (!requirePost(req, res)) return;
|
|
5040
|
+
try {
|
|
5041
|
+
const body = await readJson(req);
|
|
5042
|
+
const label = typeof body.label === "string" && body.label.trim() !== "" ? body.label.trim() : "default";
|
|
5043
|
+
const hit = (await deps.pool.resolveAll(GO_PROVIDER).catch(() => [])).find((entry) => (entry.label ?? "default") === label);
|
|
5044
|
+
if (hit === void 0) {
|
|
5045
|
+
json(res, 404, { error: `account "${label}" is not configured` });
|
|
5046
|
+
return;
|
|
5047
|
+
}
|
|
5048
|
+
deps.usage.clear();
|
|
5049
|
+
const result = await deps.usage(GO_PROVIDER, hit.key);
|
|
5050
|
+
const keyStatus = result?.keyStatus ?? "unchecked";
|
|
5051
|
+
json(res, 200, {
|
|
5052
|
+
ok: keyStatus === "ok",
|
|
5053
|
+
keyStatus,
|
|
5054
|
+
...result?.error === void 0 ? {} : { error: result.error }
|
|
5055
|
+
});
|
|
5056
|
+
} catch (error) {
|
|
5057
|
+
json(res, 500, { error: error instanceof Error ? error.message : "account check failed" });
|
|
5058
|
+
}
|
|
5059
|
+
});
|
|
4975
5060
|
route("/dsh-opencode/models/refresh", async (req, res) => {
|
|
4976
5061
|
if (!requirePost(req, res)) return;
|
|
4977
5062
|
clearCatalogCache();
|
|
@@ -5324,6 +5409,7 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
|
5324
5409
|
} catch {
|
|
5325
5410
|
return {
|
|
5326
5411
|
error: "no credential",
|
|
5412
|
+
keyStatus: "missing",
|
|
5327
5413
|
checkedAt: Date.now()
|
|
5328
5414
|
};
|
|
5329
5415
|
}
|
|
@@ -5334,7 +5420,9 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
|
5334
5420
|
if (Date.now() - cached.checkedAt < ttl) return cached;
|
|
5335
5421
|
}
|
|
5336
5422
|
const result = async () => {
|
|
5423
|
+
const startedAt = Date.now();
|
|
5337
5424
|
const signal = AbortSignal.timeout(USAGE_TIMEOUT_MS);
|
|
5425
|
+
const latency = () => Math.max(0, Date.now() - startedAt);
|
|
5338
5426
|
try {
|
|
5339
5427
|
let response;
|
|
5340
5428
|
let lastNetworkError;
|
|
@@ -5355,21 +5443,32 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
|
5355
5443
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
5356
5444
|
}
|
|
5357
5445
|
if (response === void 0) throw lastNetworkError ?? /* @__PURE__ */ new Error("usage request failed");
|
|
5358
|
-
if (response.status === 401 || response.status === 403)
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5446
|
+
if (response.status === 401 || response.status === 403) {
|
|
5447
|
+
const body = await response.text().catch(() => "");
|
|
5448
|
+
return {
|
|
5449
|
+
error: "usage endpoint rejected the key (inference may still work)",
|
|
5450
|
+
keyStatus: /expired/i.test(body) ? "expired" : "invalid",
|
|
5451
|
+
latencyMs: latency(),
|
|
5452
|
+
checkedAt: Date.now()
|
|
5453
|
+
};
|
|
5454
|
+
}
|
|
5362
5455
|
if (!response.ok) return {
|
|
5363
5456
|
error: `usage endpoint returned HTTP ${response.status}`,
|
|
5457
|
+
keyStatus: "unavailable",
|
|
5458
|
+
latencyMs: latency(),
|
|
5364
5459
|
checkedAt: Date.now()
|
|
5365
5460
|
};
|
|
5366
5461
|
return {
|
|
5367
5462
|
report: normalizeUsage(await response.json()),
|
|
5463
|
+
keyStatus: "ok",
|
|
5464
|
+
latencyMs: latency(),
|
|
5368
5465
|
checkedAt: Date.now()
|
|
5369
5466
|
};
|
|
5370
5467
|
} catch (error) {
|
|
5371
5468
|
return {
|
|
5372
5469
|
error: error instanceof Error ? error.message : "usage request failed",
|
|
5470
|
+
keyStatus: "unavailable",
|
|
5471
|
+
latencyMs: latency(),
|
|
5373
5472
|
checkedAt: Date.now()
|
|
5374
5473
|
};
|
|
5375
5474
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-opencode-provider",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.19",
|
|
4
4
|
"description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -54,17 +54,16 @@
|
|
|
54
54
|
},
|
|
55
55
|
"compatibility": {
|
|
56
56
|
"dshReleases": {
|
|
57
|
-
"0.2.0-rc.2": "compatible",
|
|
58
57
|
"0.2.1-rc.2": "compatible",
|
|
59
58
|
"0.2.1-rc.3": "compatible",
|
|
60
59
|
"0.2.1-rc.4": "compatible"
|
|
61
60
|
},
|
|
62
|
-
"dsh": "0.2.
|
|
61
|
+
"dsh": ">=0.2.1-rc.2 <0.2.2-0"
|
|
63
62
|
}
|
|
64
63
|
},
|
|
65
64
|
"engines": {
|
|
66
65
|
"node": ">=22",
|
|
67
|
-
"dsh": "0.2.
|
|
66
|
+
"dsh": ">=0.2.1-rc.2 <0.2.2-0"
|
|
68
67
|
},
|
|
69
68
|
"scripts": {
|
|
70
69
|
"build": "tsdown",
|
|
@@ -72,6 +71,8 @@
|
|
|
72
71
|
"typecheck": "tsc --noEmit",
|
|
73
72
|
"test": "node --import tsx --test tests/core.test.ts tests/routes.test.ts tests/limits.test.ts tests/analytics.test.ts tests/adapter.test.ts tests/usage.test.ts tests/request-log.test.ts tests/account-rules.test.ts tests/commands.test.ts tests/tool-schema.test.ts tests/tui-settings.test.ts tests/plan-filter.test.ts tests/image-budget.test.ts tests/tools.test.ts tests/plugin-updater.test.ts tests/client-render.test.tsx",
|
|
74
73
|
"check": "npm run typecheck && npm test && npm run build",
|
|
74
|
+
"test:engine": "node scripts/test-engines.mjs",
|
|
75
|
+
"test:install": "node scripts/test-engines.mjs --install-only",
|
|
75
76
|
"release": "npm run check && npm pack",
|
|
76
77
|
"sync:models": "node scripts/sync-models.mjs"
|
|
77
78
|
},
|
|
@@ -84,19 +85,19 @@
|
|
|
84
85
|
"llm-provider"
|
|
85
86
|
],
|
|
86
87
|
"peerDependencies": {
|
|
87
|
-
"@deepseek-ai/cordis": "~4.0.
|
|
88
|
-
"@deepseek-ai/dsh-attachment": "0.2.
|
|
89
|
-
"@deepseek-ai/dsh-client-locale": "0.2.
|
|
90
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.2.
|
|
91
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.
|
|
92
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.
|
|
93
|
-
"@deepseek-ai/dsh-commands": "0.2.
|
|
94
|
-
"@deepseek-ai/dsh-credentials": "0.2.
|
|
95
|
-
"@deepseek-ai/dsh-host-webserver": "0.2.
|
|
96
|
-
"@deepseek-ai/dsh-launch-environment": "0.2.
|
|
97
|
-
"@deepseek-ai/dsh-llm": "0.2.
|
|
98
|
-
"@deepseek-ai/dsh-settings": "0.2.
|
|
99
|
-
"@deepseek-ai/schemastery": "~3.18.
|
|
88
|
+
"@deepseek-ai/cordis": "~4.0.5",
|
|
89
|
+
"@deepseek-ai/dsh-attachment": ">=0.2.1-rc.2 <0.2.2-0",
|
|
90
|
+
"@deepseek-ai/dsh-client-locale": ">=0.2.1-rc.2 <0.2.2-0",
|
|
91
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.2.1-rc.2 <0.2.2-0",
|
|
92
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.2.1-rc.2 <0.2.2-0",
|
|
93
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.2.1-rc.2 <0.2.2-0",
|
|
94
|
+
"@deepseek-ai/dsh-commands": ">=0.2.1-rc.2 <0.2.2-0",
|
|
95
|
+
"@deepseek-ai/dsh-credentials": ">=0.2.1-rc.2 <0.2.2-0",
|
|
96
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.2.1-rc.2 <0.2.2-0",
|
|
97
|
+
"@deepseek-ai/dsh-launch-environment": ">=0.2.1-rc.2 <0.2.2-0",
|
|
98
|
+
"@deepseek-ai/dsh-llm": ">=0.2.1-rc.2 <0.2.2-0",
|
|
99
|
+
"@deepseek-ai/dsh-settings": ">=0.2.1-rc.2 <0.2.2-0",
|
|
100
|
+
"@deepseek-ai/schemastery": "~3.18.5",
|
|
100
101
|
"react": "^18.2.0"
|
|
101
102
|
},
|
|
102
103
|
"devDependencies": {
|