@goodandready/dsh-opencode-provider 0.5.19 → 0.5.21
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 +43 -38
- package/lib/index.d.ts +2 -26
- package/lib/index.js +107 -80
- package/package.json +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.21
|
|
4
|
+
|
|
5
|
+
- **Analytics completeness**: projection scans above 5000 files now select the newest files by metadata instead of an arbitrary readdir page; file-limit and unreadable-projection cases set `AnalyticsReport.partial`, surfaced in the dashboard.
|
|
6
|
+
- 113 tests.
|
|
7
|
+
|
|
8
|
+
## 0.5.20
|
|
9
|
+
|
|
10
|
+
- **CI/reproducibility**: regenerated `package-lock.json` with the normal npm resolver; clean `npm ci --ignore-scripts` now succeeds and `npm run check` passes.
|
|
11
|
+
- **Streaming correctness**: SSE parsing now normalizes CRLF/CR across chunk boundaries; all four transports reject streams that end without their protocol terminal event. Pre-aborted caller signals are rejected before provider dispatch.
|
|
12
|
+
- **Concurrent settings**: settings saves carry the draft revision and use the settings compare-and-swap; revision conflicts return 409 instead of overwriting another page's changes.
|
|
13
|
+
- **UI safety**: removing an account clears unsaved per-row key drafts; clearing a credential asks for confirmation; update checks distinguish registry failure from “up to date”; service status never reports healthy before a successful check.
|
|
14
|
+
- **Images**: request budget counts repeated image occurrences and base64 overhead; offloaded images no longer require the attachment service.
|
|
15
|
+
- **Docs/tests**: added project `AGENTS.md`; removed the unsafe local engine-matrix script pending the approved DSH test contour (`#55`).
|
|
16
|
+
|
|
3
17
|
## 0.5.19
|
|
4
18
|
|
|
5
19
|
- **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.
|
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.21.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.21.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.21
|
|
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.21.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.21.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.21
|
|
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.21.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.21.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.21
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
|
package/lib/client.js
CHANGED
|
@@ -6,7 +6,7 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
-
|
|
9
|
+
// src/client/locales.ts
|
|
10
10
|
const NS = "settings.opencode";
|
|
11
11
|
const dictionaries = {
|
|
12
12
|
en: {
|
|
@@ -41,6 +41,7 @@ window.__ModuleLoader__.load({
|
|
|
41
41
|
pasteKey: "Paste a key",
|
|
42
42
|
saveKey: "Save key",
|
|
43
43
|
clearKey: "Clear key",
|
|
44
|
+
clearKeyConfirm: "Delete the stored credential for {ref}? This cannot be undone.",
|
|
44
45
|
importAuth: "Import from OpenCode",
|
|
45
46
|
imported: "Imported the OpenCode key",
|
|
46
47
|
testConnection: "Test connection",
|
|
@@ -137,6 +138,7 @@ window.__ModuleLoader__.load({
|
|
|
137
138
|
heatmapByAccount: "Account",
|
|
138
139
|
heatmapAllAccounts: "All accounts",
|
|
139
140
|
analyticsHint: "Aggregated from local session metadata by last activity. A session that switched models is counted under its last model; failures are not exposed by the projection. Only plugin routes are counted; legacy opencode sessions belong to the built-in catalog.",
|
|
141
|
+
analyticsPartial: "This report is partial: the projection scan hit its file limit or skipped unreadable files.",
|
|
140
142
|
analyticsEmpty: "No plugin sessions in this window. Legacy opencode sessions belong to the built-in catalog.",
|
|
141
143
|
showAll: "Show all",
|
|
142
144
|
modelSearch: "Search models…",
|
|
@@ -245,6 +247,7 @@ window.__ModuleLoader__.load({
|
|
|
245
247
|
pasteKey: "粘贴密钥",
|
|
246
248
|
saveKey: "保存密钥",
|
|
247
249
|
clearKey: "清除密钥",
|
|
250
|
+
clearKeyConfirm: "删除 {ref} 的已保存凭据?此操作无法撤销。",
|
|
248
251
|
importAuth: "从 OpenCode 导入",
|
|
249
252
|
imported: "已导入 OpenCode 密钥",
|
|
250
253
|
testConnection: "测试连接",
|
|
@@ -341,6 +344,7 @@ window.__ModuleLoader__.load({
|
|
|
341
344
|
heatmapByAccount: "账户",
|
|
342
345
|
heatmapAllAccounts: "全部账户",
|
|
343
346
|
analyticsHint: "按最后活动时间聚合本地会话元数据。切换过模型的会话计入最后使用的模型;本地投影不暴露失败。仅统计插件路由;旧 opencode 会话属于内置目录。",
|
|
347
|
+
analyticsPartial: "此报告不完整:投影扫描达到文件上限或跳过了不可读文件。",
|
|
344
348
|
analyticsEmpty: "此窗口内没有插件会话。旧 opencode 会话属于内置目录。",
|
|
345
349
|
showAll: "显示全部",
|
|
346
350
|
modelSearch: "搜索模型…",
|
|
@@ -419,8 +423,7 @@ window.__ModuleLoader__.load({
|
|
|
419
423
|
}
|
|
420
424
|
};
|
|
421
425
|
const GO_PROVIDER = "opencode-go-direct";
|
|
422
|
-
|
|
423
|
-
//#region src/client/api.ts
|
|
426
|
+
// src/client/api.ts
|
|
424
427
|
async function call(path, init) {
|
|
425
428
|
const response = await fetch(path, {
|
|
426
429
|
credentials: "same-origin",
|
|
@@ -456,10 +459,13 @@ window.__ModuleLoader__.load({
|
|
|
456
459
|
body: JSON.stringify({ label })
|
|
457
460
|
});
|
|
458
461
|
}
|
|
459
|
-
function saveConfig(patch) {
|
|
462
|
+
function saveConfig(patch, revision) {
|
|
460
463
|
return call("/dsh-opencode/config", {
|
|
461
464
|
method: "POST",
|
|
462
|
-
body: JSON.stringify({ patch }
|
|
465
|
+
body: JSON.stringify(revision === void 0 ? { patch } : {
|
|
466
|
+
patch,
|
|
467
|
+
revision
|
|
468
|
+
})
|
|
463
469
|
});
|
|
464
470
|
}
|
|
465
471
|
function fetchUpdate() {
|
|
@@ -508,11 +514,9 @@ window.__ModuleLoader__.load({
|
|
|
508
514
|
})
|
|
509
515
|
});
|
|
510
516
|
}
|
|
511
|
-
|
|
512
|
-
//#region src/client/events.ts
|
|
517
|
+
// src/client/events.ts
|
|
513
518
|
const CONFIG_CHANGED_EVENT = "dsh-opencode-config-changed";
|
|
514
|
-
|
|
515
|
-
//#region src/client/heatmap.tsx
|
|
519
|
+
// src/client/heatmap.tsx
|
|
516
520
|
const DAYS = [
|
|
517
521
|
"Mon",
|
|
518
522
|
"Tue",
|
|
@@ -721,8 +725,7 @@ window.__ModuleLoader__.load({
|
|
|
721
725
|
]
|
|
722
726
|
});
|
|
723
727
|
}
|
|
724
|
-
|
|
725
|
-
//#region src/client/usage-analytics.tsx
|
|
728
|
+
// src/client/usage-analytics.tsx
|
|
726
729
|
function compact(value) {
|
|
727
730
|
if (!Number.isFinite(value) || value <= 0) return "0";
|
|
728
731
|
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
@@ -922,6 +925,10 @@ window.__ModuleLoader__.load({
|
|
|
922
925
|
})
|
|
923
926
|
]
|
|
924
927
|
}),
|
|
928
|
+
analytics.partial === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
929
|
+
className: "oc-hint",
|
|
930
|
+
children: t("analyticsPartial")
|
|
931
|
+
}) : null,
|
|
925
932
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
926
933
|
className: "oc-hint",
|
|
927
934
|
children: t("analyticsHint")
|
|
@@ -931,8 +938,7 @@ window.__ModuleLoader__.load({
|
|
|
931
938
|
tab === "sessions" ? sessionRows(analytics, t) : null
|
|
932
939
|
] });
|
|
933
940
|
}
|
|
934
|
-
|
|
935
|
-
//#region src/client/usage-bars.tsx
|
|
941
|
+
// src/client/usage-bars.tsx
|
|
936
942
|
function percentClass(percent) {
|
|
937
943
|
if (percent >= 90) return "oc-bar-fill oc-bar-fill-bad";
|
|
938
944
|
if (percent >= 75) return "oc-bar-fill oc-bar-fill-warn";
|
|
@@ -1001,8 +1007,7 @@ window.__ModuleLoader__.load({
|
|
|
1001
1007
|
]
|
|
1002
1008
|
});
|
|
1003
1009
|
}
|
|
1004
|
-
|
|
1005
|
-
//#region src/quota-runway.ts
|
|
1010
|
+
// src/quota-runway.ts
|
|
1006
1011
|
const HOUR = 36e5;
|
|
1007
1012
|
const DAY = 24 * HOUR;
|
|
1008
1013
|
const DURATIONS = {
|
|
@@ -1068,8 +1073,7 @@ window.__ModuleLoader__.load({
|
|
|
1068
1073
|
};
|
|
1069
1074
|
return known === 0 ? { kind: "unknown" } : { kind: "safe" };
|
|
1070
1075
|
}
|
|
1071
|
-
|
|
1072
|
-
//#region src/client/quota-card.tsx
|
|
1076
|
+
// src/client/quota-card.tsx
|
|
1073
1077
|
const PANEL_EVENT = "dsh-opencode-panel-state";
|
|
1074
1078
|
function broadcastPanel(open) {
|
|
1075
1079
|
if (typeof window === "undefined") return;
|
|
@@ -1611,11 +1615,11 @@ window.__ModuleLoader__.load({
|
|
|
1611
1615
|
]
|
|
1612
1616
|
});
|
|
1613
1617
|
}
|
|
1614
|
-
|
|
1615
|
-
//#region src/client/settings-draft.ts
|
|
1618
|
+
// src/client/settings-draft.ts
|
|
1616
1619
|
function draftOf(state) {
|
|
1617
1620
|
const allIds = state.models.map((model) => model.id);
|
|
1618
1621
|
return {
|
|
1622
|
+
revision: state.revision ?? 0,
|
|
1619
1623
|
apiKeyEnv: state.config.apiKeyEnv,
|
|
1620
1624
|
apiBase: state.config.apiBase,
|
|
1621
1625
|
goApiBase: state.config.goApiBase,
|
|
@@ -1701,8 +1705,7 @@ window.__ModuleLoader__.load({
|
|
|
1701
1705
|
function describeLimitsFull(limits) {
|
|
1702
1706
|
return `${planLine("Go", limits.go)} · ${planLine("Go Plus", limits.goPlus)}`;
|
|
1703
1707
|
}
|
|
1704
|
-
|
|
1705
|
-
//#region src/client/settings-widgets.tsx
|
|
1708
|
+
// src/client/settings-widgets.tsx
|
|
1706
1709
|
function Field(props) {
|
|
1707
1710
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1708
1711
|
className: "oc-field",
|
|
@@ -1765,8 +1768,7 @@ window.__ModuleLoader__.load({
|
|
|
1765
1768
|
if (account.coolingUntil <= 0) return label;
|
|
1766
1769
|
return `${label} · ${untilText(account.coolingUntil)}`;
|
|
1767
1770
|
}
|
|
1768
|
-
|
|
1769
|
-
//#region src/client/update-card.tsx
|
|
1771
|
+
// src/client/update-card.tsx
|
|
1770
1772
|
function PluginUpdateCard(props) {
|
|
1771
1773
|
const { t } = props;
|
|
1772
1774
|
const [status, setStatus] = (0, react.useState)(null);
|
|
@@ -1803,6 +1805,9 @@ window.__ModuleLoader__.load({
|
|
|
1803
1805
|
children: [t("pluginUpdate"), status === null ? null : status.updateAvailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1804
1806
|
className: "oc-badge oc-badge-warn",
|
|
1805
1807
|
children: t("updateAvailable")
|
|
1808
|
+
}) : status.latestCheckFailed || status.latestVersion === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1809
|
+
className: "oc-badge oc-badge-warn",
|
|
1810
|
+
children: t("updateCheckFailed")
|
|
1806
1811
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1807
1812
|
className: "oc-badge oc-badge-ok",
|
|
1808
1813
|
children: t("upToDate")
|
|
@@ -1849,8 +1854,7 @@ window.__ModuleLoader__.load({
|
|
|
1849
1854
|
]
|
|
1850
1855
|
});
|
|
1851
1856
|
}
|
|
1852
|
-
|
|
1853
|
-
//#region src/client/auth-ref.ts
|
|
1857
|
+
// src/client/auth-ref.ts
|
|
1854
1858
|
/**
|
|
1855
1859
|
* Collision-free credential ref for an OpenCode auth entry. Hex encoding is
|
|
1856
1860
|
* injective (unlike replacing punctuation with underscores) and uses only
|
|
@@ -1861,8 +1865,7 @@ window.__ModuleLoader__.load({
|
|
|
1861
1865
|
for (const byte of new TextEncoder().encode(entry)) hex += byte.toString(16).padStart(2, "0");
|
|
1862
1866
|
return `OPENCODE_AUTH_${hex}`;
|
|
1863
1867
|
}
|
|
1864
|
-
|
|
1865
|
-
//#region src/client/settings-page.tsx
|
|
1868
|
+
// src/client/settings-page.tsx
|
|
1866
1869
|
function keyStatusLabel(status, t) {
|
|
1867
1870
|
if (status === "ok") return t("keyStatusOk");
|
|
1868
1871
|
if (status === "expired") return t("keyStatusExpired");
|
|
@@ -1928,7 +1931,7 @@ window.__ModuleLoader__.load({
|
|
|
1928
1931
|
setSaving(true);
|
|
1929
1932
|
setError(null);
|
|
1930
1933
|
try {
|
|
1931
|
-
await saveConfig(patchOf(draft));
|
|
1934
|
+
await saveConfig(patchOf(draft), draft.revision);
|
|
1932
1935
|
window.dispatchEvent(new CustomEvent(CONFIG_CHANGED_EVENT));
|
|
1933
1936
|
setNotice(t("saved"));
|
|
1934
1937
|
setState(null);
|
|
@@ -2265,7 +2268,9 @@ window.__ModuleLoader__.load({
|
|
|
2265
2268
|
type: "button",
|
|
2266
2269
|
className: "oc-button oc-button-danger",
|
|
2267
2270
|
onClick: () => {
|
|
2268
|
-
|
|
2271
|
+
const ref = draft.apiKeyEnv.trim() === "" ? state.config.apiKeyEnv : draft.apiKeyEnv.trim();
|
|
2272
|
+
if (!window.confirm(t("clearKeyConfirm").replace("{ref}", ref))) return;
|
|
2273
|
+
clearKey(ref).then(() => {
|
|
2269
2274
|
setNotice(t("saved"));
|
|
2270
2275
|
return load();
|
|
2271
2276
|
}).catch((caught) => setError(caught instanceof Error ? caught.message : t("failed")));
|
|
@@ -2335,8 +2340,8 @@ window.__ModuleLoader__.load({
|
|
|
2335
2340
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
2336
2341
|
className: "oc-card-title",
|
|
2337
2342
|
children: [t("serviceStatus"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2338
|
-
className: `oc-badge ${state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-
|
|
2339
|
-
children: state.service.usageError === void 0 ? t("serviceOk") : t("serviceDegraded")
|
|
2343
|
+
className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2344
|
+
children: state.service.usageCheckedAt === void 0 ? t("notChecked") : state.service.usageError === void 0 ? t("serviceOk") : t("serviceDegraded")
|
|
2340
2345
|
})]
|
|
2341
2346
|
}),
|
|
2342
2347
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2347,7 +2352,7 @@ window.__ModuleLoader__.load({
|
|
|
2347
2352
|
children: t("usageEndpoint")
|
|
2348
2353
|
}),
|
|
2349
2354
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2350
|
-
className: `oc-badge ${state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2355
|
+
className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2351
2356
|
children: state.service.usageCheckedAt === void 0 ? t("notChecked") : t("checkedAt").replace("{time}", new Date(state.service.usageCheckedAt).toLocaleTimeString())
|
|
2352
2357
|
}),
|
|
2353
2358
|
state.service.usageLatencyMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2564,7 +2569,10 @@ window.__ModuleLoader__.load({
|
|
|
2564
2569
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2565
2570
|
type: "button",
|
|
2566
2571
|
className: "oc-button oc-button-danger",
|
|
2567
|
-
onClick: () =>
|
|
2572
|
+
onClick: () => {
|
|
2573
|
+
setKeyDrafts({});
|
|
2574
|
+
update({ accounts: draft.accounts.filter((_, i) => i !== index) });
|
|
2575
|
+
},
|
|
2568
2576
|
children: t("removeAccount")
|
|
2569
2577
|
})
|
|
2570
2578
|
]
|
|
@@ -3006,8 +3014,7 @@ window.__ModuleLoader__.load({
|
|
|
3006
3014
|
]
|
|
3007
3015
|
});
|
|
3008
3016
|
}
|
|
3009
|
-
|
|
3010
|
-
//#region src/client/styles.ts
|
|
3017
|
+
// src/client/styles.ts
|
|
3011
3018
|
const CSS_ID = "dsh-opencode-provider";
|
|
3012
3019
|
const CSS = `
|
|
3013
3020
|
:where(.oc-page, .oc-panel, .oc-card, .oc-foot, .oc-railButton) {
|
|
@@ -3127,8 +3134,7 @@ window.__ModuleLoader__.load({
|
|
|
3127
3134
|
.oc-table th, .oc-table td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--oc-border-soft); vertical-align: top; }
|
|
3128
3135
|
.oc-table th { font-weight: 600; opacity: 0.8; }
|
|
3129
3136
|
`;
|
|
3130
|
-
|
|
3131
|
-
//#region src/client/index.tsx
|
|
3137
|
+
// src/client/index.tsx
|
|
3132
3138
|
function injectCss() {
|
|
3133
3139
|
if (typeof document === "undefined") return () => {};
|
|
3134
3140
|
if (document.querySelector(`style[data-dsh-plugin="dsh-opencode-provider"]`) !== null) return () => {};
|
|
@@ -3174,7 +3180,6 @@ window.__ModuleLoader__.load({
|
|
|
3174
3180
|
}, QuotaCard));
|
|
3175
3181
|
});
|
|
3176
3182
|
}
|
|
3177
|
-
//#endregion
|
|
3178
3183
|
exports.apply = apply;
|
|
3179
3184
|
exports.inject = inject;
|
|
3180
3185
|
return module.exports;
|
package/lib/index.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import z from "@deepseek-ai/schemastery";
|
|
|
3
3
|
import { Context } from "@deepseek-ai/cordis";
|
|
4
4
|
import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
|
|
5
5
|
import { WebServer } from "@deepseek-ai/dsh-host-webserver";
|
|
6
|
-
//#region src/config.d.ts
|
|
7
6
|
type TransportId = 'openai-chat' | 'openai-responses' | 'anthropic' | 'google';
|
|
8
7
|
interface AccountConfig {
|
|
9
8
|
label?: string;
|
|
@@ -92,8 +91,6 @@ interface ResolvedConfig {
|
|
|
92
91
|
showSidebarQuota: boolean;
|
|
93
92
|
userAgent: string;
|
|
94
93
|
}
|
|
95
|
-
//#endregion
|
|
96
|
-
//#region src/credentials.d.ts
|
|
97
94
|
interface ResolvedKey {
|
|
98
95
|
readonly key: string;
|
|
99
96
|
readonly source: string;
|
|
@@ -115,8 +112,6 @@ declare function openCodeAuthPath(): string;
|
|
|
115
112
|
declare function createKeyPool(ctx: Context, config: () => ResolvedConfig): KeyPool;
|
|
116
113
|
/** Backwards-compatible single-key facade over the pool. */
|
|
117
114
|
declare function createKeyResolver(ctx: Context, config: () => ResolvedConfig): KeyResolver;
|
|
118
|
-
//#endregion
|
|
119
|
-
//#region src/request-log.d.ts
|
|
120
115
|
interface RequestOutcome {
|
|
121
116
|
ok: boolean;
|
|
122
117
|
latencyMs: number;
|
|
@@ -153,8 +148,6 @@ interface RequestHeatmap {
|
|
|
153
148
|
models: ModelRequestHeatmap[];
|
|
154
149
|
accounts: AccountRequestHeatmap[];
|
|
155
150
|
}
|
|
156
|
-
//#endregion
|
|
157
|
-
//#region src/adapter.d.ts
|
|
158
151
|
type RoutingReason = 'rule' | 'sticky' | 'pinned' | 'default' | 'rotation' | 'quota-fallback';
|
|
159
152
|
interface RoutingTraceEntry {
|
|
160
153
|
readonly at: number;
|
|
@@ -238,8 +231,6 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
238
231
|
private markCooldown;
|
|
239
232
|
private isTransient;
|
|
240
233
|
}
|
|
241
|
-
//#endregion
|
|
242
|
-
//#region src/usage.d.ts
|
|
243
234
|
interface UsageWindow {
|
|
244
235
|
readonly percentUsed: number;
|
|
245
236
|
readonly resetsAt?: string;
|
|
@@ -280,8 +271,6 @@ interface UsageReader {
|
|
|
280
271
|
}
|
|
281
272
|
/** Cached reader for the (undocumented) Go usage endpoint. Fail-open by design. */
|
|
282
273
|
declare function createUsageReader(config: () => ResolvedConfig, resolveKey: KeyResolver, fetchImpl?: typeof fetch): UsageReader;
|
|
283
|
-
//#endregion
|
|
284
|
-
//#region src/commands.d.ts
|
|
285
274
|
interface CommandDeps {
|
|
286
275
|
readonly config: () => ResolvedConfig;
|
|
287
276
|
readonly pool: KeyPool;
|
|
@@ -289,8 +278,6 @@ interface CommandDeps {
|
|
|
289
278
|
readonly adapter: OpenCodeAdapter;
|
|
290
279
|
}
|
|
291
280
|
declare function applyCommands(ctx: Context, deps: CommandDeps): void;
|
|
292
|
-
//#endregion
|
|
293
|
-
//#region src/analytics.d.ts
|
|
294
281
|
interface ModelUsageRow {
|
|
295
282
|
provider: string;
|
|
296
283
|
model: string;
|
|
@@ -365,9 +352,9 @@ interface AnalyticsReport {
|
|
|
365
352
|
summary: AnalyticsSummary;
|
|
366
353
|
days: DayUsageRow[];
|
|
367
354
|
sessions: SessionUsageRow[];
|
|
355
|
+
/** True when the projection scan hit a cap or skipped unreadable files. */
|
|
356
|
+
partial?: boolean;
|
|
368
357
|
}
|
|
369
|
-
//#endregion
|
|
370
|
-
//#region src/routes.d.ts
|
|
371
358
|
interface RouteDeps {
|
|
372
359
|
readonly config: () => ResolvedConfig;
|
|
373
360
|
readonly adapter: OpenCodeAdapter;
|
|
@@ -379,8 +366,6 @@ interface RouteDeps {
|
|
|
379
366
|
}
|
|
380
367
|
/** Register the Host HTTP surface consumed by the browser settings page. */
|
|
381
368
|
declare function applyRoutes(ctx: Context, web: WebServer, deps: RouteDeps): void;
|
|
382
|
-
//#endregion
|
|
383
|
-
//#region src/session-id.d.ts
|
|
384
369
|
/**
|
|
385
370
|
* A stable, opaque session id for `x-opencode-session`. The header must stay
|
|
386
371
|
* identical across every turn and retry of one conversation; deriving it from
|
|
@@ -389,16 +374,12 @@ declare function applyRoutes(ctx: Context, web: WebServer, deps: RouteDeps): voi
|
|
|
389
374
|
* routing key, so the `dsh_` prefix is safe.
|
|
390
375
|
*/
|
|
391
376
|
declare function openCodeSessionId(options: GenerateOptions): string;
|
|
392
|
-
//#endregion
|
|
393
|
-
//#region src/errors.d.ts
|
|
394
377
|
/**
|
|
395
378
|
* Translate an OpenCode HTTP failure into the harness failure taxonomy.
|
|
396
379
|
* OpenCode reports several distinct conditions as 400/401, so the body type
|
|
397
380
|
* and message decide the code — never the status alone.
|
|
398
381
|
*/
|
|
399
382
|
declare function classifyResponseError(status: number, text: string, headers: Headers): LlmError;
|
|
400
|
-
//#endregion
|
|
401
|
-
//#region src/models.generated.d.ts
|
|
402
383
|
type OpenCodeTransport = 'openai-chat' | 'openai-responses' | 'anthropic' | 'google';
|
|
403
384
|
interface OpenCodeModelMeta {
|
|
404
385
|
readonly id: string;
|
|
@@ -415,8 +396,6 @@ interface OpenCodeModelMeta {
|
|
|
415
396
|
readonly cacheRead: number;
|
|
416
397
|
};
|
|
417
398
|
}
|
|
418
|
-
//#endregion
|
|
419
|
-
//#region src/catalog.d.ts
|
|
420
399
|
interface EffectiveModel extends OpenCodeModelMeta {
|
|
421
400
|
readonly transport: OpenCodeTransport;
|
|
422
401
|
readonly contextWindow: number;
|
|
@@ -424,8 +403,6 @@ interface EffectiveModel extends OpenCodeModelMeta {
|
|
|
424
403
|
declare function effectiveModel(id: string, config: ResolvedConfig): EffectiveModel;
|
|
425
404
|
declare function describeModel(meta: EffectiveModel, provider?: string): string;
|
|
426
405
|
declare function clearCatalogCache(): void;
|
|
427
|
-
//#endregion
|
|
428
|
-
//#region src/index.d.ts
|
|
429
406
|
declare const name = "llm-opencode";
|
|
430
407
|
declare const inject: string[];
|
|
431
408
|
/**
|
|
@@ -438,6 +415,5 @@ declare const inject: string[];
|
|
|
438
415
|
* next request without a restart.
|
|
439
416
|
*/
|
|
440
417
|
declare function apply(ctx: Context, config: Config): void;
|
|
441
|
-
//#endregion
|
|
442
418
|
export { type CommandDeps, Config, type Config as OpenCodeConfig, OpenCodeAdapter, type OpenCodeAdapterDeps, type ResolvedConfig, type ResolvedKey, type RouteDeps, type TransportId, type UsageReport, type UsageResult, type UsageWindow, apply, applyCommands, applyRoutes, classifyResponseError, clearCatalogCache, createKeyPool, createKeyResolver, createUsageReader, describeModel, effectiveModel, inject, name, normalizeUsage, openCodeAuthPath, openCodeSessionId };
|
|
443
419
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { isIP } from "node:net";
|
|
13
|
-
|
|
13
|
+
// src/config.ts
|
|
14
14
|
const NS = "llm-opencode";
|
|
15
15
|
const DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
|
|
16
16
|
const DEFAULT_API_BASE = "https://opencode.ai/inference";
|
|
@@ -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.21";
|
|
22
22
|
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
23
23
|
const DEFAULT_MAX_TOKENS = 32768;
|
|
24
24
|
const DEFAULT_GENERATE_MAX_TOKENS = 32768;
|
|
@@ -151,8 +151,7 @@ function resolveConfig(config) {
|
|
|
151
151
|
userAgent: typeof config.userAgent === "string" && config.userAgent !== "" ? config.userAgent : DEFAULT_USER_AGENT
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
//#region src/models.generated.ts
|
|
154
|
+
// src/models.generated.ts
|
|
156
155
|
const GENERATED_MODELS = {
|
|
157
156
|
"beam-free": {
|
|
158
157
|
"id": "beam-free",
|
|
@@ -791,8 +790,7 @@ const GENERATED_MODELS = {
|
|
|
791
790
|
}
|
|
792
791
|
}
|
|
793
792
|
};
|
|
794
|
-
|
|
795
|
-
//#region src/provider-ids.ts
|
|
793
|
+
// src/provider-ids.ts
|
|
796
794
|
/**
|
|
797
795
|
* Provider ids owned by this plugin. The built-in llm-pi-ai catalog already
|
|
798
796
|
* declares `opencode` and `opencode-go`; the core registry rejects duplicate
|
|
@@ -801,8 +799,7 @@ const GENERATED_MODELS = {
|
|
|
801
799
|
const CONSOLE_PROVIDER = "opencode-direct";
|
|
802
800
|
const GO_PROVIDER = "opencode-go-direct";
|
|
803
801
|
const PLUGIN_PROVIDER_SET = /* @__PURE__ */ new Set([CONSOLE_PROVIDER, GO_PROVIDER]);
|
|
804
|
-
|
|
805
|
-
//#region src/routing.ts
|
|
802
|
+
// src/routing.ts
|
|
806
803
|
function isGoProvider(provider) {
|
|
807
804
|
return provider === "opencode-go-direct" || provider === "opencode-go";
|
|
808
805
|
}
|
|
@@ -826,8 +823,7 @@ function endpointFor(provider, transport, model, config) {
|
|
|
826
823
|
case "google": return `${root}/google/v1beta/models/${encodeURIComponent(model)}:streamGenerateContent?alt=sse`;
|
|
827
824
|
}
|
|
828
825
|
}
|
|
829
|
-
|
|
830
|
-
//#region src/subscription-limits.ts
|
|
826
|
+
// src/subscription-limits.ts
|
|
831
827
|
const UNLIMITED = {
|
|
832
828
|
monthlyUsd: "unlimited",
|
|
833
829
|
fiveHour: "unlimited",
|
|
@@ -974,8 +970,7 @@ function formatRequests(value) {
|
|
|
974
970
|
function describeLimitsShort(limits) {
|
|
975
971
|
return `Go ${formatRequests(limits.go.fiveHour)}/5h · ${formatRequests(limits.go.weekly)}/wk`;
|
|
976
972
|
}
|
|
977
|
-
|
|
978
|
-
//#region src/catalog.ts
|
|
973
|
+
// src/catalog.ts
|
|
979
974
|
const CATALOG_TTL_MS = 6e5;
|
|
980
975
|
const CATALOG_TIMEOUT_MS = 15e3;
|
|
981
976
|
const catalogCache = /* @__PURE__ */ new Map();
|
|
@@ -1104,8 +1099,7 @@ function catalogStatus(provider, config, count) {
|
|
|
1104
1099
|
function clearCatalogCache() {
|
|
1105
1100
|
catalogCache.clear();
|
|
1106
1101
|
}
|
|
1107
|
-
|
|
1108
|
-
//#region src/account-rules.ts
|
|
1102
|
+
// src/account-rules.ts
|
|
1109
1103
|
function glob(pattern) {
|
|
1110
1104
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1111
1105
|
return new RegExp(`^${escaped}$`, "i");
|
|
@@ -1128,8 +1122,7 @@ function orderKeysForModel(keys, model, rules) {
|
|
|
1128
1122
|
if (preferred === void 0) return keys;
|
|
1129
1123
|
return [preferred, ...keys.filter((entry) => entry !== preferred)];
|
|
1130
1124
|
}
|
|
1131
|
-
|
|
1132
|
-
//#region src/errors.ts
|
|
1125
|
+
// src/errors.ts
|
|
1133
1126
|
function parseErrorBody(text) {
|
|
1134
1127
|
try {
|
|
1135
1128
|
const body = JSON.parse(text);
|
|
@@ -1190,8 +1183,7 @@ function classifyStreamError(value, status = 200) {
|
|
|
1190
1183
|
if (value.error === void 0) return void 0;
|
|
1191
1184
|
return classifyResponseError(status, JSON.stringify(value), new Headers());
|
|
1192
1185
|
}
|
|
1193
|
-
|
|
1194
|
-
//#region src/images.ts
|
|
1186
|
+
// src/images.ts
|
|
1195
1187
|
const MAX_LONG_EDGE = 1568;
|
|
1196
1188
|
const MAX_BYTES = 1048576;
|
|
1197
1189
|
/** Total base64 image budget for one request; leaves room under the gateway cap. */
|
|
@@ -1202,7 +1194,7 @@ const MAX_REQUEST_IMAGES = 50;
|
|
|
1202
1194
|
* dropped first; the transports render them as an "image omitted" placeholder
|
|
1203
1195
|
* instead of failing the whole request.
|
|
1204
1196
|
*/
|
|
1205
|
-
function selectRequestImages(refs) {
|
|
1197
|
+
function selectRequestImages(refs, occurrences) {
|
|
1206
1198
|
const unique = [];
|
|
1207
1199
|
const seen = /* @__PURE__ */ new Set();
|
|
1208
1200
|
for (const ref of refs) {
|
|
@@ -1210,11 +1202,18 @@ function selectRequestImages(refs) {
|
|
|
1210
1202
|
seen.add(ref.attachmentId);
|
|
1211
1203
|
unique.push(ref);
|
|
1212
1204
|
}
|
|
1205
|
+
const countOf = (ref) => Math.max(1, occurrences?.get(ref.attachmentId) ?? 1);
|
|
1206
|
+
const encoded = (bytes) => Math.ceil(Math.max(0, bytes) * 4 / 3);
|
|
1213
1207
|
let start = 0;
|
|
1214
|
-
let
|
|
1215
|
-
|
|
1208
|
+
let totalImages = unique.reduce((sum, ref) => sum + countOf(ref), 0);
|
|
1209
|
+
let bytes = unique.reduce((sum, ref) => sum + encoded(ref.bytes) * countOf(ref), 0);
|
|
1210
|
+
while (start < unique.length && (totalImages > MAX_REQUEST_IMAGES || bytes > MAX_REQUEST_IMAGE_BYTES)) {
|
|
1216
1211
|
const dropped = unique[start];
|
|
1217
|
-
if (dropped !== void 0)
|
|
1212
|
+
if (dropped !== void 0) {
|
|
1213
|
+
const count = countOf(dropped);
|
|
1214
|
+
totalImages -= count;
|
|
1215
|
+
bytes -= encoded(dropped.bytes) * count;
|
|
1216
|
+
}
|
|
1218
1217
|
start += 1;
|
|
1219
1218
|
}
|
|
1220
1219
|
const kept = unique.slice(start);
|
|
@@ -1237,21 +1236,23 @@ function targetOf(ref) {
|
|
|
1237
1236
|
};
|
|
1238
1237
|
}
|
|
1239
1238
|
function hasImages(options) {
|
|
1240
|
-
return options.messages.some((message) => message.content.some((block) => block.type === "image"));
|
|
1239
|
+
return options.messages.some((message) => message.content.some((block) => block.type === "image" && block.offloaded !== true));
|
|
1241
1240
|
}
|
|
1242
1241
|
/** Decode the distinct request images that fit the request-wide budget. */
|
|
1243
1242
|
async function resolveImages(options, store, signal) {
|
|
1244
1243
|
const refs = [];
|
|
1244
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1245
1245
|
const seen = /* @__PURE__ */ new Set();
|
|
1246
1246
|
for (const message of options.messages) for (const block of message.content) {
|
|
1247
1247
|
if (block.type !== "image" || block.offloaded === true) continue;
|
|
1248
1248
|
const ref = block.attachment;
|
|
1249
|
+
counts.set(ref.attachmentId, (counts.get(ref.attachmentId) ?? 0) + 1);
|
|
1249
1250
|
if (seen.has(ref.attachmentId)) continue;
|
|
1250
1251
|
seen.add(ref.attachmentId);
|
|
1251
1252
|
refs.push(ref);
|
|
1252
1253
|
}
|
|
1253
1254
|
const images = /* @__PURE__ */ new Map();
|
|
1254
|
-
for (const ref of selectRequestImages(refs).kept) {
|
|
1255
|
+
for (const ref of selectRequestImages(refs, counts).kept) {
|
|
1255
1256
|
const request = await store.readImageRequest(ref, targetOf(ref), signal);
|
|
1256
1257
|
const base64 = Buffer.from(request.data).toString("base64");
|
|
1257
1258
|
images.set(ref.attachmentId, {
|
|
@@ -1267,8 +1268,7 @@ async function resolveImages(options, store, signal) {
|
|
|
1267
1268
|
function offloadPlaceholder(ref) {
|
|
1268
1269
|
return `[image omitted: ${ref.name ?? ref.attachmentId}]`;
|
|
1269
1270
|
}
|
|
1270
|
-
|
|
1271
|
-
//#region src/files.ts
|
|
1271
|
+
// src/files.ts
|
|
1272
1272
|
/**
|
|
1273
1273
|
* Model-facing handle for a file when the adapter receives a raw FileBlock.
|
|
1274
1274
|
* The harness normally projects files to text with an execution-world path
|
|
@@ -1278,8 +1278,7 @@ function filePlaceholder(ref) {
|
|
|
1278
1278
|
const digest = String(ref.attachmentId).slice(7, 15);
|
|
1279
1279
|
return `[File ${JSON.stringify(ref.name)} (${ref.bytes} bytes, sha256:${digest}) was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
|
|
1280
1280
|
}
|
|
1281
|
-
|
|
1282
|
-
//#region src/tool-schema.ts
|
|
1281
|
+
// src/tool-schema.ts
|
|
1283
1282
|
/**
|
|
1284
1283
|
* Normalize a tool's parameters schema to the object root the gateway
|
|
1285
1284
|
* validates. Ported behavior from the Command Code provider: pass an object
|
|
@@ -1385,8 +1384,7 @@ function normalizeToolParameters(parameters, depth = 0) {
|
|
|
1385
1384
|
additionalProperties: true
|
|
1386
1385
|
};
|
|
1387
1386
|
}
|
|
1388
|
-
|
|
1389
|
-
//#region src/messages.ts
|
|
1387
|
+
// src/messages.ts
|
|
1390
1388
|
function textOnly$3(content) {
|
|
1391
1389
|
const parts = [];
|
|
1392
1390
|
for (const block of content) if (block.type === "text") parts.push(block.text);
|
|
@@ -1534,8 +1532,7 @@ function buildChatBody(options, model, config, images) {
|
|
|
1534
1532
|
...options.stop === void 0 || options.stop.length === 0 ? {} : { stop: options.stop }
|
|
1535
1533
|
};
|
|
1536
1534
|
}
|
|
1537
|
-
|
|
1538
|
-
//#region src/request-log.ts
|
|
1535
|
+
// src/request-log.ts
|
|
1539
1536
|
const DAY_MS$1 = 864e5;
|
|
1540
1537
|
const MAX_LOG_BYTES = 2097152;
|
|
1541
1538
|
const KEEP_LINES = 5e3;
|
|
@@ -1718,8 +1715,7 @@ async function readRequestHeatmap(days, now = Date.now()) {
|
|
|
1718
1715
|
}).sort((a, b) => b.total - a.total || a.account.localeCompare(b.account))
|
|
1719
1716
|
};
|
|
1720
1717
|
}
|
|
1721
|
-
|
|
1722
|
-
//#region src/session-id.ts
|
|
1718
|
+
// src/session-id.ts
|
|
1723
1719
|
function sha256(value) {
|
|
1724
1720
|
return createHash("sha256").update(value).digest("hex");
|
|
1725
1721
|
}
|
|
@@ -1773,8 +1769,7 @@ function limitTools(tools, max = 128) {
|
|
|
1773
1769
|
const rest = out.filter((tool) => !CORE_TOOLS.has(tool.name));
|
|
1774
1770
|
return [...core, ...rest].slice(0, max);
|
|
1775
1771
|
}
|
|
1776
|
-
|
|
1777
|
-
//#region src/transports/sse.ts
|
|
1772
|
+
// src/transports/sse.ts
|
|
1778
1773
|
async function readWithIdleTimeout(reader, idleTimeoutMs, signal) {
|
|
1779
1774
|
if (signal?.aborted === true) throw signal.reason;
|
|
1780
1775
|
return await new Promise((resolve, reject) => {
|
|
@@ -1798,6 +1793,12 @@ async function readWithIdleTimeout(reader, idleTimeoutMs, signal) {
|
|
|
1798
1793
|
reader.read().then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
1799
1794
|
});
|
|
1800
1795
|
}
|
|
1796
|
+
function appendDecoded(buffer, decoded) {
|
|
1797
|
+
let text = buffer + decoded;
|
|
1798
|
+
const trailingCR = text.endsWith("\r");
|
|
1799
|
+
if (trailingCR) text = text.slice(0, -1);
|
|
1800
|
+
return text.replace(/\r\n?/g, "\n") + (trailingCR ? "\r" : "");
|
|
1801
|
+
}
|
|
1801
1802
|
/** Yield the `data:` payload of every SSE event, honoring cancellation and idle timeouts. */
|
|
1802
1803
|
async function* sseData(body, idleTimeoutMs, signal) {
|
|
1803
1804
|
const reader = body.getReader();
|
|
@@ -1807,7 +1808,7 @@ async function* sseData(body, idleTimeoutMs, signal) {
|
|
|
1807
1808
|
for (;;) {
|
|
1808
1809
|
const result = await readWithIdleTimeout(reader, idleTimeoutMs, signal);
|
|
1809
1810
|
if (result.done) break;
|
|
1810
|
-
buffer
|
|
1811
|
+
buffer = appendDecoded(buffer, decoder.decode(result.value, { stream: true }));
|
|
1811
1812
|
for (;;) {
|
|
1812
1813
|
const boundary = buffer.indexOf("\n\n");
|
|
1813
1814
|
if (boundary < 0) break;
|
|
@@ -1835,7 +1836,7 @@ async function* sseEvents(body, idleTimeoutMs, signal) {
|
|
|
1835
1836
|
for (;;) {
|
|
1836
1837
|
const result = await readWithIdleTimeout(reader, idleTimeoutMs, signal);
|
|
1837
1838
|
if (result.done) break;
|
|
1838
|
-
buffer
|
|
1839
|
+
buffer = appendDecoded(buffer, decoder.decode(result.value, { stream: true }));
|
|
1839
1840
|
for (;;) {
|
|
1840
1841
|
const boundary = buffer.indexOf("\n\n");
|
|
1841
1842
|
if (boundary < 0) break;
|
|
@@ -1865,8 +1866,7 @@ async function* sseEvents(body, idleTimeoutMs, signal) {
|
|
|
1865
1866
|
reader.releaseLock();
|
|
1866
1867
|
}
|
|
1867
1868
|
}
|
|
1868
|
-
|
|
1869
|
-
//#region src/transports/anthropic.ts
|
|
1869
|
+
// src/transports/anthropic.ts
|
|
1870
1870
|
const EFFORT_LEVELS$1 = /* @__PURE__ */ new Set([
|
|
1871
1871
|
"low",
|
|
1872
1872
|
"medium",
|
|
@@ -2051,6 +2051,7 @@ async function* parseAnthropicStream(response, idleTimeoutMs, signal) {
|
|
|
2051
2051
|
let nextIndex = 0;
|
|
2052
2052
|
let sawContent = false;
|
|
2053
2053
|
let sawToolUse = false;
|
|
2054
|
+
let sawMessageStop = false;
|
|
2054
2055
|
let stopReason;
|
|
2055
2056
|
let inputUsage;
|
|
2056
2057
|
let outputTokens;
|
|
@@ -2219,8 +2220,12 @@ async function* parseAnthropicStream(response, idleTimeoutMs, signal) {
|
|
|
2219
2220
|
if (typeof event.usage?.output_tokens === "number") outputTokens = event.usage.output_tokens;
|
|
2220
2221
|
continue;
|
|
2221
2222
|
}
|
|
2222
|
-
if (event.type === "message_stop")
|
|
2223
|
+
if (event.type === "message_stop") {
|
|
2224
|
+
sawMessageStop = true;
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2223
2227
|
}
|
|
2228
|
+
if (sawContent && !sawMessageStop) throw new LlmError("Anthropic stream ended without message_stop", "TRANSPORT");
|
|
2224
2229
|
const toolBlocks = [...blocks.values()].filter((block) => block.kind === "tool-call");
|
|
2225
2230
|
if (toolBlocks.length > 0) {
|
|
2226
2231
|
for (const block of toolBlocks) yield {
|
|
@@ -2250,8 +2255,7 @@ async function* parseAnthropicStream(response, idleTimeoutMs, signal) {
|
|
|
2250
2255
|
} } : {}
|
|
2251
2256
|
};
|
|
2252
2257
|
}
|
|
2253
|
-
|
|
2254
|
-
//#region src/transports/google.ts
|
|
2258
|
+
// src/transports/google.ts
|
|
2255
2259
|
function textOnly$1(content) {
|
|
2256
2260
|
const parts = [];
|
|
2257
2261
|
for (const block of content) if (block.type === "text") parts.push(block.text);
|
|
@@ -2494,6 +2498,7 @@ async function* parseGoogleStream(response, idleTimeoutMs, signal) {
|
|
|
2494
2498
|
}
|
|
2495
2499
|
}
|
|
2496
2500
|
}
|
|
2501
|
+
if (sawContent && finishReason === void 0) throw new LlmError("Google stream ended without a finish reason", "TRANSPORT");
|
|
2497
2502
|
if (textIndex !== void 0) yield {
|
|
2498
2503
|
type: "block-end",
|
|
2499
2504
|
index: textIndex,
|
|
@@ -2525,8 +2530,7 @@ async function* parseGoogleStream(response, idleTimeoutMs, signal) {
|
|
|
2525
2530
|
} } : {}
|
|
2526
2531
|
};
|
|
2527
2532
|
}
|
|
2528
|
-
|
|
2529
|
-
//#region src/transports/openai-chat.ts
|
|
2533
|
+
// src/transports/openai-chat.ts
|
|
2530
2534
|
function mapUsage$1(usage) {
|
|
2531
2535
|
const prompt = usage.prompt_tokens ?? 0;
|
|
2532
2536
|
const cached = usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
@@ -2565,8 +2569,12 @@ async function* parseChatStream(response, idleTimeoutMs, signal) {
|
|
|
2565
2569
|
let finishReason;
|
|
2566
2570
|
let usage;
|
|
2567
2571
|
let sawContent = false;
|
|
2572
|
+
let sawTerminal = false;
|
|
2568
2573
|
for await (const data of sseData(response.body, idleTimeoutMs, signal)) {
|
|
2569
|
-
if (data === "[DONE]")
|
|
2574
|
+
if (data === "[DONE]") {
|
|
2575
|
+
sawTerminal = true;
|
|
2576
|
+
break;
|
|
2577
|
+
}
|
|
2570
2578
|
let chunk;
|
|
2571
2579
|
try {
|
|
2572
2580
|
chunk = JSON.parse(data);
|
|
@@ -2649,9 +2657,13 @@ async function* parseChatStream(response, idleTimeoutMs, signal) {
|
|
|
2649
2657
|
argumentsDelta
|
|
2650
2658
|
};
|
|
2651
2659
|
}
|
|
2652
|
-
if (choice.finish_reason !== void 0 && choice.finish_reason !== null)
|
|
2660
|
+
if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
|
|
2661
|
+
finishReason = choice.finish_reason;
|
|
2662
|
+
sawTerminal = true;
|
|
2663
|
+
}
|
|
2653
2664
|
}
|
|
2654
2665
|
}
|
|
2666
|
+
if (sawContent && !sawTerminal) throw new LlmError("OpenCode stream ended without a terminal finish event", "TRANSPORT");
|
|
2655
2667
|
if (textIndex !== void 0) yield {
|
|
2656
2668
|
type: "block-end",
|
|
2657
2669
|
index: textIndex,
|
|
@@ -2782,8 +2794,7 @@ async function* parseCompletion(chunk) {
|
|
|
2782
2794
|
reason: finishReasonOf$1(choice?.finish_reason, (message?.tool_calls?.length ?? 0) > 0)
|
|
2783
2795
|
};
|
|
2784
2796
|
}
|
|
2785
|
-
|
|
2786
|
-
//#region src/transports/responses.ts
|
|
2797
|
+
// src/transports/responses.ts
|
|
2787
2798
|
const EFFORT_LEVELS = /* @__PURE__ */ new Set([
|
|
2788
2799
|
"minimal",
|
|
2789
2800
|
"low",
|
|
@@ -2938,6 +2949,7 @@ async function* parseResponsesStream(response, idleTimeoutMs, signal) {
|
|
|
2938
2949
|
const tools = /* @__PURE__ */ new Map();
|
|
2939
2950
|
let sawContent = false;
|
|
2940
2951
|
let hasTools = false;
|
|
2952
|
+
let completed = false;
|
|
2941
2953
|
let usage;
|
|
2942
2954
|
let incomplete;
|
|
2943
2955
|
for await (const { event: eventName, data } of sseEvents(response.body, idleTimeoutMs, signal)) {
|
|
@@ -3051,11 +3063,13 @@ async function* parseResponsesStream(response, idleTimeoutMs, signal) {
|
|
|
3051
3063
|
continue;
|
|
3052
3064
|
}
|
|
3053
3065
|
if (type === "response.completed") {
|
|
3066
|
+
completed = true;
|
|
3054
3067
|
usage = mapUsage(event.response?.usage);
|
|
3055
3068
|
incomplete = event.response?.incomplete_details?.reason;
|
|
3056
3069
|
continue;
|
|
3057
3070
|
}
|
|
3058
3071
|
}
|
|
3072
|
+
if (sawContent && !completed) throw new LlmError("OpenAI Responses stream ended without response.completed", "TRANSPORT");
|
|
3059
3073
|
if (textIndex !== void 0) yield {
|
|
3060
3074
|
type: "block-end",
|
|
3061
3075
|
index: textIndex,
|
|
@@ -3092,8 +3106,7 @@ async function* parseResponsesStream(response, idleTimeoutMs, signal) {
|
|
|
3092
3106
|
reason: finishReasonOf(hasTools, incomplete)
|
|
3093
3107
|
};
|
|
3094
3108
|
}
|
|
3095
|
-
|
|
3096
|
-
//#region src/adapter.ts
|
|
3109
|
+
// src/adapter.ts
|
|
3097
3110
|
function defaultCooldownPath() {
|
|
3098
3111
|
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
3099
3112
|
return join(home, "storages", "dsh-opencode", "cooldowns.json");
|
|
@@ -3432,6 +3445,8 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3432
3445
|
"user-agent": config.userAgent,
|
|
3433
3446
|
...transport === "anthropic" ? { "anthropic-version": "2023-06-01" } : {}
|
|
3434
3447
|
};
|
|
3448
|
+
const callerAborted = () => signal?.aborted === true;
|
|
3449
|
+
if (callerAborted()) throw signal?.reason instanceof Error ? signal.reason : new LlmError("OpenCode request was aborted before dispatch", "TRANSPORT");
|
|
3435
3450
|
const controller = new AbortController();
|
|
3436
3451
|
let timedOut = false;
|
|
3437
3452
|
const onAbort = () => controller.abort(signal?.reason);
|
|
@@ -3449,7 +3464,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3449
3464
|
signal: controller.signal
|
|
3450
3465
|
});
|
|
3451
3466
|
} catch (error) {
|
|
3452
|
-
if (
|
|
3467
|
+
if (callerAborted()) throw signal?.reason ?? error;
|
|
3453
3468
|
if (timedOut) throw new LlmError(`OpenCode did not return response headers within ${config.requestTimeoutMs}ms`, "TIMEOUT", { cause: error });
|
|
3454
3469
|
throw error;
|
|
3455
3470
|
} finally {
|
|
@@ -3586,8 +3601,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3586
3601
|
return error.code === "RATE_LIMIT" || error.code === "QUOTA" || error.code === "SERVER" || error.code === "TIMEOUT" || error.code === "TRANSPORT" || error.code === "EMPTY_RESPONSE";
|
|
3587
3602
|
}
|
|
3588
3603
|
};
|
|
3589
|
-
|
|
3590
|
-
//#region src/commands.ts
|
|
3604
|
+
// src/commands.ts
|
|
3591
3605
|
function bar(percent, width = 10) {
|
|
3592
3606
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
3593
3607
|
const filled = Math.round(clamped / 100 * width);
|
|
@@ -3773,8 +3787,7 @@ function applyCommands(ctx, deps) {
|
|
|
3773
3787
|
}
|
|
3774
3788
|
});
|
|
3775
3789
|
}
|
|
3776
|
-
|
|
3777
|
-
//#region src/credentials.ts
|
|
3790
|
+
// src/credentials.ts
|
|
3778
3791
|
/** OpenCode stores its own sign-in at this XDG data path. */
|
|
3779
3792
|
function openCodeAuthPath() {
|
|
3780
3793
|
const base = process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share");
|
|
@@ -3908,8 +3921,7 @@ function missingKey(config) {
|
|
|
3908
3921
|
function createKeyResolver(ctx, config) {
|
|
3909
3922
|
return createKeyPool(ctx, config).resolve;
|
|
3910
3923
|
}
|
|
3911
|
-
|
|
3912
|
-
//#region src/local-host.ts
|
|
3924
|
+
// src/local-host.ts
|
|
3913
3925
|
const LOOPBACK = /* @__PURE__ */ new Set([
|
|
3914
3926
|
"localhost",
|
|
3915
3927
|
"localhost.",
|
|
@@ -3950,8 +3962,7 @@ function sameOrigin(authority, origin) {
|
|
|
3950
3962
|
return false;
|
|
3951
3963
|
}
|
|
3952
3964
|
}
|
|
3953
|
-
|
|
3954
|
-
//#region src/plugin-updater.ts
|
|
3965
|
+
// src/plugin-updater.ts
|
|
3955
3966
|
const UPDATE_HEADER = "x-dsh-plugin-update";
|
|
3956
3967
|
const UPDATE_TIMEOUT_MS = 6e5;
|
|
3957
3968
|
const VERSION_CACHE_MS = 3e5;
|
|
@@ -4247,8 +4258,7 @@ function registerPluginUpdater(ctx, options) {
|
|
|
4247
4258
|
}
|
|
4248
4259
|
});
|
|
4249
4260
|
}
|
|
4250
|
-
|
|
4251
|
-
//#region src/analytics.ts
|
|
4261
|
+
// src/analytics.ts
|
|
4252
4262
|
const DAY_MS = 864e5;
|
|
4253
4263
|
const CACHE_TTL_MS = 6e4;
|
|
4254
4264
|
const MAX_FILES = 5e3;
|
|
@@ -4492,8 +4502,25 @@ async function readModelAnalytics(days = 30) {
|
|
|
4492
4502
|
if (cached !== void 0 && Date.now() - cached.at < CACHE_TTL_MS) return cached.report;
|
|
4493
4503
|
const dir = projectionDirectory();
|
|
4494
4504
|
const sessions = [];
|
|
4505
|
+
let partial = false;
|
|
4495
4506
|
try {
|
|
4496
|
-
|
|
4507
|
+
let files = (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".json"));
|
|
4508
|
+
if (files.length > MAX_FILES) {
|
|
4509
|
+
files = (await Promise.all(files.map(async (entry) => {
|
|
4510
|
+
try {
|
|
4511
|
+
return {
|
|
4512
|
+
entry,
|
|
4513
|
+
at: (await stat(join(dir, entry.name))).mtimeMs
|
|
4514
|
+
};
|
|
4515
|
+
} catch {
|
|
4516
|
+
return {
|
|
4517
|
+
entry,
|
|
4518
|
+
at: 0
|
|
4519
|
+
};
|
|
4520
|
+
}
|
|
4521
|
+
}))).sort((left, right) => right.at - left.at).slice(0, MAX_FILES).map((item) => item.entry);
|
|
4522
|
+
partial = true;
|
|
4523
|
+
}
|
|
4497
4524
|
let cursor = 0;
|
|
4498
4525
|
const workers = Array.from({ length: Math.min(16, files.length) }, async () => {
|
|
4499
4526
|
while (true) {
|
|
@@ -4507,20 +4534,24 @@ async function readModelAnalytics(days = 30) {
|
|
|
4507
4534
|
meta.id = entry.name.replace(/\.json$/i, "");
|
|
4508
4535
|
sessions.push(meta);
|
|
4509
4536
|
}
|
|
4510
|
-
} catch {
|
|
4537
|
+
} catch {
|
|
4538
|
+
partial = true;
|
|
4539
|
+
}
|
|
4511
4540
|
}
|
|
4512
4541
|
});
|
|
4513
4542
|
await Promise.all(workers);
|
|
4514
|
-
} catch {
|
|
4543
|
+
} catch {
|
|
4544
|
+
partial = true;
|
|
4545
|
+
}
|
|
4515
4546
|
const report = aggregateSessions(sessions, clamped);
|
|
4547
|
+
report.partial = partial;
|
|
4516
4548
|
cache.set(clamped, {
|
|
4517
4549
|
at: Date.now(),
|
|
4518
4550
|
report
|
|
4519
4551
|
});
|
|
4520
4552
|
return report;
|
|
4521
4553
|
}
|
|
4522
|
-
|
|
4523
|
-
//#region src/routes-public.ts
|
|
4554
|
+
// src/routes-public.ts
|
|
4524
4555
|
function publicConfig(config) {
|
|
4525
4556
|
return {
|
|
4526
4557
|
apiKeyEnv: config.apiKeyEnv,
|
|
@@ -4553,8 +4584,7 @@ function publicConfig(config) {
|
|
|
4553
4584
|
userAgent: config.userAgent
|
|
4554
4585
|
};
|
|
4555
4586
|
}
|
|
4556
|
-
|
|
4557
|
-
//#region src/routes-values.ts
|
|
4587
|
+
// src/routes-values.ts
|
|
4558
4588
|
const TRANSPORTS = /* @__PURE__ */ new Set([
|
|
4559
4589
|
"openai-chat",
|
|
4560
4590
|
"openai-responses",
|
|
@@ -4614,8 +4644,7 @@ function cleanTransports(value) {
|
|
|
4614
4644
|
for (const [key, entry] of Object.entries(value)) if (typeof entry === "string" && TRANSPORTS.has(entry)) out[key] = entry;
|
|
4615
4645
|
return out;
|
|
4616
4646
|
}
|
|
4617
|
-
|
|
4618
|
-
//#region src/routes.ts
|
|
4647
|
+
// src/routes.ts
|
|
4619
4648
|
const MAX_BODY_BYTES = 262144;
|
|
4620
4649
|
function json(res, status, value) {
|
|
4621
4650
|
res.statusCode = status;
|
|
@@ -4949,12 +4978,14 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4949
4978
|
json(res, 400, { error: "no known fields in patch" });
|
|
4950
4979
|
return;
|
|
4951
4980
|
}
|
|
4952
|
-
|
|
4981
|
+
const revision = typeof body.revision === "number" && Number.isInteger(body.revision) ? body.revision : settingsRevision(ctx);
|
|
4982
|
+
await settings.mutate(NS, ops, revision);
|
|
4953
4983
|
deps.usage.clear();
|
|
4954
4984
|
deps.refreshTui?.();
|
|
4955
4985
|
json(res, 200, { ok: true });
|
|
4956
4986
|
} catch (error) {
|
|
4957
|
-
|
|
4987
|
+
const message = error instanceof Error ? error.message : "config write failed";
|
|
4988
|
+
json(res, /revision|conflict|stale/i.test(message) ? 409 : 500, { error: message });
|
|
4958
4989
|
}
|
|
4959
4990
|
});
|
|
4960
4991
|
route("/dsh-opencode/key", async (req, res) => {
|
|
@@ -5122,8 +5153,7 @@ function applyRoutes(ctx, web, deps) {
|
|
|
5122
5153
|
for (const dispose of disposers.splice(0)) dispose();
|
|
5123
5154
|
}, "dsh-opencode-provider: HTTP routes");
|
|
5124
5155
|
}
|
|
5125
|
-
|
|
5126
|
-
//#region src/tui-settings.ts
|
|
5156
|
+
// src/tui-settings.ts
|
|
5127
5157
|
const TUI_ACCOUNT_AUTO = "auto";
|
|
5128
5158
|
function setOrClear(text) {
|
|
5129
5159
|
const trimmed = text.trim();
|
|
@@ -5367,8 +5397,7 @@ function applyOpenCodeTuiSettings(ctx, deps) {
|
|
|
5367
5397
|
}, "dsh-opencode-provider: tui settings section");
|
|
5368
5398
|
return refresh;
|
|
5369
5399
|
}
|
|
5370
|
-
|
|
5371
|
-
//#region src/usage.ts
|
|
5400
|
+
// src/usage.ts
|
|
5372
5401
|
const SUCCESS_TTL_MS = 3e5;
|
|
5373
5402
|
const FAILURE_TTL_MS = 45e3;
|
|
5374
5403
|
const USAGE_TIMEOUT_MS = 2e4;
|
|
@@ -5482,8 +5511,7 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
|
5482
5511
|
};
|
|
5483
5512
|
return read;
|
|
5484
5513
|
}
|
|
5485
|
-
|
|
5486
|
-
//#region src/index.ts
|
|
5514
|
+
// src/index.ts
|
|
5487
5515
|
const name = "llm-opencode";
|
|
5488
5516
|
const inject = ["llm"];
|
|
5489
5517
|
/**
|
|
@@ -5567,7 +5595,6 @@ function apply(ctx, config) {
|
|
|
5567
5595
|
}), "dsh-opencode-provider: plugin updater");
|
|
5568
5596
|
});
|
|
5569
5597
|
}
|
|
5570
|
-
//#endregion
|
|
5571
5598
|
export { Config, OpenCodeAdapter, apply, applyCommands, applyRoutes, classifyResponseError, clearCatalogCache, createKeyPool, createKeyResolver, createUsageReader, describeModel, effectiveModel, inject, name, normalizeUsage, openCodeAuthPath, openCodeSessionId };
|
|
5572
5599
|
|
|
5573
5600
|
//# sourceMappingURL=index.js.map
|
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.21",
|
|
4
4
|
"description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -69,10 +69,8 @@
|
|
|
69
69
|
"build": "tsdown",
|
|
70
70
|
"prepare": "tsdown 1>&2",
|
|
71
71
|
"typecheck": "tsc --noEmit",
|
|
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",
|
|
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 tests/sse.test.ts",
|
|
73
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",
|
|
76
74
|
"release": "npm run check && npm pack",
|
|
77
75
|
"sync:models": "node scripts/sync-models.mjs"
|
|
78
76
|
},
|