dsh-agy 0.3.2 → 0.4.0
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/{accounts-CKWybAfc.mjs → accounts-BN4h0AU4.mjs} +246 -64
- package/lib/accounts-BN4h0AU4.mjs.map +1 -0
- package/lib/cli/index.mjs +1 -1
- package/lib/client.js +25 -3
- package/lib/index.mjs +2 -2
- package/lib/{plugin-common-B5dyfnMH.mjs → plugin-common-N7p9Cm-3.mjs} +56 -12
- package/lib/plugin-common-N7p9Cm-3.mjs.map +1 -0
- package/lib/{translate-BQu8HLvB.mjs → translate-YhGDUuVa.mjs} +157 -23
- package/lib/translate-YhGDUuVa.mjs.map +1 -0
- package/lib/web/plugin.d.mts +28 -1
- package/lib/web/plugin.d.mts.map +1 -1
- package/lib/web/plugin.mjs +31 -15
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +4 -3
- package/lib/accounts-CKWybAfc.mjs.map +0 -1
- package/lib/plugin-common-B5dyfnMH.mjs.map +0 -1
- package/lib/translate-BQu8HLvB.mjs.map +0 -1
|
@@ -1091,14 +1091,23 @@ function earliestResetTime(a, b) {
|
|
|
1091
1091
|
* counts Claude and GPT under one third-party counter, so the weekly budget
|
|
1092
1092
|
* belongs to both. No model-id prefix rule can recover that split, which is why
|
|
1093
1093
|
* the mapping lives here rather than in `modelFamilyOf`.
|
|
1094
|
+
*
|
|
1095
|
+
* Window-aware as of #56: a bucket id resolves a family only when the window it
|
|
1096
|
+
* describes is one `CachedQuota` can hold AND the id agrees with the token (see
|
|
1097
|
+
* `agreedWindowKind`). The prefix and the window were two independent decisions
|
|
1098
|
+
* once, and an unrecognized window still claimed a family.
|
|
1094
1099
|
*/
|
|
1095
|
-
function familiesForBucketId(bucketId) {
|
|
1100
|
+
function familiesForBucketId(bucketId, window) {
|
|
1096
1101
|
const id = bucketId.toLowerCase();
|
|
1097
|
-
|
|
1098
|
-
if (id.startsWith("
|
|
1099
|
-
if (id.startsWith("
|
|
1100
|
-
if (id.startsWith("
|
|
1101
|
-
|
|
1102
|
+
const families = [];
|
|
1103
|
+
if (id.startsWith("gemini-")) families.push("google");
|
|
1104
|
+
else if (id.startsWith("3p-") || id.startsWith("third_party-")) families.push("anthropic", "openai");
|
|
1105
|
+
else if (id.startsWith("claude-")) families.push("anthropic");
|
|
1106
|
+
else if (id.startsWith("gpt-") || id.startsWith("openai-")) families.push("openai");
|
|
1107
|
+
if (families.length === 0) return [];
|
|
1108
|
+
const declared = window === void 0 ? advertisedWindowOf(id) : window.toLowerCase();
|
|
1109
|
+
if (declared === void 0 || agreedWindowKind(id, declared) === void 0) return [];
|
|
1110
|
+
return families;
|
|
1102
1111
|
}
|
|
1103
1112
|
/**
|
|
1104
1113
|
* The families a group's LABEL covers, consulted only when no bucket id was
|
|
@@ -1124,7 +1133,6 @@ function familiesForGroupName(name) {
|
|
|
1124
1133
|
if (claude && gpt) families.push("anthropic", "openai");
|
|
1125
1134
|
else if (claude) families.push("anthropic");
|
|
1126
1135
|
else if (gpt) families.push("openai");
|
|
1127
|
-
else if (families.length === 0 && (label.includes("3p") || label.includes("third"))) families.push("anthropic", "openai");
|
|
1128
1136
|
return families;
|
|
1129
1137
|
}
|
|
1130
1138
|
/**
|
|
@@ -1145,13 +1153,44 @@ function familiesForGroupName(name) {
|
|
|
1145
1153
|
function windowKind(window) {
|
|
1146
1154
|
return QUOTA_WINDOWS[window.toLowerCase()]?.kind;
|
|
1147
1155
|
}
|
|
1156
|
+
/** The window token a bucket id advertises, when it ends with one upstream names. */
|
|
1157
|
+
function advertisedWindowOf(bucketId) {
|
|
1158
|
+
const id = bucketId.toLowerCase();
|
|
1159
|
+
const token = id.slice(id.lastIndexOf("-") + 1);
|
|
1160
|
+
return QUOTA_WINDOWS[token] ? token : void 0;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* The field a window belongs in, decided from an AGREED (id, token) pair — or
|
|
1164
|
+
* `undefined` when the pair cannot be agreed.
|
|
1165
|
+
*
|
|
1166
|
+
* The family used to come from the bucket id's prefix while the field came from
|
|
1167
|
+
* the window token, as two independent decisions nothing checked against each
|
|
1168
|
+
* other. A bucket whose window the cache cannot hold still claimed the family,
|
|
1169
|
+
* and its reading was then filed by whichever field the TOKEN named. `gemini-weekly`
|
|
1170
|
+
* declaring `5h` is the shape that puts a multi-day weekly fraction into
|
|
1171
|
+
* `remainingFraction` (the 5-hour field), where `isFamilyDrained` and
|
|
1172
|
+
* `parseFutureResetMs` block the account for days on a window they misread.
|
|
1173
|
+
*
|
|
1174
|
+
* Refusing an unagreed pair is deliberately the strict option: the permissive
|
|
1175
|
+
* mapping is exactly the coupling that let two window vocabularies drift into
|
|
1176
|
+
* destroying a measurement. A bucket id that advertises no recognizable token is
|
|
1177
|
+
* held to the declared one alone, so a rename does not silently disable the cache.
|
|
1178
|
+
*/
|
|
1179
|
+
function agreedWindowKind(bucketId, window) {
|
|
1180
|
+
const declared = window.toLowerCase();
|
|
1181
|
+
const kind = windowKind(declared);
|
|
1182
|
+
if (kind === void 0) return void 0;
|
|
1183
|
+
const advertised = advertisedWindowOf(bucketId);
|
|
1184
|
+
if (advertised !== void 0 && advertised !== declared) return void 0;
|
|
1185
|
+
return kind;
|
|
1186
|
+
}
|
|
1148
1187
|
/** The families one group belongs to: its bucket ids first, its label second. */
|
|
1149
1188
|
function familiesForGroup(group) {
|
|
1150
1189
|
const families = [];
|
|
1151
1190
|
const add = (family) => {
|
|
1152
1191
|
if (!families.includes(family)) families.push(family);
|
|
1153
1192
|
};
|
|
1154
|
-
for (const window of group.windows) for (const family of familiesForBucketId(window.bucketId)) add(family);
|
|
1193
|
+
for (const window of group.windows) for (const family of familiesForBucketId(window.bucketId, window.window)) add(family);
|
|
1155
1194
|
if (families.length === 0) for (const family of familiesForGroupName(group.name)) add(family);
|
|
1156
1195
|
return families;
|
|
1157
1196
|
}
|
|
@@ -1163,9 +1202,16 @@ function familiesForGroup(group) {
|
|
|
1163
1202
|
* `remainingFraction` and `resetTime`, with no window field at all, which is why
|
|
1164
1203
|
* a weekly limit used to be invisible to rotation.
|
|
1165
1204
|
*
|
|
1166
|
-
* Two groups can land on one family (upstream may split `3p` later), so
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1205
|
+
* Two groups can land on one family (upstream may split `3p` later), so the
|
|
1206
|
+
* family record keeps the MOST pressured reading of each window across those
|
|
1207
|
+
* groups (`Math.min` below) rather than letting the last group win — the same
|
|
1208
|
+
* rule the per-model merge uses.
|
|
1209
|
+
*
|
|
1210
|
+
* WITHIN one group the rule is deliberately different: `??=` keeps upstream's
|
|
1211
|
+
* FIRST bucket of each window, because a group is upstream's own 5h + weekly
|
|
1212
|
+
* pair and a duplicate would otherwise depend on upstream's ordering. "Most
|
|
1213
|
+
* pressured" therefore applies to two GROUPS landing on one family, not to two
|
|
1214
|
+
* buckets inside one group.
|
|
1169
1215
|
*/
|
|
1170
1216
|
function ingestQuotaGroups(groups) {
|
|
1171
1217
|
const families = /* @__PURE__ */ new Map();
|
|
@@ -1175,7 +1221,7 @@ function ingestQuotaGroups(groups) {
|
|
|
1175
1221
|
let rolling;
|
|
1176
1222
|
let weekly;
|
|
1177
1223
|
for (const window of group.windows) {
|
|
1178
|
-
const kind =
|
|
1224
|
+
const kind = agreedWindowKind(window.bucketId, window.window);
|
|
1179
1225
|
if (kind === "rolling") rolling ??= window;
|
|
1180
1226
|
else if (kind === "weekly") weekly ??= window;
|
|
1181
1227
|
}
|
|
@@ -1248,21 +1294,70 @@ function ingestFamilyQuotas(discovered, groups, previous) {
|
|
|
1248
1294
|
}
|
|
1249
1295
|
return Object.fromEntries(families);
|
|
1250
1296
|
}
|
|
1251
|
-
/**
|
|
1252
|
-
|
|
1297
|
+
/**
|
|
1298
|
+
* The lowest fraction a record reports across its LIVE windows — its overall
|
|
1299
|
+
* pressure.
|
|
1300
|
+
*
|
|
1301
|
+
* A window whose reset has already passed describes a window that no longer
|
|
1302
|
+
* exists, so it does not count — the same rule `isQuotaStale` applies when it
|
|
1303
|
+
* sizes the TTL from the selected record. Scoring a dead window would let one
|
|
1304
|
+
* family's expired weekly reading win the selection and represent the account,
|
|
1305
|
+
* masking another family's live pressure (issue #69: a dead weekly at 0.004
|
|
1306
|
+
* beat a live 0.2 and stretched the refresh TTL from 5 to 15 minutes).
|
|
1307
|
+
*
|
|
1308
|
+
* `undefined` means the record carries no usable LIVE reading at all, which is
|
|
1309
|
+
* deliberately different from a measured zero: only a real number can size a
|
|
1310
|
+
* refresh interval.
|
|
1311
|
+
*/
|
|
1312
|
+
function pressureScore(entry, now) {
|
|
1313
|
+
const fractions = [];
|
|
1314
|
+
if (typeof entry.remainingFraction === "number" && Number.isFinite(entry.remainingFraction) && !resetInPast(entry.resetTime, now)) fractions.push(entry.remainingFraction);
|
|
1315
|
+
if (typeof entry.weeklyFraction === "number" && Number.isFinite(entry.weeklyFraction) && !resetInPast(entry.weeklyResetTime, now)) fractions.push(entry.weeklyFraction);
|
|
1316
|
+
return fractions.length === 0 ? void 0 : Math.min(...fractions);
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* The quota record for one family, or the most-pressured family when the model
|
|
1320
|
+
* is unknown.
|
|
1321
|
+
*
|
|
1322
|
+
* "Most pressured" compares BOTH windows and reports the record whose LOWEST
|
|
1323
|
+
* window is the lowest of all of them. That matters because `isQuotaStale` sizes
|
|
1324
|
+
* the refresh TTL from whichever record this returns: a family at 0.9 / weekly
|
|
1325
|
+
* 0.004 is more pressured than one at 0.2, and comparing the 5-hour fraction
|
|
1326
|
+
* alone picked the 0.2 record — so the weekly-driven 60s TTL was never applied
|
|
1327
|
+
* to the family actually being requested and a spent week was discovered late.
|
|
1328
|
+
*
|
|
1329
|
+
* A record carrying ONLY a weekly reading participates for the same reason: #48
|
|
1330
|
+
* produces that shape legitimately (the anthropic/openai entries of a
|
|
1331
|
+
* Gemini-only pool), and the old `typeof entry.remainingFraction !== 'number'`
|
|
1332
|
+
* guard skipped it outright, leaving the week invisible to TTL sizing.
|
|
1333
|
+
*
|
|
1334
|
+
* Selection considers LIVE windows only (`pressureScore` drops a window whose
|
|
1335
|
+
* reset has passed) — but the with-family path returns the record as-is either
|
|
1336
|
+
* way, because its consumers guard each window individually.
|
|
1337
|
+
*
|
|
1338
|
+
* @param account - the account whose cache to read.
|
|
1339
|
+
* @param family - the exact family's record; omit to select the most pressured.
|
|
1340
|
+
* @param now - current time (Unix ms), which decides whether a window is live.
|
|
1341
|
+
*/
|
|
1342
|
+
function familyQuotaFor(account, family, now = Date.now()) {
|
|
1253
1343
|
const cache = account.cachedQuota ?? {};
|
|
1254
1344
|
if (family) return cache[family];
|
|
1255
1345
|
let worst;
|
|
1346
|
+
let worstScore;
|
|
1256
1347
|
for (const entry of Object.values(cache)) {
|
|
1257
|
-
|
|
1258
|
-
if (
|
|
1348
|
+
const score = pressureScore(entry, now);
|
|
1349
|
+
if (score === void 0) continue;
|
|
1350
|
+
if (worstScore === void 0 || score < worstScore) {
|
|
1351
|
+
worst = entry;
|
|
1352
|
+
worstScore = score;
|
|
1353
|
+
}
|
|
1259
1354
|
}
|
|
1260
1355
|
return worst;
|
|
1261
1356
|
}
|
|
1262
1357
|
/** Whether the account's quota cache needs a refresh (missing, or past its health-based TTL). */
|
|
1263
1358
|
function isQuotaStale(account, now = Date.now()) {
|
|
1264
1359
|
if (!account.cachedQuota || !account.cachedQuotaUpdatedAt) return true;
|
|
1265
|
-
const mostPressured = familyQuotaFor(account);
|
|
1360
|
+
const mostPressured = familyQuotaFor(account, void 0, now);
|
|
1266
1361
|
const ttl = computeSoftQuotaCacheTtlMs(resetInPast(mostPressured?.resetTime, now) ? void 0 : mostPressured?.remainingFraction, resetInPast(mostPressured?.weeklyResetTime, now) ? void 0 : mostPressured?.weeklyFraction);
|
|
1267
1362
|
return now - account.cachedQuotaUpdatedAt > ttl;
|
|
1268
1363
|
}
|
|
@@ -1296,26 +1391,6 @@ function resetInPast(resetTime, now) {
|
|
|
1296
1391
|
return !Number.isNaN(reset) && reset <= now;
|
|
1297
1392
|
}
|
|
1298
1393
|
/**
|
|
1299
|
-
* Whether the requested family on this account is soft-quota-exhausted.
|
|
1300
|
-
*
|
|
1301
|
-
* BOTH windows are checked, because they refill on different clocks. The 5-hour
|
|
1302
|
-
* bucket can be nearly empty while the weekly budget is untouched, and the
|
|
1303
|
-
* reverse — a spent weekly budget stays spent across four 5-hour refills. Before
|
|
1304
|
-
* this, only the 5-hour reading was consulted, so an account whose week was over
|
|
1305
|
-
* kept being selected until a real request failed.
|
|
1306
|
-
*
|
|
1307
|
-
* A window whose reset has already passed is IGNORED rather than read: its
|
|
1308
|
-
* fraction describes a window that no longer exists, so the account stays
|
|
1309
|
-
* selectable until the next measurement replaces the stale value.
|
|
1310
|
-
*/
|
|
1311
|
-
function isFamilyDrained(account, family, now = Date.now()) {
|
|
1312
|
-
const quota = familyQuotaFor(account, family);
|
|
1313
|
-
if (!quota) return false;
|
|
1314
|
-
if (typeof quota.remainingFraction === "number" && !resetInPast(quota.resetTime, now) && quota.remainingFraction < .15) return true;
|
|
1315
|
-
if (typeof quota.weeklyFraction === "number" && !resetInPast(quota.weeklyResetTime, now) && quota.weeklyFraction <= .01) return true;
|
|
1316
|
-
return false;
|
|
1317
|
-
}
|
|
1318
|
-
/**
|
|
1319
1394
|
* Required drain rate: headroomFraction / remainingHours — how fast the
|
|
1320
1395
|
* family's remaining quota must be consumed to avoid expiring unused at its
|
|
1321
1396
|
* reset (mirrors AuthStorage.#computeWindowRequiredDrain with a daily window).
|
|
@@ -1350,6 +1425,32 @@ function parseFutureResetMs(resetTime, now) {
|
|
|
1350
1425
|
return reset;
|
|
1351
1426
|
}
|
|
1352
1427
|
/**
|
|
1428
|
+
* Whether the requested family on this account is effectively quota-exhausted
|
|
1429
|
+
* for account STEERING (affinity pins, the active-account preference): an
|
|
1430
|
+
* explicit quota-exhausted cooldown in force, a spent 5-hour window
|
|
1431
|
+
* (`remainingFraction <= 0` with a future reset), or a weekly window at or
|
|
1432
|
+
* below `WEEKLY_QUOTA_THRESHOLD` — a spent week refills in days, so its rule
|
|
1433
|
+
* mirrors `isFamilyDrained`'s weekly branch rather than the block wall.
|
|
1434
|
+
*
|
|
1435
|
+
* Deliberately a DIFFERENT predicate from the one in `pickAccount`'s `!picked`
|
|
1436
|
+
* branch, which applies a strict zero to BOTH windows to mirror
|
|
1437
|
+
* `rankPoolCandidates`' block wall when classifying `AgyPoolBlockedError`; and
|
|
1438
|
+
* deliberately NARROWER than `isFamilyDrained`'s 5-hour rule
|
|
1439
|
+
* (`SOFT_QUOTA_THRESHOLD`): account prioritization holds a chosen account
|
|
1440
|
+
* until it is actually spent, not until ranking would prefer another one.
|
|
1441
|
+
*/
|
|
1442
|
+
function isFamilyQuotaExhausted(account, family, now = Date.now()) {
|
|
1443
|
+
if (account.cooldownReason === "quota-exhausted" && (account.coolingDownUntil ?? 0) > now) return true;
|
|
1444
|
+
const quota = familyQuotaFor(account, family);
|
|
1445
|
+
if (!quota) return false;
|
|
1446
|
+
const spent = (fraction, resetTime, threshold = 0) => {
|
|
1447
|
+
if (typeof fraction !== "number" || fraction > threshold || !resetTime) return false;
|
|
1448
|
+
const resetMs = parseFutureResetMs(resetTime, now);
|
|
1449
|
+
return resetMs !== void 0 && resetMs > now;
|
|
1450
|
+
};
|
|
1451
|
+
return spent(quota.remainingFraction, quota.resetTime, 0) || spent(quota.weeklyFraction, quota.weeklyResetTime, .01);
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1353
1454
|
* Rank pool candidates for one request, mirroring AuthStorage's antigravity
|
|
1354
1455
|
* ordering: unblocked first (earliest unblock time among blocked), hot windows
|
|
1355
1456
|
* last, measured usage before unmeasured, required-drain descending, then
|
|
@@ -1361,10 +1462,12 @@ function rankPoolCandidates(entries, modelId, now = Date.now(), startIndex = 0)
|
|
|
1361
1462
|
const activePos = entries.findIndex((e) => e.index === startIndex);
|
|
1362
1463
|
const clampedStart = activePos >= 0 ? activePos : 0;
|
|
1363
1464
|
const candidates = (entries.length === 0 ? [] : [...entries.slice(clampedStart), ...entries.slice(0, clampedStart)]).map(({ account, index }, orderPos) => {
|
|
1364
|
-
const quota = familyQuotaFor(account, family);
|
|
1365
|
-
const
|
|
1465
|
+
const quota = familyQuotaFor(account, family, now);
|
|
1466
|
+
const live5h = quota !== void 0 && !resetInPast(quota.resetTime, now);
|
|
1467
|
+
const liveWeekly = quota !== void 0 && !resetInPast(quota.weeklyResetTime, now);
|
|
1468
|
+
const remaining = live5h ? quota.remainingFraction : void 0;
|
|
1366
1469
|
const used = typeof remaining === "number" ? Math.min(Math.max(1 - remaining, 0), 1) : void 0;
|
|
1367
|
-
const weeklyRemaining = quota
|
|
1470
|
+
const weeklyRemaining = liveWeekly ? quota.weeklyFraction : void 0;
|
|
1368
1471
|
const weeklyUsed = typeof weeklyRemaining === "number" ? Math.min(Math.max(1 - weeklyRemaining, 0), 1) : void 0;
|
|
1369
1472
|
let blockedUntil = null;
|
|
1370
1473
|
if (account.coolingDownUntil && account.coolingDownUntil > now) blockedUntil = account.coolingDownUntil;
|
|
@@ -1575,6 +1678,22 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1575
1678
|
refreshInFlight = /* @__PURE__ */ new Map();
|
|
1576
1679
|
/** In-flight quota fetches keyed by account: concurrent selections share one fetchAvailableModels call. */
|
|
1577
1680
|
quotaRefreshInFlight = /* @__PURE__ */ new Map();
|
|
1681
|
+
/**
|
|
1682
|
+
* In-flight `retrieveUserQuotaSummary` probes keyed by account.
|
|
1683
|
+
*
|
|
1684
|
+
* ONE promise per account per cycle, shared by BOTH consumers:
|
|
1685
|
+
* `refreshLimits` (display, writes `cachedLimits`) and `refreshQuotaCache`
|
|
1686
|
+
* (scheduling, writes `cachedQuota`). Since #48 both read the same endpoint for
|
|
1687
|
+
* the same account, so a stale account paid two round trips where one would
|
|
1688
|
+
* do; `quotaRefreshInFlight` de-duplicated only WITHIN the scheduling path.
|
|
1689
|
+
* Sharing makes the two caches agree on what upstream actually said.
|
|
1690
|
+
*
|
|
1691
|
+
* It carries the FETCH and nothing else: the invariant that `refreshLimits`
|
|
1692
|
+
* must never write `cachedQuota` (which `rankPoolCandidates` turns into a
|
|
1693
|
+
* `blockedUntil`) is about WHERE a result lands, and each path still decides
|
|
1694
|
+
* that for itself.
|
|
1695
|
+
*/
|
|
1696
|
+
quotaSummaryInFlight = /* @__PURE__ */ new Map();
|
|
1578
1697
|
failureCounts = /* @__PURE__ */ new Map();
|
|
1579
1698
|
/** Accounts whose request-time project discovery already failed (no retry per request). */
|
|
1580
1699
|
projectRetryFailed = /* @__PURE__ */ new Set();
|
|
@@ -1682,6 +1801,28 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1682
1801
|
clearAffinityForAccount(accountKey) {
|
|
1683
1802
|
for (const [conversation, pin] of this.affinity) if (pin.key === accountKey) this.affinity.delete(conversation);
|
|
1684
1803
|
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Clear all session affinity pins.
|
|
1806
|
+
*
|
|
1807
|
+
* Called when an account is explicitly activated by the user, so all
|
|
1808
|
+
* ongoing and upcoming conversations immediately steer to the newly
|
|
1809
|
+
* activated account rather than staying pinned to the previous one.
|
|
1810
|
+
*/
|
|
1811
|
+
clearAllAffinity() {
|
|
1812
|
+
this.affinity.clear();
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Explicitly activate an account by index: sets `storage.activeIndex = index`,
|
|
1816
|
+
* clears session affinity pins so conversations immediately switch to the
|
|
1817
|
+
* activated account, and resets transient failure counts.
|
|
1818
|
+
*/
|
|
1819
|
+
async activateAccount(index) {
|
|
1820
|
+
await this.store.mutate((storage) => {
|
|
1821
|
+
if (index < 0 || index >= storage.accounts.length) throw new Error("account not found");
|
|
1822
|
+
storage.activeIndex = index;
|
|
1823
|
+
});
|
|
1824
|
+
this.clearAllAffinity();
|
|
1825
|
+
}
|
|
1685
1826
|
constructor(options) {
|
|
1686
1827
|
this.store = options.store;
|
|
1687
1828
|
this.onRotate = options.onRotate;
|
|
@@ -1786,6 +1927,49 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1786
1927
|
return this.refreshToken(key, account, cached);
|
|
1787
1928
|
}
|
|
1788
1929
|
/**
|
|
1930
|
+
* Probe `retrieveUserQuotaSummary` for one account, sharing ONE in-flight
|
|
1931
|
+
* promise with every other caller in this cycle.
|
|
1932
|
+
*
|
|
1933
|
+
* Returns `[]` on any failure — the fetch itself already answers a dead
|
|
1934
|
+
* endpoint with an empty list — so both consumers read "no groups" the same
|
|
1935
|
+
* way. The promise is dropped as soon as it settles, so the NEXT cycle
|
|
1936
|
+
* re-probes.
|
|
1937
|
+
*
|
|
1938
|
+
* The account's proxy is honoured here or the call would egress direct and
|
|
1939
|
+
* leak the account's real IP, and one timeout bounds the whole endpoint
|
|
1940
|
+
* fallback chain.
|
|
1941
|
+
*/
|
|
1942
|
+
probeQuotaSummary(account) {
|
|
1943
|
+
const key = this.accountKey(account);
|
|
1944
|
+
const existing = this.quotaSummaryInFlight.get(key);
|
|
1945
|
+
if (existing) return existing;
|
|
1946
|
+
const probe = (async () => {
|
|
1947
|
+
try {
|
|
1948
|
+
const auth = await this.accessTokenFor(account);
|
|
1949
|
+
if (!auth) return [];
|
|
1950
|
+
const { fetchQuotaSummary } = await import("./quota-summary-adID82I2.mjs");
|
|
1951
|
+
const routed = accountFetch({ proxyUrl: account.proxy });
|
|
1952
|
+
const bounded = (input, init) => {
|
|
1953
|
+
const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
|
|
1954
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
1955
|
+
return routed(input, {
|
|
1956
|
+
...init,
|
|
1957
|
+
signal
|
|
1958
|
+
});
|
|
1959
|
+
};
|
|
1960
|
+
return await fetchQuotaSummary(auth.access, account.projectId, bounded);
|
|
1961
|
+
} catch {
|
|
1962
|
+
return [];
|
|
1963
|
+
}
|
|
1964
|
+
})();
|
|
1965
|
+
this.quotaSummaryInFlight.set(key, probe);
|
|
1966
|
+
const release = () => {
|
|
1967
|
+
if (this.quotaSummaryInFlight.get(key) === probe) this.quotaSummaryInFlight.delete(key);
|
|
1968
|
+
};
|
|
1969
|
+
probe.then(release, release);
|
|
1970
|
+
return probe;
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1789
1973
|
* Refresh the display-only 5h/weekly windows for every enabled account.
|
|
1790
1974
|
*
|
|
1791
1975
|
* SEPARATE from `refreshQuotaCache`, and deliberately so. That method writes
|
|
@@ -1833,22 +2017,7 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1833
2017
|
const probes = await Promise.all(targets.map(async (account) => {
|
|
1834
2018
|
const key = this.accountKey(account);
|
|
1835
2019
|
try {
|
|
1836
|
-
const
|
|
1837
|
-
if (!auth) return {
|
|
1838
|
-
key,
|
|
1839
|
-
ok: false
|
|
1840
|
-
};
|
|
1841
|
-
const { fetchQuotaSummary } = await import("./quota-summary-adID82I2.mjs");
|
|
1842
|
-
const routed = accountFetch({ proxyUrl: account.proxy });
|
|
1843
|
-
const bounded = (input, init) => {
|
|
1844
|
-
const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
|
|
1845
|
-
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
1846
|
-
return routed(input, {
|
|
1847
|
-
...init,
|
|
1848
|
-
signal
|
|
1849
|
-
});
|
|
1850
|
-
};
|
|
1851
|
-
const groups = await fetchQuotaSummary(auth.access, account.projectId, bounded);
|
|
2020
|
+
const groups = await this.probeQuotaSummary(account);
|
|
1852
2021
|
if (groups.length === 0) return {
|
|
1853
2022
|
key,
|
|
1854
2023
|
ok: false
|
|
@@ -1911,7 +2080,6 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1911
2080
|
const auth = await this.accessTokenFor(account);
|
|
1912
2081
|
if (!auth) return null;
|
|
1913
2082
|
const { fetchAvailableModels } = await import("./models-Qn0-sh6X.mjs").then((n) => n.r);
|
|
1914
|
-
const { fetchQuotaSummary } = await import("./quota-summary-adID82I2.mjs");
|
|
1915
2083
|
const routed = accountFetch({ proxyUrl: account.proxy });
|
|
1916
2084
|
const bounded = (input, init) => {
|
|
1917
2085
|
const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
|
|
@@ -1921,7 +2089,7 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1921
2089
|
signal
|
|
1922
2090
|
});
|
|
1923
2091
|
};
|
|
1924
|
-
const [discovered, groups] = await Promise.all([fetchAvailableModels(auth.access, account.projectId, bounded),
|
|
2092
|
+
const [discovered, groups] = await Promise.all([fetchAvailableModels(auth.access, account.projectId, bounded), this.probeQuotaSummary(account)]);
|
|
1925
2093
|
const quotas = ingestFamilyQuotas(discovered, groups, account.cachedQuota);
|
|
1926
2094
|
return {
|
|
1927
2095
|
key,
|
|
@@ -1962,7 +2130,8 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1962
2130
|
}
|
|
1963
2131
|
/**
|
|
1964
2132
|
* Pick the account for one request: the affinity pin wins while it is fresh,
|
|
1965
|
-
* healthy, and not
|
|
2133
|
+
* healthy, and not quota-exhausted for the requested model; the explicitly
|
|
2134
|
+
* activated account wins next while it is usable; otherwise the pool is
|
|
1966
2135
|
* ranked by family-scoped usage (OMP-aligned) and the best candidate wins.
|
|
1967
2136
|
*/
|
|
1968
2137
|
async pickAccount(storage, model, conversationKey) {
|
|
@@ -1975,7 +2144,7 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1975
2144
|
const lastIndex = storage.accounts.findIndex((a) => this.accountKey(a) === pinnedKey);
|
|
1976
2145
|
if (lastIndex !== -1) {
|
|
1977
2146
|
const last = storage.accounts[lastIndex];
|
|
1978
|
-
if (last.enabled !== false && !isCoolingDown(last, now) && !isFamilyRateLimited(last, familyKey, now) && !
|
|
2147
|
+
if (last.enabled !== false && !isCoolingDown(last, now) && !isFamilyRateLimited(last, familyKey, now) && !isFamilyQuotaExhausted(last, family, now)) return {
|
|
1979
2148
|
account: last,
|
|
1980
2149
|
index: lastIndex
|
|
1981
2150
|
};
|
|
@@ -1986,12 +2155,18 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1986
2155
|
index
|
|
1987
2156
|
})).filter(({ account }) => account.enabled !== false);
|
|
1988
2157
|
if (eligible.length === 0) return void 0;
|
|
2158
|
+
const active = storage.accounts[storage.activeIndex];
|
|
2159
|
+
const isActiveUsable = (acc) => acc !== void 0 && acc.enabled !== false && !isCoolingDown(acc, now) && !isFamilyRateLimited(acc, familyKey, now) && !isFamilyQuotaExhausted(acc, family, now);
|
|
2160
|
+
if (isActiveUsable(active) && this.inFlightCount(this.accountKey(active), now) < 3) return {
|
|
2161
|
+
account: active,
|
|
2162
|
+
index: storage.activeIndex
|
|
2163
|
+
};
|
|
1989
2164
|
const ranked = rankPoolCandidates(eligible, model, now, storage.activeIndex);
|
|
1990
2165
|
const picked = ranked.find((candidate) => candidate.blockedUntil === null && this.inFlightCount(this.accountKey(candidate.account), now) < 3) ?? ranked.find((candidate) => candidate.blockedUntil === null);
|
|
1991
2166
|
if (!picked) {
|
|
1992
2167
|
const quotaExhausted = (account) => {
|
|
1993
2168
|
if (account.cooldownReason === "quota-exhausted" && (account.coolingDownUntil ?? 0) > now) return true;
|
|
1994
|
-
const quota = familyQuotaFor(account, family);
|
|
2169
|
+
const quota = familyQuotaFor(account, family, now);
|
|
1995
2170
|
if (!quota) return false;
|
|
1996
2171
|
const spent = (fraction, resetTime) => {
|
|
1997
2172
|
if (typeof fraction !== "number" || fraction > 0 || !resetTime) return false;
|
|
@@ -2168,6 +2343,13 @@ var AgySessionManager = class AgySessionManager {
|
|
|
2168
2343
|
if (decision.action === "revoke") {
|
|
2169
2344
|
this.tokenCache.delete(key);
|
|
2170
2345
|
this.failureCounts.delete(key);
|
|
2346
|
+
const currentIndex = storage.accounts.findIndex((a) => this.accountKey(a) === key);
|
|
2347
|
+
const nextIndex = pickNextAccountIndex(storage.accounts, currentIndex >= 0 ? currentIndex : storage.activeIndex, Date.now());
|
|
2348
|
+
if (nextIndex !== storage.activeIndex) {
|
|
2349
|
+
storage.activeIndex = nextIndex;
|
|
2350
|
+
nextIndexToRotate = nextIndex;
|
|
2351
|
+
}
|
|
2352
|
+
this.clearAffinityForAccount(key);
|
|
2171
2353
|
return;
|
|
2172
2354
|
}
|
|
2173
2355
|
if (kind === "rate-limit" && info?.rateLimitCategory !== "soft_rate_limit") {
|
|
@@ -2183,7 +2365,7 @@ var AgySessionManager = class AgySessionManager {
|
|
|
2183
2365
|
}
|
|
2184
2366
|
}
|
|
2185
2367
|
}
|
|
2186
|
-
if (decision.action === "rotate") {
|
|
2368
|
+
if (decision.action === "rotate" || decision.action === "cool") {
|
|
2187
2369
|
const currentIndex = storage.accounts.findIndex((a) => this.accountKey(a) === key);
|
|
2188
2370
|
const familyKey = familyKeyOf(info?.model);
|
|
2189
2371
|
const nextIndex = pickNextAccountIndex(storage.accounts, currentIndex >= 0 ? currentIndex : storage.activeIndex, Date.now(), familyKey);
|
|
@@ -2241,7 +2423,7 @@ var AgySessionManager = class AgySessionManager {
|
|
|
2241
2423
|
};
|
|
2242
2424
|
try {
|
|
2243
2425
|
const account = session.account.email ?? session.account.id;
|
|
2244
|
-
const { toAgyRequestBody } = await import("./translate-
|
|
2426
|
+
const { toAgyRequestBody } = await import("./translate-YhGDUuVa.mjs").then((n) => n.n);
|
|
2245
2427
|
const { fetchAgyFirstOk } = await import("./constants-BpyRirvk.mjs").then((n) => n.f);
|
|
2246
2428
|
const { parseAgySse } = await import("./parse-Bs4rPkHA.mjs").then((n) => n.n);
|
|
2247
2429
|
const requestId = generateAntigravityRequestId();
|
|
@@ -2684,4 +2866,4 @@ var JsonAccountStore = class {
|
|
|
2684
2866
|
//#endregion
|
|
2685
2867
|
export { resolveMasterKeyCodec as _, generateFingerprint as a, describeFetchError as b, resolveAntigravityVersionBounded as c, MASTER_KEY_REF as d, createAesGcmCodec as f, resolveDshHome as g, persistMasterKey as h, isAgyDisabled as i, clearExpiredState as l, loadMasterKey as m, maskProxyUrl as n, recordFingerprintVersion as o, deriveKey as p, AgySessionManager as r, resolveAntigravityVersion as s, JsonAccountStore as t, pickProbeProxyUrl as u, classifyFetchError as v, isSessionAccumulationOverflow as x, classifyHttpError as y };
|
|
2686
2868
|
|
|
2687
|
-
//# sourceMappingURL=accounts-
|
|
2869
|
+
//# sourceMappingURL=accounts-BN4h0AU4.mjs.map
|