@goodandready/dsh-opencode-provider 0.5.23 → 0.5.25
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 +12 -0
- package/README.md +3 -3
- package/README.ru.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +119 -40
- package/lib/index.d.ts +8 -0
- package/lib/index.js +93 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.25 — release 2: stale data
|
|
4
|
+
|
|
5
|
+
- **#87 — stale usage**: a failed usage refresh now returns the last successful report with `stale: true` and `lastSuccessAt` instead of discarding it. The quota sidebar/panel and Settings label the stale snapshot; an explicit refresh no longer loses the previous reading.
|
|
6
|
+
- **#88 — stale analytics**: a failed analytics rescan keeps the previous report and heatmap visible with a stale notice and a retry button instead of blanking the panel; successful rescans clear the stale flag.
|
|
7
|
+
- 127 tests.
|
|
8
|
+
|
|
9
|
+
## 0.5.24 — release 1: account safety
|
|
10
|
+
|
|
11
|
+
- **#89 — managed credential cleanup**: keys saved through Settings or imported from `auth.json` are recorded in `~/.dsh/storages/dsh-opencode/managed-credentials.json` (refs only, never secrets). Removing an account asks for confirmation and deletes only plugin-managed credentials after the settings save succeeds; external environment/`auth.json` references are left intact. Cleanup failures are reported separately from the account removal.
|
|
12
|
+
- **#83 — autoSwitch**: new `autoSwitch` setting (default `true`, preserving prior behavior). When off, the selected/first usable account serves alone; quota/credential failures do not rotate and quota fallback is disabled. Exposed in Settings and TUI.
|
|
13
|
+
- 126 tests.
|
|
14
|
+
|
|
3
15
|
## 0.5.23
|
|
4
16
|
|
|
5
17
|
- **DSH 0.2.2 support**: compatibility now accepts every `0.2.2` prerelease and final release with `>=0.2.1-rc.2 <0.2.3-0`; `0.2.2-rc.1` is recorded as verified. Dev/typecheck dependencies moved to `@deepseek-ai/*@0.2.2-rc.1`, Cordis 4.1.0 and Schemastery 3.19.0; peer ranges admit both the 0.2.1 and 0.2.2 lines.
|
package/README.md
CHANGED
|
@@ -132,12 +132,12 @@ npm pack
|
|
|
132
132
|
Install the **packed tarball** into a profile:
|
|
133
133
|
|
|
134
134
|
```bash
|
|
135
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
135
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
|
|
136
136
|
# DSH Desktop:
|
|
137
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
137
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
|
|
138
138
|
|
|
139
139
|
# Or install the published package:
|
|
140
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
140
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.25
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
|
package/README.ru.md
CHANGED
|
@@ -131,12 +131,12 @@ npm pack
|
|
|
131
131
|
Установка из **упакованного tarball**:
|
|
132
132
|
|
|
133
133
|
```bash
|
|
134
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
134
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
|
|
135
135
|
# DSH Desktop:
|
|
136
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
136
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
|
|
137
137
|
|
|
138
138
|
# Либо установка опубликованного пакета из npm:
|
|
139
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
139
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.25
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
|
package/README.zh.md
CHANGED
|
@@ -126,12 +126,12 @@ npm pack
|
|
|
126
126
|
通过**打包文件 (.tgz)**安装:
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
129
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
|
|
130
130
|
# DSH 桌面端:
|
|
131
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
131
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
|
|
132
132
|
|
|
133
133
|
# 或从 npm 安装已发布版本:
|
|
134
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
134
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.25
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
|
package/lib/client.js
CHANGED
|
@@ -65,6 +65,9 @@ window.__ModuleLoader__.load({
|
|
|
65
65
|
dragAccount: "Drag to reorder accounts",
|
|
66
66
|
accountLimit: "At most 20 accounts are allowed.",
|
|
67
67
|
removeAccount: "Remove",
|
|
68
|
+
removeAccountConfirm: "Remove this account and delete its plugin-managed credential {ref}? This cannot be undone.",
|
|
69
|
+
managedKey: "Managed key",
|
|
70
|
+
accountCleanupFailed: "Account removed, but credential cleanup failed:",
|
|
68
71
|
pinAccount: "Pin",
|
|
69
72
|
activeAccount: "Pinned account",
|
|
70
73
|
activeAccountNow: "Active account",
|
|
@@ -93,6 +96,7 @@ window.__ModuleLoader__.load({
|
|
|
93
96
|
runwaySafe: "safe",
|
|
94
97
|
noUsage: "Quota is unavailable.",
|
|
95
98
|
usageError: "Quota error",
|
|
99
|
+
usageStale: "Showing the last successful quota reading; the latest refresh failed.",
|
|
96
100
|
refresh: "Refresh",
|
|
97
101
|
refreshed: "Refreshed",
|
|
98
102
|
loading: "Loading…",
|
|
@@ -141,6 +145,7 @@ window.__ModuleLoader__.load({
|
|
|
141
145
|
heatmapAllAccounts: "All accounts",
|
|
142
146
|
analyticsHint: "Aggregated from local session metadata by last activity. A session that switched models is counted under its last model; failures are not exposed by the projection. Only plugin routes are counted; legacy opencode sessions belong to the built-in catalog.",
|
|
143
147
|
analyticsPartial: "This report is partial: the projection scan hit its file limit or skipped unreadable files.",
|
|
148
|
+
analyticsStale: "Showing the previous successful scan; the latest refresh failed.",
|
|
144
149
|
analyticsEmpty: "No plugin sessions in this window. Legacy opencode sessions belong to the built-in catalog.",
|
|
145
150
|
showAll: "Show all",
|
|
146
151
|
modelSearch: "Search models…",
|
|
@@ -197,6 +202,8 @@ window.__ModuleLoader__.load({
|
|
|
197
202
|
routingFailed: "Failed",
|
|
198
203
|
quotaFallbackModel: "Quota fallback model",
|
|
199
204
|
quotaFallbackModelHint: "If Go rejects with a quota error, retry once on this free model through Console/Zen.",
|
|
205
|
+
autoSwitch: "Automatic account failover",
|
|
206
|
+
autoSwitchHint: "Try the next account before any output when the current account hits quota or a credential failure. Off keeps the selected account only.",
|
|
200
207
|
stickySession: "Sticky account per conversation",
|
|
201
208
|
stickySessionHint: "Prefer the last successful account for the same conversation; explicit model rules and pinned accounts win.",
|
|
202
209
|
off: "Off",
|
|
@@ -278,6 +285,9 @@ window.__ModuleLoader__.load({
|
|
|
278
285
|
dragAccount: "拖动以调整账户顺序",
|
|
279
286
|
accountLimit: "最多允许 20 个账户。",
|
|
280
287
|
removeAccount: "移除",
|
|
288
|
+
removeAccountConfirm: "移除此账户并删除插件管理的凭据 {ref}?此操作无法撤销。",
|
|
289
|
+
managedKey: "插件管理的密钥",
|
|
290
|
+
accountCleanupFailed: "账户已移除,但凭据清理失败:",
|
|
281
291
|
pinAccount: "固定",
|
|
282
292
|
activeAccount: "固定账户",
|
|
283
293
|
activeAccountNow: "当前账户",
|
|
@@ -306,6 +316,7 @@ window.__ModuleLoader__.load({
|
|
|
306
316
|
runwaySafe: "安全",
|
|
307
317
|
noUsage: "配额不可用。",
|
|
308
318
|
usageError: "配额错误",
|
|
319
|
+
usageStale: "显示上次成功读取的配额;最新刷新失败。",
|
|
309
320
|
refresh: "刷新",
|
|
310
321
|
refreshed: "已刷新",
|
|
311
322
|
loading: "加载中…",
|
|
@@ -354,6 +365,7 @@ window.__ModuleLoader__.load({
|
|
|
354
365
|
heatmapAllAccounts: "全部账户",
|
|
355
366
|
analyticsHint: "按最后活动时间聚合本地会话元数据。切换过模型的会话计入最后使用的模型;本地投影不暴露失败。仅统计插件路由;旧 opencode 会话属于内置目录。",
|
|
356
367
|
analyticsPartial: "此报告不完整:投影扫描达到文件上限或跳过了不可读文件。",
|
|
368
|
+
analyticsStale: "显示上次成功扫描的结果;最新刷新失败。",
|
|
357
369
|
analyticsEmpty: "此窗口内没有插件会话。旧 opencode 会话属于内置目录。",
|
|
358
370
|
showAll: "显示全部",
|
|
359
371
|
modelSearch: "搜索模型…",
|
|
@@ -410,6 +422,8 @@ window.__ModuleLoader__.load({
|
|
|
410
422
|
routingFailed: "失败",
|
|
411
423
|
quotaFallbackModel: "配额回退模型",
|
|
412
424
|
quotaFallbackModelHint: "Go 返回配额错误时,通过 Console/Zen 用此免费模型重试一次。",
|
|
425
|
+
autoSwitch: "自动切换账户",
|
|
426
|
+
autoSwitchHint: "当前账户额度耗尽或凭据失败时,在输出前尝试下一个账户。关闭后只使用选定账户。",
|
|
413
427
|
stickySession: "每个会话固定账户",
|
|
414
428
|
stickySessionHint: "同一会话优先使用上次成功的账户;显式模型规则和固定账户优先。",
|
|
415
429
|
off: "关闭",
|
|
@@ -1006,6 +1020,10 @@ window.__ModuleLoader__.load({
|
|
|
1006
1020
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1007
1021
|
className: "oc-bars",
|
|
1008
1022
|
children: [
|
|
1023
|
+
usage.stale === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1024
|
+
className: "oc-hint",
|
|
1025
|
+
children: [t("usageStale"), usage.lastSuccessAt === void 0 ? "" : ` (${new Date(usage.lastSuccessAt).toLocaleTimeString()})`]
|
|
1026
|
+
}) : null,
|
|
1009
1027
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1010
1028
|
label: t("fiveHour"),
|
|
1011
1029
|
window: windows.fiveHour
|
|
@@ -1334,6 +1352,7 @@ window.__ModuleLoader__.load({
|
|
|
1334
1352
|
const [stateError, setStateError] = (0, react.useState)(null);
|
|
1335
1353
|
const [analytics, setAnalytics] = (0, react.useState)(null);
|
|
1336
1354
|
const [analyticsError, setAnalyticsError] = (0, react.useState)(null);
|
|
1355
|
+
const [analyticsStale, setAnalyticsStale] = (0, react.useState)(false);
|
|
1337
1356
|
const [days, setDays] = (0, react.useState)(7);
|
|
1338
1357
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1339
1358
|
const analyticsSeq = (0, react.useRef)(0);
|
|
@@ -1349,14 +1368,16 @@ window.__ModuleLoader__.load({
|
|
|
1349
1368
|
const seq = analyticsSeq.current + 1;
|
|
1350
1369
|
analyticsSeq.current = seq;
|
|
1351
1370
|
setAnalyticsError(null);
|
|
1371
|
+
setAnalyticsStale(analytics !== null);
|
|
1352
1372
|
fetchAnalytics(windowDays).then((next) => {
|
|
1353
1373
|
if (seq !== analyticsSeq.current) return;
|
|
1354
1374
|
setAnalytics(next);
|
|
1355
1375
|
setAnalyticsError(null);
|
|
1376
|
+
setAnalyticsStale(false);
|
|
1356
1377
|
}).catch((caught) => {
|
|
1357
1378
|
if (seq !== analyticsSeq.current) return;
|
|
1358
|
-
setAnalytics(null);
|
|
1359
1379
|
setAnalyticsError(caught instanceof Error ? caught.message : "analytics failed");
|
|
1380
|
+
setAnalyticsStale(analytics !== null);
|
|
1360
1381
|
});
|
|
1361
1382
|
};
|
|
1362
1383
|
(0, react.useEffect)(() => {
|
|
@@ -1573,59 +1594,69 @@ window.__ModuleLoader__.load({
|
|
|
1573
1594
|
children: t("intro")
|
|
1574
1595
|
})]
|
|
1575
1596
|
}),
|
|
1576
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1597
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1577
1598
|
className: "oc-card",
|
|
1578
|
-
children:
|
|
1599
|
+
children: [
|
|
1579
1600
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1580
1601
|
className: "oc-card-title",
|
|
1581
1602
|
children: t("usageAnalytics")
|
|
1582
1603
|
}),
|
|
1583
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1604
|
+
analyticsError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1584
1605
|
className: "oc-hint",
|
|
1585
1606
|
children: [
|
|
1586
1607
|
t("failed"),
|
|
1587
1608
|
": ",
|
|
1588
1609
|
analyticsError
|
|
1589
1610
|
]
|
|
1590
|
-
}),
|
|
1591
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1611
|
+
}) : null,
|
|
1612
|
+
analyticsStale && analytics !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1613
|
+
className: "oc-hint",
|
|
1614
|
+
children: t("analyticsStale")
|
|
1615
|
+
}) : null,
|
|
1616
|
+
analyticsError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1592
1617
|
type: "button",
|
|
1593
1618
|
className: "oc-button",
|
|
1594
1619
|
onClick: () => loadAnalytics(days),
|
|
1595
1620
|
children: t("refresh")
|
|
1621
|
+
}) : null,
|
|
1622
|
+
analytics === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1623
|
+
className: "oc-hint",
|
|
1624
|
+
children: t("loading")
|
|
1625
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageAnalyticsView, {
|
|
1626
|
+
analytics,
|
|
1627
|
+
t,
|
|
1628
|
+
days,
|
|
1629
|
+
onDays: setDays
|
|
1596
1630
|
})
|
|
1597
|
-
]
|
|
1598
|
-
className: "oc-card-title",
|
|
1599
|
-
children: t("usageAnalytics")
|
|
1600
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1601
|
-
className: "oc-hint",
|
|
1602
|
-
children: t("loading")
|
|
1603
|
-
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageAnalyticsView, {
|
|
1604
|
-
analytics,
|
|
1605
|
-
t,
|
|
1606
|
-
days,
|
|
1607
|
-
onDays: setDays
|
|
1608
|
-
})
|
|
1631
|
+
]
|
|
1609
1632
|
}),
|
|
1610
1633
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1611
1634
|
className: "oc-card",
|
|
1612
|
-
children: [
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1635
|
+
children: [
|
|
1636
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1637
|
+
className: "oc-card-title",
|
|
1638
|
+
children: t("requestHeatmap")
|
|
1639
|
+
}),
|
|
1640
|
+
analyticsError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1641
|
+
className: "oc-hint",
|
|
1642
|
+
children: [
|
|
1643
|
+
t("failed"),
|
|
1644
|
+
": ",
|
|
1645
|
+
analyticsError
|
|
1646
|
+
]
|
|
1647
|
+
}) : null,
|
|
1648
|
+
analyticsStale && analytics !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1649
|
+
className: "oc-hint",
|
|
1650
|
+
children: t("analyticsStale")
|
|
1651
|
+
}) : null,
|
|
1652
|
+
analytics === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1653
|
+
className: "oc-hint",
|
|
1654
|
+
children: t("loading")
|
|
1655
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RequestHeatmapView, {
|
|
1656
|
+
heatmap: analytics.heatmap,
|
|
1657
|
+
t
|
|
1658
|
+
})
|
|
1659
|
+
]
|
|
1629
1660
|
})
|
|
1630
1661
|
]
|
|
1631
1662
|
});
|
|
@@ -1653,6 +1684,7 @@ window.__ModuleLoader__.load({
|
|
|
1653
1684
|
accounts: state.config.accounts.map((account) => ({ ...account })),
|
|
1654
1685
|
modelAccountRules: state.config.modelAccountRules.map((rule) => ({ ...rule })),
|
|
1655
1686
|
quotaFallbackModel: state.config.quotaFallbackModel,
|
|
1687
|
+
autoSwitch: state.config.autoSwitch !== false,
|
|
1656
1688
|
stickySession: state.config.stickySession,
|
|
1657
1689
|
visibleModels: state.config.visibleModels.length === 0 ? allIds : [...state.config.visibleModels],
|
|
1658
1690
|
filterModelsByPlan: state.config.filterModelsByPlan,
|
|
@@ -1704,6 +1736,7 @@ window.__ModuleLoader__.load({
|
|
|
1704
1736
|
account: rule.account.trim()
|
|
1705
1737
|
})).filter((rule) => rule.pattern !== "" && rule.account !== ""),
|
|
1706
1738
|
quotaFallbackModel: draft.quotaFallbackModel.trim(),
|
|
1739
|
+
autoSwitch: draft.autoSwitch,
|
|
1707
1740
|
stickySession: draft.stickySession,
|
|
1708
1741
|
visibleModels: draft.showAll ? [] : draft.visibleModels,
|
|
1709
1742
|
filterModelsByPlan: draft.filterModelsByPlan,
|
|
@@ -1935,6 +1968,7 @@ window.__ModuleLoader__.load({
|
|
|
1935
1968
|
const [importing, setImporting] = (0, react.useState)(false);
|
|
1936
1969
|
const [checking, setChecking] = (0, react.useState)(null);
|
|
1937
1970
|
const [dragFrom, setDragFrom] = (0, react.useState)(null);
|
|
1971
|
+
const [pendingCleanup, setPendingCleanup] = (0, react.useState)([]);
|
|
1938
1972
|
const load = (0, react.useCallback)(async () => {
|
|
1939
1973
|
setLoading(true);
|
|
1940
1974
|
try {
|
|
@@ -1964,12 +1998,25 @@ window.__ModuleLoader__.load({
|
|
|
1964
1998
|
setSaving(true);
|
|
1965
1999
|
setError(null);
|
|
1966
2000
|
try {
|
|
1967
|
-
|
|
2001
|
+
const patch = patchOf(draft);
|
|
2002
|
+
await saveConfig(patch, draft.revision);
|
|
2003
|
+
const stillUsed = new Set(patch.accounts.map((account) => account.apiKeyEnv?.trim() ?? "").filter((ref) => ref !== ""));
|
|
2004
|
+
let cleanupError = null;
|
|
2005
|
+
for (const ref of pendingCleanup) {
|
|
2006
|
+
if (stillUsed.has(ref)) continue;
|
|
2007
|
+
try {
|
|
2008
|
+
await clearKey(ref);
|
|
2009
|
+
} catch (caught) {
|
|
2010
|
+
cleanupError = caught instanceof Error ? caught.message : String(caught);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
setPendingCleanup([]);
|
|
1968
2014
|
window.dispatchEvent(new CustomEvent(CONFIG_CHANGED_EVENT));
|
|
1969
2015
|
setNotice(t("saved"));
|
|
1970
2016
|
setState(null);
|
|
1971
2017
|
setDraft(null);
|
|
1972
2018
|
await load();
|
|
2019
|
+
if (cleanupError !== null) setError(`${t("accountCleanupFailed")} ${cleanupError}`);
|
|
1973
2020
|
} catch (caught) {
|
|
1974
2021
|
setError(caught instanceof Error ? caught.message : t("saveFailed"));
|
|
1975
2022
|
} finally {
|
|
@@ -2308,6 +2355,10 @@ window.__ModuleLoader__.load({
|
|
|
2308
2355
|
]
|
|
2309
2356
|
});
|
|
2310
2357
|
})(),
|
|
2358
|
+
defaultAccount?.usage?.stale === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2359
|
+
className: "oc-hint",
|
|
2360
|
+
children: t("usageStale")
|
|
2361
|
+
}) : null,
|
|
2311
2362
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2312
2363
|
className: "oc-row",
|
|
2313
2364
|
children: [
|
|
@@ -2409,8 +2460,8 @@ window.__ModuleLoader__.load({
|
|
|
2409
2460
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
2410
2461
|
className: "oc-card-title",
|
|
2411
2462
|
children: [t("serviceStatus"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2412
|
-
className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2413
|
-
children: state.service.usageCheckedAt === void 0 ? t("notChecked") : state.service.usageError === void 0 ? t("serviceOk") : t("serviceDegraded")
|
|
2463
|
+
className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 && state.service.usageStale !== true ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2464
|
+
children: state.service.usageCheckedAt === void 0 ? t("notChecked") : state.service.usageError === void 0 && state.service.usageStale !== true ? t("serviceOk") : t("serviceDegraded")
|
|
2414
2465
|
})]
|
|
2415
2466
|
}),
|
|
2416
2467
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2421,7 +2472,7 @@ window.__ModuleLoader__.load({
|
|
|
2421
2472
|
children: t("usageEndpoint")
|
|
2422
2473
|
}),
|
|
2423
2474
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2424
|
-
className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2475
|
+
className: `oc-badge ${state.service.usageCheckedAt === void 0 ? "oc-badge-warn" : state.service.usageError === void 0 && state.service.usageStale !== true ? "oc-badge-ok" : "oc-badge-bad"}`,
|
|
2425
2476
|
children: state.service.usageCheckedAt === void 0 ? t("notChecked") : t("checkedAt").replace("{time}", new Date(state.service.usageCheckedAt).toLocaleTimeString())
|
|
2426
2477
|
}),
|
|
2427
2478
|
state.service.usageLatencyMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -2434,6 +2485,10 @@ window.__ModuleLoader__.load({
|
|
|
2434
2485
|
className: "oc-hint",
|
|
2435
2486
|
children: state.service.usageError
|
|
2436
2487
|
}),
|
|
2488
|
+
state.service.usageStale === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2489
|
+
className: "oc-hint",
|
|
2490
|
+
children: t("usageStale")
|
|
2491
|
+
}) : null,
|
|
2437
2492
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2438
2493
|
className: "oc-row",
|
|
2439
2494
|
children: [
|
|
@@ -2592,7 +2647,11 @@ window.__ModuleLoader__.load({
|
|
|
2592
2647
|
known === void 0 || known.mark === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2593
2648
|
className: "oc-badge oc-badge-bad",
|
|
2594
2649
|
children: accountMark(known, t)
|
|
2595
|
-
})
|
|
2650
|
+
}),
|
|
2651
|
+
known?.managed === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2652
|
+
className: "oc-badge",
|
|
2653
|
+
children: t("managedKey")
|
|
2654
|
+
}) : null
|
|
2596
2655
|
]
|
|
2597
2656
|
}),
|
|
2598
2657
|
known?.usage?.report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2611,6 +2670,10 @@ window.__ModuleLoader__.load({
|
|
|
2611
2670
|
known.usage.error
|
|
2612
2671
|
]
|
|
2613
2672
|
}),
|
|
2673
|
+
known?.usage?.stale === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2674
|
+
className: "oc-hint",
|
|
2675
|
+
children: t("usageStale")
|
|
2676
|
+
}) : null,
|
|
2614
2677
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2615
2678
|
className: "oc-row",
|
|
2616
2679
|
children: [
|
|
@@ -2661,6 +2724,10 @@ window.__ModuleLoader__.load({
|
|
|
2661
2724
|
type: "button",
|
|
2662
2725
|
className: "oc-button oc-button-danger",
|
|
2663
2726
|
onClick: () => {
|
|
2727
|
+
const ref = account.apiKeyEnv.trim();
|
|
2728
|
+
const managed = known?.managed === true && ref !== "";
|
|
2729
|
+
if (managed && !window.confirm(t("removeAccountConfirm").replace("{ref}", ref))) return;
|
|
2730
|
+
if (managed) setPendingCleanup((current) => current.includes(ref) ? current : [...current, ref]);
|
|
2664
2731
|
setKeyDrafts({});
|
|
2665
2732
|
update({ accounts: draft.accounts.filter((_, i) => i !== index) });
|
|
2666
2733
|
},
|
|
@@ -2796,6 +2863,18 @@ window.__ModuleLoader__.load({
|
|
|
2796
2863
|
className: "oc-hint",
|
|
2797
2864
|
children: t("routingHint")
|
|
2798
2865
|
}),
|
|
2866
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2867
|
+
className: "oc-row",
|
|
2868
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2869
|
+
type: "checkbox",
|
|
2870
|
+
checked: draft.autoSwitch,
|
|
2871
|
+
onChange: (event) => update({ autoSwitch: event.target.checked })
|
|
2872
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("autoSwitch") })]
|
|
2873
|
+
}),
|
|
2874
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2875
|
+
className: "oc-hint",
|
|
2876
|
+
children: t("autoSwitchHint")
|
|
2877
|
+
}),
|
|
2799
2878
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2800
2879
|
className: "oc-row",
|
|
2801
2880
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
package/lib/index.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ interface Config {
|
|
|
60
60
|
modelAccountRules?: ModelAccountRule[];
|
|
61
61
|
/** Free model used once when the Go quota rejects a request; empty disables it. */
|
|
62
62
|
quotaFallbackModel?: string;
|
|
63
|
+
/** Try the next account after a pre-stream quota/credential failure. */
|
|
64
|
+
autoSwitch?: boolean;
|
|
63
65
|
/** Prefer the last successful account for a conversation. */
|
|
64
66
|
stickySession?: boolean;
|
|
65
67
|
/** Pinned account id; empty means automatic. */
|
|
@@ -92,6 +94,7 @@ interface ResolvedConfig {
|
|
|
92
94
|
accounts: readonly AccountConfig[];
|
|
93
95
|
modelAccountRules: readonly ModelAccountRule[];
|
|
94
96
|
quotaFallbackModel: string;
|
|
97
|
+
autoSwitch: boolean;
|
|
95
98
|
stickySession: boolean;
|
|
96
99
|
activeAccount: string;
|
|
97
100
|
showSidebarQuota: boolean;
|
|
@@ -260,6 +263,9 @@ interface UsageResult {
|
|
|
260
263
|
readonly keyStatus?: UsageKeyStatus;
|
|
261
264
|
readonly latencyMs?: number;
|
|
262
265
|
readonly checkedAt: number;
|
|
266
|
+
/** True when `report` is the last successful reading after a failed refresh. */
|
|
267
|
+
readonly stale?: boolean;
|
|
268
|
+
readonly lastSuccessAt?: number;
|
|
263
269
|
}
|
|
264
270
|
interface UsageWindowPayload {
|
|
265
271
|
status?: string;
|
|
@@ -373,6 +379,8 @@ interface RouteDeps {
|
|
|
373
379
|
readonly analytics?: (days: number) => Promise<AnalyticsReport>;
|
|
374
380
|
readonly heatmap?: (days: number) => Promise<RequestHeatmap>;
|
|
375
381
|
readonly refreshTui?: () => void;
|
|
382
|
+
/** Test seam; defaults to the local managed-credentials registry. */
|
|
383
|
+
readonly managedPath?: string;
|
|
376
384
|
}
|
|
377
385
|
/** Register the Host HTTP surface consumed by the browser settings page. */
|
|
378
386
|
declare function applyRoutes(ctx: Context, web: WebServer, deps: RouteDeps): void;
|
package/lib/index.js
CHANGED
|
@@ -81,7 +81,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
|
|
|
81
81
|
const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
|
|
82
82
|
const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
83
83
|
const DEFAULT_AUTH_ENTRY = "opencode-go";
|
|
84
|
-
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.
|
|
84
|
+
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.25";
|
|
85
85
|
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
86
86
|
const DEFAULT_MAX_TOKENS = 32768;
|
|
87
87
|
const DEFAULT_GENERATE_MAX_TOKENS = 32768;
|
|
@@ -123,6 +123,7 @@ const Config = z.object({
|
|
|
123
123
|
account: z.string()
|
|
124
124
|
})).default([])),
|
|
125
125
|
quotaFallbackModel: markVolatile(z.string().default("")),
|
|
126
|
+
autoSwitch: markVolatile(z.boolean().default(true)),
|
|
126
127
|
stickySession: markVolatile(z.boolean().default(true)),
|
|
127
128
|
activeAccount: markVolatile(z.string().default("")),
|
|
128
129
|
showSidebarQuota: markVolatile(z.boolean().default(false)),
|
|
@@ -218,6 +219,7 @@ function resolveConfig(config) {
|
|
|
218
219
|
accounts: Array.isArray(config.accounts) ? config.accounts : [],
|
|
219
220
|
modelAccountRules: modelAccountRuleList(config.modelAccountRules),
|
|
220
221
|
quotaFallbackModel: typeof config.quotaFallbackModel === "string" ? config.quotaFallbackModel.trim() : "",
|
|
222
|
+
autoSwitch: config.autoSwitch !== false,
|
|
221
223
|
stickySession: config.stickySession !== false,
|
|
222
224
|
activeAccount: typeof config.activeAccount === "string" ? config.activeAccount : "",
|
|
223
225
|
showSidebarQuota: config.showSidebarQuota === true,
|
|
@@ -3327,8 +3329,9 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3327
3329
|
}
|
|
3328
3330
|
const eligible = fallback ? keys : keys.filter((entry) => !this.coolingDown(entry.key) && !this.modelCoolingDown(entry.key, options.model));
|
|
3329
3331
|
const ordered = eligible.length > 0 ? eligible : keys;
|
|
3332
|
+
const candidates = config.autoSwitch ? ordered : ordered.slice(0, 1);
|
|
3330
3333
|
const forced = !fallback && eligible.length === 0;
|
|
3331
|
-
const initial =
|
|
3334
|
+
const initial = candidates[0];
|
|
3332
3335
|
const url = endpointFor(options.provider, model.transport, options.model, config);
|
|
3333
3336
|
let images;
|
|
3334
3337
|
if (hasImages(options)) {
|
|
@@ -3352,7 +3355,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3352
3355
|
let lastError;
|
|
3353
3356
|
let lastAccount = initial?.label ?? "default";
|
|
3354
3357
|
let loggedAny = false;
|
|
3355
|
-
for (const [index, entry] of
|
|
3358
|
+
for (const [index, entry] of candidates.entries()) {
|
|
3356
3359
|
if (!fallback && !forced && (this.coolingDown(entry.key) || this.modelCoolingDown(entry.key, options.model))) continue;
|
|
3357
3360
|
lastAccount = entry.label ?? "default";
|
|
3358
3361
|
const attemptReason = index === 0 ? reason : "rotation";
|
|
@@ -3380,7 +3383,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3380
3383
|
const error = classifyResponseError(response.status, text, response.headers);
|
|
3381
3384
|
lastError = error;
|
|
3382
3385
|
logAttempt(false, error.code);
|
|
3383
|
-
const canRotate = index <
|
|
3386
|
+
const canRotate = index < candidates.length - 1;
|
|
3384
3387
|
if (error.code === "INVALID_CREDENTIAL" || error.code === "RATE_LIMIT" || error.code === "QUOTA") {
|
|
3385
3388
|
if (!fallback) this.markRejection(entry.key, options.model, response.status, response.headers, error.code);
|
|
3386
3389
|
responseFailure = true;
|
|
@@ -3425,8 +3428,8 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3425
3428
|
lastError = classified;
|
|
3426
3429
|
const transient = this.isTransient(classified);
|
|
3427
3430
|
if (transient && !responseFailure && !fallback) this.markRejection(entry.key, options.model, 0, void 0, classified.code);
|
|
3428
|
-
if (!(index <
|
|
3429
|
-
if (!fallback && config.quotaFallbackModel !== "" && isGoProvider(options.provider) && classified.code === "QUOTA") break;
|
|
3431
|
+
if (!(index < candidates.length - 1 && transient)) {
|
|
3432
|
+
if (!fallback && config.autoSwitch && config.quotaFallbackModel !== "" && isGoProvider(options.provider) && classified.code === "QUOTA") break;
|
|
3430
3433
|
throw classified;
|
|
3431
3434
|
}
|
|
3432
3435
|
}
|
|
@@ -3448,7 +3451,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3448
3451
|
ok: false,
|
|
3449
3452
|
error: finalError.code
|
|
3450
3453
|
});
|
|
3451
|
-
if (!fallback && config.quotaFallbackModel !== "" && isGoProvider(options.provider) && (lastError === void 0 || lastError.code === "QUOTA")) try {
|
|
3454
|
+
if (!fallback && config.autoSwitch && config.quotaFallbackModel !== "" && isGoProvider(options.provider) && (lastError === void 0 || lastError.code === "QUOTA")) try {
|
|
3452
3455
|
yield* this.run({
|
|
3453
3456
|
...options,
|
|
3454
3457
|
provider: CONSOLE_PROVIDER,
|
|
@@ -4661,6 +4664,44 @@ async function readModelAnalytics(days = 30) {
|
|
|
4661
4664
|
});
|
|
4662
4665
|
return report;
|
|
4663
4666
|
}
|
|
4667
|
+
// src/managed-credentials.ts
|
|
4668
|
+
const REF_LIMIT = 128;
|
|
4669
|
+
function managedCredentialsPath() {
|
|
4670
|
+
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
4671
|
+
return join(home, "storages", "dsh-opencode", "managed-credentials.json");
|
|
4672
|
+
}
|
|
4673
|
+
function validRef(value) {
|
|
4674
|
+
return typeof value === "string" && value !== "" && value.length <= REF_LIMIT;
|
|
4675
|
+
}
|
|
4676
|
+
async function readRefs(path) {
|
|
4677
|
+
try {
|
|
4678
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
4679
|
+
if (!Array.isArray(parsed.refs)) return /* @__PURE__ */ new Set();
|
|
4680
|
+
return new Set(parsed.refs.filter(validRef));
|
|
4681
|
+
} catch {
|
|
4682
|
+
return /* @__PURE__ */ new Set();
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
async function writeRefs(path, refs) {
|
|
4686
|
+
await mkdir(dirname(path), { recursive: true });
|
|
4687
|
+
await writeFile(path, JSON.stringify({ refs: [...refs].sort() }), "utf8");
|
|
4688
|
+
}
|
|
4689
|
+
async function readManagedCredentialRefs(path = managedCredentialsPath()) {
|
|
4690
|
+
return readRefs(path);
|
|
4691
|
+
}
|
|
4692
|
+
async function mutate(path, ref, add) {
|
|
4693
|
+
if (!validRef(ref)) return;
|
|
4694
|
+
const refs = await readRefs(path);
|
|
4695
|
+
if (add) refs.add(ref);
|
|
4696
|
+
else refs.delete(ref);
|
|
4697
|
+
await writeRefs(path, refs);
|
|
4698
|
+
}
|
|
4699
|
+
async function addManagedCredentialRef(ref, path = managedCredentialsPath()) {
|
|
4700
|
+
await mutate(path, ref, true);
|
|
4701
|
+
}
|
|
4702
|
+
async function removeManagedCredentialRef(ref, path = managedCredentialsPath()) {
|
|
4703
|
+
await mutate(path, ref, false);
|
|
4704
|
+
}
|
|
4664
4705
|
// src/routes-public.ts
|
|
4665
4706
|
function publicConfig(config) {
|
|
4666
4707
|
return {
|
|
@@ -4690,6 +4731,7 @@ function publicConfig(config) {
|
|
|
4690
4731
|
account: rule.account
|
|
4691
4732
|
})),
|
|
4692
4733
|
quotaFallbackModel: config.quotaFallbackModel,
|
|
4734
|
+
autoSwitch: config.autoSwitch,
|
|
4693
4735
|
stickySession: config.stickySession,
|
|
4694
4736
|
activeAccount: config.activeAccount,
|
|
4695
4737
|
showSidebarQuota: config.showSidebarQuota,
|
|
@@ -4878,6 +4920,7 @@ function configOps(patch) {
|
|
|
4878
4920
|
if ("accounts" in patch) set("accounts", cleanAccounts(patch.accounts));
|
|
4879
4921
|
if ("modelAccountRules" in patch) set("modelAccountRules", cleanModelAccountRules(patch.modelAccountRules));
|
|
4880
4922
|
if ("quotaFallbackModel" in patch) set("quotaFallbackModel", patch.quotaFallbackModel);
|
|
4923
|
+
if ("autoSwitch" in patch) set("autoSwitch", patch.autoSwitch === true);
|
|
4881
4924
|
if ("stickySession" in patch) set("stickySession", patch.stickySession === true);
|
|
4882
4925
|
if ("activeAccount" in patch) set("activeAccount", patch.activeAccount);
|
|
4883
4926
|
if ("showSidebarQuota" in patch) set("showSidebarQuota", patch.showSidebarQuota === true);
|
|
@@ -4911,6 +4954,7 @@ async function buildState(ctx, deps) {
|
|
|
4911
4954
|
}));
|
|
4912
4955
|
const usageByLabel = /* @__PURE__ */ new Map();
|
|
4913
4956
|
for (const entry of resolved) usageByLabel.set(entry.label ?? "default", usageByKey.get(entry.key) ?? null);
|
|
4957
|
+
const managedRefs = await readManagedCredentialRefs(deps.managedPath);
|
|
4914
4958
|
const slots = [{
|
|
4915
4959
|
label: "default",
|
|
4916
4960
|
ref: config.apiKeyEnv,
|
|
@@ -4951,7 +4995,8 @@ async function buildState(ctx, deps) {
|
|
|
4951
4995
|
coolingUntil: health?.coolingUntil ?? 0,
|
|
4952
4996
|
mark: health?.reason ?? "",
|
|
4953
4997
|
keyStatus,
|
|
4954
|
-
usage
|
|
4998
|
+
usage,
|
|
4999
|
+
managed: slot.ref !== "" && managedRefs.has(slot.ref)
|
|
4955
5000
|
});
|
|
4956
5001
|
}
|
|
4957
5002
|
let models = [];
|
|
@@ -4988,7 +5033,8 @@ async function buildState(ctx, deps) {
|
|
|
4988
5033
|
service: {
|
|
4989
5034
|
...usage?.checkedAt === void 0 ? {} : { usageCheckedAt: usage.checkedAt },
|
|
4990
5035
|
...usage?.latencyMs === void 0 ? {} : { usageLatencyMs: usage.latencyMs },
|
|
4991
|
-
...usage?.error === void 0 ? {} : { usageError: usage.error }
|
|
5036
|
+
...usage?.error === void 0 ? {} : { usageError: usage.error },
|
|
5037
|
+
...usage?.stale === true ? { usageStale: true } : {}
|
|
4992
5038
|
},
|
|
4993
5039
|
routing: deps.adapter.routingTrace(),
|
|
4994
5040
|
auth: {
|
|
@@ -5125,6 +5171,7 @@ function applyRoutes(ctx, web, deps) {
|
|
|
5125
5171
|
return;
|
|
5126
5172
|
}
|
|
5127
5173
|
await credentials.set(credentialRef(refName), key);
|
|
5174
|
+
await addManagedCredentialRef(refName, deps.managedPath);
|
|
5128
5175
|
json(res, 200, {
|
|
5129
5176
|
ok: true,
|
|
5130
5177
|
ref: refName
|
|
@@ -5144,6 +5191,7 @@ function applyRoutes(ctx, web, deps) {
|
|
|
5144
5191
|
const body = await readJson(req);
|
|
5145
5192
|
const refName = typeof body.ref === "string" && body.ref.trim() !== "" ? body.ref.trim() : deps.config().apiKeyEnv;
|
|
5146
5193
|
await credentials.unset(credentialRef(refName));
|
|
5194
|
+
await removeManagedCredentialRef(refName, deps.managedPath);
|
|
5147
5195
|
json(res, 200, {
|
|
5148
5196
|
ok: true,
|
|
5149
5197
|
ref: refName
|
|
@@ -5169,6 +5217,7 @@ function applyRoutes(ctx, web, deps) {
|
|
|
5169
5217
|
return;
|
|
5170
5218
|
}
|
|
5171
5219
|
await credentials.set(credentialRef(ref), key);
|
|
5220
|
+
await addManagedCredentialRef(ref, deps.managedPath);
|
|
5172
5221
|
json(res, 200, {
|
|
5173
5222
|
ok: true,
|
|
5174
5223
|
entry,
|
|
@@ -5404,6 +5453,18 @@ function buildOpenCodeTuiSection(deps) {
|
|
|
5404
5453
|
format: () => deps.quotaFallbackModel(),
|
|
5405
5454
|
parse: setOrClear
|
|
5406
5455
|
},
|
|
5456
|
+
{
|
|
5457
|
+
path: ["autoSwitch"],
|
|
5458
|
+
group: "advanced",
|
|
5459
|
+
kind: "boolean",
|
|
5460
|
+
label: "Automatic account failover",
|
|
5461
|
+
hint: "Try the next account before output on quota or credential failure.",
|
|
5462
|
+
format: (value) => String(value === void 0 ? deps.autoSwitch() : value !== false),
|
|
5463
|
+
parse: (text) => ({
|
|
5464
|
+
kind: "set",
|
|
5465
|
+
value: text.trim() === "true"
|
|
5466
|
+
})
|
|
5467
|
+
},
|
|
5407
5468
|
{
|
|
5408
5469
|
path: ["stickySession"],
|
|
5409
5470
|
group: "advanced",
|
|
@@ -5558,6 +5619,8 @@ function fingerprint(key) {
|
|
|
5558
5619
|
/** Cached reader for the (undocumented) Go usage endpoint. Fail-open by design. */
|
|
5559
5620
|
function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
5560
5621
|
const cache = /* @__PURE__ */ new Map();
|
|
5622
|
+
/** Last successful reading per key, kept across explicit cache clears. */
|
|
5623
|
+
const lastSuccess = /* @__PURE__ */ new Map();
|
|
5561
5624
|
const read = async (provider, explicitKey) => {
|
|
5562
5625
|
if (provider !== "opencode-go-direct" && provider !== "opencode-go") return void 0;
|
|
5563
5626
|
const current = config();
|
|
@@ -5575,7 +5638,7 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
|
5575
5638
|
const cacheKey = `${provider}:${fingerprint(key)}`;
|
|
5576
5639
|
const cached = cache.get(cacheKey);
|
|
5577
5640
|
if (cached !== void 0) {
|
|
5578
|
-
const ttl = cached.report === void 0 ? FAILURE_TTL_MS : SUCCESS_TTL_MS;
|
|
5641
|
+
const ttl = cached.report === void 0 || cached.stale === true ? FAILURE_TTL_MS : SUCCESS_TTL_MS;
|
|
5579
5642
|
if (Date.now() - cached.checkedAt < ttl) return cached;
|
|
5580
5643
|
}
|
|
5581
5644
|
const result = async () => {
|
|
@@ -5633,8 +5696,25 @@ function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
|
5633
5696
|
}
|
|
5634
5697
|
};
|
|
5635
5698
|
const fresh = await result();
|
|
5636
|
-
|
|
5637
|
-
|
|
5699
|
+
const previous = cached?.report !== void 0 ? cached : lastSuccess.get(cacheKey);
|
|
5700
|
+
if (fresh.report === void 0 && previous?.report !== void 0) {
|
|
5701
|
+
const stale = {
|
|
5702
|
+
...fresh,
|
|
5703
|
+
report: previous.report,
|
|
5704
|
+
stale: true,
|
|
5705
|
+
lastSuccessAt: previous.lastSuccessAt ?? previous.checkedAt
|
|
5706
|
+
};
|
|
5707
|
+
cache.set(cacheKey, stale);
|
|
5708
|
+
return stale;
|
|
5709
|
+
}
|
|
5710
|
+
const next = fresh.report === void 0 ? fresh : {
|
|
5711
|
+
...fresh,
|
|
5712
|
+
stale: false,
|
|
5713
|
+
lastSuccessAt: fresh.checkedAt
|
|
5714
|
+
};
|
|
5715
|
+
if (next.report !== void 0) lastSuccess.set(cacheKey, next);
|
|
5716
|
+
cache.set(cacheKey, next);
|
|
5717
|
+
return next;
|
|
5638
5718
|
};
|
|
5639
5719
|
read.clear = () => {
|
|
5640
5720
|
cache.clear();
|
|
@@ -5711,6 +5791,7 @@ function apply(ctx, config) {
|
|
|
5711
5791
|
activeAccount: () => current().activeAccount,
|
|
5712
5792
|
filterModelsByPlan: () => current().filterModelsByPlan,
|
|
5713
5793
|
quotaFallbackModel: () => current().quotaFallbackModel,
|
|
5794
|
+
autoSwitch: () => current().autoSwitch,
|
|
5714
5795
|
stickySession: () => current().stickySession,
|
|
5715
5796
|
showSidebarQuota: () => current().showSidebarQuota
|
|
5716
5797
|
});
|