@goodandready/dsh-opencode-provider 0.5.1 → 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 +15 -0
- package/README.md +307 -301
- package/README.ru.md +208 -202
- package/README.zh.md +184 -177
- package/lib/client.js +314 -6
- package/lib/index.d.ts +31 -1
- package/lib/index.js +188 -32
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -38,6 +38,10 @@ window.__ModuleLoader__.load({
|
|
|
38
38
|
accountRef: "Credential ref",
|
|
39
39
|
accountSource: "Source",
|
|
40
40
|
addAccount: "Add account",
|
|
41
|
+
importAllAuth: "Import all from auth.json",
|
|
42
|
+
checkAllAccounts: "Check all accounts",
|
|
43
|
+
importedAll: "Imported all auth.json keys; review the accounts and save.",
|
|
44
|
+
accountsImported: "Imported keys",
|
|
41
45
|
removeAccount: "Remove",
|
|
42
46
|
pinAccount: "Pin",
|
|
43
47
|
activeAccount: "Pinned account",
|
|
@@ -63,6 +67,8 @@ window.__ModuleLoader__.load({
|
|
|
63
67
|
fiveHour: "5-hour",
|
|
64
68
|
weekly: "Weekly",
|
|
65
69
|
monthly: "Monthly",
|
|
70
|
+
runway: "Runway (est.)",
|
|
71
|
+
runwaySafe: "safe",
|
|
66
72
|
noUsage: "Quota is unavailable.",
|
|
67
73
|
usageError: "Quota error",
|
|
68
74
|
refresh: "Refresh",
|
|
@@ -107,6 +113,8 @@ window.__ModuleLoader__.load({
|
|
|
107
113
|
heatmapByHour: "By hour (all days)",
|
|
108
114
|
heatmapAllModels: "All models",
|
|
109
115
|
heatmapModel: "Model",
|
|
116
|
+
heatmapByAccount: "Account",
|
|
117
|
+
heatmapAllAccounts: "All accounts",
|
|
110
118
|
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.",
|
|
111
119
|
analyticsEmpty: "No OpenCode sessions in this window.",
|
|
112
120
|
showAll: "Show all",
|
|
@@ -144,6 +152,21 @@ window.__ModuleLoader__.load({
|
|
|
144
152
|
maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
|
|
145
153
|
contextWindow: "Context override",
|
|
146
154
|
showSidebarQuota: "Show the quota card in the sidebar",
|
|
155
|
+
routing: "Routing",
|
|
156
|
+
routingHint: "Recent account/model choices. No prompts or keys are recorded.",
|
|
157
|
+
routingEmpty: "No routing decisions yet.",
|
|
158
|
+
routingRule: "Model rule",
|
|
159
|
+
routingSticky: "Sticky session",
|
|
160
|
+
routingPinned: "Pinned account",
|
|
161
|
+
routingDefault: "Automatic",
|
|
162
|
+
routingRotation: "Rotation",
|
|
163
|
+
routingFallback: "Quota fallback",
|
|
164
|
+
routingFailed: "Failed",
|
|
165
|
+
quotaFallbackModel: "Quota fallback model",
|
|
166
|
+
quotaFallbackModelHint: "If Go rejects with a quota error, retry once on this free model through Console/Zen.",
|
|
167
|
+
stickySession: "Sticky account per conversation",
|
|
168
|
+
stickySessionHint: "Prefer the last successful account for the same conversation; explicit model rules and pinned accounts win.",
|
|
169
|
+
off: "Off",
|
|
147
170
|
pluginUpdate: "Plugin update",
|
|
148
171
|
currentVersion: "Current version",
|
|
149
172
|
latestVersion: "Latest version",
|
|
@@ -195,6 +218,10 @@ window.__ModuleLoader__.load({
|
|
|
195
218
|
accountRef: "凭据引用",
|
|
196
219
|
accountSource: "来源",
|
|
197
220
|
addAccount: "添加账户",
|
|
221
|
+
importAllAuth: "从 auth.json 导入全部",
|
|
222
|
+
checkAllAccounts: "检查全部账户",
|
|
223
|
+
importedAll: "已导入 auth.json 全部密钥;请检查账户并保存。",
|
|
224
|
+
accountsImported: "已导入密钥",
|
|
198
225
|
removeAccount: "移除",
|
|
199
226
|
pinAccount: "固定",
|
|
200
227
|
activeAccount: "固定账户",
|
|
@@ -220,6 +247,8 @@ window.__ModuleLoader__.load({
|
|
|
220
247
|
fiveHour: "5 小时",
|
|
221
248
|
weekly: "每周",
|
|
222
249
|
monthly: "每月",
|
|
250
|
+
runway: "预计可用时长",
|
|
251
|
+
runwaySafe: "安全",
|
|
223
252
|
noUsage: "配额不可用。",
|
|
224
253
|
usageError: "配额错误",
|
|
225
254
|
refresh: "刷新",
|
|
@@ -264,6 +293,8 @@ window.__ModuleLoader__.load({
|
|
|
264
293
|
heatmapByHour: "按小时(全部日期)",
|
|
265
294
|
heatmapAllModels: "全部模型",
|
|
266
295
|
heatmapModel: "模型",
|
|
296
|
+
heatmapByAccount: "账户",
|
|
297
|
+
heatmapAllAccounts: "全部账户",
|
|
267
298
|
analyticsHint: "按最后活动时间聚合本地会话元数据。切换过模型的会话计入最后使用的模型;本地投影不暴露失败。",
|
|
268
299
|
analyticsEmpty: "此窗口内没有 OpenCode 会话。",
|
|
269
300
|
showAll: "显示全部",
|
|
@@ -301,6 +332,21 @@ window.__ModuleLoader__.load({
|
|
|
301
332
|
maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
|
|
302
333
|
contextWindow: "上下文覆盖",
|
|
303
334
|
showSidebarQuota: "在侧边栏显示配额卡片",
|
|
335
|
+
routing: "路由",
|
|
336
|
+
routingHint: "最近使用的账户/模型选择。不会记录提示词或密钥。",
|
|
337
|
+
routingEmpty: "尚无路由决策。",
|
|
338
|
+
routingRule: "模型规则",
|
|
339
|
+
routingSticky: "会话粘性",
|
|
340
|
+
routingPinned: "固定账户",
|
|
341
|
+
routingDefault: "自动",
|
|
342
|
+
routingRotation: "轮换",
|
|
343
|
+
routingFallback: "配额回退",
|
|
344
|
+
routingFailed: "失败",
|
|
345
|
+
quotaFallbackModel: "配额回退模型",
|
|
346
|
+
quotaFallbackModelHint: "Go 返回配额错误时,通过 Console/Zen 用此免费模型重试一次。",
|
|
347
|
+
stickySession: "每个会话固定账户",
|
|
348
|
+
stickySessionHint: "同一会话优先使用上次成功的账户;显式模型规则和固定账户优先。",
|
|
349
|
+
off: "关闭",
|
|
304
350
|
pluginUpdate: "插件更新",
|
|
305
351
|
currentVersion: "当前版本",
|
|
306
352
|
latestVersion: "最新版本",
|
|
@@ -386,10 +432,13 @@ window.__ModuleLoader__.load({
|
|
|
386
432
|
body: JSON.stringify({ ref })
|
|
387
433
|
});
|
|
388
434
|
}
|
|
389
|
-
function importAuth(entry) {
|
|
435
|
+
function importAuth(entry, ref) {
|
|
390
436
|
return call("/dsh-opencode/auth-import", {
|
|
391
437
|
method: "POST",
|
|
392
|
-
body: JSON.stringify({ entry }
|
|
438
|
+
body: JSON.stringify(ref === void 0 ? { entry } : {
|
|
439
|
+
entry,
|
|
440
|
+
ref
|
|
441
|
+
})
|
|
393
442
|
});
|
|
394
443
|
}
|
|
395
444
|
function fetchAnalytics(days = 30) {
|
|
@@ -429,10 +478,12 @@ window.__ModuleLoader__.load({
|
|
|
429
478
|
function RequestHeatmapView(props) {
|
|
430
479
|
const { heatmap, t } = props;
|
|
431
480
|
const [selected, setSelected] = (0, react.useState)("all");
|
|
481
|
+
const [selectedAccount, setSelectedAccount] = (0, react.useState)("all");
|
|
432
482
|
if (heatmap === void 0) return null;
|
|
433
483
|
const models = heatmap.models ?? [];
|
|
434
|
-
const
|
|
435
|
-
const
|
|
484
|
+
const accounts = heatmap.accounts ?? [];
|
|
485
|
+
const selectedModel = selectedAccount === "all" ? models.find((entry) => `${entry.provider}/${entry.model}` === selected) : void 0;
|
|
486
|
+
const view = accounts.find((entry) => entry.account === selectedAccount) ?? selectedModel ?? heatmap;
|
|
436
487
|
const selectedKey = selectedModel === void 0 ? "all" : selected;
|
|
437
488
|
const max = Math.max(0, ...view.grid.flat());
|
|
438
489
|
const maxHours = Math.max(0, ...view.hours);
|
|
@@ -452,10 +503,37 @@ window.__ModuleLoader__.load({
|
|
|
452
503
|
t("requests")
|
|
453
504
|
]
|
|
454
505
|
}),
|
|
506
|
+
accounts.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
507
|
+
className: "oc-select",
|
|
508
|
+
value: selectedAccount,
|
|
509
|
+
title: t("heatmapByAccount"),
|
|
510
|
+
onChange: (event) => {
|
|
511
|
+
setSelectedAccount(event.target.value);
|
|
512
|
+
setSelected("all");
|
|
513
|
+
},
|
|
514
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
515
|
+
value: "all",
|
|
516
|
+
children: [
|
|
517
|
+
t("heatmapAllAccounts"),
|
|
518
|
+
" (",
|
|
519
|
+
heatmap.total,
|
|
520
|
+
")"
|
|
521
|
+
]
|
|
522
|
+
}), accounts.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
523
|
+
value: entry.account,
|
|
524
|
+
children: [
|
|
525
|
+
entry.account,
|
|
526
|
+
" (",
|
|
527
|
+
entry.total,
|
|
528
|
+
")"
|
|
529
|
+
]
|
|
530
|
+
}, entry.account))]
|
|
531
|
+
}) : null,
|
|
455
532
|
models.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
456
533
|
className: "oc-select",
|
|
457
534
|
value: selectedKey,
|
|
458
535
|
title: t("heatmapModel"),
|
|
536
|
+
disabled: selectedAccount !== "all",
|
|
459
537
|
onChange: (event) => setSelected(event.target.value),
|
|
460
538
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
461
539
|
value: "all",
|
|
@@ -841,6 +919,73 @@ window.__ModuleLoader__.load({
|
|
|
841
919
|
});
|
|
842
920
|
}
|
|
843
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
|
|
844
989
|
//#region src/client/quota-card.tsx
|
|
845
990
|
const PANEL_EVENT = "dsh-opencode-panel-state";
|
|
846
991
|
function broadcastPanel(open) {
|
|
@@ -895,6 +1040,18 @@ window.__ModuleLoader__.load({
|
|
|
895
1040
|
function usagePercent(window) {
|
|
896
1041
|
return window === void 0 ? "—" : `${Math.round(window.percentUsed)}%`;
|
|
897
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
|
+
}
|
|
898
1055
|
function accountStatus(account, t) {
|
|
899
1056
|
if (account.mark !== "") {
|
|
900
1057
|
const label = account.mark === "invalid-credential" ? t("markInvalid") : account.mark === "quota" ? t("markQuota") : account.mark === "rate-limit" ? t("markRateLimit") : t("markTransport");
|
|
@@ -907,6 +1064,14 @@ window.__ModuleLoader__.load({
|
|
|
907
1064
|
windows?.monthly
|
|
908
1065
|
].map((window) => window?.status).find((value) => value !== void 0 && value !== "ok");
|
|
909
1066
|
}
|
|
1067
|
+
function routingLabel(reason, t) {
|
|
1068
|
+
if (reason === "rule") return t("routingRule");
|
|
1069
|
+
if (reason === "sticky") return t("routingSticky");
|
|
1070
|
+
if (reason === "pinned") return t("routingPinned");
|
|
1071
|
+
if (reason === "rotation") return t("routingRotation");
|
|
1072
|
+
if (reason === "quota-fallback") return t("routingFallback");
|
|
1073
|
+
return t("routingDefault");
|
|
1074
|
+
}
|
|
910
1075
|
function fillClass(value) {
|
|
911
1076
|
if (value >= 90) return "oc-footFill oc-footFillBad";
|
|
912
1077
|
if (value >= 75) return "oc-footFill oc-footFillWarn";
|
|
@@ -1195,9 +1360,11 @@ window.__ModuleLoader__.load({
|
|
|
1195
1360
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("accountLabel") }),
|
|
1196
1361
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("fiveHour") }),
|
|
1197
1362
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("weekly") }),
|
|
1198
|
-
/* @__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") })
|
|
1199
1365
|
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: state.accounts.map((account) => {
|
|
1200
1366
|
const accountWindows = account.usage?.report?.windows;
|
|
1367
|
+
const runway = accountRunway(accountWindows);
|
|
1201
1368
|
const status = accountStatus(account, t);
|
|
1202
1369
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
1203
1370
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [
|
|
@@ -1212,7 +1379,11 @@ window.__ModuleLoader__.load({
|
|
|
1212
1379
|
] }),
|
|
1213
1380
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: usagePercent(accountWindows?.fiveHour) }),
|
|
1214
1381
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: usagePercent(accountWindows?.weekly) }),
|
|
1215
|
-
/* @__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
|
+
})
|
|
1216
1387
|
] }, account.label);
|
|
1217
1388
|
}) })]
|
|
1218
1389
|
}),
|
|
@@ -1222,6 +1393,43 @@ window.__ModuleLoader__.load({
|
|
|
1222
1393
|
})
|
|
1223
1394
|
]
|
|
1224
1395
|
}),
|
|
1396
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1397
|
+
className: "oc-card",
|
|
1398
|
+
children: [
|
|
1399
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1400
|
+
className: "oc-card-title",
|
|
1401
|
+
children: t("routing")
|
|
1402
|
+
}),
|
|
1403
|
+
(state?.routing ?? []).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1404
|
+
className: "oc-hint",
|
|
1405
|
+
children: t("routingEmpty")
|
|
1406
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1407
|
+
className: "oc-table-scroll",
|
|
1408
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
1409
|
+
className: "oc-table",
|
|
1410
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
1411
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("date") }),
|
|
1412
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("accountLabel") }),
|
|
1413
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("model") }),
|
|
1414
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("routing") })
|
|
1415
|
+
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: (state?.routing ?? []).slice(0, 8).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
1416
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: new Date(row.at).toLocaleTimeString() }),
|
|
1417
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.account }),
|
|
1418
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.model }),
|
|
1419
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [
|
|
1420
|
+
routingLabel(row.reason, t),
|
|
1421
|
+
row.fallback ? ` · ${t("routingFallback")}` : "",
|
|
1422
|
+
row.ok ? "" : ` · ${t("routingFailed")}${row.error === void 0 ? "" : ` (${row.error})`}`
|
|
1423
|
+
] })
|
|
1424
|
+
] }, `${row.at}-${row.account}-${row.model}-${row.reason}`)) })]
|
|
1425
|
+
})
|
|
1426
|
+
}),
|
|
1427
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1428
|
+
className: "oc-hint",
|
|
1429
|
+
children: t("routingHint")
|
|
1430
|
+
})
|
|
1431
|
+
]
|
|
1432
|
+
}),
|
|
1225
1433
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1226
1434
|
className: "oc-card",
|
|
1227
1435
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1329,6 +1537,8 @@ window.__ModuleLoader__.load({
|
|
|
1329
1537
|
activeAccount: state.config.activeAccount,
|
|
1330
1538
|
accounts: state.config.accounts.map((account) => ({ ...account })),
|
|
1331
1539
|
modelAccountRules: state.config.modelAccountRules.map((rule) => ({ ...rule })),
|
|
1540
|
+
quotaFallbackModel: state.config.quotaFallbackModel,
|
|
1541
|
+
stickySession: state.config.stickySession,
|
|
1332
1542
|
visibleModels: state.config.visibleModels.length === 0 ? allIds : [...state.config.visibleModels],
|
|
1333
1543
|
filterModelsByPlan: state.config.filterModelsByPlan,
|
|
1334
1544
|
showAll: state.config.visibleModels.length === 0,
|
|
@@ -1364,6 +1574,8 @@ window.__ModuleLoader__.load({
|
|
|
1364
1574
|
pattern: rule.pattern.trim(),
|
|
1365
1575
|
account: rule.account.trim()
|
|
1366
1576
|
})).filter((rule) => rule.pattern !== "" && rule.account !== ""),
|
|
1577
|
+
quotaFallbackModel: draft.quotaFallbackModel.trim(),
|
|
1578
|
+
stickySession: draft.stickySession,
|
|
1367
1579
|
visibleModels: draft.showAll ? [] : draft.visibleModels,
|
|
1368
1580
|
filterModelsByPlan: draft.filterModelsByPlan,
|
|
1369
1581
|
modelTransports: Object.fromEntries(Object.entries(draft.modelTransports).filter(([, value]) => value !== ""))
|
|
@@ -1560,6 +1772,7 @@ window.__ModuleLoader__.load({
|
|
|
1560
1772
|
const [testModel, setTestModel] = (0, react.useState)("deepseek-v4.1-flash");
|
|
1561
1773
|
const [testing, setTesting] = (0, react.useState)(false);
|
|
1562
1774
|
const [testResult, setTestResult] = (0, react.useState)(null);
|
|
1775
|
+
const [importing, setImporting] = (0, react.useState)(false);
|
|
1563
1776
|
const load = (0, react.useCallback)(async () => {
|
|
1564
1777
|
setLoading(true);
|
|
1565
1778
|
try {
|
|
@@ -1610,6 +1823,29 @@ window.__ModuleLoader__.load({
|
|
|
1610
1823
|
setError(caught instanceof Error ? caught.message : t("failed"));
|
|
1611
1824
|
}
|
|
1612
1825
|
};
|
|
1826
|
+
const importAllAuth = async () => {
|
|
1827
|
+
if (state === null || draft === null) return;
|
|
1828
|
+
const refFor = (entry) => `OPENCODE_AUTH_${entry.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}`;
|
|
1829
|
+
const missing = state.auth.entries.filter((entry) => !draft.accounts.some((account) => account.label === entry || account.apiKeyEnv === refFor(entry)));
|
|
1830
|
+
if (missing.length === 0) {
|
|
1831
|
+
setNotice(t("importedAll"));
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
setImporting(true);
|
|
1835
|
+
setError(null);
|
|
1836
|
+
try {
|
|
1837
|
+
for (const entry of missing) await importAuth(entry, refFor(entry));
|
|
1838
|
+
update({ accounts: [...draft.accounts, ...missing.map((entry) => ({
|
|
1839
|
+
label: entry,
|
|
1840
|
+
apiKeyEnv: refFor(entry)
|
|
1841
|
+
}))] });
|
|
1842
|
+
setNotice(t("importedAll"));
|
|
1843
|
+
} catch (caught) {
|
|
1844
|
+
setError(caught instanceof Error ? caught.message : t("failed"));
|
|
1845
|
+
} finally {
|
|
1846
|
+
setImporting(false);
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1613
1849
|
if (loading && state === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1614
1850
|
className: "oc-hint",
|
|
1615
1851
|
children: t("loading")
|
|
@@ -2126,6 +2362,23 @@ window.__ModuleLoader__.load({
|
|
|
2126
2362
|
}] }),
|
|
2127
2363
|
children: t("addAccount")
|
|
2128
2364
|
}),
|
|
2365
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2366
|
+
type: "button",
|
|
2367
|
+
className: "oc-button",
|
|
2368
|
+
disabled: importing,
|
|
2369
|
+
onClick: () => {
|
|
2370
|
+
importAllAuth();
|
|
2371
|
+
},
|
|
2372
|
+
children: importing ? t("loading") : t("importAllAuth")
|
|
2373
|
+
}),
|
|
2374
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2375
|
+
type: "button",
|
|
2376
|
+
className: "oc-button",
|
|
2377
|
+
onClick: () => {
|
|
2378
|
+
refresh();
|
|
2379
|
+
},
|
|
2380
|
+
children: t("checkAllAccounts")
|
|
2381
|
+
}),
|
|
2129
2382
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2130
2383
|
className: "oc-badge",
|
|
2131
2384
|
children: [
|
|
@@ -2206,6 +2459,61 @@ window.__ModuleLoader__.load({
|
|
|
2206
2459
|
})
|
|
2207
2460
|
]
|
|
2208
2461
|
}),
|
|
2462
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2463
|
+
className: "oc-card",
|
|
2464
|
+
children: [
|
|
2465
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2466
|
+
className: "oc-card-title",
|
|
2467
|
+
children: t("routing")
|
|
2468
|
+
}),
|
|
2469
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2470
|
+
className: "oc-hint",
|
|
2471
|
+
children: t("routingHint")
|
|
2472
|
+
}),
|
|
2473
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2474
|
+
className: "oc-row",
|
|
2475
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2476
|
+
type: "checkbox",
|
|
2477
|
+
checked: draft.stickySession,
|
|
2478
|
+
onChange: (event) => update({ stickySession: event.target.checked })
|
|
2479
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("stickySession") })]
|
|
2480
|
+
}),
|
|
2481
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2482
|
+
className: "oc-hint",
|
|
2483
|
+
children: t("stickySessionHint")
|
|
2484
|
+
}),
|
|
2485
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2486
|
+
className: "oc-field",
|
|
2487
|
+
children: [
|
|
2488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2489
|
+
className: "oc-label",
|
|
2490
|
+
children: t("quotaFallbackModel")
|
|
2491
|
+
}),
|
|
2492
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2493
|
+
className: "oc-select",
|
|
2494
|
+
value: draft.quotaFallbackModel,
|
|
2495
|
+
onChange: (event) => update({ quotaFallbackModel: event.target.value }),
|
|
2496
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2497
|
+
value: "",
|
|
2498
|
+
children: t("off")
|
|
2499
|
+
}), state.models.filter((model) => model.free).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
2500
|
+
value: model.id,
|
|
2501
|
+
children: [
|
|
2502
|
+
model.name,
|
|
2503
|
+
" (",
|
|
2504
|
+
model.id,
|
|
2505
|
+
")"
|
|
2506
|
+
]
|
|
2507
|
+
}, model.id))]
|
|
2508
|
+
}),
|
|
2509
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2510
|
+
className: "oc-hint",
|
|
2511
|
+
children: t("quotaFallbackModelHint")
|
|
2512
|
+
})
|
|
2513
|
+
]
|
|
2514
|
+
})
|
|
2515
|
+
]
|
|
2516
|
+
}),
|
|
2209
2517
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2210
2518
|
className: "oc-card",
|
|
2211
2519
|
children: [
|
package/lib/index.d.ts
CHANGED
|
@@ -55,6 +55,10 @@ interface Config {
|
|
|
55
55
|
accounts?: AccountConfig[];
|
|
56
56
|
/** Preferred account per model glob; other accounts remain fallbacks. */
|
|
57
57
|
modelAccountRules?: ModelAccountRule[];
|
|
58
|
+
/** Free model used once when the Go quota rejects a request; empty disables it. */
|
|
59
|
+
quotaFallbackModel?: string;
|
|
60
|
+
/** Prefer the last successful account for a conversation. */
|
|
61
|
+
stickySession?: boolean;
|
|
58
62
|
/** Pinned account id; empty means automatic. */
|
|
59
63
|
activeAccount?: string;
|
|
60
64
|
/** Show the quota card in the sidebar. */
|
|
@@ -82,6 +86,8 @@ interface ResolvedConfig {
|
|
|
82
86
|
maxOutputTokens?: number;
|
|
83
87
|
accounts: readonly AccountConfig[];
|
|
84
88
|
modelAccountRules: readonly ModelAccountRule[];
|
|
89
|
+
quotaFallbackModel: string;
|
|
90
|
+
stickySession: boolean;
|
|
85
91
|
activeAccount: string;
|
|
86
92
|
showSidebarQuota: boolean;
|
|
87
93
|
userAgent: string;
|
|
@@ -111,6 +117,17 @@ declare function createKeyPool(ctx: Context, config: () => ResolvedConfig): KeyP
|
|
|
111
117
|
declare function createKeyResolver(ctx: Context, config: () => ResolvedConfig): KeyResolver;
|
|
112
118
|
//#endregion
|
|
113
119
|
//#region src/adapter.d.ts
|
|
120
|
+
type RoutingReason = 'rule' | 'sticky' | 'pinned' | 'default' | 'rotation' | 'quota-fallback';
|
|
121
|
+
interface RoutingTraceEntry {
|
|
122
|
+
readonly at: number;
|
|
123
|
+
readonly provider: string;
|
|
124
|
+
readonly model: string;
|
|
125
|
+
readonly account: string;
|
|
126
|
+
readonly reason: RoutingReason;
|
|
127
|
+
readonly fallback: boolean;
|
|
128
|
+
readonly ok: boolean;
|
|
129
|
+
readonly error?: string;
|
|
130
|
+
}
|
|
114
131
|
interface OpenCodeAdapterDeps {
|
|
115
132
|
readonly config: () => ResolvedConfig;
|
|
116
133
|
readonly resolveKey: KeyResolver;
|
|
@@ -118,7 +135,7 @@ interface OpenCodeAdapterDeps {
|
|
|
118
135
|
readonly resolveAttachments?: () => AttachmentStore | undefined;
|
|
119
136
|
readonly fetchImpl?: typeof fetch;
|
|
120
137
|
/** Test seam; defaults to the local append-only request log. */
|
|
121
|
-
readonly recordRequest?: (provider: string, model: string, purpose?: string) => void;
|
|
138
|
+
readonly recordRequest?: (provider: string, model: string, purpose?: string, account?: string, reason?: string) => void;
|
|
122
139
|
/** Test seam; defaults to ~/.dsh/storages/dsh-opencode/cooldowns.json. */
|
|
123
140
|
readonly cooldownPath?: string;
|
|
124
141
|
/** Receives a one-time warning when the provider tool limit drops tools. */
|
|
@@ -131,6 +148,8 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
131
148
|
private cooldownWrites;
|
|
132
149
|
/** key fingerprint → cooldown deadline and the rejection that caused it. */
|
|
133
150
|
private readonly cooldowns;
|
|
151
|
+
private readonly sticky;
|
|
152
|
+
private readonly routing;
|
|
134
153
|
private warnedTools;
|
|
135
154
|
constructor(deps: OpenCodeAdapterDeps);
|
|
136
155
|
providerInfo(provider: string): LlmProviderInfo;
|
|
@@ -138,9 +157,13 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
138
157
|
listModels(provider: string, opts?: {
|
|
139
158
|
unfiltered?: boolean;
|
|
140
159
|
}): Promise<readonly LlmModelInfo[]>;
|
|
160
|
+
/** Recent routing decisions, newest first; no keys or prompt content. */
|
|
161
|
+
routingTrace(): readonly RoutingTraceEntry[];
|
|
141
162
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
142
163
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
143
164
|
private run;
|
|
165
|
+
private rememberSticky;
|
|
166
|
+
private pushTrace;
|
|
144
167
|
private parse;
|
|
145
168
|
private send;
|
|
146
169
|
private keyId;
|
|
@@ -292,6 +315,12 @@ interface ModelRequestHeatmap {
|
|
|
292
315
|
hours: number[];
|
|
293
316
|
grid: number[][];
|
|
294
317
|
}
|
|
318
|
+
interface AccountRequestHeatmap {
|
|
319
|
+
account: string;
|
|
320
|
+
total: number;
|
|
321
|
+
hours: number[];
|
|
322
|
+
grid: number[][];
|
|
323
|
+
}
|
|
295
324
|
interface RequestHeatmap {
|
|
296
325
|
since: number;
|
|
297
326
|
total: number;
|
|
@@ -302,6 +331,7 @@ interface RequestHeatmap {
|
|
|
302
331
|
firstAt: number | null;
|
|
303
332
|
lastAt: number | null;
|
|
304
333
|
models: ModelRequestHeatmap[];
|
|
334
|
+
accounts: AccountRequestHeatmap[];
|
|
305
335
|
}
|
|
306
336
|
//#endregion
|
|
307
337
|
//#region src/routes.d.ts
|