@goodandready/dsh-opencode-provider 0.5.23 → 0.5.24
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 +6 -0
- package/README.md +3 -3
- package/README.ru.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +48 -2
- package/lib/index.d.ts +5 -0
- package/lib/index.js +69 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.24 — release 1: account safety
|
|
4
|
+
|
|
5
|
+
- **#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.
|
|
6
|
+
- **#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.
|
|
7
|
+
- 126 tests.
|
|
8
|
+
|
|
3
9
|
## 0.5.23
|
|
4
10
|
|
|
5
11
|
- **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.24.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.24.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.24
|
|
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.24.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.24.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.24
|
|
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.24.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.24.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.24
|
|
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",
|
|
@@ -197,6 +200,8 @@ window.__ModuleLoader__.load({
|
|
|
197
200
|
routingFailed: "Failed",
|
|
198
201
|
quotaFallbackModel: "Quota fallback model",
|
|
199
202
|
quotaFallbackModelHint: "If Go rejects with a quota error, retry once on this free model through Console/Zen.",
|
|
203
|
+
autoSwitch: "Automatic account failover",
|
|
204
|
+
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
205
|
stickySession: "Sticky account per conversation",
|
|
201
206
|
stickySessionHint: "Prefer the last successful account for the same conversation; explicit model rules and pinned accounts win.",
|
|
202
207
|
off: "Off",
|
|
@@ -278,6 +283,9 @@ window.__ModuleLoader__.load({
|
|
|
278
283
|
dragAccount: "拖动以调整账户顺序",
|
|
279
284
|
accountLimit: "最多允许 20 个账户。",
|
|
280
285
|
removeAccount: "移除",
|
|
286
|
+
removeAccountConfirm: "移除此账户并删除插件管理的凭据 {ref}?此操作无法撤销。",
|
|
287
|
+
managedKey: "插件管理的密钥",
|
|
288
|
+
accountCleanupFailed: "账户已移除,但凭据清理失败:",
|
|
281
289
|
pinAccount: "固定",
|
|
282
290
|
activeAccount: "固定账户",
|
|
283
291
|
activeAccountNow: "当前账户",
|
|
@@ -410,6 +418,8 @@ window.__ModuleLoader__.load({
|
|
|
410
418
|
routingFailed: "失败",
|
|
411
419
|
quotaFallbackModel: "配额回退模型",
|
|
412
420
|
quotaFallbackModelHint: "Go 返回配额错误时,通过 Console/Zen 用此免费模型重试一次。",
|
|
421
|
+
autoSwitch: "自动切换账户",
|
|
422
|
+
autoSwitchHint: "当前账户额度耗尽或凭据失败时,在输出前尝试下一个账户。关闭后只使用选定账户。",
|
|
413
423
|
stickySession: "每个会话固定账户",
|
|
414
424
|
stickySessionHint: "同一会话优先使用上次成功的账户;显式模型规则和固定账户优先。",
|
|
415
425
|
off: "关闭",
|
|
@@ -1653,6 +1663,7 @@ window.__ModuleLoader__.load({
|
|
|
1653
1663
|
accounts: state.config.accounts.map((account) => ({ ...account })),
|
|
1654
1664
|
modelAccountRules: state.config.modelAccountRules.map((rule) => ({ ...rule })),
|
|
1655
1665
|
quotaFallbackModel: state.config.quotaFallbackModel,
|
|
1666
|
+
autoSwitch: state.config.autoSwitch !== false,
|
|
1656
1667
|
stickySession: state.config.stickySession,
|
|
1657
1668
|
visibleModels: state.config.visibleModels.length === 0 ? allIds : [...state.config.visibleModels],
|
|
1658
1669
|
filterModelsByPlan: state.config.filterModelsByPlan,
|
|
@@ -1704,6 +1715,7 @@ window.__ModuleLoader__.load({
|
|
|
1704
1715
|
account: rule.account.trim()
|
|
1705
1716
|
})).filter((rule) => rule.pattern !== "" && rule.account !== ""),
|
|
1706
1717
|
quotaFallbackModel: draft.quotaFallbackModel.trim(),
|
|
1718
|
+
autoSwitch: draft.autoSwitch,
|
|
1707
1719
|
stickySession: draft.stickySession,
|
|
1708
1720
|
visibleModels: draft.showAll ? [] : draft.visibleModels,
|
|
1709
1721
|
filterModelsByPlan: draft.filterModelsByPlan,
|
|
@@ -1935,6 +1947,7 @@ window.__ModuleLoader__.load({
|
|
|
1935
1947
|
const [importing, setImporting] = (0, react.useState)(false);
|
|
1936
1948
|
const [checking, setChecking] = (0, react.useState)(null);
|
|
1937
1949
|
const [dragFrom, setDragFrom] = (0, react.useState)(null);
|
|
1950
|
+
const [pendingCleanup, setPendingCleanup] = (0, react.useState)([]);
|
|
1938
1951
|
const load = (0, react.useCallback)(async () => {
|
|
1939
1952
|
setLoading(true);
|
|
1940
1953
|
try {
|
|
@@ -1964,12 +1977,25 @@ window.__ModuleLoader__.load({
|
|
|
1964
1977
|
setSaving(true);
|
|
1965
1978
|
setError(null);
|
|
1966
1979
|
try {
|
|
1967
|
-
|
|
1980
|
+
const patch = patchOf(draft);
|
|
1981
|
+
await saveConfig(patch, draft.revision);
|
|
1982
|
+
const stillUsed = new Set(patch.accounts.map((account) => account.apiKeyEnv?.trim() ?? "").filter((ref) => ref !== ""));
|
|
1983
|
+
let cleanupError = null;
|
|
1984
|
+
for (const ref of pendingCleanup) {
|
|
1985
|
+
if (stillUsed.has(ref)) continue;
|
|
1986
|
+
try {
|
|
1987
|
+
await clearKey(ref);
|
|
1988
|
+
} catch (caught) {
|
|
1989
|
+
cleanupError = caught instanceof Error ? caught.message : String(caught);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
setPendingCleanup([]);
|
|
1968
1993
|
window.dispatchEvent(new CustomEvent(CONFIG_CHANGED_EVENT));
|
|
1969
1994
|
setNotice(t("saved"));
|
|
1970
1995
|
setState(null);
|
|
1971
1996
|
setDraft(null);
|
|
1972
1997
|
await load();
|
|
1998
|
+
if (cleanupError !== null) setError(`${t("accountCleanupFailed")} ${cleanupError}`);
|
|
1973
1999
|
} catch (caught) {
|
|
1974
2000
|
setError(caught instanceof Error ? caught.message : t("saveFailed"));
|
|
1975
2001
|
} finally {
|
|
@@ -2592,7 +2618,11 @@ window.__ModuleLoader__.load({
|
|
|
2592
2618
|
known === void 0 || known.mark === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2593
2619
|
className: "oc-badge oc-badge-bad",
|
|
2594
2620
|
children: accountMark(known, t)
|
|
2595
|
-
})
|
|
2621
|
+
}),
|
|
2622
|
+
known?.managed === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2623
|
+
className: "oc-badge",
|
|
2624
|
+
children: t("managedKey")
|
|
2625
|
+
}) : null
|
|
2596
2626
|
]
|
|
2597
2627
|
}),
|
|
2598
2628
|
known?.usage?.report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2661,6 +2691,10 @@ window.__ModuleLoader__.load({
|
|
|
2661
2691
|
type: "button",
|
|
2662
2692
|
className: "oc-button oc-button-danger",
|
|
2663
2693
|
onClick: () => {
|
|
2694
|
+
const ref = account.apiKeyEnv.trim();
|
|
2695
|
+
const managed = known?.managed === true && ref !== "";
|
|
2696
|
+
if (managed && !window.confirm(t("removeAccountConfirm").replace("{ref}", ref))) return;
|
|
2697
|
+
if (managed) setPendingCleanup((current) => current.includes(ref) ? current : [...current, ref]);
|
|
2664
2698
|
setKeyDrafts({});
|
|
2665
2699
|
update({ accounts: draft.accounts.filter((_, i) => i !== index) });
|
|
2666
2700
|
},
|
|
@@ -2796,6 +2830,18 @@ window.__ModuleLoader__.load({
|
|
|
2796
2830
|
className: "oc-hint",
|
|
2797
2831
|
children: t("routingHint")
|
|
2798
2832
|
}),
|
|
2833
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2834
|
+
className: "oc-row",
|
|
2835
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2836
|
+
type: "checkbox",
|
|
2837
|
+
checked: draft.autoSwitch,
|
|
2838
|
+
onChange: (event) => update({ autoSwitch: event.target.checked })
|
|
2839
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("autoSwitch") })]
|
|
2840
|
+
}),
|
|
2841
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2842
|
+
className: "oc-hint",
|
|
2843
|
+
children: t("autoSwitchHint")
|
|
2844
|
+
}),
|
|
2799
2845
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2800
2846
|
className: "oc-row",
|
|
2801
2847
|
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;
|
|
@@ -373,6 +376,8 @@ interface RouteDeps {
|
|
|
373
376
|
readonly analytics?: (days: number) => Promise<AnalyticsReport>;
|
|
374
377
|
readonly heatmap?: (days: number) => Promise<RequestHeatmap>;
|
|
375
378
|
readonly refreshTui?: () => void;
|
|
379
|
+
/** Test seam; defaults to the local managed-credentials registry. */
|
|
380
|
+
readonly managedPath?: string;
|
|
376
381
|
}
|
|
377
382
|
/** Register the Host HTTP surface consumed by the browser settings page. */
|
|
378
383
|
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.24";
|
|
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 = [];
|
|
@@ -5125,6 +5170,7 @@ function applyRoutes(ctx, web, deps) {
|
|
|
5125
5170
|
return;
|
|
5126
5171
|
}
|
|
5127
5172
|
await credentials.set(credentialRef(refName), key);
|
|
5173
|
+
await addManagedCredentialRef(refName, deps.managedPath);
|
|
5128
5174
|
json(res, 200, {
|
|
5129
5175
|
ok: true,
|
|
5130
5176
|
ref: refName
|
|
@@ -5144,6 +5190,7 @@ function applyRoutes(ctx, web, deps) {
|
|
|
5144
5190
|
const body = await readJson(req);
|
|
5145
5191
|
const refName = typeof body.ref === "string" && body.ref.trim() !== "" ? body.ref.trim() : deps.config().apiKeyEnv;
|
|
5146
5192
|
await credentials.unset(credentialRef(refName));
|
|
5193
|
+
await removeManagedCredentialRef(refName, deps.managedPath);
|
|
5147
5194
|
json(res, 200, {
|
|
5148
5195
|
ok: true,
|
|
5149
5196
|
ref: refName
|
|
@@ -5169,6 +5216,7 @@ function applyRoutes(ctx, web, deps) {
|
|
|
5169
5216
|
return;
|
|
5170
5217
|
}
|
|
5171
5218
|
await credentials.set(credentialRef(ref), key);
|
|
5219
|
+
await addManagedCredentialRef(ref, deps.managedPath);
|
|
5172
5220
|
json(res, 200, {
|
|
5173
5221
|
ok: true,
|
|
5174
5222
|
entry,
|
|
@@ -5404,6 +5452,18 @@ function buildOpenCodeTuiSection(deps) {
|
|
|
5404
5452
|
format: () => deps.quotaFallbackModel(),
|
|
5405
5453
|
parse: setOrClear
|
|
5406
5454
|
},
|
|
5455
|
+
{
|
|
5456
|
+
path: ["autoSwitch"],
|
|
5457
|
+
group: "advanced",
|
|
5458
|
+
kind: "boolean",
|
|
5459
|
+
label: "Automatic account failover",
|
|
5460
|
+
hint: "Try the next account before output on quota or credential failure.",
|
|
5461
|
+
format: (value) => String(value === void 0 ? deps.autoSwitch() : value !== false),
|
|
5462
|
+
parse: (text) => ({
|
|
5463
|
+
kind: "set",
|
|
5464
|
+
value: text.trim() === "true"
|
|
5465
|
+
})
|
|
5466
|
+
},
|
|
5407
5467
|
{
|
|
5408
5468
|
path: ["stickySession"],
|
|
5409
5469
|
group: "advanced",
|
|
@@ -5711,6 +5771,7 @@ function apply(ctx, config) {
|
|
|
5711
5771
|
activeAccount: () => current().activeAccount,
|
|
5712
5772
|
filterModelsByPlan: () => current().filterModelsByPlan,
|
|
5713
5773
|
quotaFallbackModel: () => current().quotaFallbackModel,
|
|
5774
|
+
autoSwitch: () => current().autoSwitch,
|
|
5714
5775
|
stickySession: () => current().stickySession,
|
|
5715
5776
|
showSidebarQuota: () => current().showSidebarQuota
|
|
5716
5777
|
});
|