dsh-workbuddy-xdpool 0.3.0 → 0.4.1

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
@@ -115,6 +115,14 @@ window.__ModuleLoader__.load({
115
115
  .dsm-workbuddy-xdpool-usage-status{display:flex;align-items:center;gap:10px;font-size:15px;font-weight:500;color:var(--dsw-alias-label-primary,#e6e6e6)}
116
116
  .dsm-workbuddy-xdpool-usage-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto}
117
117
  .dsm-workbuddy-xdpool-usage-hint{padding-left:19px;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
118
+ /* Distribution switch: priority (drain one) vs round-robin (spread). */
119
+ .dsm-workbuddy-xdpool-dist{display:flex;align-items:center;gap:8px;padding-left:19px;flex-wrap:wrap}
120
+ .dsm-workbuddy-xdpool-dist-title{color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
121
+ .dsm-workbuddy-xdpool-dist-option{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:999px;padding:2px 10px;font-size:11px;line-height:18px;background:transparent;color:var(--dsw-alias-label-tertiary,#9aa0a8);transition:color .16s,border-color .16s,background .16s}
122
+ .dsm-workbuddy-xdpool-dist-option:hover:not(:disabled):not(.dsm-workbuddy-xdpool-dist-option-active){color:var(--dsw-alias-label-primary,#e6e6e6);border-color:var(--dsw-alias-label-dimmed,#777)}
123
+ .dsm-workbuddy-xdpool-dist-option:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
124
+ .dsm-workbuddy-xdpool-dist-option-active{background:var(--dsw-alias-state-success-subtle,rgba(34,160,107,.14));border-color:var(--dsw-alias-state-success-primary,#22a06b);color:var(--dsw-alias-state-success-primary,#22a06b)}
125
+ .dsm-workbuddy-xdpool-dist-option:disabled{cursor:default;opacity:.6}
118
126
  .dsm-workbuddy-xdpool-usage-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
119
127
 
120
128
  /* Account list (each account = a labeled subpanel, same as dingminhua). */
@@ -143,9 +151,18 @@ window.__ModuleLoader__.load({
143
151
  .dsm-workbuddy-xdpool-panel-foot{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-top:9px;padding-top:9px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
144
152
  .dsm-workbuddy-xdpool-panel-foot strong{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:15px;font-variant-numeric:tabular-nums}
145
153
  .dsm-workbuddy-xdpool-packages{display:flex;flex-direction:column;gap:5px;margin:0;padding:0;list-style:none}
146
- .dsm-workbuddy-xdpool-packages li{display:flex;align-items:baseline;justify-content:space-between;gap:10px;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
147
- .dsm-workbuddy-xdpool-packages-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
148
- .dsm-workbuddy-xdpool-packages-value{flex:none;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-variant-numeric:tabular-nums}
154
+ /* One credit package: name + amount on the first line, its deadline beneath.
155
+ A two-row grid keeps the columns aligned across rows; a wrapping flex row
156
+ dropped the deadline onto a second line that started at the container edge,
157
+ so the list read as ragged text rather than a table. */
158
+ .dsm-workbuddy-xdpool-packages li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:10px;row-gap:1px;align-items:baseline;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
159
+ .dsm-workbuddy-xdpool-packages-name{grid-column:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
160
+ .dsm-workbuddy-xdpool-packages-value{grid-column:2;justify-self:end;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-variant-numeric:tabular-nums}
161
+ /* Per-package deadline: the upstream grants one-off packs at arbitrary clock
162
+ times, so each row carries its own timestamp; the soon ones are tinted and
163
+ fold onto their own line so a long package name cannot squeeze them out. */
164
+ .dsm-workbuddy-xdpool-packages-when{grid-column:1/-1;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:11px;line-height:15px;font-variant-numeric:tabular-nums}
165
+ .dsm-workbuddy-xdpool-packages-when-soon{color:var(--dsw-alias-state-warning-primary,#d97706)}
149
166
  .dsm-workbuddy-xdpool-panel-total{position:relative;align-items:center;text-align:center;overflow:hidden}
150
167
  .dsm-workbuddy-xdpool-panel-total::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;opacity:.9;background:var(--dsw-alias-state-success-primary,#22a06b)}
151
168
  .dsm-workbuddy-xdpool-total-value{color:var(--dsw-alias-state-success-primary,#22a06b);font-size:30px;line-height:34px;font-weight:700;letter-spacing:-.5px;white-space:nowrap;font-variant-numeric:tabular-nums}
@@ -307,6 +324,30 @@ window.__ModuleLoader__.load({
307
324
  }
308
325
  return false;
309
326
  }
327
+ /** Absolute expiry with the time of day: the upstream grants one-off packages at
328
+ * arbitrary clock times, so "expires 09/19 15:36" is what the user needs — a
329
+ * date alone would read as if it lapsed at midnight. */
330
+ function formatExpiry(ms) {
331
+ if (ms === void 0 || !Number.isFinite(ms)) return "";
332
+ return new Intl.DateTimeFormat(void 0, {
333
+ month: "2-digit",
334
+ day: "2-digit",
335
+ hour: "2-digit",
336
+ minute: "2-digit",
337
+ hour12: false
338
+ }).format(new Date(ms));
339
+ }
340
+ /** Whole days until `ms`, floored at 0; undefined when there is no deadline. */
341
+ function daysUntil(ms) {
342
+ if (ms === void 0 || !Number.isFinite(ms)) return void 0;
343
+ return Math.max(0, Math.floor((ms - Date.now()) / 864e5));
344
+ }
345
+ /** True when a one-off package lapses inside the "expiring soon" window. */
346
+ function isExpiringSoon(pack) {
347
+ if (pack.monthly === true) return false;
348
+ const days = daysUntil(pack.expiresAtMs);
349
+ return days !== void 0 && days <= 3;
350
+ }
310
351
  function tagFor(model) {
311
352
  const tags = model.tags ?? [];
312
353
  if (tags.includes("free")) return "free";
@@ -522,6 +563,25 @@ window.__ModuleLoader__.load({
522
563
  * The card refuses an empty enable-list: saving one would leave the picker
523
564
  * with nothing to offer and no obvious way back.
524
565
  */
566
+ /**
567
+ * Switch how the pool spreads requests. Written straight through the
568
+ * settings scope (that is where the host keeps the pool options), so the
569
+ * change lands without a restart and survives the next card refresh.
570
+ */
571
+ const setDistribution = async (next) => {
572
+ const write = settingsScope?.set;
573
+ if (write === void 0) {
574
+ setError(t?.("row.modelsSaveError", { message: "settings scope is read-only" }) ?? "settings scope is read-only");
575
+ return;
576
+ }
577
+ setFlash(void 0);
578
+ try {
579
+ await write.call(settingsScope, "distribution", next);
580
+ await refresh(activeRegion);
581
+ } catch (cause) {
582
+ if (mounted.current) setError(String(cause));
583
+ }
584
+ };
525
585
  const saveModels = async () => {
526
586
  if (draft === void 0 || status === void 0) return;
527
587
  if (enabledCount === 0) {
@@ -639,6 +699,31 @@ window.__ModuleLoader__.load({
639
699
  shimHint === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
640
700
  className: "dsm-workbuddy-xdpool-usage-hint",
641
701
  children: shimHint
702
+ }),
703
+ status === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
704
+ className: "dsm-workbuddy-xdpool-dist",
705
+ role: "radiogroup",
706
+ "aria-label": t?.("row.distTitle") ?? "Account usage",
707
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
708
+ className: "dsm-workbuddy-xdpool-dist-title",
709
+ children: t?.("row.distTitle") ?? "Account usage"
710
+ }), ["priority", "round-robin"].map((option) => {
711
+ const active = (status.distribution ?? "priority") === option;
712
+ const label = option === "priority" ? t?.("row.distPriority") ?? "Priority" : t?.("row.distRoundRobin") ?? "Round-robin";
713
+ const hint = option === "priority" ? t?.("row.distPriorityHint") ?? "" : t?.("row.distRoundRobinHint") ?? "";
714
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
715
+ type: "button",
716
+ role: "radio",
717
+ "aria-checked": active,
718
+ title: hint,
719
+ disabled: !modelsEditable,
720
+ className: `dsm-workbuddy-xdpool-dist-option${active ? " dsm-workbuddy-xdpool-dist-option-active" : ""}`,
721
+ onClick: () => {
722
+ setDistribution(option);
723
+ },
724
+ children: label
725
+ }, option);
726
+ })]
642
727
  })
643
728
  ]
644
729
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -878,16 +963,30 @@ window.__ModuleLoader__.load({
878
963
  children: "–"
879
964
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
880
965
  className: "dsm-workbuddy-xdpool-packages",
881
- children: packages.map((pack, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
882
- className: "dsm-workbuddy-xdpool-packages-name",
883
- children: pack.packageName
884
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
885
- className: "dsm-workbuddy-xdpool-packages-value",
886
- children: t?.("row.creditsPackage", {
887
- remain: formatNumber(pack.remain),
888
- size: formatNumber(pack.size)
889
- }) ?? `${formatNumber(pack.remain)} / ${formatNumber(pack.size)}`
890
- })] }, `${pack.packageName}-${String(index)}`))
966
+ children: packages.map((pack, index) => {
967
+ const expiry = formatExpiry(pack.expiresAtMs);
968
+ const refresh = formatExpiry(pack.cycleRefreshMs);
969
+ const soon = isExpiringSoon(pack);
970
+ const when = pack.monthly === true ? refresh === "" ? null : t?.("row.creditsRefreshAt", { time: refresh }) ?? `Refreshes ${refresh}` : expiry === "" ? null : t?.("row.creditsExpiresAt", { time: expiry }) ?? `Expires ${expiry}`;
971
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
972
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
973
+ className: "dsm-workbuddy-xdpool-packages-name",
974
+ children: pack.packageName
975
+ }),
976
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
977
+ className: "dsm-workbuddy-xdpool-packages-value",
978
+ children: t?.("row.creditsPackage", {
979
+ remain: formatNumber(pack.remain),
980
+ size: formatNumber(pack.size)
981
+ }) ?? `${formatNumber(pack.remain)} / ${formatNumber(pack.size)}`
982
+ }),
983
+ when === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
984
+ className: `dsm-workbuddy-xdpool-packages-when${soon ? " dsm-workbuddy-xdpool-packages-when-soon" : ""}`,
985
+ title: t?.("row.creditsExpiresSoonTitle") ?? "Expiring within 3 days",
986
+ children: when
987
+ })
988
+ ] }, `${pack.packageName}-${String(index)}`);
989
+ })
891
990
  }),
892
991
  credits?.expiringSoon !== void 0 && credits.expiringSoon > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
893
992
  className: "dsm-workbuddy-xdpool-panel-foot",
@@ -1082,6 +1181,9 @@ window.__ModuleLoader__.load({
1082
1181
  "row.creditsPackage": "{remain} / {size}",
1083
1182
  "row.creditsError": "credits unavailable",
1084
1183
  "row.creditsSoon": "Expiring in 3 days",
1184
+ "row.creditsExpiresAt": "Expires {time}",
1185
+ "row.creditsExpiresSoonTitle": "Expiring within 3 days",
1186
+ "row.creditsRefreshAt": "Refreshes {time}",
1085
1187
  "row.checkinTitle": "Daily check-in",
1086
1188
  "row.checkinClaim": "Check in",
1087
1189
  "row.checkinClaiming": "Checking in…",
@@ -1116,6 +1218,11 @@ window.__ModuleLoader__.load({
1116
1218
  "row.imageCapable": "image input",
1117
1219
  "row.rate": "{rate}x credits",
1118
1220
  "row.accountsRescan": "Detect accounts again",
1221
+ "row.distTitle": "Account usage",
1222
+ "row.distPriority": "Priority",
1223
+ "row.distPriorityHint": "Drain one account before moving to the next",
1224
+ "row.distRoundRobin": "Round-robin",
1225
+ "row.distRoundRobinHint": "Spread the spend evenly across accounts",
1119
1226
  "row.accountsScanning": "Detecting…",
1120
1227
  "row.resetCooldowns": "Clear all cooldowns",
1121
1228
  "row.resetCooldownsBusy": "Clearing…",
@@ -1160,6 +1267,9 @@ window.__ModuleLoader__.load({
1160
1267
  "row.creditsPackage": "{remain} / {size}",
1161
1268
  "row.creditsError": "积分不可用",
1162
1269
  "row.creditsSoon": "3 天内到期",
1270
+ "row.creditsExpiresAt": "到期 {time}",
1271
+ "row.creditsExpiresSoonTitle": "3 天内到期",
1272
+ "row.creditsRefreshAt": "刷新 {time}",
1163
1273
  "row.checkinTitle": "每日签到",
1164
1274
  "row.checkinClaim": "签到",
1165
1275
  "row.checkinClaiming": "签到中…",
@@ -1194,6 +1304,11 @@ window.__ModuleLoader__.load({
1194
1304
  "row.imageCapable": "图片输入",
1195
1305
  "row.rate": "{rate}x 积分",
1196
1306
  "row.accountsRescan": "重新检测账号",
1307
+ "row.distTitle": "账号使用方式",
1308
+ "row.distPriority": "优先模式",
1309
+ "row.distPriorityHint": "优先用完一个账号再换下一个",
1310
+ "row.distRoundRobin": "轮换模式",
1311
+ "row.distRoundRobinHint": "积分平均分摊到各账号",
1197
1312
  "row.accountsScanning": "正在检测…",
1198
1313
  "row.resetCooldowns": "清除所有冷却",
1199
1314
  "row.resetCooldownsBusy": "正在清除…",
package/lib/index.d.ts CHANGED
@@ -20,7 +20,12 @@ interface WorkBuddyUpstreamModel {
20
20
  contextWindow: number;
21
21
  maxTokens: number;
22
22
  creditMultiplier?: number;
23
- multimodal?: boolean;
23
+ /** Upstream image-input flag. Both gateways spell it `supportsImages`; some entries
24
+ * also carry `disabledMultimodal`, the negative spelling. Reading anything else
25
+ * reported every model as text-only, which made a vision shim register a second
26
+ * route under the same display name and split the model picker group.
27
+ */
28
+ supportsImages?: boolean;
24
29
  reasoning?: {
25
30
  supportedEfforts?: readonly string[];
26
31
  defaultEffort?: string;
@@ -112,7 +117,21 @@ export declare class WorkBuddyUpstreamClient {
112
117
  chatStream(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal): Promise<ChatStreamResult>;
113
118
  /** POST the token-refresh endpoint; the caller merges the outcome. */
114
119
  refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
115
- /** GET the personal model catalog, keeping the `cli` agent's models only. */
120
+ /**
121
+ * Fetch the model catalog, keeping the `cli` agent's models only.
122
+ *
123
+ * The two gateways are read differently, because they answer differently:
124
+ *
125
+ * - **CN** serves the roster at `/v2/enterprises/personal/models` and expects
126
+ * the CLI client spelling.
127
+ * - **Global** serves it as part of the product config at `/v3/config`, and
128
+ * only to the DESKTOP client channel. Asking the global host with the CLI UA
129
+ * yields a truncated roster, and the CN path answers HTTP 500 there — which
130
+ * is what left the international provider on its static fallback.
131
+ *
132
+ * Both documents share the `{ models, agents }` entry shape, so the parsing
133
+ * below is common to the two branches.
134
+ */
116
135
  fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
117
136
  /** Read-only credits query, aggregated by package. Does not consume credits. */
118
137
  fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
@@ -142,6 +161,7 @@ interface TokenRefresher {
142
161
  }
143
162
  /** Live auth file name the WorkBuddy desktop app writes. */
144
163
  export declare const WORKBUDDY_LIVE_FILENAME = "workbuddy-desktop.info";
164
+ /** Snapshot files left behind by previous logins share this prefix. */
145
165
  /** Env override for the auth file or its directory. */
146
166
  export declare const WORKBUDDY_AUTH_FILE_ENV = "WORKBUDDY_AUTH_FILE";
147
167
  /** One parsed WorkBuddy credential. */
@@ -150,6 +170,16 @@ interface WorkBuddyCredential {
150
170
  refreshToken: string;
151
171
  expiresAtMs: number;
152
172
  refreshExpiresAtMs?: number;
173
+ /**
174
+ * When the upstream says it issued this token (`auth.lastRefreshTime`).
175
+ *
176
+ * This, not `expiresAtMs`, is the reliable freshness signal: the upstream
177
+ * never rewrites a stored expiry when it revokes a token, so a long-dead
178
+ * backup can claim to expire later than the token that actually works.
179
+ * Absent on documents the desktop app did not write (the plugin's own
180
+ * refreshed copy, older builds).
181
+ */
182
+ lastRefreshAtMs?: number;
153
183
  nickname?: string;
154
184
  uin?: string;
155
185
  uid?: string;
@@ -194,13 +224,11 @@ export declare function defaultDesktopAuthDirs(platform?: NodeJS.Platform, home?
194
224
  * when there is no usable access token.
195
225
  */
196
226
  export declare function parseWorkBuddyAuth(text: string, sourcePath: string): WorkBuddyCredential | undefined;
197
- /**
198
- * Stable account id. `uin` is the billing identity the upstream keys on and
199
- * survives re-login; `uid` is the fallback.
200
- */
201
227
  export declare function workbuddyAccountId(credential: Pick<WorkBuddyCredential, 'uin' | 'uid' | 'nickname'>): string;
202
228
  /** Every directory the pool should scan, in probe order. */
203
229
  export declare function candidateAuthDirs(env?: NodeJS.ProcessEnv): string[];
230
+ /** How the pool chooses which account serves the next request. */
231
+ type AccountDistribution = 'priority' | 'round-robin';
204
232
  interface AccountPoolOptions {
205
233
  /** Logger for discovery and rotation events. */
206
234
  logger?: {
@@ -216,6 +244,17 @@ interface AccountPoolOptions {
216
244
  client?: TokenRefresher;
217
245
  /** Refresh this long before actual expiry; default five minutes. */
218
246
  refreshMarginMs?: number;
247
+ /**
248
+ * How requests are spread across the pool.
249
+ *
250
+ * - `priority` (default): one account serves every request until it is
251
+ * rate-limited, then the next in order takes over. Credits drain one
252
+ * account at a time, and a cooled account resumes at the head of the
253
+ * queue the moment its window resets.
254
+ * - `round-robin`: consecutive requests rotate through the pool so the
255
+ * spend spreads evenly.
256
+ */
257
+ distribution?: AccountDistribution;
219
258
  }
220
259
  /**
221
260
  * Read-only pool of every discovered WorkBuddy account, with rate-limit
@@ -228,6 +267,8 @@ export declare class WorkBuddyAccountPool {
228
267
  private readonly client;
229
268
  private readonly refreshMarginMs;
230
269
  private accounts;
270
+ private distribution;
271
+ /** Cursor for round-robin mode; unused under priority distribution. */
231
272
  private cursor;
232
273
  private lastScanAtMs;
233
274
  private preferredId;
@@ -241,6 +282,7 @@ export declare class WorkBuddyAccountPool {
241
282
  applyConfig(options: {
242
283
  authDirs?: readonly string[];
243
284
  cooldownMs?: number;
285
+ distribution?: AccountDistribution;
244
286
  }): void;
245
287
  /** Rescan the auth directories and merge newly discovered accounts. */
246
288
  scan(): Promise<WorkBuddyAccount[]>;
@@ -257,15 +299,29 @@ export declare class WorkBuddyAccountPool {
257
299
  */
258
300
  private available;
259
301
  /**
260
- * Pick the next usable account for an optional model. Scans on first use,
261
- * and rescans when every known account is cooling down — a fresh desktop
262
- * login is the usual way out of an exhausted pool. A preferred
263
- * (user-selected) account that is healthy is tried first; otherwise the
264
- * cursor round-robins so consecutive requests spread across accounts and a
265
- * still-cooling preferred account is skipped.
302
+ * Pick the account to serve a request.
303
+ *
304
+ * Two distributions, chosen by the `distribution` setting:
305
+ *
306
+ * - **priority** (default, and what the card ships with): one account serves
307
+ * every request until it is rate-limited, then the next in order takes over.
308
+ * Credits drain one account at a time, and a cooling account returns to the
309
+ * head of the queue the moment its window resets — it was never consumed, so
310
+ * it resumes straight away.
311
+ * - **round-robin**: consecutive requests rotate through the pool so spend
312
+ * spreads evenly across every account.
313
+ *
314
+ * In both modes an explicit user selection (`prefer`) heads the list, a
315
+ * cooling account is skipped for that model only, and an unrecognised setting
316
+ * falls back to priority.
317
+ *
318
+ * Scans on first use, and rescans when every known account is cooling down: a
319
+ * fresh desktop login is the usual way out of an exhausted pool.
266
320
  */
267
321
  acquire(modelId?: string, region?: WorkBuddyRegion): Promise<WorkBuddyAccount | undefined>;
268
322
  /** Pin the account the plugin card should prefer; tokens stay out of settings. */
323
+ /** How the pool currently spreads requests. Shown on the card. */
324
+ currentDistribution(): AccountDistribution;
269
325
  prefer(accountId: string | undefined): void;
270
326
  /** Best-effort refresh of one account after a session-dead upstream answer. */
271
327
  refreshAccount(accountId: string): Promise<void>;
@@ -351,7 +407,7 @@ interface ModelSelection {
351
407
  /** Absent = each model follows its upstream image capability. */
352
408
  imageModelIds?: readonly string[];
353
409
  /** Per-model context-window cap, keyed by model id. */
354
- contextBudgets?: Readonly<Record<string, number>>;
410
+ contextBudgets?: Readonly<Record<string, number | undefined>>;
355
411
  }
356
412
  //#endregion
357
413
  //#region src/shim.d.ts
@@ -452,6 +508,7 @@ interface WorkBuddyStatus {
452
508
  }
453
509
  interface StatusOptions {
454
510
  pool: WorkBuddyAccountPool;
511
+ /** The catalog to report. Regional callers pass their own region's. */
455
512
  catalog: WorkBuddyCatalog;
456
513
  client: WorkBuddyUpstreamClient;
457
514
  shim?: {
@@ -605,7 +662,7 @@ interface PoolWebModelSelection {
605
662
  /** Absent = each model follows its upstream image capability. */
606
663
  imageModelIds?: readonly string[];
607
664
  /** Per-model context-window cap, keyed by model id. */
608
- contextBudgets?: Readonly<Record<string, number>>;
665
+ contextBudgets?: Readonly<Record<string, number | undefined>>;
609
666
  }
610
667
  /** The JSON document the pool card renders. */
611
668
  interface PoolWebStatus {
@@ -617,6 +674,11 @@ interface PoolWebStatus {
617
674
  models: readonly PoolWebModel[];
618
675
  /** The saved selection the card diffs its draft against. */
619
676
  selection: PoolWebModelSelection;
677
+ /**
678
+ * How the pool spreads requests: `priority` drains one account before
679
+ * moving on, `round-robin` splits the spend evenly.
680
+ */
681
+ distribution: PoolDistribution;
620
682
  /** Which region this document describes. */
621
683
  region: PoolRegion;
622
684
  /** Every region holding at least one account, in display order. */
@@ -632,6 +694,8 @@ interface PoolWebStatus {
632
694
  * international one (`workbuddy.ai`).
633
695
  */
634
696
  type PoolRegion = 'cn' | 'global';
697
+ /** How the pool spreads requests across its accounts. */
698
+ type PoolDistribution = 'priority' | 'round-robin';
635
699
  //#endregion
636
700
  //#region src/index.d.ts
637
701
  /** Stable Cordis plugin name. */
@@ -651,6 +715,14 @@ export interface Config {
651
715
  authFile?: string;
652
716
  /** Rate-limit cooldown per account, milliseconds. */
653
717
  cooldownMs?: number;
718
+ /**
719
+ * How the pool spreads requests across accounts.
720
+ *
721
+ * `priority` (default) drains one account before moving to the next, which
722
+ * is what a pool of your own accounts is for. `round-robin` splits the
723
+ * spend evenly instead. Absent reads as `priority`.
724
+ */
725
+ distribution?: 'priority' | 'round-robin';
654
726
  /**
655
727
  * Model ids enabled in the picker. Absent means "every model the catalog
656
728
  * advertises" — an unconfigured install should never present an empty model
@@ -668,7 +740,7 @@ export interface Config {
668
740
  * advertise more than DSH wants to hand a single turn, so the card lets the
669
741
  * user cap a model without touching the catalog.
670
742
  */
671
- contextBudgets?: Partial<Record<string, number>>;
743
+ contextBudgets?: Record<string, number>;
672
744
  }
673
745
  /** Upper bound the card offers as the "default" context window, in tokens. */
674
746
  export declare const DEFAULT_CONTEXT_BUDGET = 200000;
@@ -686,7 +758,8 @@ export declare const Config: z<Config>;
686
758
  /** Everything the CLI needs from a live plugin instance. */
687
759
  export interface WorkBuddyPoolApi {
688
760
  pool: WorkBuddyAccountPool;
689
- catalog: WorkBuddyCatalog;
761
+ /** One catalog per region, matching the two registered providers. */
762
+ catalogs: Readonly<Record<WorkBuddyRegion, WorkBuddyCatalog>>;
690
763
  client: WorkBuddyUpstreamClient;
691
764
  shim: WorkBuddyShim;
692
765
  adapter: WorkBuddyAdapter | undefined;
@@ -699,11 +772,22 @@ export declare function currentApi(): WorkBuddyPoolApi | undefined;
699
772
  /** Test seam: install an API instance without booting cordis. */
700
773
  export declare function setApi(next: WorkBuddyPoolApi | undefined): void;
701
774
  /** Assemble the runtime objects without registering anything. */
775
+ /**
776
+ * Assemble the runtime objects without registering anything.
777
+ *
778
+ * One catalog per region, mirroring the two shims: the CN and global gateways
779
+ * do not advertise the same roster, and a shared catalog meant the picker showed
780
+ * whichever list happened to be fetched first (always the CN one, since the
781
+ * seeding step read `accounts[0]`).
782
+ */
702
783
  export declare function createCore(logger?: {
703
784
  warn(...args: unknown[]): void;
704
785
  }): {
705
786
  pool: WorkBuddyAccountPool;
706
- catalog: WorkBuddyCatalog;
787
+ catalogs: {
788
+ readonly cn: WorkBuddyCatalog;
789
+ readonly global: WorkBuddyCatalog;
790
+ };
707
791
  client: WorkBuddyUpstreamClient;
708
792
  };
709
793
  /**