@goodandready/dsh-opencode-provider 0.5.19 → 0.5.20
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 +9 -0
- package/README.md +3 -3
- package/README.ru.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +37 -38
- package/lib/index.d.ts +0 -26
- package/lib/index.js +82 -77
- package/package.json +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.20
|
|
4
|
+
|
|
5
|
+
- **CI/reproducibility**: regenerated `package-lock.json` with the normal npm resolver; clean `npm ci --ignore-scripts` now succeeds and `npm run check` passes.
|
|
6
|
+
- **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.
|
|
7
|
+
- **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.
|
|
8
|
+
- **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.
|
|
9
|
+
- **Images**: request budget counts repeated image occurrences and base64 overhead; offloaded images no longer require the attachment service.
|
|
10
|
+
- **Docs/tests**: added project `AGENTS.md`; removed the unsafe local engine-matrix script pending the approved DSH test contour (`#55`).
|
|
11
|
+
|
|
3
12
|
## 0.5.19
|
|
4
13
|
|
|
5
14
|
- **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.20.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.20.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.20
|
|
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.20.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.20.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.20
|
|
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.20.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.20.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.20
|
|
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",
|
|
@@ -245,6 +246,7 @@ window.__ModuleLoader__.load({
|
|
|
245
246
|
pasteKey: "粘贴密钥",
|
|
246
247
|
saveKey: "保存密钥",
|
|
247
248
|
clearKey: "清除密钥",
|
|
249
|
+
clearKeyConfirm: "删除 {ref} 的已保存凭据?此操作无法撤销。",
|
|
248
250
|
importAuth: "从 OpenCode 导入",
|
|
249
251
|
imported: "已导入 OpenCode 密钥",
|
|
250
252
|
testConnection: "测试连接",
|
|
@@ -419,8 +421,7 @@ window.__ModuleLoader__.load({
|
|
|
419
421
|
}
|
|
420
422
|
};
|
|
421
423
|
const GO_PROVIDER = "opencode-go-direct";
|
|
422
|
-
|
|
423
|
-
//#region src/client/api.ts
|
|
424
|
+
// src/client/api.ts
|
|
424
425
|
async function call(path, init) {
|
|
425
426
|
const response = await fetch(path, {
|
|
426
427
|
credentials: "same-origin",
|
|
@@ -456,10 +457,13 @@ window.__ModuleLoader__.load({
|
|
|
456
457
|
body: JSON.stringify({ label })
|
|
457
458
|
});
|
|
458
459
|
}
|
|
459
|
-
function saveConfig(patch) {
|
|
460
|
+
function saveConfig(patch, revision) {
|
|
460
461
|
return call("/dsh-opencode/config", {
|
|
461
462
|
method: "POST",
|
|
462
|
-
body: JSON.stringify({ patch }
|
|
463
|
+
body: JSON.stringify(revision === void 0 ? { patch } : {
|
|
464
|
+
patch,
|
|
465
|
+
revision
|
|
466
|
+
})
|
|
463
467
|
});
|
|
464
468
|
}
|
|
465
469
|
function fetchUpdate() {
|
|
@@ -508,11 +512,9 @@ window.__ModuleLoader__.load({
|
|
|
508
512
|
})
|
|
509
513
|
});
|
|
510
514
|
}
|
|
511
|
-
|
|
512
|
-
//#region src/client/events.ts
|
|
515
|
+
// src/client/events.ts
|
|
513
516
|
const CONFIG_CHANGED_EVENT = "dsh-opencode-config-changed";
|
|
514
|
-
|
|
515
|
-
//#region src/client/heatmap.tsx
|
|
517
|
+
// src/client/heatmap.tsx
|
|
516
518
|
const DAYS = [
|
|
517
519
|
"Mon",
|
|
518
520
|
"Tue",
|
|
@@ -721,8 +723,7 @@ window.__ModuleLoader__.load({
|
|
|
721
723
|
]
|
|
722
724
|
});
|
|
723
725
|
}
|
|
724
|
-
|
|
725
|
-
//#region src/client/usage-analytics.tsx
|
|
726
|
+
// src/client/usage-analytics.tsx
|
|
726
727
|
function compact(value) {
|
|
727
728
|
if (!Number.isFinite(value) || value <= 0) return "0";
|
|
728
729
|
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
@@ -931,8 +932,7 @@ window.__ModuleLoader__.load({
|
|
|
931
932
|
tab === "sessions" ? sessionRows(analytics, t) : null
|
|
932
933
|
] });
|
|
933
934
|
}
|
|
934
|
-
|
|
935
|
-
//#region src/client/usage-bars.tsx
|
|
935
|
+
// src/client/usage-bars.tsx
|
|
936
936
|
function percentClass(percent) {
|
|
937
937
|
if (percent >= 90) return "oc-bar-fill oc-bar-fill-bad";
|
|
938
938
|
if (percent >= 75) return "oc-bar-fill oc-bar-fill-warn";
|
|
@@ -1001,8 +1001,7 @@ window.__ModuleLoader__.load({
|
|
|
1001
1001
|
]
|
|
1002
1002
|
});
|
|
1003
1003
|
}
|
|
1004
|
-
|
|
1005
|
-
//#region src/quota-runway.ts
|
|
1004
|
+
// src/quota-runway.ts
|
|
1006
1005
|
const HOUR = 36e5;
|
|
1007
1006
|
const DAY = 24 * HOUR;
|
|
1008
1007
|
const DURATIONS = {
|
|
@@ -1068,8 +1067,7 @@ window.__ModuleLoader__.load({
|
|
|
1068
1067
|
};
|
|
1069
1068
|
return known === 0 ? { kind: "unknown" } : { kind: "safe" };
|
|
1070
1069
|
}
|
|
1071
|
-
|
|
1072
|
-
//#region src/client/quota-card.tsx
|
|
1070
|
+
// src/client/quota-card.tsx
|
|
1073
1071
|
const PANEL_EVENT = "dsh-opencode-panel-state";
|
|
1074
1072
|
function broadcastPanel(open) {
|
|
1075
1073
|
if (typeof window === "undefined") return;
|
|
@@ -1611,11 +1609,11 @@ window.__ModuleLoader__.load({
|
|
|
1611
1609
|
]
|
|
1612
1610
|
});
|
|
1613
1611
|
}
|
|
1614
|
-
|
|
1615
|
-
//#region src/client/settings-draft.ts
|
|
1612
|
+
// src/client/settings-draft.ts
|
|
1616
1613
|
function draftOf(state) {
|
|
1617
1614
|
const allIds = state.models.map((model) => model.id);
|
|
1618
1615
|
return {
|
|
1616
|
+
revision: state.revision ?? 0,
|
|
1619
1617
|
apiKeyEnv: state.config.apiKeyEnv,
|
|
1620
1618
|
apiBase: state.config.apiBase,
|
|
1621
1619
|
goApiBase: state.config.goApiBase,
|
|
@@ -1701,8 +1699,7 @@ window.__ModuleLoader__.load({
|
|
|
1701
1699
|
function describeLimitsFull(limits) {
|
|
1702
1700
|
return `${planLine("Go", limits.go)} · ${planLine("Go Plus", limits.goPlus)}`;
|
|
1703
1701
|
}
|
|
1704
|
-
|
|
1705
|
-
//#region src/client/settings-widgets.tsx
|
|
1702
|
+
// src/client/settings-widgets.tsx
|
|
1706
1703
|
function Field(props) {
|
|
1707
1704
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1708
1705
|
className: "oc-field",
|
|
@@ -1765,8 +1762,7 @@ window.__ModuleLoader__.load({
|
|
|
1765
1762
|
if (account.coolingUntil <= 0) return label;
|
|
1766
1763
|
return `${label} · ${untilText(account.coolingUntil)}`;
|
|
1767
1764
|
}
|
|
1768
|
-
|
|
1769
|
-
//#region src/client/update-card.tsx
|
|
1765
|
+
// src/client/update-card.tsx
|
|
1770
1766
|
function PluginUpdateCard(props) {
|
|
1771
1767
|
const { t } = props;
|
|
1772
1768
|
const [status, setStatus] = (0, react.useState)(null);
|
|
@@ -1803,6 +1799,9 @@ window.__ModuleLoader__.load({
|
|
|
1803
1799
|
children: [t("pluginUpdate"), status === null ? null : status.updateAvailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1804
1800
|
className: "oc-badge oc-badge-warn",
|
|
1805
1801
|
children: t("updateAvailable")
|
|
1802
|
+
}) : status.latestCheckFailed || status.latestVersion === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1803
|
+
className: "oc-badge oc-badge-warn",
|
|
1804
|
+
children: t("updateCheckFailed")
|
|
1806
1805
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1807
1806
|
className: "oc-badge oc-badge-ok",
|
|
1808
1807
|
children: t("upToDate")
|
|
@@ -1849,8 +1848,7 @@ window.__ModuleLoader__.load({
|
|
|
1849
1848
|
]
|
|
1850
1849
|
});
|
|
1851
1850
|
}
|
|
1852
|
-
|
|
1853
|
-
//#region src/client/auth-ref.ts
|
|
1851
|
+
// src/client/auth-ref.ts
|
|
1854
1852
|
/**
|
|
1855
1853
|
* Collision-free credential ref for an OpenCode auth entry. Hex encoding is
|
|
1856
1854
|
* injective (unlike replacing punctuation with underscores) and uses only
|
|
@@ -1861,8 +1859,7 @@ window.__ModuleLoader__.load({
|
|
|
1861
1859
|
for (const byte of new TextEncoder().encode(entry)) hex += byte.toString(16).padStart(2, "0");
|
|
1862
1860
|
return `OPENCODE_AUTH_${hex}`;
|
|
1863
1861
|
}
|
|
1864
|
-
|
|
1865
|
-
//#region src/client/settings-page.tsx
|
|
1862
|
+
// src/client/settings-page.tsx
|
|
1866
1863
|
function keyStatusLabel(status, t) {
|
|
1867
1864
|
if (status === "ok") return t("keyStatusOk");
|
|
1868
1865
|
if (status === "expired") return t("keyStatusExpired");
|
|
@@ -1928,7 +1925,7 @@ window.__ModuleLoader__.load({
|
|
|
1928
1925
|
setSaving(true);
|
|
1929
1926
|
setError(null);
|
|
1930
1927
|
try {
|
|
1931
|
-
await saveConfig(patchOf(draft));
|
|
1928
|
+
await saveConfig(patchOf(draft), draft.revision);
|
|
1932
1929
|
window.dispatchEvent(new CustomEvent(CONFIG_CHANGED_EVENT));
|
|
1933
1930
|
setNotice(t("saved"));
|
|
1934
1931
|
setState(null);
|
|
@@ -2265,7 +2262,9 @@ window.__ModuleLoader__.load({
|
|
|
2265
2262
|
type: "button",
|
|
2266
2263
|
className: "oc-button oc-button-danger",
|
|
2267
2264
|
onClick: () => {
|
|
2268
|
-
|
|
2265
|
+
const ref = draft.apiKeyEnv.trim() === "" ? state.config.apiKeyEnv : draft.apiKeyEnv.trim();
|
|
2266
|
+
if (!window.confirm(t("clearKeyConfirm").replace("{ref}", ref))) return;
|
|
2267
|
+
clearKey(ref).then(() => {
|
|
2269
2268
|
setNotice(t("saved"));
|
|
2270
2269
|
return load();
|
|
2271
2270
|
}).catch((caught) => setError(caught instanceof Error ? caught.message : t("failed")));
|
|
@@ -2335,8 +2334,8 @@ window.__ModuleLoader__.load({
|
|
|
2335
2334
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
2336
2335
|
className: "oc-card-title",
|
|
2337
2336
|
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")
|
|
2337
|
+
className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2338
|
+
children: state.service.usageCheckedAt === void 0 ? t("notChecked") : state.service.usageError === void 0 ? t("serviceOk") : t("serviceDegraded")
|
|
2340
2339
|
})]
|
|
2341
2340
|
}),
|
|
2342
2341
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2347,7 +2346,7 @@ window.__ModuleLoader__.load({
|
|
|
2347
2346
|
children: t("usageEndpoint")
|
|
2348
2347
|
}),
|
|
2349
2348
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2350
|
-
className: `oc-badge ${state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2349
|
+
className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2351
2350
|
children: state.service.usageCheckedAt === void 0 ? t("notChecked") : t("checkedAt").replace("{time}", new Date(state.service.usageCheckedAt).toLocaleTimeString())
|
|
2352
2351
|
}),
|
|
2353
2352
|
state.service.usageLatencyMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2564,7 +2563,10 @@ window.__ModuleLoader__.load({
|
|
|
2564
2563
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2565
2564
|
type: "button",
|
|
2566
2565
|
className: "oc-button oc-button-danger",
|
|
2567
|
-
onClick: () =>
|
|
2566
|
+
onClick: () => {
|
|
2567
|
+
setKeyDrafts({});
|
|
2568
|
+
update({ accounts: draft.accounts.filter((_, i) => i !== index) });
|
|
2569
|
+
},
|
|
2568
2570
|
children: t("removeAccount")
|
|
2569
2571
|
})
|
|
2570
2572
|
]
|
|
@@ -3006,8 +3008,7 @@ window.__ModuleLoader__.load({
|
|
|
3006
3008
|
]
|
|
3007
3009
|
});
|
|
3008
3010
|
}
|
|
3009
|
-
|
|
3010
|
-
//#region src/client/styles.ts
|
|
3011
|
+
// src/client/styles.ts
|
|
3011
3012
|
const CSS_ID = "dsh-opencode-provider";
|
|
3012
3013
|
const CSS = `
|
|
3013
3014
|
:where(.oc-page, .oc-panel, .oc-card, .oc-foot, .oc-railButton) {
|
|
@@ -3127,8 +3128,7 @@ window.__ModuleLoader__.load({
|
|
|
3127
3128
|
.oc-table th, .oc-table td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--oc-border-soft); vertical-align: top; }
|
|
3128
3129
|
.oc-table th { font-weight: 600; opacity: 0.8; }
|
|
3129
3130
|
`;
|
|
3130
|
-
|
|
3131
|
-
//#region src/client/index.tsx
|
|
3131
|
+
// src/client/index.tsx
|
|
3132
3132
|
function injectCss() {
|
|
3133
3133
|
if (typeof document === "undefined") return () => {};
|
|
3134
3134
|
if (document.querySelector(`style[data-dsh-plugin="dsh-opencode-provider"]`) !== null) return () => {};
|
|
@@ -3174,7 +3174,6 @@ window.__ModuleLoader__.load({
|
|
|
3174
3174
|
}, QuotaCard));
|
|
3175
3175
|
});
|
|
3176
3176
|
}
|
|
3177
|
-
//#endregion
|
|
3178
3177
|
exports.apply = apply;
|
|
3179
3178
|
exports.inject = inject;
|
|
3180
3179
|
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;
|
|
@@ -366,8 +353,6 @@ interface AnalyticsReport {
|
|
|
366
353
|
days: DayUsageRow[];
|
|
367
354
|
sessions: SessionUsageRow[];
|
|
368
355
|
}
|
|
369
|
-
//#endregion
|
|
370
|
-
//#region src/routes.d.ts
|
|
371
356
|
interface RouteDeps {
|
|
372
357
|
readonly config: () => ResolvedConfig;
|
|
373
358
|
readonly adapter: OpenCodeAdapter;
|
|
@@ -379,8 +364,6 @@ interface RouteDeps {
|
|
|
379
364
|
}
|
|
380
365
|
/** Register the Host HTTP surface consumed by the browser settings page. */
|
|
381
366
|
declare function applyRoutes(ctx: Context, web: WebServer, deps: RouteDeps): void;
|
|
382
|
-
//#endregion
|
|
383
|
-
//#region src/session-id.d.ts
|
|
384
367
|
/**
|
|
385
368
|
* A stable, opaque session id for `x-opencode-session`. The header must stay
|
|
386
369
|
* identical across every turn and retry of one conversation; deriving it from
|
|
@@ -389,16 +372,12 @@ declare function applyRoutes(ctx: Context, web: WebServer, deps: RouteDeps): voi
|
|
|
389
372
|
* routing key, so the `dsh_` prefix is safe.
|
|
390
373
|
*/
|
|
391
374
|
declare function openCodeSessionId(options: GenerateOptions): string;
|
|
392
|
-
//#endregion
|
|
393
|
-
//#region src/errors.d.ts
|
|
394
375
|
/**
|
|
395
376
|
* Translate an OpenCode HTTP failure into the harness failure taxonomy.
|
|
396
377
|
* OpenCode reports several distinct conditions as 400/401, so the body type
|
|
397
378
|
* and message decide the code — never the status alone.
|
|
398
379
|
*/
|
|
399
380
|
declare function classifyResponseError(status: number, text: string, headers: Headers): LlmError;
|
|
400
|
-
//#endregion
|
|
401
|
-
//#region src/models.generated.d.ts
|
|
402
381
|
type OpenCodeTransport = 'openai-chat' | 'openai-responses' | 'anthropic' | 'google';
|
|
403
382
|
interface OpenCodeModelMeta {
|
|
404
383
|
readonly id: string;
|
|
@@ -415,8 +394,6 @@ interface OpenCodeModelMeta {
|
|
|
415
394
|
readonly cacheRead: number;
|
|
416
395
|
};
|
|
417
396
|
}
|
|
418
|
-
//#endregion
|
|
419
|
-
//#region src/catalog.d.ts
|
|
420
397
|
interface EffectiveModel extends OpenCodeModelMeta {
|
|
421
398
|
readonly transport: OpenCodeTransport;
|
|
422
399
|
readonly contextWindow: number;
|
|
@@ -424,8 +401,6 @@ interface EffectiveModel extends OpenCodeModelMeta {
|
|
|
424
401
|
declare function effectiveModel(id: string, config: ResolvedConfig): EffectiveModel;
|
|
425
402
|
declare function describeModel(meta: EffectiveModel, provider?: string): string;
|
|
426
403
|
declare function clearCatalogCache(): void;
|
|
427
|
-
//#endregion
|
|
428
|
-
//#region src/index.d.ts
|
|
429
404
|
declare const name = "llm-opencode";
|
|
430
405
|
declare const inject: string[];
|
|
431
406
|
/**
|
|
@@ -438,6 +413,5 @@ declare const inject: string[];
|
|
|
438
413
|
* next request without a restart.
|
|
439
414
|
*/
|
|
440
415
|
declare function apply(ctx: Context, config: Config): void;
|
|
441
|
-
//#endregion
|
|
442
416
|
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
417
|
//# 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.20";
|
|
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;
|
|
@@ -4519,8 +4529,7 @@ async function readModelAnalytics(days = 30) {
|
|
|
4519
4529
|
});
|
|
4520
4530
|
return report;
|
|
4521
4531
|
}
|
|
4522
|
-
|
|
4523
|
-
//#region src/routes-public.ts
|
|
4532
|
+
// src/routes-public.ts
|
|
4524
4533
|
function publicConfig(config) {
|
|
4525
4534
|
return {
|
|
4526
4535
|
apiKeyEnv: config.apiKeyEnv,
|
|
@@ -4553,8 +4562,7 @@ function publicConfig(config) {
|
|
|
4553
4562
|
userAgent: config.userAgent
|
|
4554
4563
|
};
|
|
4555
4564
|
}
|
|
4556
|
-
|
|
4557
|
-
//#region src/routes-values.ts
|
|
4565
|
+
// src/routes-values.ts
|
|
4558
4566
|
const TRANSPORTS = /* @__PURE__ */ new Set([
|
|
4559
4567
|
"openai-chat",
|
|
4560
4568
|
"openai-responses",
|
|
@@ -4614,8 +4622,7 @@ function cleanTransports(value) {
|
|
|
4614
4622
|
for (const [key, entry] of Object.entries(value)) if (typeof entry === "string" && TRANSPORTS.has(entry)) out[key] = entry;
|
|
4615
4623
|
return out;
|
|
4616
4624
|
}
|
|
4617
|
-
|
|
4618
|
-
//#region src/routes.ts
|
|
4625
|
+
// src/routes.ts
|
|
4619
4626
|
const MAX_BODY_BYTES = 262144;
|
|
4620
4627
|
function json(res, status, value) {
|
|
4621
4628
|
res.statusCode = status;
|
|
@@ -4949,12 +4956,14 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4949
4956
|
json(res, 400, { error: "no known fields in patch" });
|
|
4950
4957
|
return;
|
|
4951
4958
|
}
|
|
4952
|
-
|
|
4959
|
+
const revision = typeof body.revision === "number" && Number.isInteger(body.revision) ? body.revision : settingsRevision(ctx);
|
|
4960
|
+
await settings.mutate(NS, ops, revision);
|
|
4953
4961
|
deps.usage.clear();
|
|
4954
4962
|
deps.refreshTui?.();
|
|
4955
4963
|
json(res, 200, { ok: true });
|
|
4956
4964
|
} catch (error) {
|
|
4957
|
-
|
|
4965
|
+
const message = error instanceof Error ? error.message : "config write failed";
|
|
4966
|
+
json(res, /revision|conflict|stale/i.test(message) ? 409 : 500, { error: message });
|
|
4958
4967
|
}
|
|
4959
4968
|
});
|
|
4960
4969
|
route("/dsh-opencode/key", async (req, res) => {
|
|
@@ -5122,8 +5131,7 @@ function applyRoutes(ctx, web, deps) {
|
|
|
5122
5131
|
for (const dispose of disposers.splice(0)) dispose();
|
|
5123
5132
|
}, "dsh-opencode-provider: HTTP routes");
|
|
5124
5133
|
}
|
|
5125
|
-
|
|
5126
|
-
//#region src/tui-settings.ts
|
|
5134
|
+
// src/tui-settings.ts
|
|
5127
5135
|
const TUI_ACCOUNT_AUTO = "auto";
|
|
5128
5136
|
function setOrClear(text) {
|
|
5129
5137
|
const trimmed = text.trim();
|
|
@@ -5367,8 +5375,7 @@ function applyOpenCodeTuiSettings(ctx, deps) {
|
|
|
5367
5375
|
}, "dsh-opencode-provider: tui settings section");
|
|
5368
5376
|
return refresh;
|
|
5369
5377
|
}
|
|
5370
|
-
|
|
5371
|
-
//#region src/usage.ts
|
|
5378
|
+
// src/usage.ts
|
|
5372
5379
|
const SUCCESS_TTL_MS = 3e5;
|
|
5373
5380
|
const FAILURE_TTL_MS = 45e3;
|
|
5374
5381
|
const USAGE_TIMEOUT_MS = 2e4;
|
|
@@ -5482,8 +5489,7 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
|
5482
5489
|
};
|
|
5483
5490
|
return read;
|
|
5484
5491
|
}
|
|
5485
|
-
|
|
5486
|
-
//#region src/index.ts
|
|
5492
|
+
// src/index.ts
|
|
5487
5493
|
const name = "llm-opencode";
|
|
5488
5494
|
const inject = ["llm"];
|
|
5489
5495
|
/**
|
|
@@ -5567,7 +5573,6 @@ function apply(ctx, config) {
|
|
|
5567
5573
|
}), "dsh-opencode-provider: plugin updater");
|
|
5568
5574
|
});
|
|
5569
5575
|
}
|
|
5570
|
-
//#endregion
|
|
5571
5576
|
export { Config, OpenCodeAdapter, apply, applyCommands, applyRoutes, classifyResponseError, clearCatalogCache, createKeyPool, createKeyResolver, createUsageReader, describeModel, effectiveModel, inject, name, normalizeUsage, openCodeAuthPath, openCodeSessionId };
|
|
5572
5577
|
|
|
5573
5578
|
//# 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.20",
|
|
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
|
},
|