@goodandready/dsh-opencode-provider 0.5.17 → 0.5.18

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.18
4
+
5
+ - **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.
6
+ - **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.
7
+ - **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.
8
+ - 104 tests.
9
+
3
10
  ## 0.5.17
4
11
 
5
12
  - **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.17.tgz
133
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.18.tgz
134
134
  # DSH Desktop:
135
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.17.tgz
135
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.18.tgz
136
136
 
137
137
  # Or install the published package:
138
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.17
138
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.18
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.17.tgz
132
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.18.tgz
133
133
  # DSH Desktop:
134
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.17.tgz
134
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.18.tgz
135
135
 
136
136
  # Либо установка опубликованного пакета из npm:
137
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.17
137
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.18
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.17.tgz
127
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.18.tgz
128
128
  # DSH 桌面端:
129
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.17.tgz
129
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.18.tgz
130
130
 
131
131
  # 或从 npm 安装已发布版本:
132
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.17
132
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.18
133
133
  ```
134
134
 
135
135
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
package/lib/client.js CHANGED
@@ -17,6 +17,14 @@ 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.",
20
28
  keySource: "Source",
21
29
  keyWritable: "Writable",
22
30
  pasteKey: "Paste a key",
@@ -202,6 +210,14 @@ window.__ModuleLoader__.load({
202
210
  keyHint: "粘贴 OpenCode 控制台的密钥,或导入 OpenCode 已保存的密钥。",
203
211
  keyConfigured: "已配置",
204
212
  keyMissing: "未配置",
213
+ keyStatusOk: "密钥正常",
214
+ keyStatusExpired: "密钥已过期",
215
+ keyStatusInvalid: "密钥被拒绝",
216
+ keyStatusMissing: "缺少密钥",
217
+ keyStatusUnavailable: "检查失败",
218
+ keyStatusUnchecked: "未检查",
219
+ checkKey: "检查密钥",
220
+ keyStatusHint: "检查仅读取用量端点,不会发送模型请求。",
205
221
  keySource: "来源",
206
222
  keyWritable: "可写",
207
223
  pasteKey: "粘贴密钥",
@@ -412,6 +428,12 @@ window.__ModuleLoader__.load({
412
428
  body: "{}"
413
429
  });
414
430
  }
431
+ function checkAccount(label) {
432
+ return call("/dsh-opencode/accounts/check", {
433
+ method: "POST",
434
+ body: JSON.stringify({ label })
435
+ });
436
+ }
415
437
  function saveConfig(patch) {
416
438
  return call("/dsh-opencode/config", {
417
439
  method: "POST",
@@ -1819,6 +1841,19 @@ window.__ModuleLoader__.load({
1819
1841
  }
1820
1842
  //#endregion
1821
1843
  //#region src/client/settings-page.tsx
1844
+ function keyStatusLabel(status, t) {
1845
+ if (status === "ok") return t("keyStatusOk");
1846
+ if (status === "expired") return t("keyStatusExpired");
1847
+ if (status === "invalid") return t("keyStatusInvalid");
1848
+ if (status === "missing") return t("keyStatusMissing");
1849
+ if (status === "unavailable") return t("keyStatusUnavailable");
1850
+ return t("keyStatusUnchecked");
1851
+ }
1852
+ function keyStatusClass(status) {
1853
+ if (status === "ok") return "oc-badge-ok";
1854
+ if (status === "expired" || status === "invalid") return "oc-badge-bad";
1855
+ return "oc-badge-warn";
1856
+ }
1822
1857
  function OpenCodeSettingsPage(props) {
1823
1858
  const { t } = props;
1824
1859
  const [state, setState] = (0, react.useState)(null);
@@ -1834,6 +1869,7 @@ window.__ModuleLoader__.load({
1834
1869
  const [testing, setTesting] = (0, react.useState)(false);
1835
1870
  const [testResult, setTestResult] = (0, react.useState)(null);
1836
1871
  const [importing, setImporting] = (0, react.useState)(false);
1872
+ const [checking, setChecking] = (0, react.useState)(null);
1837
1873
  const load = (0, react.useCallback)(async () => {
1838
1874
  setLoading(true);
1839
1875
  try {
@@ -1875,6 +1911,18 @@ window.__ModuleLoader__.load({
1875
1911
  setSaving(false);
1876
1912
  }
1877
1913
  };
1914
+ const checkKey = async (label) => {
1915
+ setChecking(label);
1916
+ setError(null);
1917
+ try {
1918
+ await checkAccount(label);
1919
+ await load();
1920
+ } catch (caught) {
1921
+ setError(caught instanceof Error ? caught.message : t("failed"));
1922
+ } finally {
1923
+ setChecking(null);
1924
+ }
1925
+ };
1878
1926
  const refresh = async () => {
1879
1927
  try {
1880
1928
  const next = await refreshState();
@@ -2125,7 +2173,11 @@ window.__ModuleLoader__.load({
2125
2173
  className: "oc-badge oc-badge-bad",
2126
2174
  children: mark
2127
2175
  });
2128
- })()
2176
+ })(),
2177
+ defaultAccount === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2178
+ className: `oc-badge ${keyStatusClass(defaultAccount.keyStatus)}`,
2179
+ children: keyStatusLabel(defaultAccount.keyStatus, t)
2180
+ })
2129
2181
  ]
2130
2182
  }),
2131
2183
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -2310,6 +2362,10 @@ window.__ModuleLoader__.load({
2310
2362
  className: "oc-hint",
2311
2363
  children: t("accountsHint")
2312
2364
  }),
2365
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2366
+ className: "oc-hint",
2367
+ children: t("keyStatusHint")
2368
+ }),
2313
2369
  draft.accounts.map((account, index) => {
2314
2370
  const known = state.accounts.find((row) => account.apiKeyEnv !== "" && row.ref === account.apiKeyEnv || account.label !== "" && row.label === account.label);
2315
2371
  const configured = known?.configured === true;
@@ -2348,6 +2404,10 @@ window.__ModuleLoader__.load({
2348
2404
  className: `oc-badge ${configured ? "oc-badge-ok" : "oc-badge-warn"}`,
2349
2405
  children: configured ? t("keyConfigured") : t("keyMissing")
2350
2406
  }),
2407
+ known === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2408
+ className: `oc-badge ${keyStatusClass(known.keyStatus)}`,
2409
+ children: keyStatusLabel(known.keyStatus, t)
2410
+ }),
2351
2411
  known === void 0 || known.mark === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2352
2412
  className: "oc-badge oc-badge-bad",
2353
2413
  children: accountMark(known, t)
@@ -2401,6 +2461,15 @@ window.__ModuleLoader__.load({
2401
2461
  },
2402
2462
  children: t("saveKey")
2403
2463
  }),
2464
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2465
+ type: "button",
2466
+ className: "oc-button",
2467
+ disabled: known === void 0 || checking === known.label,
2468
+ onClick: () => {
2469
+ if (known !== void 0) checkKey(known.label);
2470
+ },
2471
+ children: checking === known?.label ? t("loading") : t("checkKey")
2472
+ }),
2404
2473
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2405
2474
  type: "button",
2406
2475
  className: "oc-button",
package/lib/index.d.ts CHANGED
@@ -242,9 +242,11 @@ interface UsageReport {
242
242
  readonly monthly?: UsageWindow;
243
243
  };
244
244
  }
245
+ type UsageKeyStatus = 'ok' | 'invalid' | 'expired' | 'missing' | 'unavailable';
245
246
  interface UsageResult {
246
247
  readonly report?: UsageReport;
247
248
  readonly error?: string;
249
+ readonly keyStatus?: UsageKeyStatus;
248
250
  readonly checkedAt: number;
249
251
  }
250
252
  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.17";
21
+ const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.18";
22
22
  const DEFAULT_CONTEXT_WINDOW = 262144;
23
23
  const DEFAULT_MAX_TOKENS = 32768;
24
24
  const DEFAULT_GENERATE_MAX_TOKENS = 32768;
@@ -4737,6 +4737,8 @@ async function buildState(ctx, deps) {
4737
4737
  source = hit.source;
4738
4738
  }
4739
4739
  const health = hit === void 0 ? void 0 : deps.adapter.accountHealth(hit.key);
4740
+ const usage = hit === void 0 ? null : usageByLabel.get(slot.label) ?? null;
4741
+ const keyStatus = !configured ? "missing" : health?.reason === "invalid-credential" ? "invalid" : usage?.keyStatus ?? "unchecked";
4740
4742
  accounts.push({
4741
4743
  ...slot,
4742
4744
  configured,
@@ -4744,7 +4746,8 @@ async function buildState(ctx, deps) {
4744
4746
  active: activeLabel === slot.label,
4745
4747
  coolingUntil: health?.coolingUntil ?? 0,
4746
4748
  mark: health?.reason ?? "",
4747
- usage: hit === void 0 ? null : usageByLabel.get(slot.label) ?? null
4749
+ keyStatus,
4750
+ usage
4748
4751
  });
4749
4752
  }
4750
4753
  let models = [];
@@ -4972,6 +4975,28 @@ function applyRoutes(ctx, web, deps) {
4972
4975
  json(res, 500, { error: error instanceof Error ? error.message : "refresh failed" });
4973
4976
  }
4974
4977
  });
4978
+ route("/dsh-opencode/accounts/check", async (req, res) => {
4979
+ if (!requirePost(req, res)) return;
4980
+ try {
4981
+ const body = await readJson(req);
4982
+ const label = typeof body.label === "string" && body.label.trim() !== "" ? body.label.trim() : "default";
4983
+ const hit = (await deps.pool.resolveAll(GO_PROVIDER).catch(() => [])).find((entry) => (entry.label ?? "default") === label);
4984
+ if (hit === void 0) {
4985
+ json(res, 404, { error: `account "${label}" is not configured` });
4986
+ return;
4987
+ }
4988
+ deps.usage.clear();
4989
+ const result = await deps.usage(GO_PROVIDER, hit.key);
4990
+ const keyStatus = result?.keyStatus ?? "unchecked";
4991
+ json(res, 200, {
4992
+ ok: keyStatus === "ok",
4993
+ keyStatus,
4994
+ ...result?.error === void 0 ? {} : { error: result.error }
4995
+ });
4996
+ } catch (error) {
4997
+ json(res, 500, { error: error instanceof Error ? error.message : "account check failed" });
4998
+ }
4999
+ });
4975
5000
  route("/dsh-opencode/models/refresh", async (req, res) => {
4976
5001
  if (!requirePost(req, res)) return;
4977
5002
  clearCatalogCache();
@@ -5324,6 +5349,7 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
5324
5349
  } catch {
5325
5350
  return {
5326
5351
  error: "no credential",
5352
+ keyStatus: "missing",
5327
5353
  checkedAt: Date.now()
5328
5354
  };
5329
5355
  }
@@ -5355,21 +5381,28 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
5355
5381
  await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
5356
5382
  }
5357
5383
  if (response === void 0) throw lastNetworkError ?? /* @__PURE__ */ new Error("usage request failed");
5358
- if (response.status === 401 || response.status === 403) return {
5359
- error: "usage endpoint rejected the key (inference may still work)",
5360
- checkedAt: Date.now()
5361
- };
5384
+ if (response.status === 401 || response.status === 403) {
5385
+ const body = await response.text().catch(() => "");
5386
+ return {
5387
+ error: "usage endpoint rejected the key (inference may still work)",
5388
+ keyStatus: /expired/i.test(body) ? "expired" : "invalid",
5389
+ checkedAt: Date.now()
5390
+ };
5391
+ }
5362
5392
  if (!response.ok) return {
5363
5393
  error: `usage endpoint returned HTTP ${response.status}`,
5394
+ keyStatus: "unavailable",
5364
5395
  checkedAt: Date.now()
5365
5396
  };
5366
5397
  return {
5367
5398
  report: normalizeUsage(await response.json()),
5399
+ keyStatus: "ok",
5368
5400
  checkedAt: Date.now()
5369
5401
  };
5370
5402
  } catch (error) {
5371
5403
  return {
5372
5404
  error: error instanceof Error ? error.message : "usage request failed",
5405
+ keyStatus: "unavailable",
5373
5406
  checkedAt: Date.now()
5374
5407
  };
5375
5408
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
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.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4"
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.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4"
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.4 || ~4.0.5",
88
- "@deepseek-ai/dsh-attachment": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
89
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
90
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
91
- "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
92
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
93
- "@deepseek-ai/dsh-commands": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
94
- "@deepseek-ai/dsh-credentials": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
95
- "@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
96
- "@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
97
- "@deepseek-ai/dsh-llm": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
98
- "@deepseek-ai/dsh-settings": "0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4",
99
- "@deepseek-ai/schemastery": "~3.18.4 || ~3.18.5",
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": {