@goodandready/dsh-opencode-provider 0.5.11 → 0.5.13
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 +14 -0
- package/README.md +3 -3
- package/README.ru.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +15 -4
- package/lib/index.js +58 -9
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.13
|
|
4
|
+
|
|
5
|
+
- Removed the unused `LEGACY_CONSOLE_PROVIDER` constant and the dead `settings.models.provider-card` slot declaration left by the custom provider-card removal.
|
|
6
|
+
- Removed a stray zero-byte `$null` file from the repository root.
|
|
7
|
+
- No runtime behavior change; 100 tests, typecheck and build pass.
|
|
8
|
+
|
|
9
|
+
## 0.5.12
|
|
10
|
+
|
|
11
|
+
- **Security**: read routes now require a literal loopback Host; `/state` is GET-only, `/health` and `/analytics` reject DNS-rebinding hosts, and updater status GET uses a loopback read guard. Added regression tests.
|
|
12
|
+
- **Routing correctness**: sticky selection and the success trace are recorded only after the stream completes; a mid-stream failure now records a failed trace, not a false success.
|
|
13
|
+
- **Onboarding**: auth-entry credential refs use injective hex encoding, so entries differing only by punctuation cannot overwrite each other. Added a collision test.
|
|
14
|
+
- **Client tests**: added React server-render smoke tests for analytics, heatmap, settings and quota panel; 100 tests.
|
|
15
|
+
- Read routes now work only from the local machine; a LAN-exposed web profile can no longer read plugin state (write routes were already loopback-only).
|
|
16
|
+
|
|
3
17
|
## 0.5.11
|
|
4
18
|
|
|
5
19
|
- **Usage fetch retry**: the quota reader retries one transient Node/undici network failure before caching `fetch failed`, so a brief connect timeout no longer leaves the quota panel without usage data.
|
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.13.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.13.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.13
|
|
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.13.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.13.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.13
|
|
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.13.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.13.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.13
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
|
package/lib/client.js
CHANGED
|
@@ -1806,6 +1806,18 @@ window.__ModuleLoader__.load({
|
|
|
1806
1806
|
});
|
|
1807
1807
|
}
|
|
1808
1808
|
//#endregion
|
|
1809
|
+
//#region src/client/auth-ref.ts
|
|
1810
|
+
/**
|
|
1811
|
+
* Collision-free credential ref for an OpenCode auth entry. Hex encoding is
|
|
1812
|
+
* injective (unlike replacing punctuation with underscores) and uses only
|
|
1813
|
+
* characters accepted by credential references.
|
|
1814
|
+
*/
|
|
1815
|
+
function authRefForEntry(entry) {
|
|
1816
|
+
let hex = "";
|
|
1817
|
+
for (const byte of new TextEncoder().encode(entry)) hex += byte.toString(16).padStart(2, "0");
|
|
1818
|
+
return `OPENCODE_AUTH_${hex}`;
|
|
1819
|
+
}
|
|
1820
|
+
//#endregion
|
|
1809
1821
|
//#region src/client/settings-page.tsx
|
|
1810
1822
|
function OpenCodeSettingsPage(props) {
|
|
1811
1823
|
const { t } = props;
|
|
@@ -1875,8 +1887,7 @@ window.__ModuleLoader__.load({
|
|
|
1875
1887
|
};
|
|
1876
1888
|
const importAllAuth = async () => {
|
|
1877
1889
|
if (state === null || draft === null) return;
|
|
1878
|
-
const
|
|
1879
|
-
const missing = state.auth.entries.filter((entry) => !draft.accounts.some((account) => account.label === entry || account.apiKeyEnv === refFor(entry)));
|
|
1890
|
+
const missing = state.auth.entries.filter((entry) => !draft.accounts.some((account) => account.label === entry || account.apiKeyEnv === authRefForEntry(entry)));
|
|
1880
1891
|
if (missing.length === 0) {
|
|
1881
1892
|
setNotice(t("importedAll"));
|
|
1882
1893
|
return;
|
|
@@ -1884,10 +1895,10 @@ window.__ModuleLoader__.load({
|
|
|
1884
1895
|
setImporting(true);
|
|
1885
1896
|
setError(null);
|
|
1886
1897
|
try {
|
|
1887
|
-
for (const entry of missing) await importAuth(entry,
|
|
1898
|
+
for (const entry of missing) await importAuth(entry, authRefForEntry(entry));
|
|
1888
1899
|
update({ accounts: [...draft.accounts, ...missing.map((entry) => ({
|
|
1889
1900
|
label: entry,
|
|
1890
|
-
apiKeyEnv:
|
|
1901
|
+
apiKeyEnv: authRefForEntry(entry)
|
|
1891
1902
|
}))] });
|
|
1892
1903
|
setNotice(t("importedAll"));
|
|
1893
1904
|
} catch (caught) {
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
|
|
|
17
17
|
const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
|
|
18
18
|
const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
19
19
|
const DEFAULT_AUTH_ENTRY = "opencode-go";
|
|
20
|
-
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.
|
|
20
|
+
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.13";
|
|
21
21
|
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
22
22
|
const DEFAULT_MAX_TOKENS = 32768;
|
|
23
23
|
const DEFAULT_GENERATE_MAX_TOKENS = 32768;
|
|
@@ -3261,6 +3261,12 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3261
3261
|
}
|
|
3262
3262
|
throw error;
|
|
3263
3263
|
}
|
|
3264
|
+
for await (const chunk of this.parse(response, model.transport, config, options.signal)) {
|
|
3265
|
+
if (firstChunkAt === void 0) firstChunkAt = Date.now();
|
|
3266
|
+
started = true;
|
|
3267
|
+
yield chunk;
|
|
3268
|
+
}
|
|
3269
|
+
logAttempt(true);
|
|
3264
3270
|
this.rememberSticky(sessionId, options, entry);
|
|
3265
3271
|
this.pushTrace({
|
|
3266
3272
|
at: Date.now(),
|
|
@@ -3271,17 +3277,23 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3271
3277
|
fallback,
|
|
3272
3278
|
ok: true
|
|
3273
3279
|
});
|
|
3274
|
-
for await (const chunk of this.parse(response, model.transport, config, options.signal)) {
|
|
3275
|
-
if (firstChunkAt === void 0) firstChunkAt = Date.now();
|
|
3276
|
-
started = true;
|
|
3277
|
-
yield chunk;
|
|
3278
|
-
}
|
|
3279
|
-
logAttempt(true);
|
|
3280
3280
|
return;
|
|
3281
3281
|
} catch (error) {
|
|
3282
3282
|
const classified = error instanceof LlmError ? error : new LlmError(error instanceof Error ? error.message : "OpenCode request failed", "TRANSPORT", { cause: error });
|
|
3283
3283
|
logAttempt(false, classified.code);
|
|
3284
|
-
if (started || options.signal?.aborted === true)
|
|
3284
|
+
if (started || options.signal?.aborted === true) {
|
|
3285
|
+
this.pushTrace({
|
|
3286
|
+
at: Date.now(),
|
|
3287
|
+
provider: options.provider,
|
|
3288
|
+
model: options.model,
|
|
3289
|
+
account: entry.label ?? "default",
|
|
3290
|
+
reason: attemptReason,
|
|
3291
|
+
fallback,
|
|
3292
|
+
ok: false,
|
|
3293
|
+
error: classified.code
|
|
3294
|
+
});
|
|
3295
|
+
throw error;
|
|
3296
|
+
}
|
|
3285
3297
|
lastError = classified;
|
|
3286
3298
|
const transient = this.isTransient(classified);
|
|
3287
3299
|
if (transient && !responseFailure && !fallback) this.markCooldown(entry.key, 0);
|
|
@@ -3870,6 +3882,27 @@ function isTrustedUpdateRequest(request) {
|
|
|
3870
3882
|
return false;
|
|
3871
3883
|
}
|
|
3872
3884
|
}
|
|
3885
|
+
/** Local, loopback-only reads for the update card; blocks DNS-rebinding hosts. */
|
|
3886
|
+
function isTrustedReadRequest(request) {
|
|
3887
|
+
if (!isLoopback(request.socket?.remoteAddress)) return false;
|
|
3888
|
+
const host = header(request, "host");
|
|
3889
|
+
if (host === void 0) return false;
|
|
3890
|
+
try {
|
|
3891
|
+
if (!isLoopback(new URL(`http://${host}`).hostname)) return false;
|
|
3892
|
+
} catch {
|
|
3893
|
+
return false;
|
|
3894
|
+
}
|
|
3895
|
+
const site = header(request, "sec-fetch-site");
|
|
3896
|
+
if (site !== void 0 && site !== "same-origin") return false;
|
|
3897
|
+
const origin = header(request, "origin");
|
|
3898
|
+
if (origin === void 0) return true;
|
|
3899
|
+
try {
|
|
3900
|
+
const url = new URL(origin);
|
|
3901
|
+
return (url.protocol === "http:" || url.protocol === "https:") && isLoopback(url.hostname) && url.host === host;
|
|
3902
|
+
} catch {
|
|
3903
|
+
return false;
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3873
3906
|
function validProfileName(value) {
|
|
3874
3907
|
return typeof value === "string" && value !== "" && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") && !/[\0-\x1f\x7f]/.test(value);
|
|
3875
3908
|
}
|
|
@@ -4075,6 +4108,10 @@ function registerPluginUpdater(ctx, options) {
|
|
|
4075
4108
|
try {
|
|
4076
4109
|
const target = runtime();
|
|
4077
4110
|
if (request.method === "GET" || request.method === "HEAD") {
|
|
4111
|
+
if (!isTrustedReadRequest(request)) {
|
|
4112
|
+
json$1(response, 403, { error: "Rejected non-local update status request." });
|
|
4113
|
+
return;
|
|
4114
|
+
}
|
|
4078
4115
|
const payload = await status(options, target);
|
|
4079
4116
|
response.writeHead(200, {
|
|
4080
4117
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -4732,10 +4769,14 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4732
4769
|
}));
|
|
4733
4770
|
};
|
|
4734
4771
|
route("/dsh-opencode/state", async (req, res) => {
|
|
4735
|
-
if (req.method !== "GET"
|
|
4772
|
+
if (req.method !== "GET") {
|
|
4736
4773
|
json(res, 405, { error: "GET required" });
|
|
4737
4774
|
return;
|
|
4738
4775
|
}
|
|
4776
|
+
if (!loopbackHost(req)) {
|
|
4777
|
+
json(res, 403, { error: "non-loopback Host refused" });
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4739
4780
|
if (!allowedOrigin(req)) {
|
|
4740
4781
|
json(res, 403, { error: "cross-origin request refused" });
|
|
4741
4782
|
return;
|
|
@@ -4751,6 +4792,10 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4751
4792
|
json(res, 405, { error: "GET required" });
|
|
4752
4793
|
return;
|
|
4753
4794
|
}
|
|
4795
|
+
if (!loopbackHost(req)) {
|
|
4796
|
+
json(res, 403, { error: "non-loopback Host refused" });
|
|
4797
|
+
return;
|
|
4798
|
+
}
|
|
4754
4799
|
if (!allowedOrigin(req)) {
|
|
4755
4800
|
json(res, 403, { error: "cross-origin request refused" });
|
|
4756
4801
|
return;
|
|
@@ -4773,6 +4818,10 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4773
4818
|
json(res, 405, { error: "GET required" });
|
|
4774
4819
|
return;
|
|
4775
4820
|
}
|
|
4821
|
+
if (!loopbackHost(req)) {
|
|
4822
|
+
json(res, 403, { error: "non-loopback Host refused" });
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
4776
4825
|
if (!allowedOrigin(req)) {
|
|
4777
4826
|
json(res, 403, { error: "cross-origin request refused" });
|
|
4778
4827
|
return;
|
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.13",
|
|
4
4
|
"description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"build": "tsdown",
|
|
69
69
|
"prepare": "tsdown",
|
|
70
70
|
"typecheck": "tsc --noEmit",
|
|
71
|
-
"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",
|
|
71
|
+
"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",
|
|
72
72
|
"check": "npm run typecheck && npm test && npm run build",
|
|
73
73
|
"release": "npm run check && npm pack",
|
|
74
74
|
"sync:models": "node scripts/sync-models.mjs"
|
|
@@ -114,7 +114,9 @@
|
|
|
114
114
|
"@deepseek-ai/schemastery": "~3.18.5",
|
|
115
115
|
"@types/node": "^22.0.0",
|
|
116
116
|
"@types/react": "^18.3.1",
|
|
117
|
+
"@types/react-dom": "^18.3.0",
|
|
117
118
|
"react": "^18.3.1",
|
|
119
|
+
"react-dom": "^18.3.1",
|
|
118
120
|
"tsdown": "^0.22.2",
|
|
119
121
|
"tsx": "^4.23.12",
|
|
120
122
|
"typescript": "^5.7.2"
|