@goodandready/dsh-opencode-provider 0.5.0 → 0.5.2

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/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",
@@ -107,6 +111,8 @@ window.__ModuleLoader__.load({
107
111
  heatmapByHour: "By hour (all days)",
108
112
  heatmapAllModels: "All models",
109
113
  heatmapModel: "Model",
114
+ heatmapByAccount: "Account",
115
+ heatmapAllAccounts: "All accounts",
110
116
  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
117
  analyticsEmpty: "No OpenCode sessions in this window.",
112
118
  showAll: "Show all",
@@ -144,6 +150,21 @@ window.__ModuleLoader__.load({
144
150
  maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
145
151
  contextWindow: "Context override",
146
152
  showSidebarQuota: "Show the quota card in the sidebar",
153
+ routing: "Routing",
154
+ routingHint: "Recent account/model choices. No prompts or keys are recorded.",
155
+ routingEmpty: "No routing decisions yet.",
156
+ routingRule: "Model rule",
157
+ routingSticky: "Sticky session",
158
+ routingPinned: "Pinned account",
159
+ routingDefault: "Automatic",
160
+ routingRotation: "Rotation",
161
+ routingFallback: "Quota fallback",
162
+ routingFailed: "Failed",
163
+ quotaFallbackModel: "Quota fallback model",
164
+ quotaFallbackModelHint: "If Go rejects with a quota error, retry once on this free model through Console/Zen.",
165
+ stickySession: "Sticky account per conversation",
166
+ stickySessionHint: "Prefer the last successful account for the same conversation; explicit model rules and pinned accounts win.",
167
+ off: "Off",
147
168
  pluginUpdate: "Plugin update",
148
169
  currentVersion: "Current version",
149
170
  latestVersion: "Latest version",
@@ -195,6 +216,10 @@ window.__ModuleLoader__.load({
195
216
  accountRef: "凭据引用",
196
217
  accountSource: "来源",
197
218
  addAccount: "添加账户",
219
+ importAllAuth: "从 auth.json 导入全部",
220
+ checkAllAccounts: "检查全部账户",
221
+ importedAll: "已导入 auth.json 全部密钥;请检查账户并保存。",
222
+ accountsImported: "已导入密钥",
198
223
  removeAccount: "移除",
199
224
  pinAccount: "固定",
200
225
  activeAccount: "固定账户",
@@ -264,6 +289,8 @@ window.__ModuleLoader__.load({
264
289
  heatmapByHour: "按小时(全部日期)",
265
290
  heatmapAllModels: "全部模型",
266
291
  heatmapModel: "模型",
292
+ heatmapByAccount: "账户",
293
+ heatmapAllAccounts: "全部账户",
267
294
  analyticsHint: "按最后活动时间聚合本地会话元数据。切换过模型的会话计入最后使用的模型;本地投影不暴露失败。",
268
295
  analyticsEmpty: "此窗口内没有 OpenCode 会话。",
269
296
  showAll: "显示全部",
@@ -301,6 +328,21 @@ window.__ModuleLoader__.load({
301
328
  maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
302
329
  contextWindow: "上下文覆盖",
303
330
  showSidebarQuota: "在侧边栏显示配额卡片",
331
+ routing: "路由",
332
+ routingHint: "最近使用的账户/模型选择。不会记录提示词或密钥。",
333
+ routingEmpty: "尚无路由决策。",
334
+ routingRule: "模型规则",
335
+ routingSticky: "会话粘性",
336
+ routingPinned: "固定账户",
337
+ routingDefault: "自动",
338
+ routingRotation: "轮换",
339
+ routingFallback: "配额回退",
340
+ routingFailed: "失败",
341
+ quotaFallbackModel: "配额回退模型",
342
+ quotaFallbackModelHint: "Go 返回配额错误时,通过 Console/Zen 用此免费模型重试一次。",
343
+ stickySession: "每个会话固定账户",
344
+ stickySessionHint: "同一会话优先使用上次成功的账户;显式模型规则和固定账户优先。",
345
+ off: "关闭",
304
346
  pluginUpdate: "插件更新",
305
347
  currentVersion: "当前版本",
306
348
  latestVersion: "最新版本",
@@ -386,10 +428,13 @@ window.__ModuleLoader__.load({
386
428
  body: JSON.stringify({ ref })
387
429
  });
388
430
  }
389
- function importAuth(entry) {
431
+ function importAuth(entry, ref) {
390
432
  return call("/dsh-opencode/auth-import", {
391
433
  method: "POST",
392
- body: JSON.stringify({ entry })
434
+ body: JSON.stringify(ref === void 0 ? { entry } : {
435
+ entry,
436
+ ref
437
+ })
393
438
  });
394
439
  }
395
440
  function fetchAnalytics(days = 30) {
@@ -429,10 +474,12 @@ window.__ModuleLoader__.load({
429
474
  function RequestHeatmapView(props) {
430
475
  const { heatmap, t } = props;
431
476
  const [selected, setSelected] = (0, react.useState)("all");
477
+ const [selectedAccount, setSelectedAccount] = (0, react.useState)("all");
432
478
  if (heatmap === void 0) return null;
433
479
  const models = heatmap.models ?? [];
434
- const selectedModel = models.find((entry) => `${entry.provider}/${entry.model}` === selected);
435
- const view = selectedModel ?? heatmap;
480
+ const accounts = heatmap.accounts ?? [];
481
+ const selectedModel = selectedAccount === "all" ? models.find((entry) => `${entry.provider}/${entry.model}` === selected) : void 0;
482
+ const view = accounts.find((entry) => entry.account === selectedAccount) ?? selectedModel ?? heatmap;
436
483
  const selectedKey = selectedModel === void 0 ? "all" : selected;
437
484
  const max = Math.max(0, ...view.grid.flat());
438
485
  const maxHours = Math.max(0, ...view.hours);
@@ -452,10 +499,37 @@ window.__ModuleLoader__.load({
452
499
  t("requests")
453
500
  ]
454
501
  }),
502
+ accounts.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
503
+ className: "oc-select",
504
+ value: selectedAccount,
505
+ title: t("heatmapByAccount"),
506
+ onChange: (event) => {
507
+ setSelectedAccount(event.target.value);
508
+ setSelected("all");
509
+ },
510
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
511
+ value: "all",
512
+ children: [
513
+ t("heatmapAllAccounts"),
514
+ " (",
515
+ heatmap.total,
516
+ ")"
517
+ ]
518
+ }), accounts.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
519
+ value: entry.account,
520
+ children: [
521
+ entry.account,
522
+ " (",
523
+ entry.total,
524
+ ")"
525
+ ]
526
+ }, entry.account))]
527
+ }) : null,
455
528
  models.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
456
529
  className: "oc-select",
457
530
  value: selectedKey,
458
531
  title: t("heatmapModel"),
532
+ disabled: selectedAccount !== "all",
459
533
  onChange: (event) => setSelected(event.target.value),
460
534
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
461
535
  value: "all",
@@ -907,6 +981,14 @@ window.__ModuleLoader__.load({
907
981
  windows?.monthly
908
982
  ].map((window) => window?.status).find((value) => value !== void 0 && value !== "ok");
909
983
  }
984
+ function routingLabel(reason, t) {
985
+ if (reason === "rule") return t("routingRule");
986
+ if (reason === "sticky") return t("routingSticky");
987
+ if (reason === "pinned") return t("routingPinned");
988
+ if (reason === "rotation") return t("routingRotation");
989
+ if (reason === "quota-fallback") return t("routingFallback");
990
+ return t("routingDefault");
991
+ }
910
992
  function fillClass(value) {
911
993
  if (value >= 90) return "oc-footFill oc-footFillBad";
912
994
  if (value >= 75) return "oc-footFill oc-footFillWarn";
@@ -1222,6 +1304,43 @@ window.__ModuleLoader__.load({
1222
1304
  })
1223
1305
  ]
1224
1306
  }),
1307
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1308
+ className: "oc-card",
1309
+ children: [
1310
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1311
+ className: "oc-card-title",
1312
+ children: t("routing")
1313
+ }),
1314
+ (state?.routing ?? []).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1315
+ className: "oc-hint",
1316
+ children: t("routingEmpty")
1317
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1318
+ className: "oc-table-scroll",
1319
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1320
+ className: "oc-table",
1321
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("date") }),
1323
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("accountLabel") }),
1324
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("model") }),
1325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("routing") })
1326
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: (state?.routing ?? []).slice(0, 8).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: new Date(row.at).toLocaleTimeString() }),
1328
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.account }),
1329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: row.model }),
1330
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [
1331
+ routingLabel(row.reason, t),
1332
+ row.fallback ? ` · ${t("routingFallback")}` : "",
1333
+ row.ok ? "" : ` · ${t("routingFailed")}${row.error === void 0 ? "" : ` (${row.error})`}`
1334
+ ] })
1335
+ ] }, `${row.at}-${row.account}-${row.model}-${row.reason}`)) })]
1336
+ })
1337
+ }),
1338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1339
+ className: "oc-hint",
1340
+ children: t("routingHint")
1341
+ })
1342
+ ]
1343
+ }),
1225
1344
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1226
1345
  className: "oc-card",
1227
1346
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1311,96 +1430,6 @@ window.__ModuleLoader__.load({
1311
1430
  });
1312
1431
  }
1313
1432
  //#endregion
1314
- //#region src/client/provider-card.tsx
1315
- /**
1316
- * The provider card on Settings → Models. It is deliberately a narrow key
1317
- * surface: the full connection, account and model controls stay on the
1318
- * dedicated Settings → OpenCode page, while first-run key entry is available
1319
- * right where the Models page lists this provider.
1320
- */
1321
- function OpenCodeProviderCard(props) {
1322
- const { t } = props;
1323
- const [state, setState] = (0, react.useState)(null);
1324
- const [draft, setDraft] = (0, react.useState)("");
1325
- const [notice, setNotice] = (0, react.useState)(null);
1326
- const [error, setError] = (0, react.useState)(null);
1327
- const [saving, setSaving] = (0, react.useState)(false);
1328
- const load = () => {
1329
- fetchState().then((next) => {
1330
- setState(next);
1331
- setError(null);
1332
- }).catch((caught) => setError(caught instanceof Error ? caught.message : t("failed")));
1333
- };
1334
- (0, react.useEffect)(() => {
1335
- let cancelled = false;
1336
- fetchState().then((next) => {
1337
- if (cancelled) return;
1338
- setState(next);
1339
- setError(null);
1340
- }).catch((caught) => {
1341
- if (!cancelled) setError(caught instanceof Error ? caught.message : t("failed"));
1342
- });
1343
- return () => {
1344
- cancelled = true;
1345
- };
1346
- }, [t]);
1347
- const configured = state?.key.configured === true;
1348
- const ref = state?.config.apiKeyEnv ?? "OPENCODE_API_KEY";
1349
- const save = () => {
1350
- if (draft.trim() === "") return;
1351
- setSaving(true);
1352
- setNotice(null);
1353
- setError(null);
1354
- saveKey(ref, draft.trim()).then(() => {
1355
- setDraft("");
1356
- setNotice(t("saved"));
1357
- load();
1358
- }).catch((caught) => setError(caught instanceof Error ? caught.message : t("failed"))).finally(() => setSaving(false));
1359
- };
1360
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1361
- className: "oc-card",
1362
- children: [
1363
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
1364
- className: "oc-card-title",
1365
- children: [t("title"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1366
- className: `oc-badge ${configured ? "oc-badge-ok" : "oc-badge-warn"}`,
1367
- children: configured ? t("keyConfigured") : t("keyMissing")
1368
- })]
1369
- }),
1370
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1371
- className: "oc-hint",
1372
- children: t("keyHint")
1373
- }),
1374
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1375
- className: "oc-row",
1376
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1377
- className: "oc-input",
1378
- style: { flex: "1 1 220px" },
1379
- type: "password",
1380
- autoComplete: "off",
1381
- placeholder: t("pasteKey"),
1382
- value: draft,
1383
- onChange: (event) => setDraft(event.target.value)
1384
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1385
- type: "button",
1386
- className: "oc-button oc-button-primary",
1387
- disabled: saving || draft.trim() === "",
1388
- onClick: save,
1389
- children: saving ? t("saving") : t("saveKey")
1390
- })]
1391
- }),
1392
- notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1393
- className: "oc-notice",
1394
- children: notice
1395
- }),
1396
- error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1397
- className: "oc-error",
1398
- children: error
1399
- })
1400
- ]
1401
- });
1402
- }
1403
- //#endregion
1404
1433
  //#region src/client/settings-draft.ts
1405
1434
  function draftOf(state) {
1406
1435
  const allIds = state.models.map((model) => model.id);
@@ -1419,6 +1448,8 @@ window.__ModuleLoader__.load({
1419
1448
  activeAccount: state.config.activeAccount,
1420
1449
  accounts: state.config.accounts.map((account) => ({ ...account })),
1421
1450
  modelAccountRules: state.config.modelAccountRules.map((rule) => ({ ...rule })),
1451
+ quotaFallbackModel: state.config.quotaFallbackModel,
1452
+ stickySession: state.config.stickySession,
1422
1453
  visibleModels: state.config.visibleModels.length === 0 ? allIds : [...state.config.visibleModels],
1423
1454
  filterModelsByPlan: state.config.filterModelsByPlan,
1424
1455
  showAll: state.config.visibleModels.length === 0,
@@ -1454,6 +1485,8 @@ window.__ModuleLoader__.load({
1454
1485
  pattern: rule.pattern.trim(),
1455
1486
  account: rule.account.trim()
1456
1487
  })).filter((rule) => rule.pattern !== "" && rule.account !== ""),
1488
+ quotaFallbackModel: draft.quotaFallbackModel.trim(),
1489
+ stickySession: draft.stickySession,
1457
1490
  visibleModels: draft.showAll ? [] : draft.visibleModels,
1458
1491
  filterModelsByPlan: draft.filterModelsByPlan,
1459
1492
  modelTransports: Object.fromEntries(Object.entries(draft.modelTransports).filter(([, value]) => value !== ""))
@@ -1650,6 +1683,7 @@ window.__ModuleLoader__.load({
1650
1683
  const [testModel, setTestModel] = (0, react.useState)("deepseek-v4.1-flash");
1651
1684
  const [testing, setTesting] = (0, react.useState)(false);
1652
1685
  const [testResult, setTestResult] = (0, react.useState)(null);
1686
+ const [importing, setImporting] = (0, react.useState)(false);
1653
1687
  const load = (0, react.useCallback)(async () => {
1654
1688
  setLoading(true);
1655
1689
  try {
@@ -1700,6 +1734,29 @@ window.__ModuleLoader__.load({
1700
1734
  setError(caught instanceof Error ? caught.message : t("failed"));
1701
1735
  }
1702
1736
  };
1737
+ const importAllAuth = async () => {
1738
+ if (state === null || draft === null) return;
1739
+ const refFor = (entry) => `OPENCODE_AUTH_${entry.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}`;
1740
+ const missing = state.auth.entries.filter((entry) => !draft.accounts.some((account) => account.label === entry || account.apiKeyEnv === refFor(entry)));
1741
+ if (missing.length === 0) {
1742
+ setNotice(t("importedAll"));
1743
+ return;
1744
+ }
1745
+ setImporting(true);
1746
+ setError(null);
1747
+ try {
1748
+ for (const entry of missing) await importAuth(entry, refFor(entry));
1749
+ update({ accounts: [...draft.accounts, ...missing.map((entry) => ({
1750
+ label: entry,
1751
+ apiKeyEnv: refFor(entry)
1752
+ }))] });
1753
+ setNotice(t("importedAll"));
1754
+ } catch (caught) {
1755
+ setError(caught instanceof Error ? caught.message : t("failed"));
1756
+ } finally {
1757
+ setImporting(false);
1758
+ }
1759
+ };
1703
1760
  if (loading && state === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1704
1761
  className: "oc-hint",
1705
1762
  children: t("loading")
@@ -2216,6 +2273,23 @@ window.__ModuleLoader__.load({
2216
2273
  }] }),
2217
2274
  children: t("addAccount")
2218
2275
  }),
2276
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2277
+ type: "button",
2278
+ className: "oc-button",
2279
+ disabled: importing,
2280
+ onClick: () => {
2281
+ importAllAuth();
2282
+ },
2283
+ children: importing ? t("loading") : t("importAllAuth")
2284
+ }),
2285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2286
+ type: "button",
2287
+ className: "oc-button",
2288
+ onClick: () => {
2289
+ refresh();
2290
+ },
2291
+ children: t("checkAllAccounts")
2292
+ }),
2219
2293
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2220
2294
  className: "oc-badge",
2221
2295
  children: [
@@ -2296,6 +2370,61 @@ window.__ModuleLoader__.load({
2296
2370
  })
2297
2371
  ]
2298
2372
  }),
2373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2374
+ className: "oc-card",
2375
+ children: [
2376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2377
+ className: "oc-card-title",
2378
+ children: t("routing")
2379
+ }),
2380
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2381
+ className: "oc-hint",
2382
+ children: t("routingHint")
2383
+ }),
2384
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2385
+ className: "oc-row",
2386
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2387
+ type: "checkbox",
2388
+ checked: draft.stickySession,
2389
+ onChange: (event) => update({ stickySession: event.target.checked })
2390
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("stickySession") })]
2391
+ }),
2392
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2393
+ className: "oc-hint",
2394
+ children: t("stickySessionHint")
2395
+ }),
2396
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2397
+ className: "oc-field",
2398
+ children: [
2399
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2400
+ className: "oc-label",
2401
+ children: t("quotaFallbackModel")
2402
+ }),
2403
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2404
+ className: "oc-select",
2405
+ value: draft.quotaFallbackModel,
2406
+ onChange: (event) => update({ quotaFallbackModel: event.target.value }),
2407
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2408
+ value: "",
2409
+ children: t("off")
2410
+ }), state.models.filter((model) => model.free).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2411
+ value: model.id,
2412
+ children: [
2413
+ model.name,
2414
+ " (",
2415
+ model.id,
2416
+ ")"
2417
+ ]
2418
+ }, model.id))]
2419
+ }),
2420
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2421
+ className: "oc-hint",
2422
+ children: t("quotaFallbackModelHint")
2423
+ })
2424
+ ]
2425
+ })
2426
+ ]
2427
+ }),
2299
2428
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2300
2429
  className: "oc-card",
2301
2430
  children: [
@@ -2707,11 +2836,6 @@ window.__ModuleLoader__.load({
2707
2836
  label: () => t("nav"),
2708
2837
  locale: NS
2709
2838
  }, OpenCodeSettingsPage));
2710
- ctx.slots.inject("settings.models.provider-card", () => slots.register({
2711
- name: "settings.models.provider-card",
2712
- key: "llm-opencode",
2713
- locale: NS
2714
- }, OpenCodeProviderCard));
2715
2839
  ctx.slots.inject("main", () => slots.register({
2716
2840
  name: "main",
2717
2841
  key: "opencode-panel",
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