@goodandready/dsh-opencode-provider 0.5.18 → 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 +15 -0
- package/README.md +3 -3
- package/README.ru.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +119 -35
- package/lib/index.d.ts +11 -26
- package/lib/index.js +155 -84
- package/package.json +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
12
|
+
## 0.5.19
|
|
13
|
+
|
|
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.
|
|
15
|
+
- **Service status**: `/state` exposes the usage check time, latency and error, plus catalog source (`live`/`snapshot`), count and fetch time. Settings shows a compact Service status card.
|
|
16
|
+
- 105 tests; engine matrix/install matrix remain green.
|
|
17
|
+
|
|
3
18
|
## 0.5.18
|
|
4
19
|
|
|
5
20
|
- **Compatibility range**: `engines.dsh`, DSH peer ranges and `dsh.compatibility.dsh` now use `>=0.2.1-rc.2 <0.2.2-0`; `dshReleases` keeps the tested rc.2/rc.3/rc.4 records. Future `0.2.1` RCs are admitted automatically; the next minor remains excluded.
|
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: {
|
|
@@ -25,11 +25,23 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
keyStatusUnchecked: "Key unchecked",
|
|
26
26
|
checkKey: "Check key",
|
|
27
27
|
keyStatusHint: "A key check reads the usage endpoint; it does not send a model request.",
|
|
28
|
+
serviceStatus: "Service status",
|
|
29
|
+
serviceOk: "Reachable",
|
|
30
|
+
serviceDegraded: "Degraded",
|
|
31
|
+
usageEndpoint: "Usage endpoint",
|
|
32
|
+
catalogStatus: "Model catalog",
|
|
33
|
+
catalogLive: "live",
|
|
34
|
+
catalogSnapshot: "snapshot",
|
|
35
|
+
catalogAge: "updated {age}",
|
|
36
|
+
checkedAt: "checked {time}",
|
|
37
|
+
latencyValue: "{ms} ms",
|
|
38
|
+
notChecked: "not checked",
|
|
28
39
|
keySource: "Source",
|
|
29
40
|
keyWritable: "Writable",
|
|
30
41
|
pasteKey: "Paste a key",
|
|
31
42
|
saveKey: "Save key",
|
|
32
43
|
clearKey: "Clear key",
|
|
44
|
+
clearKeyConfirm: "Delete the stored credential for {ref}? This cannot be undone.",
|
|
33
45
|
importAuth: "Import from OpenCode",
|
|
34
46
|
imported: "Imported the OpenCode key",
|
|
35
47
|
testConnection: "Test connection",
|
|
@@ -218,11 +230,23 @@ window.__ModuleLoader__.load({
|
|
|
218
230
|
keyStatusUnchecked: "未检查",
|
|
219
231
|
checkKey: "检查密钥",
|
|
220
232
|
keyStatusHint: "检查仅读取用量端点,不会发送模型请求。",
|
|
233
|
+
serviceStatus: "服务状态",
|
|
234
|
+
serviceOk: "可访问",
|
|
235
|
+
serviceDegraded: "异常",
|
|
236
|
+
usageEndpoint: "用量端点",
|
|
237
|
+
catalogStatus: "模型目录",
|
|
238
|
+
catalogLive: "实时",
|
|
239
|
+
catalogSnapshot: "快照",
|
|
240
|
+
catalogAge: "更新于 {age}",
|
|
241
|
+
checkedAt: "检查于 {time}",
|
|
242
|
+
latencyValue: "{ms} ms",
|
|
243
|
+
notChecked: "未检查",
|
|
221
244
|
keySource: "来源",
|
|
222
245
|
keyWritable: "可写",
|
|
223
246
|
pasteKey: "粘贴密钥",
|
|
224
247
|
saveKey: "保存密钥",
|
|
225
248
|
clearKey: "清除密钥",
|
|
249
|
+
clearKeyConfirm: "删除 {ref} 的已保存凭据?此操作无法撤销。",
|
|
226
250
|
importAuth: "从 OpenCode 导入",
|
|
227
251
|
imported: "已导入 OpenCode 密钥",
|
|
228
252
|
testConnection: "测试连接",
|
|
@@ -397,8 +421,7 @@ window.__ModuleLoader__.load({
|
|
|
397
421
|
}
|
|
398
422
|
};
|
|
399
423
|
const GO_PROVIDER = "opencode-go-direct";
|
|
400
|
-
|
|
401
|
-
//#region src/client/api.ts
|
|
424
|
+
// src/client/api.ts
|
|
402
425
|
async function call(path, init) {
|
|
403
426
|
const response = await fetch(path, {
|
|
404
427
|
credentials: "same-origin",
|
|
@@ -434,10 +457,13 @@ window.__ModuleLoader__.load({
|
|
|
434
457
|
body: JSON.stringify({ label })
|
|
435
458
|
});
|
|
436
459
|
}
|
|
437
|
-
function saveConfig(patch) {
|
|
460
|
+
function saveConfig(patch, revision) {
|
|
438
461
|
return call("/dsh-opencode/config", {
|
|
439
462
|
method: "POST",
|
|
440
|
-
body: JSON.stringify({ patch }
|
|
463
|
+
body: JSON.stringify(revision === void 0 ? { patch } : {
|
|
464
|
+
patch,
|
|
465
|
+
revision
|
|
466
|
+
})
|
|
441
467
|
});
|
|
442
468
|
}
|
|
443
469
|
function fetchUpdate() {
|
|
@@ -486,11 +512,9 @@ window.__ModuleLoader__.load({
|
|
|
486
512
|
})
|
|
487
513
|
});
|
|
488
514
|
}
|
|
489
|
-
|
|
490
|
-
//#region src/client/events.ts
|
|
515
|
+
// src/client/events.ts
|
|
491
516
|
const CONFIG_CHANGED_EVENT = "dsh-opencode-config-changed";
|
|
492
|
-
|
|
493
|
-
//#region src/client/heatmap.tsx
|
|
517
|
+
// src/client/heatmap.tsx
|
|
494
518
|
const DAYS = [
|
|
495
519
|
"Mon",
|
|
496
520
|
"Tue",
|
|
@@ -699,8 +723,7 @@ window.__ModuleLoader__.load({
|
|
|
699
723
|
]
|
|
700
724
|
});
|
|
701
725
|
}
|
|
702
|
-
|
|
703
|
-
//#region src/client/usage-analytics.tsx
|
|
726
|
+
// src/client/usage-analytics.tsx
|
|
704
727
|
function compact(value) {
|
|
705
728
|
if (!Number.isFinite(value) || value <= 0) return "0";
|
|
706
729
|
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
@@ -909,8 +932,7 @@ window.__ModuleLoader__.load({
|
|
|
909
932
|
tab === "sessions" ? sessionRows(analytics, t) : null
|
|
910
933
|
] });
|
|
911
934
|
}
|
|
912
|
-
|
|
913
|
-
//#region src/client/usage-bars.tsx
|
|
935
|
+
// src/client/usage-bars.tsx
|
|
914
936
|
function percentClass(percent) {
|
|
915
937
|
if (percent >= 90) return "oc-bar-fill oc-bar-fill-bad";
|
|
916
938
|
if (percent >= 75) return "oc-bar-fill oc-bar-fill-warn";
|
|
@@ -979,8 +1001,7 @@ window.__ModuleLoader__.load({
|
|
|
979
1001
|
]
|
|
980
1002
|
});
|
|
981
1003
|
}
|
|
982
|
-
|
|
983
|
-
//#region src/quota-runway.ts
|
|
1004
|
+
// src/quota-runway.ts
|
|
984
1005
|
const HOUR = 36e5;
|
|
985
1006
|
const DAY = 24 * HOUR;
|
|
986
1007
|
const DURATIONS = {
|
|
@@ -1046,8 +1067,7 @@ window.__ModuleLoader__.load({
|
|
|
1046
1067
|
};
|
|
1047
1068
|
return known === 0 ? { kind: "unknown" } : { kind: "safe" };
|
|
1048
1069
|
}
|
|
1049
|
-
|
|
1050
|
-
//#region src/client/quota-card.tsx
|
|
1070
|
+
// src/client/quota-card.tsx
|
|
1051
1071
|
const PANEL_EVENT = "dsh-opencode-panel-state";
|
|
1052
1072
|
function broadcastPanel(open) {
|
|
1053
1073
|
if (typeof window === "undefined") return;
|
|
@@ -1589,11 +1609,11 @@ window.__ModuleLoader__.load({
|
|
|
1589
1609
|
]
|
|
1590
1610
|
});
|
|
1591
1611
|
}
|
|
1592
|
-
|
|
1593
|
-
//#region src/client/settings-draft.ts
|
|
1612
|
+
// src/client/settings-draft.ts
|
|
1594
1613
|
function draftOf(state) {
|
|
1595
1614
|
const allIds = state.models.map((model) => model.id);
|
|
1596
1615
|
return {
|
|
1616
|
+
revision: state.revision ?? 0,
|
|
1597
1617
|
apiKeyEnv: state.config.apiKeyEnv,
|
|
1598
1618
|
apiBase: state.config.apiBase,
|
|
1599
1619
|
goApiBase: state.config.goApiBase,
|
|
@@ -1679,8 +1699,7 @@ window.__ModuleLoader__.load({
|
|
|
1679
1699
|
function describeLimitsFull(limits) {
|
|
1680
1700
|
return `${planLine("Go", limits.go)} · ${planLine("Go Plus", limits.goPlus)}`;
|
|
1681
1701
|
}
|
|
1682
|
-
|
|
1683
|
-
//#region src/client/settings-widgets.tsx
|
|
1702
|
+
// src/client/settings-widgets.tsx
|
|
1684
1703
|
function Field(props) {
|
|
1685
1704
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1686
1705
|
className: "oc-field",
|
|
@@ -1743,8 +1762,7 @@ window.__ModuleLoader__.load({
|
|
|
1743
1762
|
if (account.coolingUntil <= 0) return label;
|
|
1744
1763
|
return `${label} · ${untilText(account.coolingUntil)}`;
|
|
1745
1764
|
}
|
|
1746
|
-
|
|
1747
|
-
//#region src/client/update-card.tsx
|
|
1765
|
+
// src/client/update-card.tsx
|
|
1748
1766
|
function PluginUpdateCard(props) {
|
|
1749
1767
|
const { t } = props;
|
|
1750
1768
|
const [status, setStatus] = (0, react.useState)(null);
|
|
@@ -1781,6 +1799,9 @@ window.__ModuleLoader__.load({
|
|
|
1781
1799
|
children: [t("pluginUpdate"), status === null ? null : status.updateAvailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1782
1800
|
className: "oc-badge oc-badge-warn",
|
|
1783
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")
|
|
1784
1805
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1785
1806
|
className: "oc-badge oc-badge-ok",
|
|
1786
1807
|
children: t("upToDate")
|
|
@@ -1827,8 +1848,7 @@ window.__ModuleLoader__.load({
|
|
|
1827
1848
|
]
|
|
1828
1849
|
});
|
|
1829
1850
|
}
|
|
1830
|
-
|
|
1831
|
-
//#region src/client/auth-ref.ts
|
|
1851
|
+
// src/client/auth-ref.ts
|
|
1832
1852
|
/**
|
|
1833
1853
|
* Collision-free credential ref for an OpenCode auth entry. Hex encoding is
|
|
1834
1854
|
* injective (unlike replacing punctuation with underscores) and uses only
|
|
@@ -1839,8 +1859,7 @@ window.__ModuleLoader__.load({
|
|
|
1839
1859
|
for (const byte of new TextEncoder().encode(entry)) hex += byte.toString(16).padStart(2, "0");
|
|
1840
1860
|
return `OPENCODE_AUTH_${hex}`;
|
|
1841
1861
|
}
|
|
1842
|
-
|
|
1843
|
-
//#region src/client/settings-page.tsx
|
|
1862
|
+
// src/client/settings-page.tsx
|
|
1844
1863
|
function keyStatusLabel(status, t) {
|
|
1845
1864
|
if (status === "ok") return t("keyStatusOk");
|
|
1846
1865
|
if (status === "expired") return t("keyStatusExpired");
|
|
@@ -1854,6 +1873,13 @@ window.__ModuleLoader__.load({
|
|
|
1854
1873
|
if (status === "expired" || status === "invalid") return "oc-badge-bad";
|
|
1855
1874
|
return "oc-badge-warn";
|
|
1856
1875
|
}
|
|
1876
|
+
function ageText(at) {
|
|
1877
|
+
if (at === void 0) return "";
|
|
1878
|
+
const minutes = Math.max(0, Math.round((Date.now() - at) / 6e4));
|
|
1879
|
+
if (minutes < 1) return "now";
|
|
1880
|
+
if (minutes < 60) return `${minutes}m`;
|
|
1881
|
+
return `${Math.round(minutes / 60)}h`;
|
|
1882
|
+
}
|
|
1857
1883
|
function OpenCodeSettingsPage(props) {
|
|
1858
1884
|
const { t } = props;
|
|
1859
1885
|
const [state, setState] = (0, react.useState)(null);
|
|
@@ -1899,7 +1925,7 @@ window.__ModuleLoader__.load({
|
|
|
1899
1925
|
setSaving(true);
|
|
1900
1926
|
setError(null);
|
|
1901
1927
|
try {
|
|
1902
|
-
await saveConfig(patchOf(draft));
|
|
1928
|
+
await saveConfig(patchOf(draft), draft.revision);
|
|
1903
1929
|
window.dispatchEvent(new CustomEvent(CONFIG_CHANGED_EVENT));
|
|
1904
1930
|
setNotice(t("saved"));
|
|
1905
1931
|
setState(null);
|
|
@@ -2236,7 +2262,9 @@ window.__ModuleLoader__.load({
|
|
|
2236
2262
|
type: "button",
|
|
2237
2263
|
className: "oc-button oc-button-danger",
|
|
2238
2264
|
onClick: () => {
|
|
2239
|
-
|
|
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(() => {
|
|
2240
2268
|
setNotice(t("saved"));
|
|
2241
2269
|
return load();
|
|
2242
2270
|
}).catch((caught) => setError(caught instanceof Error ? caught.message : t("failed")));
|
|
@@ -2300,6 +2328,62 @@ window.__ModuleLoader__.load({
|
|
|
2300
2328
|
})
|
|
2301
2329
|
]
|
|
2302
2330
|
}),
|
|
2331
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2332
|
+
className: "oc-card",
|
|
2333
|
+
children: [
|
|
2334
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
2335
|
+
className: "oc-card-title",
|
|
2336
|
+
children: [t("serviceStatus"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
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")
|
|
2339
|
+
})]
|
|
2340
|
+
}),
|
|
2341
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2342
|
+
className: "oc-row",
|
|
2343
|
+
children: [
|
|
2344
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2345
|
+
className: "oc-hint",
|
|
2346
|
+
children: t("usageEndpoint")
|
|
2347
|
+
}),
|
|
2348
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2349
|
+
className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2350
|
+
children: state.service.usageCheckedAt === void 0 ? t("notChecked") : t("checkedAt").replace("{time}", new Date(state.service.usageCheckedAt).toLocaleTimeString())
|
|
2351
|
+
}),
|
|
2352
|
+
state.service.usageLatencyMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2353
|
+
className: "oc-badge",
|
|
2354
|
+
children: t("latencyValue").replace("{ms}", String(state.service.usageLatencyMs))
|
|
2355
|
+
})
|
|
2356
|
+
]
|
|
2357
|
+
}),
|
|
2358
|
+
state.service.usageError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2359
|
+
className: "oc-hint",
|
|
2360
|
+
children: state.service.usageError
|
|
2361
|
+
}),
|
|
2362
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2363
|
+
className: "oc-row",
|
|
2364
|
+
children: [
|
|
2365
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2366
|
+
className: "oc-hint",
|
|
2367
|
+
children: t("catalogStatus")
|
|
2368
|
+
}),
|
|
2369
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2370
|
+
className: `oc-badge ${state.catalog.source === "live" ? "oc-badge-ok" : "oc-badge-warn"}`,
|
|
2371
|
+
children: [
|
|
2372
|
+
state.catalog.source === "live" ? t("catalogLive") : t("catalogSnapshot"),
|
|
2373
|
+
" · ",
|
|
2374
|
+
state.catalog.count,
|
|
2375
|
+
" ",
|
|
2376
|
+
t("modelCount")
|
|
2377
|
+
]
|
|
2378
|
+
}),
|
|
2379
|
+
state.catalog.fetchedAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2380
|
+
className: "oc-badge",
|
|
2381
|
+
children: t("catalogAge").replace("{age}", ageText(state.catalog.fetchedAt))
|
|
2382
|
+
})
|
|
2383
|
+
]
|
|
2384
|
+
})
|
|
2385
|
+
]
|
|
2386
|
+
}),
|
|
2303
2387
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2304
2388
|
className: "oc-card",
|
|
2305
2389
|
children: [
|
|
@@ -2479,7 +2563,10 @@ window.__ModuleLoader__.load({
|
|
|
2479
2563
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2480
2564
|
type: "button",
|
|
2481
2565
|
className: "oc-button oc-button-danger",
|
|
2482
|
-
onClick: () =>
|
|
2566
|
+
onClick: () => {
|
|
2567
|
+
setKeyDrafts({});
|
|
2568
|
+
update({ accounts: draft.accounts.filter((_, i) => i !== index) });
|
|
2569
|
+
},
|
|
2483
2570
|
children: t("removeAccount")
|
|
2484
2571
|
})
|
|
2485
2572
|
]
|
|
@@ -2921,8 +3008,7 @@ window.__ModuleLoader__.load({
|
|
|
2921
3008
|
]
|
|
2922
3009
|
});
|
|
2923
3010
|
}
|
|
2924
|
-
|
|
2925
|
-
//#region src/client/styles.ts
|
|
3011
|
+
// src/client/styles.ts
|
|
2926
3012
|
const CSS_ID = "dsh-opencode-provider";
|
|
2927
3013
|
const CSS = `
|
|
2928
3014
|
:where(.oc-page, .oc-panel, .oc-card, .oc-foot, .oc-railButton) {
|
|
@@ -3042,8 +3128,7 @@ window.__ModuleLoader__.load({
|
|
|
3042
3128
|
.oc-table th, .oc-table td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--oc-border-soft); vertical-align: top; }
|
|
3043
3129
|
.oc-table th { font-weight: 600; opacity: 0.8; }
|
|
3044
3130
|
`;
|
|
3045
|
-
|
|
3046
|
-
//#region src/client/index.tsx
|
|
3131
|
+
// src/client/index.tsx
|
|
3047
3132
|
function injectCss() {
|
|
3048
3133
|
if (typeof document === "undefined") return () => {};
|
|
3049
3134
|
if (document.querySelector(`style[data-dsh-plugin="dsh-opencode-provider"]`) !== null) return () => {};
|
|
@@ -3089,7 +3174,6 @@ window.__ModuleLoader__.load({
|
|
|
3089
3174
|
}, QuotaCard));
|
|
3090
3175
|
});
|
|
3091
3176
|
}
|
|
3092
|
-
//#endregion
|
|
3093
3177
|
exports.apply = apply;
|
|
3094
3178
|
exports.inject = inject;
|
|
3095
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;
|
|
@@ -190,6 +183,8 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
190
183
|
private stickyWrites;
|
|
191
184
|
/** key fingerprint → cooldown deadline and the rejection that caused it. */
|
|
192
185
|
private readonly cooldowns;
|
|
186
|
+
/** key + model → short soft rejection; other models stay usable. */
|
|
187
|
+
private readonly modelCooldowns;
|
|
193
188
|
private readonly sticky;
|
|
194
189
|
private readonly routing;
|
|
195
190
|
private warnedTools;
|
|
@@ -215,6 +210,11 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
215
210
|
private send;
|
|
216
211
|
private keyId;
|
|
217
212
|
private coolingDown;
|
|
213
|
+
/** Current model-scoped soft cooldown for one key, if any. */
|
|
214
|
+
modelHealth(key: string, model: string): {
|
|
215
|
+
coolingUntil: number;
|
|
216
|
+
reason: string;
|
|
217
|
+
} | undefined;
|
|
218
218
|
/** Current cooldown for one resolved key, if any. */
|
|
219
219
|
accountHealth(key: string): {
|
|
220
220
|
coolingUntil: number;
|
|
@@ -225,11 +225,12 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
225
225
|
private loadCooldowns;
|
|
226
226
|
private pruneCooldowns;
|
|
227
227
|
private persistCooldowns;
|
|
228
|
+
private modelCoolingDown;
|
|
229
|
+
private markRejection;
|
|
230
|
+
private markModelCooldown;
|
|
228
231
|
private markCooldown;
|
|
229
232
|
private isTransient;
|
|
230
233
|
}
|
|
231
|
-
//#endregion
|
|
232
|
-
//#region src/usage.d.ts
|
|
233
234
|
interface UsageWindow {
|
|
234
235
|
readonly percentUsed: number;
|
|
235
236
|
readonly resetsAt?: string;
|
|
@@ -247,6 +248,7 @@ interface UsageResult {
|
|
|
247
248
|
readonly report?: UsageReport;
|
|
248
249
|
readonly error?: string;
|
|
249
250
|
readonly keyStatus?: UsageKeyStatus;
|
|
251
|
+
readonly latencyMs?: number;
|
|
250
252
|
readonly checkedAt: number;
|
|
251
253
|
}
|
|
252
254
|
interface UsageWindowPayload {
|
|
@@ -269,8 +271,6 @@ interface UsageReader {
|
|
|
269
271
|
}
|
|
270
272
|
/** Cached reader for the (undocumented) Go usage endpoint. Fail-open by design. */
|
|
271
273
|
declare function createUsageReader(config: () => ResolvedConfig, resolveKey: KeyResolver, fetchImpl?: typeof fetch): UsageReader;
|
|
272
|
-
//#endregion
|
|
273
|
-
//#region src/commands.d.ts
|
|
274
274
|
interface CommandDeps {
|
|
275
275
|
readonly config: () => ResolvedConfig;
|
|
276
276
|
readonly pool: KeyPool;
|
|
@@ -278,8 +278,6 @@ interface CommandDeps {
|
|
|
278
278
|
readonly adapter: OpenCodeAdapter;
|
|
279
279
|
}
|
|
280
280
|
declare function applyCommands(ctx: Context, deps: CommandDeps): void;
|
|
281
|
-
//#endregion
|
|
282
|
-
//#region src/analytics.d.ts
|
|
283
281
|
interface ModelUsageRow {
|
|
284
282
|
provider: string;
|
|
285
283
|
model: string;
|
|
@@ -355,8 +353,6 @@ interface AnalyticsReport {
|
|
|
355
353
|
days: DayUsageRow[];
|
|
356
354
|
sessions: SessionUsageRow[];
|
|
357
355
|
}
|
|
358
|
-
//#endregion
|
|
359
|
-
//#region src/routes.d.ts
|
|
360
356
|
interface RouteDeps {
|
|
361
357
|
readonly config: () => ResolvedConfig;
|
|
362
358
|
readonly adapter: OpenCodeAdapter;
|
|
@@ -368,8 +364,6 @@ interface RouteDeps {
|
|
|
368
364
|
}
|
|
369
365
|
/** Register the Host HTTP surface consumed by the browser settings page. */
|
|
370
366
|
declare function applyRoutes(ctx: Context, web: WebServer, deps: RouteDeps): void;
|
|
371
|
-
//#endregion
|
|
372
|
-
//#region src/session-id.d.ts
|
|
373
367
|
/**
|
|
374
368
|
* A stable, opaque session id for `x-opencode-session`. The header must stay
|
|
375
369
|
* identical across every turn and retry of one conversation; deriving it from
|
|
@@ -378,16 +372,12 @@ declare function applyRoutes(ctx: Context, web: WebServer, deps: RouteDeps): voi
|
|
|
378
372
|
* routing key, so the `dsh_` prefix is safe.
|
|
379
373
|
*/
|
|
380
374
|
declare function openCodeSessionId(options: GenerateOptions): string;
|
|
381
|
-
//#endregion
|
|
382
|
-
//#region src/errors.d.ts
|
|
383
375
|
/**
|
|
384
376
|
* Translate an OpenCode HTTP failure into the harness failure taxonomy.
|
|
385
377
|
* OpenCode reports several distinct conditions as 400/401, so the body type
|
|
386
378
|
* and message decide the code — never the status alone.
|
|
387
379
|
*/
|
|
388
380
|
declare function classifyResponseError(status: number, text: string, headers: Headers): LlmError;
|
|
389
|
-
//#endregion
|
|
390
|
-
//#region src/models.generated.d.ts
|
|
391
381
|
type OpenCodeTransport = 'openai-chat' | 'openai-responses' | 'anthropic' | 'google';
|
|
392
382
|
interface OpenCodeModelMeta {
|
|
393
383
|
readonly id: string;
|
|
@@ -404,8 +394,6 @@ interface OpenCodeModelMeta {
|
|
|
404
394
|
readonly cacheRead: number;
|
|
405
395
|
};
|
|
406
396
|
}
|
|
407
|
-
//#endregion
|
|
408
|
-
//#region src/catalog.d.ts
|
|
409
397
|
interface EffectiveModel extends OpenCodeModelMeta {
|
|
410
398
|
readonly transport: OpenCodeTransport;
|
|
411
399
|
readonly contextWindow: number;
|
|
@@ -413,8 +401,6 @@ interface EffectiveModel extends OpenCodeModelMeta {
|
|
|
413
401
|
declare function effectiveModel(id: string, config: ResolvedConfig): EffectiveModel;
|
|
414
402
|
declare function describeModel(meta: EffectiveModel, provider?: string): string;
|
|
415
403
|
declare function clearCatalogCache(): void;
|
|
416
|
-
//#endregion
|
|
417
|
-
//#region src/index.d.ts
|
|
418
404
|
declare const name = "llm-opencode";
|
|
419
405
|
declare const inject: string[];
|
|
420
406
|
/**
|
|
@@ -427,6 +413,5 @@ declare const inject: string[];
|
|
|
427
413
|
* next request without a restart.
|
|
428
414
|
*/
|
|
429
415
|
declare function apply(ctx: Context, config: Config): void;
|
|
430
|
-
//#endregion
|
|
431
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 };
|
|
432
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();
|
|
@@ -1090,11 +1085,21 @@ function describeModel(meta, provider) {
|
|
|
1090
1085
|
}
|
|
1091
1086
|
return parts.join(" · ");
|
|
1092
1087
|
}
|
|
1088
|
+
function catalogStatus(provider, config, count) {
|
|
1089
|
+
const cached = catalogCache.get(`${provider}|${modelsUrlFor(provider, config)}`);
|
|
1090
|
+
return cached === void 0 ? {
|
|
1091
|
+
source: "snapshot",
|
|
1092
|
+
count
|
|
1093
|
+
} : {
|
|
1094
|
+
source: "live",
|
|
1095
|
+
count,
|
|
1096
|
+
fetchedAt: cached.at
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1093
1099
|
function clearCatalogCache() {
|
|
1094
1100
|
catalogCache.clear();
|
|
1095
1101
|
}
|
|
1096
|
-
|
|
1097
|
-
//#region src/account-rules.ts
|
|
1102
|
+
// src/account-rules.ts
|
|
1098
1103
|
function glob(pattern) {
|
|
1099
1104
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1100
1105
|
return new RegExp(`^${escaped}$`, "i");
|
|
@@ -1117,8 +1122,7 @@ function orderKeysForModel(keys, model, rules) {
|
|
|
1117
1122
|
if (preferred === void 0) return keys;
|
|
1118
1123
|
return [preferred, ...keys.filter((entry) => entry !== preferred)];
|
|
1119
1124
|
}
|
|
1120
|
-
|
|
1121
|
-
//#region src/errors.ts
|
|
1125
|
+
// src/errors.ts
|
|
1122
1126
|
function parseErrorBody(text) {
|
|
1123
1127
|
try {
|
|
1124
1128
|
const body = JSON.parse(text);
|
|
@@ -1179,8 +1183,7 @@ function classifyStreamError(value, status = 200) {
|
|
|
1179
1183
|
if (value.error === void 0) return void 0;
|
|
1180
1184
|
return classifyResponseError(status, JSON.stringify(value), new Headers());
|
|
1181
1185
|
}
|
|
1182
|
-
|
|
1183
|
-
//#region src/images.ts
|
|
1186
|
+
// src/images.ts
|
|
1184
1187
|
const MAX_LONG_EDGE = 1568;
|
|
1185
1188
|
const MAX_BYTES = 1048576;
|
|
1186
1189
|
/** Total base64 image budget for one request; leaves room under the gateway cap. */
|
|
@@ -1191,7 +1194,7 @@ const MAX_REQUEST_IMAGES = 50;
|
|
|
1191
1194
|
* dropped first; the transports render them as an "image omitted" placeholder
|
|
1192
1195
|
* instead of failing the whole request.
|
|
1193
1196
|
*/
|
|
1194
|
-
function selectRequestImages(refs) {
|
|
1197
|
+
function selectRequestImages(refs, occurrences) {
|
|
1195
1198
|
const unique = [];
|
|
1196
1199
|
const seen = /* @__PURE__ */ new Set();
|
|
1197
1200
|
for (const ref of refs) {
|
|
@@ -1199,11 +1202,18 @@ function selectRequestImages(refs) {
|
|
|
1199
1202
|
seen.add(ref.attachmentId);
|
|
1200
1203
|
unique.push(ref);
|
|
1201
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);
|
|
1202
1207
|
let start = 0;
|
|
1203
|
-
let
|
|
1204
|
-
|
|
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)) {
|
|
1205
1211
|
const dropped = unique[start];
|
|
1206
|
-
if (dropped !== void 0)
|
|
1212
|
+
if (dropped !== void 0) {
|
|
1213
|
+
const count = countOf(dropped);
|
|
1214
|
+
totalImages -= count;
|
|
1215
|
+
bytes -= encoded(dropped.bytes) * count;
|
|
1216
|
+
}
|
|
1207
1217
|
start += 1;
|
|
1208
1218
|
}
|
|
1209
1219
|
const kept = unique.slice(start);
|
|
@@ -1226,21 +1236,23 @@ function targetOf(ref) {
|
|
|
1226
1236
|
};
|
|
1227
1237
|
}
|
|
1228
1238
|
function hasImages(options) {
|
|
1229
|
-
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));
|
|
1230
1240
|
}
|
|
1231
1241
|
/** Decode the distinct request images that fit the request-wide budget. */
|
|
1232
1242
|
async function resolveImages(options, store, signal) {
|
|
1233
1243
|
const refs = [];
|
|
1244
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1234
1245
|
const seen = /* @__PURE__ */ new Set();
|
|
1235
1246
|
for (const message of options.messages) for (const block of message.content) {
|
|
1236
1247
|
if (block.type !== "image" || block.offloaded === true) continue;
|
|
1237
1248
|
const ref = block.attachment;
|
|
1249
|
+
counts.set(ref.attachmentId, (counts.get(ref.attachmentId) ?? 0) + 1);
|
|
1238
1250
|
if (seen.has(ref.attachmentId)) continue;
|
|
1239
1251
|
seen.add(ref.attachmentId);
|
|
1240
1252
|
refs.push(ref);
|
|
1241
1253
|
}
|
|
1242
1254
|
const images = /* @__PURE__ */ new Map();
|
|
1243
|
-
for (const ref of selectRequestImages(refs).kept) {
|
|
1255
|
+
for (const ref of selectRequestImages(refs, counts).kept) {
|
|
1244
1256
|
const request = await store.readImageRequest(ref, targetOf(ref), signal);
|
|
1245
1257
|
const base64 = Buffer.from(request.data).toString("base64");
|
|
1246
1258
|
images.set(ref.attachmentId, {
|
|
@@ -1256,8 +1268,7 @@ async function resolveImages(options, store, signal) {
|
|
|
1256
1268
|
function offloadPlaceholder(ref) {
|
|
1257
1269
|
return `[image omitted: ${ref.name ?? ref.attachmentId}]`;
|
|
1258
1270
|
}
|
|
1259
|
-
|
|
1260
|
-
//#region src/files.ts
|
|
1271
|
+
// src/files.ts
|
|
1261
1272
|
/**
|
|
1262
1273
|
* Model-facing handle for a file when the adapter receives a raw FileBlock.
|
|
1263
1274
|
* The harness normally projects files to text with an execution-world path
|
|
@@ -1267,8 +1278,7 @@ function filePlaceholder(ref) {
|
|
|
1267
1278
|
const digest = String(ref.attachmentId).slice(7, 15);
|
|
1268
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.]`;
|
|
1269
1280
|
}
|
|
1270
|
-
|
|
1271
|
-
//#region src/tool-schema.ts
|
|
1281
|
+
// src/tool-schema.ts
|
|
1272
1282
|
/**
|
|
1273
1283
|
* Normalize a tool's parameters schema to the object root the gateway
|
|
1274
1284
|
* validates. Ported behavior from the Command Code provider: pass an object
|
|
@@ -1374,8 +1384,7 @@ function normalizeToolParameters(parameters, depth = 0) {
|
|
|
1374
1384
|
additionalProperties: true
|
|
1375
1385
|
};
|
|
1376
1386
|
}
|
|
1377
|
-
|
|
1378
|
-
//#region src/messages.ts
|
|
1387
|
+
// src/messages.ts
|
|
1379
1388
|
function textOnly$3(content) {
|
|
1380
1389
|
const parts = [];
|
|
1381
1390
|
for (const block of content) if (block.type === "text") parts.push(block.text);
|
|
@@ -1523,8 +1532,7 @@ function buildChatBody(options, model, config, images) {
|
|
|
1523
1532
|
...options.stop === void 0 || options.stop.length === 0 ? {} : { stop: options.stop }
|
|
1524
1533
|
};
|
|
1525
1534
|
}
|
|
1526
|
-
|
|
1527
|
-
//#region src/request-log.ts
|
|
1535
|
+
// src/request-log.ts
|
|
1528
1536
|
const DAY_MS$1 = 864e5;
|
|
1529
1537
|
const MAX_LOG_BYTES = 2097152;
|
|
1530
1538
|
const KEEP_LINES = 5e3;
|
|
@@ -1707,8 +1715,7 @@ async function readRequestHeatmap(days, now = Date.now()) {
|
|
|
1707
1715
|
}).sort((a, b) => b.total - a.total || a.account.localeCompare(b.account))
|
|
1708
1716
|
};
|
|
1709
1717
|
}
|
|
1710
|
-
|
|
1711
|
-
//#region src/session-id.ts
|
|
1718
|
+
// src/session-id.ts
|
|
1712
1719
|
function sha256(value) {
|
|
1713
1720
|
return createHash("sha256").update(value).digest("hex");
|
|
1714
1721
|
}
|
|
@@ -1762,8 +1769,7 @@ function limitTools(tools, max = 128) {
|
|
|
1762
1769
|
const rest = out.filter((tool) => !CORE_TOOLS.has(tool.name));
|
|
1763
1770
|
return [...core, ...rest].slice(0, max);
|
|
1764
1771
|
}
|
|
1765
|
-
|
|
1766
|
-
//#region src/transports/sse.ts
|
|
1772
|
+
// src/transports/sse.ts
|
|
1767
1773
|
async function readWithIdleTimeout(reader, idleTimeoutMs, signal) {
|
|
1768
1774
|
if (signal?.aborted === true) throw signal.reason;
|
|
1769
1775
|
return await new Promise((resolve, reject) => {
|
|
@@ -1787,6 +1793,12 @@ async function readWithIdleTimeout(reader, idleTimeoutMs, signal) {
|
|
|
1787
1793
|
reader.read().then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
1788
1794
|
});
|
|
1789
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
|
+
}
|
|
1790
1802
|
/** Yield the `data:` payload of every SSE event, honoring cancellation and idle timeouts. */
|
|
1791
1803
|
async function* sseData(body, idleTimeoutMs, signal) {
|
|
1792
1804
|
const reader = body.getReader();
|
|
@@ -1796,7 +1808,7 @@ async function* sseData(body, idleTimeoutMs, signal) {
|
|
|
1796
1808
|
for (;;) {
|
|
1797
1809
|
const result = await readWithIdleTimeout(reader, idleTimeoutMs, signal);
|
|
1798
1810
|
if (result.done) break;
|
|
1799
|
-
buffer
|
|
1811
|
+
buffer = appendDecoded(buffer, decoder.decode(result.value, { stream: true }));
|
|
1800
1812
|
for (;;) {
|
|
1801
1813
|
const boundary = buffer.indexOf("\n\n");
|
|
1802
1814
|
if (boundary < 0) break;
|
|
@@ -1824,7 +1836,7 @@ async function* sseEvents(body, idleTimeoutMs, signal) {
|
|
|
1824
1836
|
for (;;) {
|
|
1825
1837
|
const result = await readWithIdleTimeout(reader, idleTimeoutMs, signal);
|
|
1826
1838
|
if (result.done) break;
|
|
1827
|
-
buffer
|
|
1839
|
+
buffer = appendDecoded(buffer, decoder.decode(result.value, { stream: true }));
|
|
1828
1840
|
for (;;) {
|
|
1829
1841
|
const boundary = buffer.indexOf("\n\n");
|
|
1830
1842
|
if (boundary < 0) break;
|
|
@@ -1854,8 +1866,7 @@ async function* sseEvents(body, idleTimeoutMs, signal) {
|
|
|
1854
1866
|
reader.releaseLock();
|
|
1855
1867
|
}
|
|
1856
1868
|
}
|
|
1857
|
-
|
|
1858
|
-
//#region src/transports/anthropic.ts
|
|
1869
|
+
// src/transports/anthropic.ts
|
|
1859
1870
|
const EFFORT_LEVELS$1 = /* @__PURE__ */ new Set([
|
|
1860
1871
|
"low",
|
|
1861
1872
|
"medium",
|
|
@@ -2040,6 +2051,7 @@ async function* parseAnthropicStream(response, idleTimeoutMs, signal) {
|
|
|
2040
2051
|
let nextIndex = 0;
|
|
2041
2052
|
let sawContent = false;
|
|
2042
2053
|
let sawToolUse = false;
|
|
2054
|
+
let sawMessageStop = false;
|
|
2043
2055
|
let stopReason;
|
|
2044
2056
|
let inputUsage;
|
|
2045
2057
|
let outputTokens;
|
|
@@ -2208,8 +2220,12 @@ async function* parseAnthropicStream(response, idleTimeoutMs, signal) {
|
|
|
2208
2220
|
if (typeof event.usage?.output_tokens === "number") outputTokens = event.usage.output_tokens;
|
|
2209
2221
|
continue;
|
|
2210
2222
|
}
|
|
2211
|
-
if (event.type === "message_stop")
|
|
2223
|
+
if (event.type === "message_stop") {
|
|
2224
|
+
sawMessageStop = true;
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2212
2227
|
}
|
|
2228
|
+
if (sawContent && !sawMessageStop) throw new LlmError("Anthropic stream ended without message_stop", "TRANSPORT");
|
|
2213
2229
|
const toolBlocks = [...blocks.values()].filter((block) => block.kind === "tool-call");
|
|
2214
2230
|
if (toolBlocks.length > 0) {
|
|
2215
2231
|
for (const block of toolBlocks) yield {
|
|
@@ -2239,8 +2255,7 @@ async function* parseAnthropicStream(response, idleTimeoutMs, signal) {
|
|
|
2239
2255
|
} } : {}
|
|
2240
2256
|
};
|
|
2241
2257
|
}
|
|
2242
|
-
|
|
2243
|
-
//#region src/transports/google.ts
|
|
2258
|
+
// src/transports/google.ts
|
|
2244
2259
|
function textOnly$1(content) {
|
|
2245
2260
|
const parts = [];
|
|
2246
2261
|
for (const block of content) if (block.type === "text") parts.push(block.text);
|
|
@@ -2483,6 +2498,7 @@ async function* parseGoogleStream(response, idleTimeoutMs, signal) {
|
|
|
2483
2498
|
}
|
|
2484
2499
|
}
|
|
2485
2500
|
}
|
|
2501
|
+
if (sawContent && finishReason === void 0) throw new LlmError("Google stream ended without a finish reason", "TRANSPORT");
|
|
2486
2502
|
if (textIndex !== void 0) yield {
|
|
2487
2503
|
type: "block-end",
|
|
2488
2504
|
index: textIndex,
|
|
@@ -2514,8 +2530,7 @@ async function* parseGoogleStream(response, idleTimeoutMs, signal) {
|
|
|
2514
2530
|
} } : {}
|
|
2515
2531
|
};
|
|
2516
2532
|
}
|
|
2517
|
-
|
|
2518
|
-
//#region src/transports/openai-chat.ts
|
|
2533
|
+
// src/transports/openai-chat.ts
|
|
2519
2534
|
function mapUsage$1(usage) {
|
|
2520
2535
|
const prompt = usage.prompt_tokens ?? 0;
|
|
2521
2536
|
const cached = usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
@@ -2554,8 +2569,12 @@ async function* parseChatStream(response, idleTimeoutMs, signal) {
|
|
|
2554
2569
|
let finishReason;
|
|
2555
2570
|
let usage;
|
|
2556
2571
|
let sawContent = false;
|
|
2572
|
+
let sawTerminal = false;
|
|
2557
2573
|
for await (const data of sseData(response.body, idleTimeoutMs, signal)) {
|
|
2558
|
-
if (data === "[DONE]")
|
|
2574
|
+
if (data === "[DONE]") {
|
|
2575
|
+
sawTerminal = true;
|
|
2576
|
+
break;
|
|
2577
|
+
}
|
|
2559
2578
|
let chunk;
|
|
2560
2579
|
try {
|
|
2561
2580
|
chunk = JSON.parse(data);
|
|
@@ -2638,9 +2657,13 @@ async function* parseChatStream(response, idleTimeoutMs, signal) {
|
|
|
2638
2657
|
argumentsDelta
|
|
2639
2658
|
};
|
|
2640
2659
|
}
|
|
2641
|
-
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
|
+
}
|
|
2642
2664
|
}
|
|
2643
2665
|
}
|
|
2666
|
+
if (sawContent && !sawTerminal) throw new LlmError("OpenCode stream ended without a terminal finish event", "TRANSPORT");
|
|
2644
2667
|
if (textIndex !== void 0) yield {
|
|
2645
2668
|
type: "block-end",
|
|
2646
2669
|
index: textIndex,
|
|
@@ -2771,8 +2794,7 @@ async function* parseCompletion(chunk) {
|
|
|
2771
2794
|
reason: finishReasonOf$1(choice?.finish_reason, (message?.tool_calls?.length ?? 0) > 0)
|
|
2772
2795
|
};
|
|
2773
2796
|
}
|
|
2774
|
-
|
|
2775
|
-
//#region src/transports/responses.ts
|
|
2797
|
+
// src/transports/responses.ts
|
|
2776
2798
|
const EFFORT_LEVELS = /* @__PURE__ */ new Set([
|
|
2777
2799
|
"minimal",
|
|
2778
2800
|
"low",
|
|
@@ -2927,6 +2949,7 @@ async function* parseResponsesStream(response, idleTimeoutMs, signal) {
|
|
|
2927
2949
|
const tools = /* @__PURE__ */ new Map();
|
|
2928
2950
|
let sawContent = false;
|
|
2929
2951
|
let hasTools = false;
|
|
2952
|
+
let completed = false;
|
|
2930
2953
|
let usage;
|
|
2931
2954
|
let incomplete;
|
|
2932
2955
|
for await (const { event: eventName, data } of sseEvents(response.body, idleTimeoutMs, signal)) {
|
|
@@ -3040,11 +3063,13 @@ async function* parseResponsesStream(response, idleTimeoutMs, signal) {
|
|
|
3040
3063
|
continue;
|
|
3041
3064
|
}
|
|
3042
3065
|
if (type === "response.completed") {
|
|
3066
|
+
completed = true;
|
|
3043
3067
|
usage = mapUsage(event.response?.usage);
|
|
3044
3068
|
incomplete = event.response?.incomplete_details?.reason;
|
|
3045
3069
|
continue;
|
|
3046
3070
|
}
|
|
3047
3071
|
}
|
|
3072
|
+
if (sawContent && !completed) throw new LlmError("OpenAI Responses stream ended without response.completed", "TRANSPORT");
|
|
3048
3073
|
if (textIndex !== void 0) yield {
|
|
3049
3074
|
type: "block-end",
|
|
3050
3075
|
index: textIndex,
|
|
@@ -3081,8 +3106,7 @@ async function* parseResponsesStream(response, idleTimeoutMs, signal) {
|
|
|
3081
3106
|
reason: finishReasonOf(hasTools, incomplete)
|
|
3082
3107
|
};
|
|
3083
3108
|
}
|
|
3084
|
-
|
|
3085
|
-
//#region src/adapter.ts
|
|
3109
|
+
// src/adapter.ts
|
|
3086
3110
|
function defaultCooldownPath() {
|
|
3087
3111
|
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
3088
3112
|
return join(home, "storages", "dsh-opencode", "cooldowns.json");
|
|
@@ -3100,6 +3124,8 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3100
3124
|
stickyWrites = Promise.resolve();
|
|
3101
3125
|
/** key fingerprint → cooldown deadline and the rejection that caused it. */
|
|
3102
3126
|
cooldowns = /* @__PURE__ */ new Map();
|
|
3127
|
+
/** key + model → short soft rejection; other models stay usable. */
|
|
3128
|
+
modelCooldowns = /* @__PURE__ */ new Map();
|
|
3103
3129
|
sticky = /* @__PURE__ */ new Map();
|
|
3104
3130
|
routing = [];
|
|
3105
3131
|
warnedTools = false;
|
|
@@ -3202,7 +3228,10 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3202
3228
|
}
|
|
3203
3229
|
}
|
|
3204
3230
|
}
|
|
3205
|
-
const
|
|
3231
|
+
const eligible = fallback ? keys : keys.filter((entry) => !this.coolingDown(entry.key) && !this.modelCoolingDown(entry.key, options.model));
|
|
3232
|
+
const ordered = eligible.length > 0 ? eligible : keys;
|
|
3233
|
+
const forced = !fallback && eligible.length === 0;
|
|
3234
|
+
const initial = ordered[0];
|
|
3206
3235
|
const url = endpointFor(options.provider, model.transport, options.model, config);
|
|
3207
3236
|
let images;
|
|
3208
3237
|
if (hasImages(options)) {
|
|
@@ -3226,8 +3255,8 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3226
3255
|
let lastError;
|
|
3227
3256
|
let lastAccount = initial?.label ?? "default";
|
|
3228
3257
|
let loggedAny = false;
|
|
3229
|
-
for (const [index, entry] of
|
|
3230
|
-
if (!fallback && this.coolingDown(entry.key)) continue;
|
|
3258
|
+
for (const [index, entry] of ordered.entries()) {
|
|
3259
|
+
if (!fallback && !forced && (this.coolingDown(entry.key) || this.modelCoolingDown(entry.key, options.model))) continue;
|
|
3231
3260
|
lastAccount = entry.label ?? "default";
|
|
3232
3261
|
const attemptReason = index === 0 ? reason : "rotation";
|
|
3233
3262
|
const attemptStartedAt = Date.now();
|
|
@@ -3254,9 +3283,9 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3254
3283
|
const error = classifyResponseError(response.status, text, response.headers);
|
|
3255
3284
|
lastError = error;
|
|
3256
3285
|
logAttempt(false, error.code);
|
|
3257
|
-
const canRotate = index <
|
|
3286
|
+
const canRotate = index < ordered.length - 1;
|
|
3258
3287
|
if (error.code === "INVALID_CREDENTIAL" || error.code === "RATE_LIMIT" || error.code === "QUOTA") {
|
|
3259
|
-
if (!fallback) this.
|
|
3288
|
+
if (!fallback) this.markRejection(entry.key, options.model, response.status, response.headers, error.code);
|
|
3260
3289
|
responseFailure = true;
|
|
3261
3290
|
if (canRotate) continue;
|
|
3262
3291
|
}
|
|
@@ -3297,8 +3326,8 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3297
3326
|
}
|
|
3298
3327
|
lastError = classified;
|
|
3299
3328
|
const transient = this.isTransient(classified);
|
|
3300
|
-
if (transient && !responseFailure && !fallback) this.
|
|
3301
|
-
if (!(index <
|
|
3329
|
+
if (transient && !responseFailure && !fallback) this.markRejection(entry.key, options.model, 0, void 0, classified.code);
|
|
3330
|
+
if (!(index < ordered.length - 1 && transient)) {
|
|
3302
3331
|
if (!fallback && config.quotaFallbackModel !== "" && isGoProvider(options.provider) && classified.code === "QUOTA") break;
|
|
3303
3332
|
throw classified;
|
|
3304
3333
|
}
|
|
@@ -3416,6 +3445,8 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3416
3445
|
"user-agent": config.userAgent,
|
|
3417
3446
|
...transport === "anthropic" ? { "anthropic-version": "2023-06-01" } : {}
|
|
3418
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");
|
|
3419
3450
|
const controller = new AbortController();
|
|
3420
3451
|
let timedOut = false;
|
|
3421
3452
|
const onAbort = () => controller.abort(signal?.reason);
|
|
@@ -3433,7 +3464,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3433
3464
|
signal: controller.signal
|
|
3434
3465
|
});
|
|
3435
3466
|
} catch (error) {
|
|
3436
|
-
if (
|
|
3467
|
+
if (callerAborted()) throw signal?.reason ?? error;
|
|
3437
3468
|
if (timedOut) throw new LlmError(`OpenCode did not return response headers within ${config.requestTimeoutMs}ms`, "TIMEOUT", { cause: error });
|
|
3438
3469
|
throw error;
|
|
3439
3470
|
} finally {
|
|
@@ -3453,6 +3484,20 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3453
3484
|
}
|
|
3454
3485
|
return true;
|
|
3455
3486
|
}
|
|
3487
|
+
/** Current model-scoped soft cooldown for one key, if any. */
|
|
3488
|
+
modelHealth(key, model) {
|
|
3489
|
+
const id = `${this.keyId(key)}:${model}`;
|
|
3490
|
+
const entry = this.modelCooldowns.get(id);
|
|
3491
|
+
if (entry === void 0) return void 0;
|
|
3492
|
+
if (entry.until <= Date.now()) {
|
|
3493
|
+
this.modelCooldowns.delete(id);
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
return {
|
|
3497
|
+
coolingUntil: entry.until,
|
|
3498
|
+
reason: entry.reason
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3456
3501
|
/** Current cooldown for one resolved key, if any. */
|
|
3457
3502
|
accountHealth(key) {
|
|
3458
3503
|
const id = this.keyId(key);
|
|
@@ -3505,6 +3550,30 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3505
3550
|
await writeFile(this.cooldownPath, payload, "utf8");
|
|
3506
3551
|
}).catch(() => {});
|
|
3507
3552
|
}
|
|
3553
|
+
modelCoolingDown(key, model) {
|
|
3554
|
+
const id = `${this.keyId(key)}:${model}`;
|
|
3555
|
+
const entry = this.modelCooldowns.get(id);
|
|
3556
|
+
if (entry === void 0) return false;
|
|
3557
|
+
if (entry.until <= Date.now()) {
|
|
3558
|
+
this.modelCooldowns.delete(id);
|
|
3559
|
+
return false;
|
|
3560
|
+
}
|
|
3561
|
+
return true;
|
|
3562
|
+
}
|
|
3563
|
+
markRejection(key, model, status, headers, code) {
|
|
3564
|
+
if (code === "QUOTA" || code === "RATE_LIMIT") {
|
|
3565
|
+
this.markModelCooldown(key, model, code);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
this.markCooldown(key, status, headers, code);
|
|
3569
|
+
}
|
|
3570
|
+
markModelCooldown(key, model, code) {
|
|
3571
|
+
const delayMs = code === "QUOTA" ? 3e5 : 6e4;
|
|
3572
|
+
this.modelCooldowns.set(`${this.keyId(key)}:${model}`, {
|
|
3573
|
+
until: Date.now() + delayMs,
|
|
3574
|
+
reason: code === "QUOTA" ? "quota" : "rate-limit"
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3508
3577
|
markCooldown(key, status, headers, code) {
|
|
3509
3578
|
let delayMs = 3e4;
|
|
3510
3579
|
let reason = "transport";
|
|
@@ -3532,8 +3601,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3532
3601
|
return error.code === "RATE_LIMIT" || error.code === "QUOTA" || error.code === "SERVER" || error.code === "TIMEOUT" || error.code === "TRANSPORT" || error.code === "EMPTY_RESPONSE";
|
|
3533
3602
|
}
|
|
3534
3603
|
};
|
|
3535
|
-
|
|
3536
|
-
//#region src/commands.ts
|
|
3604
|
+
// src/commands.ts
|
|
3537
3605
|
function bar(percent, width = 10) {
|
|
3538
3606
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
3539
3607
|
const filled = Math.round(clamped / 100 * width);
|
|
@@ -3719,8 +3787,7 @@ function applyCommands(ctx, deps) {
|
|
|
3719
3787
|
}
|
|
3720
3788
|
});
|
|
3721
3789
|
}
|
|
3722
|
-
|
|
3723
|
-
//#region src/credentials.ts
|
|
3790
|
+
// src/credentials.ts
|
|
3724
3791
|
/** OpenCode stores its own sign-in at this XDG data path. */
|
|
3725
3792
|
function openCodeAuthPath() {
|
|
3726
3793
|
const base = process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share");
|
|
@@ -3854,8 +3921,7 @@ function missingKey(config) {
|
|
|
3854
3921
|
function createKeyResolver(ctx, config) {
|
|
3855
3922
|
return createKeyPool(ctx, config).resolve;
|
|
3856
3923
|
}
|
|
3857
|
-
|
|
3858
|
-
//#region src/local-host.ts
|
|
3924
|
+
// src/local-host.ts
|
|
3859
3925
|
const LOOPBACK = /* @__PURE__ */ new Set([
|
|
3860
3926
|
"localhost",
|
|
3861
3927
|
"localhost.",
|
|
@@ -3896,8 +3962,7 @@ function sameOrigin(authority, origin) {
|
|
|
3896
3962
|
return false;
|
|
3897
3963
|
}
|
|
3898
3964
|
}
|
|
3899
|
-
|
|
3900
|
-
//#region src/plugin-updater.ts
|
|
3965
|
+
// src/plugin-updater.ts
|
|
3901
3966
|
const UPDATE_HEADER = "x-dsh-plugin-update";
|
|
3902
3967
|
const UPDATE_TIMEOUT_MS = 6e5;
|
|
3903
3968
|
const VERSION_CACHE_MS = 3e5;
|
|
@@ -4193,8 +4258,7 @@ function registerPluginUpdater(ctx, options) {
|
|
|
4193
4258
|
}
|
|
4194
4259
|
});
|
|
4195
4260
|
}
|
|
4196
|
-
|
|
4197
|
-
//#region src/analytics.ts
|
|
4261
|
+
// src/analytics.ts
|
|
4198
4262
|
const DAY_MS = 864e5;
|
|
4199
4263
|
const CACHE_TTL_MS = 6e4;
|
|
4200
4264
|
const MAX_FILES = 5e3;
|
|
@@ -4465,8 +4529,7 @@ async function readModelAnalytics(days = 30) {
|
|
|
4465
4529
|
});
|
|
4466
4530
|
return report;
|
|
4467
4531
|
}
|
|
4468
|
-
|
|
4469
|
-
//#region src/routes-public.ts
|
|
4532
|
+
// src/routes-public.ts
|
|
4470
4533
|
function publicConfig(config) {
|
|
4471
4534
|
return {
|
|
4472
4535
|
apiKeyEnv: config.apiKeyEnv,
|
|
@@ -4499,8 +4562,7 @@ function publicConfig(config) {
|
|
|
4499
4562
|
userAgent: config.userAgent
|
|
4500
4563
|
};
|
|
4501
4564
|
}
|
|
4502
|
-
|
|
4503
|
-
//#region src/routes-values.ts
|
|
4565
|
+
// src/routes-values.ts
|
|
4504
4566
|
const TRANSPORTS = /* @__PURE__ */ new Set([
|
|
4505
4567
|
"openai-chat",
|
|
4506
4568
|
"openai-responses",
|
|
@@ -4560,8 +4622,7 @@ function cleanTransports(value) {
|
|
|
4560
4622
|
for (const [key, entry] of Object.entries(value)) if (typeof entry === "string" && TRANSPORTS.has(entry)) out[key] = entry;
|
|
4561
4623
|
return out;
|
|
4562
4624
|
}
|
|
4563
|
-
|
|
4564
|
-
//#region src/routes.ts
|
|
4625
|
+
// src/routes.ts
|
|
4565
4626
|
const MAX_BODY_BYTES = 262144;
|
|
4566
4627
|
function json(res, status, value) {
|
|
4567
4628
|
res.statusCode = status;
|
|
@@ -4779,7 +4840,13 @@ async function buildState(ctx, deps) {
|
|
|
4779
4840
|
key: keyInfo,
|
|
4780
4841
|
accounts,
|
|
4781
4842
|
models,
|
|
4843
|
+
catalog: catalogStatus(GO_PROVIDER, config, models.length),
|
|
4782
4844
|
usage: usage ?? null,
|
|
4845
|
+
service: {
|
|
4846
|
+
...usage?.checkedAt === void 0 ? {} : { usageCheckedAt: usage.checkedAt },
|
|
4847
|
+
...usage?.latencyMs === void 0 ? {} : { usageLatencyMs: usage.latencyMs },
|
|
4848
|
+
...usage?.error === void 0 ? {} : { usageError: usage.error }
|
|
4849
|
+
},
|
|
4783
4850
|
routing: deps.adapter.routingTrace(),
|
|
4784
4851
|
auth: {
|
|
4785
4852
|
entry: config.openCodeAuthEntry,
|
|
@@ -4889,12 +4956,14 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4889
4956
|
json(res, 400, { error: "no known fields in patch" });
|
|
4890
4957
|
return;
|
|
4891
4958
|
}
|
|
4892
|
-
|
|
4959
|
+
const revision = typeof body.revision === "number" && Number.isInteger(body.revision) ? body.revision : settingsRevision(ctx);
|
|
4960
|
+
await settings.mutate(NS, ops, revision);
|
|
4893
4961
|
deps.usage.clear();
|
|
4894
4962
|
deps.refreshTui?.();
|
|
4895
4963
|
json(res, 200, { ok: true });
|
|
4896
4964
|
} catch (error) {
|
|
4897
|
-
|
|
4965
|
+
const message = error instanceof Error ? error.message : "config write failed";
|
|
4966
|
+
json(res, /revision|conflict|stale/i.test(message) ? 409 : 500, { error: message });
|
|
4898
4967
|
}
|
|
4899
4968
|
});
|
|
4900
4969
|
route("/dsh-opencode/key", async (req, res) => {
|
|
@@ -5062,8 +5131,7 @@ function applyRoutes(ctx, web, deps) {
|
|
|
5062
5131
|
for (const dispose of disposers.splice(0)) dispose();
|
|
5063
5132
|
}, "dsh-opencode-provider: HTTP routes");
|
|
5064
5133
|
}
|
|
5065
|
-
|
|
5066
|
-
//#region src/tui-settings.ts
|
|
5134
|
+
// src/tui-settings.ts
|
|
5067
5135
|
const TUI_ACCOUNT_AUTO = "auto";
|
|
5068
5136
|
function setOrClear(text) {
|
|
5069
5137
|
const trimmed = text.trim();
|
|
@@ -5307,8 +5375,7 @@ function applyOpenCodeTuiSettings(ctx, deps) {
|
|
|
5307
5375
|
}, "dsh-opencode-provider: tui settings section");
|
|
5308
5376
|
return refresh;
|
|
5309
5377
|
}
|
|
5310
|
-
|
|
5311
|
-
//#region src/usage.ts
|
|
5378
|
+
// src/usage.ts
|
|
5312
5379
|
const SUCCESS_TTL_MS = 3e5;
|
|
5313
5380
|
const FAILURE_TTL_MS = 45e3;
|
|
5314
5381
|
const USAGE_TIMEOUT_MS = 2e4;
|
|
@@ -5360,7 +5427,9 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
|
5360
5427
|
if (Date.now() - cached.checkedAt < ttl) return cached;
|
|
5361
5428
|
}
|
|
5362
5429
|
const result = async () => {
|
|
5430
|
+
const startedAt = Date.now();
|
|
5363
5431
|
const signal = AbortSignal.timeout(USAGE_TIMEOUT_MS);
|
|
5432
|
+
const latency = () => Math.max(0, Date.now() - startedAt);
|
|
5364
5433
|
try {
|
|
5365
5434
|
let response;
|
|
5366
5435
|
let lastNetworkError;
|
|
@@ -5386,23 +5455,27 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
|
5386
5455
|
return {
|
|
5387
5456
|
error: "usage endpoint rejected the key (inference may still work)",
|
|
5388
5457
|
keyStatus: /expired/i.test(body) ? "expired" : "invalid",
|
|
5458
|
+
latencyMs: latency(),
|
|
5389
5459
|
checkedAt: Date.now()
|
|
5390
5460
|
};
|
|
5391
5461
|
}
|
|
5392
5462
|
if (!response.ok) return {
|
|
5393
5463
|
error: `usage endpoint returned HTTP ${response.status}`,
|
|
5394
5464
|
keyStatus: "unavailable",
|
|
5465
|
+
latencyMs: latency(),
|
|
5395
5466
|
checkedAt: Date.now()
|
|
5396
5467
|
};
|
|
5397
5468
|
return {
|
|
5398
5469
|
report: normalizeUsage(await response.json()),
|
|
5399
5470
|
keyStatus: "ok",
|
|
5471
|
+
latencyMs: latency(),
|
|
5400
5472
|
checkedAt: Date.now()
|
|
5401
5473
|
};
|
|
5402
5474
|
} catch (error) {
|
|
5403
5475
|
return {
|
|
5404
5476
|
error: error instanceof Error ? error.message : "usage request failed",
|
|
5405
5477
|
keyStatus: "unavailable",
|
|
5478
|
+
latencyMs: latency(),
|
|
5406
5479
|
checkedAt: Date.now()
|
|
5407
5480
|
};
|
|
5408
5481
|
}
|
|
@@ -5416,8 +5489,7 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
|
5416
5489
|
};
|
|
5417
5490
|
return read;
|
|
5418
5491
|
}
|
|
5419
|
-
|
|
5420
|
-
//#region src/index.ts
|
|
5492
|
+
// src/index.ts
|
|
5421
5493
|
const name = "llm-opencode";
|
|
5422
5494
|
const inject = ["llm"];
|
|
5423
5495
|
/**
|
|
@@ -5501,7 +5573,6 @@ function apply(ctx, config) {
|
|
|
5501
5573
|
}), "dsh-opencode-provider: plugin updater");
|
|
5502
5574
|
});
|
|
5503
5575
|
}
|
|
5504
|
-
//#endregion
|
|
5505
5576
|
export { Config, OpenCodeAdapter, apply, applyCommands, applyRoutes, classifyResponseError, clearCatalogCache, createKeyPool, createKeyResolver, createUsageReader, describeModel, effectiveModel, inject, name, normalizeUsage, openCodeAuthPath, openCodeSessionId };
|
|
5506
5577
|
|
|
5507
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
|
},
|