dsh-agy 0.3.0 → 0.3.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/README.md CHANGED
@@ -20,8 +20,8 @@ fingerprinting, and both CLI and web management.
20
20
  rate limits, per-account cooldown to the real reset time, per-account device
21
21
  fingerprints.
22
22
  - **Inline Settings UI**: an Antigravity section inside DSH Settings with four
23
- tabs — accounts (login, activation, per-model quota bars, test calls,
24
- fingerprint and proxy management), models (per-model visibility), usage
23
+ tabs — accounts (login, activation, grouped 5-hour/weekly quota windows, test
24
+ calls, fingerprint and proxy management), models (per-model visibility), usage
25
25
  (cumulative token and request statistics), and credentials (import/export).
26
26
  No separate page: the surface only exists where DSH Settings does.
27
27
  - **Model visibility toggles**: hide individual models from the DSH model
@@ -37,10 +37,14 @@ fingerprinting, and both CLI and web management.
37
37
 
38
38
  ## Screenshots
39
39
 
40
- The Antigravity section inside DSH Settings — accounts, per-model quota, and
41
- one-shot model tests:
40
+ The Antigravity section inside DSH Settings. Account emails and project names are
41
+ redacted.
42
42
 
43
- ![dsh-agy settings](https://raw.githubusercontent.com/chaos-03x/dsh-agy/main/assets/screenshot_en.png)
43
+ | Accounts | Models |
44
+ |:---:|:---:|
45
+ | ![accounts](https://raw.githubusercontent.com/chaos-03x/dsh-agy/main/assets/en_accounts.png) | ![models](https://raw.githubusercontent.com/chaos-03x/dsh-agy/main/assets/en_models.png) |
46
+ | **Limits** — 5-hour and weekly windows | **Usage** — cumulative, per-model and per-account |
47
+ | ![limits](https://raw.githubusercontent.com/chaos-03x/dsh-agy/main/assets/en_limits.png) | ![usage](https://raw.githubusercontent.com/chaos-03x/dsh-agy/main/assets/en_usage.png) |
44
48
 
45
49
  ## Quickstart
46
50
 
@@ -193,6 +197,26 @@ reset with headroom left are used first ("use it or lose it"), near-exhausted
193
197
  families are avoided, and exhausted families block the account until the real
194
198
  reset time.
195
199
 
200
+ Each family is measured on **both** of its windows, because they refill on
201
+ different clocks:
202
+
203
+ | Window | Refills | Exhausted when | Blocks until |
204
+ |---|---|---|---|
205
+ | Rolling 5-hour | every 5 hours | below 15% left | the 5-hour reset |
206
+ | Weekly | every 7 days | 1% or less left | the **weekly** reset |
207
+
208
+ The two gates are independent. A family can sit at 90% of its 5-hour budget
209
+ while its week is spent — four 5-hour refills do not return a weekly budget —
210
+ so a week-exhausted account is rotated away instead of being selected and
211
+ failing. The thresholds are deliberately different (15% of five hours is ~45
212
+ minutes of runway; 1% of a week is ~1.7 hours): judging the 5-hour window too
213
+ late costs a wait of at most five hours, while judging the week too late parks
214
+ the account for days.
215
+
216
+ The weekly reading comes only from `retrieveUserQuotaSummary` — the per-model
217
+ `fetchAvailableModels` probe has no window field at all — and a probe that
218
+ reports neither window cannot erase a known-drained one.
219
+
196
220
  429 (Too Many Requests) responses:
197
221
 
198
222
  | Category | Behavior |
@@ -1,4 +1,4 @@
1
- import { _ as setResolvedAgyVersion, a as AGY_PLATFORM_ENUM, b as isProxyRouted, d as antigravityUserAgent, g as resolveAgyClientCredentials, l as OAUTH_TOKEN_URL, p as currentAgyVersion, t as AGY_CLIENT_ID, v as AgyAuthError, y as AgyPoolBlockedError } from "./constants-Db4tmyfb.mjs";
1
+ import { _ as setResolvedAgyVersion, a as AGY_PLATFORM_ENUM, b as QUOTA_WINDOWS, d as antigravityUserAgent, g as resolveAgyClientCredentials, l as OAUTH_TOKEN_URL, p as currentAgyVersion, t as AGY_CLIENT_ID, v as AgyAuthError, x as isProxyRouted, y as AgyPoolBlockedError } from "./constants-BpyRirvk.mjs";
2
2
  import { a as probeFetch, l as redactCredentials, o as proxiedFetch, r as isProxyUnreachableError, t as accountFetch } from "./proxy-DfaL73mL.mjs";
3
3
  import { i as generateAntigravityRequestId, r as deriveAntigravitySessionId } from "./identity-Ci8EGc1F.mjs";
4
4
  import { i as parseRefreshParts, n as calculateTokenExpiry, r as formatRefreshParts, t as accessTokenExpired } from "./auth-DPPVFwio.mjs";
@@ -552,8 +552,6 @@ const BACKOFF_TIERS_MS = [
552
552
  3e4,
553
553
  6e4
554
554
  ];
555
- /** Below this remaining fraction the account is treated as soft-quota-exhausted. */
556
- const SOFT_QUOTA_THRESHOLD = .15;
557
555
  function backoffFor(consecutiveFailures, maxJitterMs = 1e3) {
558
556
  const index = Math.min(Math.max(consecutiveFailures, 0), BACKOFF_TIERS_MS.length - 1);
559
557
  return (BACKOFF_TIERS_MS[index] ?? BACKOFF_TIERS_MS[BACKOFF_TIERS_MS.length - 1]) + Math.floor(Math.random() * maxJitterMs);
@@ -725,11 +723,28 @@ function pickNextAccountIndex(accounts, currentIndex, now = Date.now(), modelOrF
725
723
  if (enabled.length === 0) return currentIndex;
726
724
  return (enabled.find((e) => e.index > currentIndex) ?? enabled[0]).index;
727
725
  }
728
- /** Build the soft-quota cache TTL: short when low, long when healthy. */
729
- function computeSoftQuotaCacheTtlMs(remainingFraction, now = Date.now()) {
730
- if (typeof remainingFraction !== "number") return 6e5;
731
- if (remainingFraction < .15) return 6e4;
732
- if (remainingFraction < .5) return 3e5;
726
+ /**
727
+ * Build the soft-quota cache TTL: short when low, long when healthy.
728
+ *
729
+ * Driven by the MOST pressured of the two windows, because they drain on
730
+ * different clocks: an account sitting at 90% of its 5-hour bucket but 2% of its
731
+ * week still has to be re-measured on the short interval, or a weekly exhaustion
732
+ * is discovered only when a request fails.
733
+ *
734
+ * `weeklyFraction` is the optional SECOND parameter, so the two fractions sit
735
+ * adjacent.
736
+ *
737
+ * Takes NO timestamp on purpose: deciding which fractions are still meaningful is
738
+ * `isQuotaStale`'s job, because a window whose reset has passed must not drive the
739
+ * interval either. It applies that guard before calling in here, so both
740
+ * arguments this function sees describe windows that still exist.
741
+ */
742
+ function computeSoftQuotaCacheTtlMs(remainingFraction, weeklyFraction) {
743
+ if (typeof remainingFraction === "number" && remainingFraction < .15) return 6e4;
744
+ if (typeof weeklyFraction === "number" && weeklyFraction <= .01) return 6e4;
745
+ if (typeof remainingFraction !== "number" && typeof weeklyFraction !== "number") return 6e5;
746
+ if (typeof remainingFraction === "number" && remainingFraction < .5) return 3e5;
747
+ if (typeof weeklyFraction === "number" && weeklyFraction < .2) return 3e5;
733
748
  return 9e5;
734
749
  }
735
750
  //#endregion
@@ -1065,12 +1080,149 @@ function earliestResetTime(a, b) {
1065
1080
  return ta <= tb ? a : b;
1066
1081
  }
1067
1082
  /**
1068
- * Aggregate a fetchAvailableModels response into per-family quota records:
1069
- * the family's remaining fraction is its most-pressured model's, and the
1070
- * reset time is the earliest across the family (the bottleneck resets first).
1083
+ * The families a quota group's WINDOW BUCKET ID belongs to.
1084
+ *
1085
+ * `retrieveUserQuotaSummary` labels its buckets with upstream's own counters —
1086
+ * measured ids are `gemini-5h`, `gemini-weekly`, `3p-5h`, `3p-weekly` — which
1087
+ * is the same taxonomy `modelFamilyOf` mirrors. Reading the bucket id is
1088
+ * therefore a machine-readable lookup rather than a guess at a human label.
1089
+ *
1090
+ * `3p-` maps to TWO families on purpose, and that is not a shortcut: upstream
1091
+ * counts Claude and GPT under one third-party counter, so the weekly budget
1092
+ * belongs to both. No model-id prefix rule can recover that split, which is why
1093
+ * the mapping lives here rather than in `modelFamilyOf`.
1094
+ */
1095
+ function familiesForBucketId(bucketId) {
1096
+ const id = bucketId.toLowerCase();
1097
+ if (id.startsWith("gemini-")) return ["google"];
1098
+ if (id.startsWith("3p-") || id.startsWith("third_party-")) return ["anthropic", "openai"];
1099
+ if (id.startsWith("claude-")) return ["anthropic"];
1100
+ if (id.startsWith("gpt-") || id.startsWith("openai-")) return ["openai"];
1101
+ return [];
1102
+ }
1103
+ /**
1104
+ * The families a group's LABEL covers, consulted only when no bucket id was
1105
+ * recognizable.
1106
+ *
1107
+ * Upstream's groups are its own taxonomy, not ours, and the measured labels are
1108
+ * `Gemini Models` and `Claude and GPT models` — the second spans two families,
1109
+ * so this is a 1-to-N mapping. It is a FALLBACK, not the primary rule: matching
1110
+ * words in a display string is the fragile half, and it survives only so that a
1111
+ * bucket id upstream renames still lands somewhere sensible.
1112
+ *
1113
+ * A label naming nothing we recognize maps to NO family deliberately.
1114
+ * Attributing an unknown group's budget to `google` would block healthy
1115
+ * accounts; leaving it unmapped only means that family stays unmeasured, which
1116
+ * is exactly the state before this function existed.
1117
+ */
1118
+ function familiesForGroupName(name) {
1119
+ const label = name.toLowerCase();
1120
+ const families = [];
1121
+ if (label.includes("gemini") || label.includes("google")) families.push("google");
1122
+ const claude = label.includes("claude");
1123
+ const gpt = label.includes("gpt");
1124
+ if (claude && gpt) families.push("anthropic", "openai");
1125
+ else if (claude) families.push("anthropic");
1126
+ else if (gpt) families.push("openai");
1127
+ else if (families.length === 0 && (label.includes("3p") || label.includes("third"))) families.push("anthropic", "openai");
1128
+ return families;
1129
+ }
1130
+ /**
1131
+ * Which of the two tracked windows an upstream token names, or `undefined` for a
1132
+ * token `CachedQuota` has no field for (`daily`, `monthly`).
1133
+ *
1134
+ * Read from `QUOTA_WINDOWS` (`types.ts`), the SAME table `adapter/quota-summary`
1135
+ * orders windows by. These were two independent tables once, and the gap between
1136
+ * them was a real defect: `daily` was orderable in the parser but unknown here, so
1137
+ * the group materialized an empty record that overwrote a live measurement. One
1138
+ * table makes that drift structurally impossible rather than merely tested for.
1139
+ *
1140
+ * Lookup is exact on the measured vocabulary (`5h`, `weekly`). A variant spelling
1141
+ * therefore classifies as unknown — which is safe, because the guard in
1142
+ * `ingestQuotaGroups` then skips the record rather than emptying it, and the
1143
+ * per-model probe still supplies the 5-hour reading.
1144
+ */
1145
+ function windowKind(window) {
1146
+ return QUOTA_WINDOWS[window.toLowerCase()]?.kind;
1147
+ }
1148
+ /** The families one group belongs to: its bucket ids first, its label second. */
1149
+ function familiesForGroup(group) {
1150
+ const families = [];
1151
+ const add = (family) => {
1152
+ if (!families.includes(family)) families.push(family);
1153
+ };
1154
+ for (const window of group.windows) for (const family of familiesForBucketId(window.bucketId)) add(family);
1155
+ if (families.length === 0) for (const family of familiesForGroupName(group.name)) add(family);
1156
+ return families;
1157
+ }
1158
+ /**
1159
+ * Aggregate the grouped `retrieveUserQuotaSummary` windows into per-family
1160
+ * records — the ONLY source of the weekly window.
1161
+ *
1162
+ * `fetchAvailableModels` cannot supply it: its `quotaInfo` carries exactly
1163
+ * `remainingFraction` and `resetTime`, with no window field at all, which is why
1164
+ * a weekly limit used to be invisible to rotation.
1165
+ *
1166
+ * Two groups can land on one family (upstream may split `3p` later), so each
1167
+ * window keeps the MOST pressured reading rather than letting the last group
1168
+ * win — the same rule the per-model merge below uses.
1169
+ */
1170
+ function ingestQuotaGroups(groups) {
1171
+ const families = /* @__PURE__ */ new Map();
1172
+ for (const group of groups) {
1173
+ const keys = familiesForGroup(group);
1174
+ if (keys.length === 0) continue;
1175
+ let rolling;
1176
+ let weekly;
1177
+ for (const window of group.windows) {
1178
+ const kind = windowKind(window.window);
1179
+ if (kind === "rolling") rolling ??= window;
1180
+ else if (kind === "weekly") weekly ??= window;
1181
+ }
1182
+ for (const key of keys) {
1183
+ const current = families.get(key);
1184
+ const record = { ...current };
1185
+ if (typeof rolling?.remainingFraction === "number") {
1186
+ record.remainingFraction = current?.remainingFraction === void 0 ? rolling.remainingFraction : Math.min(current.remainingFraction, rolling.remainingFraction);
1187
+ const resetTime = earliestResetTime(current?.resetTime, rolling.resetTime ?? void 0);
1188
+ if (resetTime) record.resetTime = resetTime;
1189
+ }
1190
+ if (typeof weekly?.remainingFraction === "number") {
1191
+ record.weeklyFraction = current?.weeklyFraction === void 0 ? weekly.remainingFraction : Math.min(current.weeklyFraction, weekly.remainingFraction);
1192
+ const weeklyResetTime = earliestResetTime(current?.weeklyResetTime, weekly.resetTime ?? void 0);
1193
+ if (weeklyResetTime) record.weeklyResetTime = weeklyResetTime;
1194
+ }
1195
+ if (record.remainingFraction === void 0 && record.weeklyFraction === void 0 && !current) continue;
1196
+ families.set(key, record);
1197
+ }
1198
+ }
1199
+ return Object.fromEntries(families);
1200
+ }
1201
+ /**
1202
+ * Aggregate a `fetchAvailableModels` response, and when available the grouped
1203
+ * `retrieveUserQuotaSummary` windows, into per-family quota records.
1204
+ *
1205
+ * The two sources describe the same counters from different angles, so they are
1206
+ * merged rather than kept apart:
1207
+ * - per MODEL `quotaInfo.remainingFraction` is the rolling 5-hour counter, and
1208
+ * the family takes its most-pressured model (the bottleneck resets first);
1209
+ * - per GROUP summary windows add the weekly budget, which has no per-model
1210
+ * representation at all.
1211
+ *
1212
+ * `previous` is the account's existing cache, and it exists so a probe that
1213
+ * reports only one of the two windows cannot ERASE the other. The measured
1214
+ * reason: `fetchQuotaSummary` returns `[]` instead of throwing, so without the
1215
+ * carry-forward one timing-out endpoint would drop a known-drained weekly window
1216
+ * and put an exhausted account straight back into rotation.
1217
+ *
1218
+ * Carrying that value is safe next to a fresh 5-hour reading because it keeps its
1219
+ * own `weeklyResetTime`: once that moment passes, `isFamilyDrained` and
1220
+ * `parseFutureResetMs` both ignore the reading, so a stale weekly cannot outlive
1221
+ * the window it describes.
1071
1222
  */
1072
- function ingestFamilyQuotas(discovered) {
1223
+ function ingestFamilyQuotas(discovered, groups, previous) {
1073
1224
  const families = /* @__PURE__ */ new Map();
1225
+ if (groups && groups.length > 0) for (const [key, quota] of Object.entries(ingestQuotaGroups(groups))) families.set(key, { ...quota });
1074
1226
  for (const [modelId, entry] of Object.entries(discovered.models ?? {})) {
1075
1227
  const remaining = entry.quotaInfo?.remainingFraction;
1076
1228
  if (typeof remaining !== "number" || !Number.isFinite(remaining)) continue;
@@ -1078,11 +1230,22 @@ function ingestFamilyQuotas(discovered) {
1078
1230
  const current = families.get(key);
1079
1231
  const resetTime = earliestResetTime(current?.resetTime, entry.quotaInfo?.resetTime);
1080
1232
  families.set(key, {
1081
- remainingFraction: current ? Math.min(current.remainingFraction ?? 1, remaining) : remaining,
1233
+ ...current,
1234
+ remainingFraction: current?.remainingFraction === void 0 ? remaining : Math.min(current.remainingFraction, remaining),
1082
1235
  ...resetTime ? { resetTime } : {},
1083
1236
  modelCount: (current?.modelCount ?? 0) + 1
1084
1237
  });
1085
1238
  }
1239
+ if (previous) for (const [key, prev] of Object.entries(previous)) {
1240
+ const current = families.get(key);
1241
+ if (prev.weeklyFraction === void 0) continue;
1242
+ if (current?.weeklyFraction !== void 0) continue;
1243
+ families.set(key, {
1244
+ ...current,
1245
+ weeklyFraction: prev.weeklyFraction,
1246
+ ...current?.weeklyResetTime === void 0 && prev.weeklyResetTime !== void 0 ? { weeklyResetTime: prev.weeklyResetTime } : {}
1247
+ });
1248
+ }
1086
1249
  return Object.fromEntries(families);
1087
1250
  }
1088
1251
  /** The quota record for one family, or the most-pressured family when the model is unknown. */
@@ -1099,7 +1262,8 @@ function familyQuotaFor(account, family) {
1099
1262
  /** Whether the account's quota cache needs a refresh (missing, or past its health-based TTL). */
1100
1263
  function isQuotaStale(account, now = Date.now()) {
1101
1264
  if (!account.cachedQuota || !account.cachedQuotaUpdatedAt) return true;
1102
- const ttl = computeSoftQuotaCacheTtlMs(familyQuotaFor(account)?.remainingFraction);
1265
+ const mostPressured = familyQuotaFor(account);
1266
+ const ttl = computeSoftQuotaCacheTtlMs(resetInPast(mostPressured?.resetTime, now) ? void 0 : mostPressured?.remainingFraction, resetInPast(mostPressured?.weeklyResetTime, now) ? void 0 : mostPressured?.weeklyFraction);
1103
1267
  return now - account.cachedQuotaUpdatedAt > ttl;
1104
1268
  }
1105
1269
  /** How long a measured 5h/weekly window snapshot stays fresh. */
@@ -1125,20 +1289,46 @@ function isLimitsStale(account, now = Date.now()) {
1125
1289
  if (typeof updatedAt !== "number" || !Number.isFinite(updatedAt)) return true;
1126
1290
  return now - updatedAt > LIMITS_CACHE_TTL_MS;
1127
1291
  }
1128
- /** Whether the requested family on this account is soft-quota-exhausted (below the pre-check threshold). */
1292
+ /** Whether a reset moment has already passed. An absent or malformed one has not. */
1293
+ function resetInPast(resetTime, now) {
1294
+ if (!resetTime) return false;
1295
+ const reset = Date.parse(resetTime);
1296
+ return !Number.isNaN(reset) && reset <= now;
1297
+ }
1298
+ /**
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
+ */
1129
1311
  function isFamilyDrained(account, family, now = Date.now()) {
1130
1312
  const quota = familyQuotaFor(account, family);
1131
- if (!quota || typeof quota.remainingFraction !== "number") return false;
1132
- if (quota.resetTime) {
1133
- const reset = Date.parse(quota.resetTime);
1134
- if (!Number.isNaN(reset) && reset <= now) return false;
1135
- }
1136
- return quota.remainingFraction < SOFT_QUOTA_THRESHOLD;
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;
1137
1317
  }
1138
1318
  /**
1139
1319
  * Required drain rate: headroomFraction / remainingHours — how fast the
1140
1320
  * family's remaining quota must be consumed to avoid expiring unused at its
1141
1321
  * reset (mirrors AuthStorage.#computeWindowRequiredDrain with a daily window).
1322
+ *
1323
+ * DELIBERATELY FIVE-HOUR ONLY, and this is a decision rather than an omission.
1324
+ * The weekly window influences `blockedUntil` and `hot` instead of the ranking
1325
+ * ORDER, because a week is not a spend-by deadline: the `DAY_MS` clamp below
1326
+ * caps the horizon at 24h, so a weekly reset ~5 days out would report ~24h of
1327
+ * urgency for a budget that cannot be spent faster to any benefit — and every
1328
+ * family would then be ranked by a number that no longer distinguishes them.
1329
+ * Making this weekly-aware therefore means replacing that clamp with a horizon
1330
+ * the weekly window can actually express, not adding a second fraction to the
1331
+ * numerator.
1142
1332
  */
1143
1333
  function requiredDrainFor(quota, now = Date.now()) {
1144
1334
  const remaining = quota?.remainingFraction;
@@ -1174,13 +1364,17 @@ function rankPoolCandidates(entries, modelId, now = Date.now(), startIndex = 0)
1174
1364
  const quota = familyQuotaFor(account, family);
1175
1365
  const remaining = quota?.remainingFraction;
1176
1366
  const used = typeof remaining === "number" ? Math.min(Math.max(1 - remaining, 0), 1) : void 0;
1367
+ const weeklyRemaining = quota?.weeklyFraction;
1368
+ const weeklyUsed = typeof weeklyRemaining === "number" ? Math.min(Math.max(1 - weeklyRemaining, 0), 1) : void 0;
1177
1369
  let blockedUntil = null;
1178
1370
  if (account.coolingDownUntil && account.coolingDownUntil > now) blockedUntil = account.coolingDownUntil;
1179
1371
  const familyLimit = account.rateLimitResetTimes?.[familyKeyOf(modelId)];
1180
1372
  if (familyLimit !== void 0 && familyLimit > now) blockedUntil = blockedUntil === null ? familyLimit : Math.max(blockedUntil, familyLimit);
1181
- if (blockedUntil === null && quota && typeof remaining === "number" && remaining <= 0) {
1182
- const resetMs = parseFutureResetMs(quota.resetTime, now);
1373
+ if (blockedUntil === null && quota) {
1374
+ const resetMs = typeof remaining === "number" && remaining <= 0 ? parseFutureResetMs(quota.resetTime, now) : void 0;
1183
1375
  if (resetMs !== void 0) blockedUntil = resetMs;
1376
+ const weeklyResetMs = typeof weeklyRemaining === "number" && weeklyRemaining <= 0 ? parseFutureResetMs(quota.weeklyResetTime, now) : void 0;
1377
+ if (weeklyResetMs !== void 0) blockedUntil = blockedUntil === null ? weeklyResetMs : Math.max(blockedUntil, weeklyResetMs);
1184
1378
  }
1185
1379
  return {
1186
1380
  account,
@@ -1189,8 +1383,8 @@ function rankPoolCandidates(entries, modelId, now = Date.now(), startIndex = 0)
1189
1383
  blockedUntil,
1190
1384
  usedFraction: used,
1191
1385
  requiredDrain: requiredDrainFor(quota, now),
1192
- hot: used !== void 0 && used >= .85,
1193
- measured: used !== void 0
1386
+ hot: used !== void 0 && used >= .85 || weeklyUsed !== void 0 && weeklyUsed >= .85,
1387
+ measured: used !== void 0 || weeklyUsed !== void 0
1194
1388
  };
1195
1389
  });
1196
1390
  candidates.sort((left, right) => {
@@ -1644,7 +1838,7 @@ var AgySessionManager = class AgySessionManager {
1644
1838
  key,
1645
1839
  ok: false
1646
1840
  };
1647
- const { fetchQuotaSummary } = await import("./quota-summary-5CypVffd.mjs");
1841
+ const { fetchQuotaSummary } = await import("./quota-summary-adID82I2.mjs");
1648
1842
  const routed = accountFetch({ proxyUrl: account.proxy });
1649
1843
  const bounded = (input, init) => {
1650
1844
  const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
@@ -1716,16 +1910,19 @@ var AgySessionManager = class AgySessionManager {
1716
1910
  try {
1717
1911
  const auth = await this.accessTokenFor(account);
1718
1912
  if (!auth) return null;
1719
- const { fetchAvailableModels } = await import("./models-BGuwl50d.mjs").then((n) => n.r);
1913
+ const { fetchAvailableModels } = await import("./models-Qn0-sh6X.mjs").then((n) => n.r);
1914
+ const { fetchQuotaSummary } = await import("./quota-summary-adID82I2.mjs");
1720
1915
  const routed = accountFetch({ proxyUrl: account.proxy });
1721
- const quotas = ingestFamilyQuotas(await fetchAvailableModels(auth.access, account.projectId, (input, init) => {
1916
+ const bounded = (input, init) => {
1722
1917
  const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
1723
1918
  const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
1724
1919
  return routed(input, {
1725
1920
  ...init,
1726
1921
  signal
1727
1922
  });
1728
- }));
1923
+ };
1924
+ const [discovered, groups] = await Promise.all([fetchAvailableModels(auth.access, account.projectId, bounded), fetchQuotaSummary(auth.access, account.projectId, bounded)]);
1925
+ const quotas = ingestFamilyQuotas(discovered, groups, account.cachedQuota);
1729
1926
  return {
1730
1927
  key,
1731
1928
  quotas,
@@ -1795,9 +1992,13 @@ var AgySessionManager = class AgySessionManager {
1795
1992
  const quotaExhausted = (account) => {
1796
1993
  if (account.cooldownReason === "quota-exhausted" && (account.coolingDownUntil ?? 0) > now) return true;
1797
1994
  const quota = familyQuotaFor(account, family);
1798
- if ((quota?.remainingFraction ?? 1) > 0 || !quota?.resetTime) return false;
1799
- const resetAt = Date.parse(quota.resetTime);
1800
- return !Number.isNaN(resetAt) && resetAt > now;
1995
+ if (!quota) return false;
1996
+ const spent = (fraction, resetTime) => {
1997
+ if (typeof fraction !== "number" || fraction > 0 || !resetTime) return false;
1998
+ const resetAt = Date.parse(resetTime);
1999
+ return !Number.isNaN(resetAt) && resetAt > now;
2000
+ };
2001
+ return spent(quota.remainingFraction, quota.resetTime) || spent(quota.weeklyFraction, quota.weeklyResetTime);
1801
2002
  };
1802
2003
  const retryable = ranked.filter((candidate) => !quotaExhausted(candidate.account));
1803
2004
  const blocked = retryable.length > 0 ? retryable : ranked;
@@ -1885,7 +2086,7 @@ var AgySessionManager = class AgySessionManager {
1885
2086
  }
1886
2087
  const key = this.accountKey(picked.account);
1887
2088
  if (!picked.account.projectId && !this.projectRetryFailed.has(key)) try {
1888
- const { loadCodeAssist } = await import("./exchange-yZPIZumk.mjs").then((n) => n.n);
2089
+ const { loadCodeAssist } = await import("./exchange-LzEyxve1.mjs").then((n) => n.n);
1889
2090
  const { projectId } = await loadCodeAssist(auth.access, { proxyUrl: picked.account.proxy });
1890
2091
  if (projectId) {
1891
2092
  await this.store.mutate((s) => {
@@ -2041,7 +2242,7 @@ var AgySessionManager = class AgySessionManager {
2041
2242
  try {
2042
2243
  const account = session.account.email ?? session.account.id;
2043
2244
  const { toAgyRequestBody } = await import("./translate-BQu8HLvB.mjs").then((n) => n.n);
2044
- const { fetchAgyFirstOk } = await import("./constants-Db4tmyfb.mjs").then((n) => n.f);
2245
+ const { fetchAgyFirstOk } = await import("./constants-BpyRirvk.mjs").then((n) => n.f);
2045
2246
  const { parseAgySse } = await import("./parse-Bs4rPkHA.mjs").then((n) => n.n);
2046
2247
  const requestId = generateAntigravityRequestId();
2047
2248
  const body = toAgyRequestBody({
@@ -2483,4 +2684,4 @@ var JsonAccountStore = class {
2483
2684
  //#endregion
2484
2685
  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 };
2485
2686
 
2486
- //# sourceMappingURL=accounts-CkaHhb5H.mjs.map
2687
+ //# sourceMappingURL=accounts-CKWybAfc.mjs.map