@goodandready/dsh-opencode-provider 0.5.2 → 0.5.3
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 +5 -0
- package/README.md +4 -3
- package/README.ru.md +4 -3
- package/README.zh.md +4 -3
- package/lib/client.js +91 -2
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.3
|
|
4
|
+
|
|
5
|
+
- **Per-account runway**: the quota panel estimates time to exhaustion for each account from the 5-hour, weekly and monthly burn rate and flags the first window projected to run out before its reset. The estimate is intentionally approximate; only the current percentage and reset time are exposed by the usage endpoint.
|
|
6
|
+
- 84 tests.
|
|
7
|
+
|
|
3
8
|
## 0.5.2
|
|
4
9
|
|
|
5
10
|
- **Quota fallback**: choose a free model in Settings → OpenCode. A Go quota rejection retries the same request once on Console/Zen with that model; failures surface the original quota error.
|
package/README.md
CHANGED
|
@@ -86,6 +86,7 @@ graph LR
|
|
|
86
86
|
- Gemini `thoughtSignature` on historical function calls.
|
|
87
87
|
- **Multi-account rotation** before a stream starts: 401/403/429 and quota/usage-limit bodies (400/402/403/429). An invalid key cools down for 24h, a quota rejection for 10 minutes or `Retry-After`, a rate limit for 60s or `Retry-After`. A request already streaming is never replayed on another account; the next request uses the next usable key.
|
|
88
88
|
- **Per-account quotas**: each account row shows its own 5-hour / weekly / monthly usage, the active account drives the sidebar and quota panel, and the panel lists every account side by side.
|
|
89
|
+
- **Per-account runway**: estimates time to exhaustion from the average burn rate of each window and marks the first window projected to run out before its reset; approximate because the API exposes only the current percentage and reset time.
|
|
89
90
|
- **Model → account rules**: first matching glob pins a preferred account for that model; the other accounts remain fallbacks if it fails or is cooling.
|
|
90
91
|
- **Request heatmap** in the quota panel: a Monday-first weekday × hour grid in local time, plus a 24-hour aggregate strip underneath. Selectors switch between all requests, any single model, and any single account. The adapter appends `{at, provider, model, purpose?, account, reason}` to `~/.dsh/storages/dsh-opencode/requests.jsonl`; no prompt, response or key is stored. Auxiliary `session-title`/`compaction` calls are excluded from the heatmap and exact duplicate records collapse. The log compacts at 2 MB to the newest 5000 lines. Collection starts with this build — the projection cache only has `lastPromptAt`, not per-request timestamps.
|
|
91
92
|
- **Stable `x-opencode-session`** derived from the durable session id, so retries and every turn of a conversation reuse the same routing key.
|
|
@@ -122,12 +123,12 @@ npm pack
|
|
|
122
123
|
Install the **packed tarball** into a profile:
|
|
123
124
|
|
|
124
125
|
```bash
|
|
125
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
126
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.3.tgz
|
|
126
127
|
# DSH Desktop:
|
|
127
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
128
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.3.tgz
|
|
128
129
|
|
|
129
130
|
# Or install the published package:
|
|
130
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
131
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.3
|
|
131
132
|
```
|
|
132
133
|
|
|
133
134
|
Restart dsh (or DSH Desktop), then pick `OpenCode` or `OpenCode Go` in the model picker.
|
package/README.ru.md
CHANGED
|
@@ -89,6 +89,7 @@ graph LR
|
|
|
89
89
|
- Недействительный ключ охлаждается 24 часа, превышение квоты — 10 минут (или по заголовку `Retry-After`), rate limit — 60 секунд (или по `Retry-After`).
|
|
90
90
|
- Если запрос уже начал стриминг, он не переключается посреди ответа; следующий запрос берёт следующий готовый ключ.
|
|
91
91
|
- **Квоты по каждому аккаунту**: отображение индивидуального расхода за 5 часов, неделю и месяц; активный аккаунт управляет сайдбаром и панелью.
|
|
92
|
+
- **Runway по каждому аккаунту**: оценка времени до исчерпания по среднему темпу каждого окна; отмечается первое окно, которое закончится раньше сброса. Оценка приблизительная: API отдаёт только текущий процент и время сброса.
|
|
92
93
|
- **Онбординг аккаунтов**: **Import all from auth.json** переносит все ключи OpenCode в отдельные credential ref и добавляет аккаунты; **Check all accounts** обновляет квоты и health по каждому аккаунту.
|
|
93
94
|
- **Правила «модель ➔ аккаунт»**: сопоставление по glob-паттернам закрепляет предпочтительный аккаунт за моделью; остальные аккаунты остаются в резерве.
|
|
94
95
|
- **Квотный fallback**: выберите бесплатную модель в настройках; при ошибке квоты Go тот же запрос один раз повторяется через Console/Zen. Если fallback тоже не сработал, возвращается исходная ошибка квоты.
|
|
@@ -119,12 +120,12 @@ npm pack
|
|
|
119
120
|
Установка из **упакованного tarball**:
|
|
120
121
|
|
|
121
122
|
```bash
|
|
122
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
123
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.3.tgz
|
|
123
124
|
# DSH Desktop:
|
|
124
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
125
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.3.tgz
|
|
125
126
|
|
|
126
127
|
# Либо установка опубликованного пакета из npm:
|
|
127
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
128
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.3
|
|
128
129
|
```
|
|
129
130
|
|
|
130
131
|
После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode** или **OpenCode Go** в списке моделей.
|
package/README.zh.md
CHANGED
|
@@ -90,6 +90,7 @@ graph LR
|
|
|
90
90
|
- 无效密钥冷却 24 小时,超额冷却 10 分钟(或遵循 `Retry-After`),速率限制冷却 60 秒。
|
|
91
91
|
- 正在流式传输中的请求不会被打断,下一个请求会自动使用下一个可用密钥。
|
|
92
92
|
- **多账户额度与健康监控**:每行独立展示 5 小时、每周与每月使用情况,活跃账户驱动侧边栏与配额面板。
|
|
93
|
+
- **每账户预计可用时长**:按各窗口的平均消耗速度估算耗尽时间,并标记在重置前最先耗尽的窗口;API 仅提供当前百分比与重置时间,因此为近似值。
|
|
93
94
|
- **模型 ➔ 账户路由规则**:支持 Glob 通配符(如 `*free*`、`deepseek-*`)将模型优先绑定至特定账户,其他账户保留为备用。
|
|
94
95
|
- **配额回退**:在设置中选择免费模型;Go 返回配额错误时,同一请求通过 Console/Zen 重试一次。若回退也失败,返回原始配额错误。
|
|
95
96
|
- **会话粘性账户**:同一会话优先使用上次成功的账户;显式模型规则、固定账户和配额回退优先。
|
|
@@ -116,12 +117,12 @@ npm pack
|
|
|
116
117
|
通过**打包文件 (.tgz)**安装:
|
|
117
118
|
|
|
118
119
|
```bash
|
|
119
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
120
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.3.tgz
|
|
120
121
|
# DSH 桌面端:
|
|
121
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
122
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.3.tgz
|
|
122
123
|
|
|
123
124
|
# 或从 npm 安装已发布版本:
|
|
124
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
125
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.3
|
|
125
126
|
```
|
|
126
127
|
|
|
127
128
|
安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode** 或 **OpenCode Go** 即可。
|
package/lib/client.js
CHANGED
|
@@ -67,6 +67,8 @@ window.__ModuleLoader__.load({
|
|
|
67
67
|
fiveHour: "5-hour",
|
|
68
68
|
weekly: "Weekly",
|
|
69
69
|
monthly: "Monthly",
|
|
70
|
+
runway: "Runway (est.)",
|
|
71
|
+
runwaySafe: "safe",
|
|
70
72
|
noUsage: "Quota is unavailable.",
|
|
71
73
|
usageError: "Quota error",
|
|
72
74
|
refresh: "Refresh",
|
|
@@ -245,6 +247,8 @@ window.__ModuleLoader__.load({
|
|
|
245
247
|
fiveHour: "5 小时",
|
|
246
248
|
weekly: "每周",
|
|
247
249
|
monthly: "每月",
|
|
250
|
+
runway: "预计可用时长",
|
|
251
|
+
runwaySafe: "安全",
|
|
248
252
|
noUsage: "配额不可用。",
|
|
249
253
|
usageError: "配额错误",
|
|
250
254
|
refresh: "刷新",
|
|
@@ -915,6 +919,73 @@ window.__ModuleLoader__.load({
|
|
|
915
919
|
});
|
|
916
920
|
}
|
|
917
921
|
//#endregion
|
|
922
|
+
//#region src/quota-runway.ts
|
|
923
|
+
const HOUR = 36e5;
|
|
924
|
+
const DAY = 24 * HOUR;
|
|
925
|
+
const DURATIONS = {
|
|
926
|
+
fiveHour: 5 * HOUR,
|
|
927
|
+
weekly: 7 * DAY,
|
|
928
|
+
monthly: 30 * DAY
|
|
929
|
+
};
|
|
930
|
+
function percentOf(window) {
|
|
931
|
+
const value = Number(window.percentUsed);
|
|
932
|
+
return Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Estimate when one window reaches 100% at the average burn rate observed
|
|
936
|
+
* since it started. The API exposes only the current percentage and reset
|
|
937
|
+
* time; the start is derived from the known window length, so this is a
|
|
938
|
+
* deliberately conservative, approximate runway.
|
|
939
|
+
*/
|
|
940
|
+
function estimateRunway(window, durationMs, now = Date.now()) {
|
|
941
|
+
if (window === void 0 || window.resetsAt === void 0) return void 0;
|
|
942
|
+
const resetsAtMs = Date.parse(window.resetsAt);
|
|
943
|
+
if (!Number.isFinite(resetsAtMs) || resetsAtMs <= now) return void 0;
|
|
944
|
+
const used = percentOf(window);
|
|
945
|
+
const timeToReset = resetsAtMs - now;
|
|
946
|
+
if (used >= 100) return {
|
|
947
|
+
etaMs: 0,
|
|
948
|
+
resetsAtMs,
|
|
949
|
+
exhaustsBeforeReset: true
|
|
950
|
+
};
|
|
951
|
+
const elapsed = now - (resetsAtMs - durationMs);
|
|
952
|
+
if (elapsed <= 0) return void 0;
|
|
953
|
+
const rate = used / elapsed;
|
|
954
|
+
if (!Number.isFinite(rate) || rate <= 0) return {
|
|
955
|
+
etaMs: Number.POSITIVE_INFINITY,
|
|
956
|
+
resetsAtMs,
|
|
957
|
+
exhaustsBeforeReset: false
|
|
958
|
+
};
|
|
959
|
+
const etaMs = (100 - used) / rate;
|
|
960
|
+
return {
|
|
961
|
+
etaMs,
|
|
962
|
+
resetsAtMs,
|
|
963
|
+
exhaustsBeforeReset: etaMs < timeToReset
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
/** Pick the window that is projected to run out first, if any. */
|
|
967
|
+
function accountRunway(windows, now = Date.now()) {
|
|
968
|
+
if (windows === void 0) return { kind: "unknown" };
|
|
969
|
+
let best;
|
|
970
|
+
let known = 0;
|
|
971
|
+
for (const name of Object.keys(DURATIONS)) {
|
|
972
|
+
const estimate = estimateRunway(windows[name], DURATIONS[name], now);
|
|
973
|
+
if (estimate === void 0) continue;
|
|
974
|
+
known += 1;
|
|
975
|
+
if (!estimate.exhaustsBeforeReset) continue;
|
|
976
|
+
if (best === void 0 || estimate.etaMs < best.etaMs) best = {
|
|
977
|
+
etaMs: estimate.etaMs,
|
|
978
|
+
window: name
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
if (best !== void 0) return {
|
|
982
|
+
kind: "eta",
|
|
983
|
+
etaMs: best.etaMs,
|
|
984
|
+
window: best.window
|
|
985
|
+
};
|
|
986
|
+
return known === 0 ? { kind: "unknown" } : { kind: "safe" };
|
|
987
|
+
}
|
|
988
|
+
//#endregion
|
|
918
989
|
//#region src/client/quota-card.tsx
|
|
919
990
|
const PANEL_EVENT = "dsh-opencode-panel-state";
|
|
920
991
|
function broadcastPanel(open) {
|
|
@@ -969,6 +1040,18 @@ window.__ModuleLoader__.load({
|
|
|
969
1040
|
function usagePercent(window) {
|
|
970
1041
|
return window === void 0 ? "—" : `${Math.round(window.percentUsed)}%`;
|
|
971
1042
|
}
|
|
1043
|
+
function runwayText(summary) {
|
|
1044
|
+
if (summary.kind === "safe") return "safe";
|
|
1045
|
+
if (summary.kind === "unknown") return "—";
|
|
1046
|
+
if (summary.etaMs <= 0) return "now";
|
|
1047
|
+
const hours = summary.etaMs / 36e5;
|
|
1048
|
+
if (hours >= 24) return `~${Math.round(hours / 24)}d`;
|
|
1049
|
+
if (hours >= 1) return `~${Math.round(hours)}h`;
|
|
1050
|
+
return `~${Math.max(1, Math.round(hours * 60))}m`;
|
|
1051
|
+
}
|
|
1052
|
+
function runwayTitle(summary, t) {
|
|
1053
|
+
return summary.kind === "eta" ? `${t("runway")}: ${summary.window}` : "";
|
|
1054
|
+
}
|
|
972
1055
|
function accountStatus(account, t) {
|
|
973
1056
|
if (account.mark !== "") {
|
|
974
1057
|
const label = account.mark === "invalid-credential" ? t("markInvalid") : account.mark === "quota" ? t("markQuota") : account.mark === "rate-limit" ? t("markRateLimit") : t("markTransport");
|
|
@@ -1277,9 +1360,11 @@ window.__ModuleLoader__.load({
|
|
|
1277
1360
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("accountLabel") }),
|
|
1278
1361
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("fiveHour") }),
|
|
1279
1362
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("weekly") }),
|
|
1280
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("monthly") })
|
|
1363
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("monthly") }),
|
|
1364
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("runway") })
|
|
1281
1365
|
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: state.accounts.map((account) => {
|
|
1282
1366
|
const accountWindows = account.usage?.report?.windows;
|
|
1367
|
+
const runway = accountRunway(accountWindows);
|
|
1283
1368
|
const status = accountStatus(account, t);
|
|
1284
1369
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
1285
1370
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [
|
|
@@ -1294,7 +1379,11 @@ window.__ModuleLoader__.load({
|
|
|
1294
1379
|
] }),
|
|
1295
1380
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: usagePercent(accountWindows?.fiveHour) }),
|
|
1296
1381
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: usagePercent(accountWindows?.weekly) }),
|
|
1297
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: usagePercent(accountWindows?.monthly) })
|
|
1382
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: usagePercent(accountWindows?.monthly) }),
|
|
1383
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1384
|
+
title: runwayTitle(runway, t),
|
|
1385
|
+
children: runway.kind === "safe" ? t("runwaySafe") : runwayText(runway)
|
|
1386
|
+
})
|
|
1298
1387
|
] }, account.label);
|
|
1299
1388
|
}) })]
|
|
1300
1389
|
}),
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
|
|
|
17
17
|
const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
|
|
18
18
|
const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
19
19
|
const DEFAULT_AUTH_ENTRY = "opencode-go";
|
|
20
|
-
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.
|
|
20
|
+
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.3";
|
|
21
21
|
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
22
22
|
const DEFAULT_MAX_TOKENS = 32768;
|
|
23
23
|
const DEFAULT_GENERATE_MAX_TOKENS = 32768;
|