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/CHANGELOG.md +75 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/assets/model-picker.png +0 -0
- package/assets/settings-card.png +0 -0
- package/lib/bin.js +252 -46
- package/lib/client.js +128 -13
- package/lib/index.d.ts +101 -17
- package/lib/index.js +280 -66
- package/package.json +1 -1
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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) =>
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
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?:
|
|
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
|
|
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
|
-
|
|
787
|
+
catalogs: {
|
|
788
|
+
readonly cn: WorkBuddyCatalog;
|
|
789
|
+
readonly global: WorkBuddyCatalog;
|
|
790
|
+
};
|
|
707
791
|
client: WorkBuddyUpstreamClient;
|
|
708
792
|
};
|
|
709
793
|
/**
|