@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 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.23.tgz
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.23.tgz
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.23
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.23.tgz
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.23.tgz
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.23
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.23.tgz
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.23.tgz
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.23
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
- await saveConfig(patchOf(draft), draft.revision);
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.23";
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 = ordered[0];
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 ordered.entries()) {
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 < ordered.length - 1;
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 < ordered.length - 1 && transient)) {
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.23",
3
+ "version": "0.5.24",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",