@eddyskywalker/dsh-chatgpt-subscription 0.12.6 → 0.13.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.
Files changed (147) hide show
  1. package/CHANGELOG.md +646 -619
  2. package/README.md +5 -0
  3. package/lib/client.js +3975 -4521
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +1243 -300
  6. package/lib/types/client/CodexComposerQuota.d.ts.map +1 -1
  7. package/lib/types/client/CodexSubscriptionSection.d.ts +1 -3
  8. package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
  9. package/lib/types/client/ProviderHubSection.d.ts +6 -5
  10. package/lib/types/client/ProviderHubSection.d.ts.map +1 -1
  11. package/lib/types/client/antigravity/AntigravityComposerQuota.d.ts +22 -1
  12. package/lib/types/client/antigravity/AntigravityComposerQuota.d.ts.map +1 -1
  13. package/lib/types/client/antigravity/AntigravitySection.d.ts +0 -2
  14. package/lib/types/client/antigravity/AntigravitySection.d.ts.map +1 -1
  15. package/lib/types/client/antigravity/locales.d.ts +33 -26
  16. package/lib/types/client/antigravity/locales.d.ts.map +1 -1
  17. package/lib/types/client/antigravity/styles.d.ts.map +1 -1
  18. package/lib/types/client/claude/ClaudeComposerQuota.d.ts +3 -7
  19. package/lib/types/client/claude/ClaudeComposerQuota.d.ts.map +1 -1
  20. package/lib/types/client/claude/ClaudeSection.d.ts +10 -9
  21. package/lib/types/client/claude/ClaudeSection.d.ts.map +1 -1
  22. package/lib/types/client/claude/locales.d.ts +29 -29
  23. package/lib/types/client/claude/locales.d.ts.map +1 -1
  24. package/lib/types/client/claude/useClaudeSection.d.ts +93 -0
  25. package/lib/types/client/claude/useClaudeSection.d.ts.map +1 -0
  26. package/lib/types/client/command-code/CommandCodeComposerQuota.d.ts +3 -7
  27. package/lib/types/client/command-code/CommandCodeComposerQuota.d.ts.map +1 -1
  28. package/lib/types/client/command-code/CommandCodeSection.d.ts +0 -3
  29. package/lib/types/client/command-code/CommandCodeSection.d.ts.map +1 -1
  30. package/lib/types/client/command-code/locales.d.ts +32 -29
  31. package/lib/types/client/command-code/locales.d.ts.map +1 -1
  32. package/lib/types/client/common/AccountPoolSection.d.ts +16 -4
  33. package/lib/types/client/common/AccountPoolSection.d.ts.map +1 -1
  34. package/lib/types/client/common/ComposerQuotaBadge.d.ts +35 -0
  35. package/lib/types/client/common/ComposerQuotaBadge.d.ts.map +1 -0
  36. package/lib/types/client/common/ContextWindowEditor.d.ts +69 -0
  37. package/lib/types/client/common/ContextWindowEditor.d.ts.map +1 -0
  38. package/lib/types/client/common/ModelChecklist.d.ts +29 -0
  39. package/lib/types/client/common/ModelChecklist.d.ts.map +1 -0
  40. package/lib/types/client/common/account-pool-labels.d.ts +18 -2
  41. package/lib/types/client/common/account-pool-labels.d.ts.map +1 -1
  42. package/lib/types/client/common/account-quota.d.ts +60 -0
  43. package/lib/types/client/common/account-quota.d.ts.map +1 -0
  44. package/lib/types/client/common/format.d.ts +30 -0
  45. package/lib/types/client/common/format.d.ts.map +1 -0
  46. package/lib/types/client/common/line-api.d.ts +8 -0
  47. package/lib/types/client/common/line-api.d.ts.map +1 -0
  48. package/lib/types/client/common/model-checklist-styles.d.ts +2 -0
  49. package/lib/types/client/common/model-checklist-styles.d.ts.map +1 -0
  50. package/lib/types/client/common/styles.d.ts.map +1 -1
  51. package/lib/types/client/common/use-store.d.ts +3 -0
  52. package/lib/types/client/common/use-store.d.ts.map +1 -0
  53. package/lib/types/client/hub/HubOverview.d.ts +14 -0
  54. package/lib/types/client/hub/HubOverview.d.ts.map +1 -0
  55. package/lib/types/client/hub/brand-icons.d.ts +46 -0
  56. package/lib/types/client/hub/brand-icons.d.ts.map +1 -0
  57. package/lib/types/client/hub/brand-svg.d.ts +51 -0
  58. package/lib/types/client/hub/brand-svg.d.ts.map +1 -0
  59. package/lib/types/client/hub/hub-styles.d.ts +2 -0
  60. package/lib/types/client/hub/hub-styles.d.ts.map +1 -0
  61. package/lib/types/client/hub/providers.d.ts +38 -0
  62. package/lib/types/client/hub/providers.d.ts.map +1 -0
  63. package/lib/types/client/index.d.ts +20 -6
  64. package/lib/types/client/index.d.ts.map +1 -1
  65. package/lib/types/client/kimi-code/KimiCodeComposerQuota.d.ts +3 -7
  66. package/lib/types/client/kimi-code/KimiCodeComposerQuota.d.ts.map +1 -1
  67. package/lib/types/client/kimi-code/KimiCodeSection.d.ts +0 -3
  68. package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
  69. package/lib/types/client/kimi-code/locales.d.ts +32 -32
  70. package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
  71. package/lib/types/client/kimi-code/styles.d.ts.map +1 -1
  72. package/lib/types/client/locales.d.ts +45 -35
  73. package/lib/types/client/locales.d.ts.map +1 -1
  74. package/lib/types/client/minimax-code/MinimaxCodeComposerQuota.d.ts +4 -8
  75. package/lib/types/client/minimax-code/MinimaxCodeComposerQuota.d.ts.map +1 -1
  76. package/lib/types/client/minimax-code/MinimaxCodeSection.d.ts +8 -37
  77. package/lib/types/client/minimax-code/MinimaxCodeSection.d.ts.map +1 -1
  78. package/lib/types/client/minimax-code/api.d.ts +3 -3
  79. package/lib/types/client/minimax-code/api.d.ts.map +1 -1
  80. package/lib/types/client/minimax-code/locales.d.ts +29 -56
  81. package/lib/types/client/minimax-code/locales.d.ts.map +1 -1
  82. package/lib/types/client/minimax-code/useMinimaxCodeSection.d.ts +115 -0
  83. package/lib/types/client/minimax-code/useMinimaxCodeSection.d.ts.map +1 -0
  84. package/lib/types/client/ollama/OllamaSection.d.ts +2 -2
  85. package/lib/types/client/ollama/OllamaSection.d.ts.map +1 -1
  86. package/lib/types/client/ollama/locales.d.ts +54 -24
  87. package/lib/types/client/ollama/locales.d.ts.map +1 -1
  88. package/lib/types/client/styles.d.ts.map +1 -1
  89. package/lib/types/client/workbuddy/WorkBuddyComposerQuota.d.ts +3 -7
  90. package/lib/types/client/workbuddy/WorkBuddyComposerQuota.d.ts.map +1 -1
  91. package/lib/types/client/workbuddy/WorkBuddySection.d.ts +10 -37
  92. package/lib/types/client/workbuddy/WorkBuddySection.d.ts.map +1 -1
  93. package/lib/types/client/workbuddy/locales.d.ts +0 -66
  94. package/lib/types/client/workbuddy/locales.d.ts.map +1 -1
  95. package/lib/types/client/workbuddy/useWorkBuddySection.d.ts +141 -0
  96. package/lib/types/client/workbuddy/useWorkBuddySection.d.ts.map +1 -0
  97. package/lib/types/host/antigravity/account-pool.d.ts +18 -1
  98. package/lib/types/host/antigravity/account-pool.d.ts.map +1 -1
  99. package/lib/types/host/antigravity/client.d.ts.map +1 -1
  100. package/lib/types/host/claude/account-pool.d.ts +22 -2
  101. package/lib/types/host/claude/account-pool.d.ts.map +1 -1
  102. package/lib/types/host/claude/client.d.ts +37 -5
  103. package/lib/types/host/claude/client.d.ts.map +1 -1
  104. package/lib/types/host/claude/routes.d.ts.map +1 -1
  105. package/lib/types/host/codex-account-pool.d.ts +26 -1
  106. package/lib/types/host/codex-account-pool.d.ts.map +1 -1
  107. package/lib/types/host/command-code/account-pool.d.ts +13 -1
  108. package/lib/types/host/command-code/account-pool.d.ts.map +1 -1
  109. package/lib/types/host/command-code/client.d.ts +2 -1
  110. package/lib/types/host/command-code/client.d.ts.map +1 -1
  111. package/lib/types/host/common/account-quota.d.ts +44 -0
  112. package/lib/types/host/common/account-quota.d.ts.map +1 -0
  113. package/lib/types/host/common/catalog-snapshot.d.ts +9 -0
  114. package/lib/types/host/common/catalog-snapshot.d.ts.map +1 -1
  115. package/lib/types/host/common/provider-route.d.ts +56 -0
  116. package/lib/types/host/common/provider-route.d.ts.map +1 -0
  117. package/lib/types/host/hub-overview.d.ts +26 -0
  118. package/lib/types/host/hub-overview.d.ts.map +1 -0
  119. package/lib/types/host/kimi-code/account-pool.d.ts +24 -1
  120. package/lib/types/host/kimi-code/account-pool.d.ts.map +1 -1
  121. package/lib/types/host/kimi-code/client.d.ts +23 -2
  122. package/lib/types/host/kimi-code/client.d.ts.map +1 -1
  123. package/lib/types/host/minimax-code/account-pool.d.ts +34 -2
  124. package/lib/types/host/minimax-code/account-pool.d.ts.map +1 -1
  125. package/lib/types/host/minimax-code/client.d.ts +23 -0
  126. package/lib/types/host/minimax-code/client.d.ts.map +1 -1
  127. package/lib/types/host/minimax-code/routes.d.ts.map +1 -1
  128. package/lib/types/host/oauth-service.d.ts +16 -0
  129. package/lib/types/host/oauth-service.d.ts.map +1 -1
  130. package/lib/types/host/responses-client.d.ts.map +1 -1
  131. package/lib/types/host/responses-mapper.d.ts +55 -0
  132. package/lib/types/host/responses-mapper.d.ts.map +1 -1
  133. package/lib/types/host/usage-service.d.ts +27 -2
  134. package/lib/types/host/usage-service.d.ts.map +1 -1
  135. package/lib/types/host/workbuddy/account-pool.d.ts +13 -1
  136. package/lib/types/host/workbuddy/account-pool.d.ts.map +1 -1
  137. package/lib/types/host/workbuddy/client.d.ts +15 -0
  138. package/lib/types/host/workbuddy/client.d.ts.map +1 -1
  139. package/lib/types/host/workbuddy/routes.d.ts.map +1 -1
  140. package/lib/types/index.d.ts.map +1 -1
  141. package/lib/types/shared/account-pool-contracts.d.ts +44 -0
  142. package/lib/types/shared/account-pool-contracts.d.ts.map +1 -1
  143. package/lib/types/shared/antigravity-contracts.d.ts +9 -3
  144. package/lib/types/shared/antigravity-contracts.d.ts.map +1 -1
  145. package/lib/types/shared/hub-contracts.d.ts +37 -0
  146. package/lib/types/shared/hub-contracts.d.ts.map +1 -0
  147. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -516,6 +516,17 @@ function catalogSnapshotPath(name) {
516
516
  return path.join(dshHomeDir(), "storages", `${name}-catalog.json`);
517
517
  }
518
518
  /**
519
+ * How many models one line's catalog holds, for the hub overview's "N/M models".
520
+ *
521
+ * An empty catalog is reported as `null` rather than `0`: "this line has synced
522
+ * nothing yet" and "this line offers no models" are different statements, and
523
+ * only the second one is a fact. The overview hides the model count for a null,
524
+ * which is what a line that has never synced should show.
525
+ */
526
+ function catalogTotal(count) {
527
+ return count === 0 ? null : count;
528
+ }
529
+ /**
519
530
  * Snapshot name for one catalog *and* one credential scope.
520
531
  *
521
532
  * A persisted snapshot answers before any credential is read — that is the whole
@@ -681,7 +692,7 @@ let snapshotRehydrated = null;
681
692
  * directly; returning it here under a different entry type would force every
682
693
  * one of them to convert it.
683
694
  */
684
- function getCachedCatalog() {
695
+ function getCachedCatalog$2() {
685
696
  return catalogCache$2?.models;
686
697
  }
687
698
  /**
@@ -740,7 +751,7 @@ async function performCatalogLoad$1(credentials, options) {
740
751
  live = true;
741
752
  writeCatalogSnapshot(catalogSnapshotName(SNAPSHOT_SCOPE, key), [...models], Date.now());
742
753
  } catch {
743
- models = getCachedCatalog() ?? [];
754
+ models = getCachedCatalog$2() ?? [];
744
755
  live = false;
745
756
  }
746
757
  catalogCache$2 = {
@@ -2521,13 +2532,31 @@ var OAuthService = class {
2521
2532
  * "credentials are required" on every web search.
2522
2533
  */
2523
2534
  async credentials(forceRefresh = false, access = {}) {
2535
+ return (await this.credentialSelection(forceRefresh, access)).credentials;
2536
+ }
2537
+ /**
2538
+ * The credential to use next, together with the pool row that owns it.
2539
+ *
2540
+ * The settings card keys each account's quota snapshot by the row the reading
2541
+ * was made for, and the credential cannot do that job: a refresh rotates its
2542
+ * tokens, and an account may state no id of its own. Callers that care about
2543
+ * identity ask here; every other caller keeps using {@link credentials} and
2544
+ * sees no difference.
2545
+ *
2546
+ * @param forceRefresh - refresh even a token that is not close to expiry.
2547
+ * @param access - who is asking; see {@link credentials}.
2548
+ */
2549
+ async credentialSelection(forceRefresh = false, access = {}) {
2524
2550
  if (this.pool !== null) {
2525
- if (access.purpose === "tool") return (await this.pool.getCredentialAccount(this.fetchFn, forceRefresh)).credentials;
2526
- return (await this.pool.getEffectiveAccount(void 0, this.fetchFn, forceRefresh)).credentials;
2551
+ const picked = access.purpose === "tool" ? await this.pool.getCredentialAccount(this.fetchFn, forceRefresh) : await this.pool.getEffectiveAccount(void 0, this.fetchFn, forceRefresh);
2552
+ return {
2553
+ accountId: picked.account.id,
2554
+ credentials: picked.credentials
2555
+ };
2527
2556
  }
2528
2557
  const stored = await this.loadAuthenticated();
2529
- if (forceRefresh || stored.expiresAt - this.now() <= 6e4) return this.refreshCredentials(stored);
2530
- return stored;
2558
+ if (forceRefresh || stored.expiresAt - this.now() <= 6e4) return { credentials: await this.refreshCredentials(stored) };
2559
+ return { credentials: stored };
2531
2560
  }
2532
2561
  dispose() {
2533
2562
  if (this.disposed) return;
@@ -2823,6 +2852,49 @@ async function oauthErrorIdentifier(response) {
2823
2852
  return null;
2824
2853
  }
2825
2854
  //#endregion
2855
+ //#region src/host/common/account-quota.ts
2856
+ /**
2857
+ * A reset instant as Unix milliseconds.
2858
+ *
2859
+ * Providers state this in seconds, in milliseconds, or not at all, and the two
2860
+ * numeric forms are told apart by magnitude rather than by a flag: 10^10
2861
+ * seconds is the year 2286, so anything larger is already milliseconds.
2862
+ */
2863
+ function quotaResetMs(value) {
2864
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
2865
+ return value > 1e10 ? value : value * 1e3;
2866
+ }
2867
+ /**
2868
+ * One window, or null when its consumed share was never measured.
2869
+ *
2870
+ * `label` may be empty: the card names an unlabeled window from its length, and
2871
+ * only falls back to a generic name when the line stated neither.
2872
+ */
2873
+ function quotaWindow$1(label, usedPercent, options = {}) {
2874
+ if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent)) return null;
2875
+ const windowDurationMins = typeof options.windowDurationMins === "number" && Number.isFinite(options.windowDurationMins) ? options.windowDurationMins : null;
2876
+ return {
2877
+ label,
2878
+ usedPercent: Math.min(100, Math.max(0, usedPercent)),
2879
+ windowDurationMins,
2880
+ resetsAt: quotaResetMs(options.resetsAt)
2881
+ };
2882
+ }
2883
+ /**
2884
+ * The snapshot to publish for one account, or undefined when there is none.
2885
+ *
2886
+ * Undefined is the honest answer for "never read" and for "read before this
2887
+ * process started": both mean the card has nothing to show, and neither means
2888
+ * the account is unspent.
2889
+ */
2890
+ function poolQuota(fetchedAt, windows) {
2891
+ if (typeof fetchedAt !== "number" || !Number.isFinite(fetchedAt) || fetchedAt <= 0) return void 0;
2892
+ return {
2893
+ windows: windows.filter((window) => window !== null),
2894
+ fetchedAt
2895
+ };
2896
+ }
2897
+ //#endregion
2826
2898
  //#region src/host/codex-account-pool.ts
2827
2899
  /** Encrypted pool file the ChatGPT line owns; the single-credential store stays the mirror. */
2828
2900
  function codexPoolPath() {
@@ -2891,13 +2963,32 @@ const concurrency = new ConcurrencyGate();
2891
2963
  function chatGPTConcurrency() {
2892
2964
  return concurrency;
2893
2965
  }
2966
+ /**
2967
+ * One ChatGPT usage snapshot as the account card's per-account quota.
2968
+ *
2969
+ * A bucket lists its windows either in `windows` or, on older payloads, only as
2970
+ * the `primary`/`secondary` pair — the same fallback the quota card itself
2971
+ * applies, so the account row cannot show fewer windows than the card does. The
2972
+ * label stays empty on purpose: a ChatGPT window is named by its length, and
2973
+ * the card localizes that length in the reader's own language.
2974
+ */
2975
+ function codexAccountQuota(snapshot) {
2976
+ return poolQuota(snapshot.fetchedAt, snapshot.usage.buckets.flatMap((bucket) => {
2977
+ return (bucket.windows.length > 0 ? bucket.windows : [bucket.primary, bucket.secondary].filter((window) => window !== null)).map((window) => quotaWindow$1("", window.usedPercent, {
2978
+ windowDurationMins: window.windowDurationMins,
2979
+ resetsAt: window.resetsAt
2980
+ }));
2981
+ }));
2982
+ }
2894
2983
  var CodexAccountPool = class extends AccountPoolCore {
2895
2984
  refresherRef;
2896
2985
  store;
2986
+ quotaSnapshotRef;
2897
2987
  quotaBlockedUntil;
2898
2988
  constructor(options = {}) {
2899
2989
  const store = options.store ?? createPlatformTokenStore();
2900
2990
  const ref = {};
2991
+ const quotaSnapshotRef = {};
2901
2992
  const hooks = {
2902
2993
  providerId: "codex-chatgpt",
2903
2994
  displayName: "ChatGPT",
@@ -2948,7 +3039,11 @@ var CodexAccountPool = class extends AccountPoolCore {
2948
3039
  extendSummary: (account, base) => ({
2949
3040
  ...base,
2950
3041
  ...account.email === void 0 ? {} : { email: account.email },
2951
- ...account.planType === void 0 ? {} : { planLabel: account.planType }
3042
+ ...account.planType === void 0 ? {} : { planLabel: account.planType },
3043
+ ...(() => {
3044
+ const quota = quotaSnapshotRef.current?.(account);
3045
+ return quota === void 0 ? {} : { quota };
3046
+ })()
2952
3047
  }),
2953
3048
  emptyMessage: "未登录 ChatGPT 账号,请在「设置 → 订阅服务 → ChatGPT」中添加并登录账号。",
2954
3049
  ...options.backend === void 0 ? {} : { backend: options.backend },
@@ -2956,6 +3051,7 @@ var CodexAccountPool = class extends AccountPoolCore {
2956
3051
  };
2957
3052
  super(hooks);
2958
3053
  this.refresherRef = ref;
3054
+ this.quotaSnapshotRef = quotaSnapshotRef;
2959
3055
  this.store = store;
2960
3056
  }
2961
3057
  /** Hand the pool the OAuth service's per-account refresh implementation. */
@@ -2973,6 +3069,18 @@ var CodexAccountPool = class extends AccountPoolCore {
2973
3069
  setQuotaBlockedUntil(lookup) {
2974
3070
  this.quotaBlockedUntil = lookup;
2975
3071
  }
3072
+ /**
3073
+ * Wire in the account's own newest quota snapshot, for display only.
3074
+ *
3075
+ * The same map that already spares an exhausted account a wasted request also
3076
+ * answers "what did this account's window look like last time we read it".
3077
+ * The lookup must be a memory read: it runs while the settings card builds
3078
+ * its account list, and that page must never fan out an upstream request per
3079
+ * pooled account.
3080
+ */
3081
+ setQuotaSnapshot(lookup) {
3082
+ this.quotaSnapshotRef.current = lookup;
3083
+ }
2976
3084
  isEligible(account, now, triedAccountIds) {
2977
3085
  if (!super.isEligible(account, now, triedAccountIds)) return false;
2978
3086
  const blockedUntil = this.quotaBlockedUntil?.(account, now);
@@ -4088,6 +4196,56 @@ function createControlledModelFetch(baseFetch, options) {
4088
4196
  };
4089
4197
  }
4090
4198
  //#endregion
4199
+ //#region src/host/common/provider-route.ts
4200
+ /**
4201
+ * Claim one provider route for this plugin and keep claiming it.
4202
+ *
4203
+ * The route is taken immediately, and every `llm/adapters-updated` reports the
4204
+ * claim again — it is a no-op while the registration is held, and takes the
4205
+ * route back once another owner releases it. A rejected claim is remembered
4206
+ * rather than thrown, because the line whose route table asks through
4207
+ * {@link ProviderRouteClaim.conflict} still has to render.
4208
+ * @param ctx - The plugin context owning the `llm` service and the event seam.
4209
+ * @param options - The route id, the display label the two log lines name, and the adapter serving it.
4210
+ * @returns The claim's state and its disposer.
4211
+ */
4212
+ function claimProviderRoute(ctx, options) {
4213
+ let registration;
4214
+ let conflict = null;
4215
+ const claim = () => {
4216
+ if (registration !== void 0) return;
4217
+ try {
4218
+ registration = ctx.llm.registerAdapter([options.providerId], options.adapter);
4219
+ if (conflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${options.label} route "${options.providerId}" is now served by this plugin`);
4220
+ conflict = null;
4221
+ } catch (error) {
4222
+ conflict = error instanceof Error ? error.message : String(error);
4223
+ ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${options.providerId}" is already owned by another adapter; ${options.label} models keep being served by that one until its configuration is removed (${conflict})`);
4224
+ }
4225
+ };
4226
+ claim();
4227
+ const watch = options.watch === false || typeof ctx.on !== "function" ? void 0 : ctx.on("llm/adapters-updated", () => {
4228
+ claim();
4229
+ });
4230
+ return {
4231
+ serving: () => registration !== void 0,
4232
+ conflict: () => conflict,
4233
+ dispose: () => {
4234
+ releaseHandle(watch);
4235
+ registration?.();
4236
+ registration = void 0;
4237
+ }
4238
+ };
4239
+ }
4240
+ /** Cordis event handles are either a disposer function or a disposable object. */
4241
+ function releaseHandle(handle) {
4242
+ if (typeof handle === "function") {
4243
+ handle();
4244
+ return;
4245
+ }
4246
+ handle?.dispose?.();
4247
+ }
4248
+ //#endregion
4091
4249
  //#region src/host/common/request-diagnostics.ts
4092
4250
  /**
4093
4251
  * Reusable request diagnostics wrapper for model provider calls.
@@ -5126,6 +5284,100 @@ async function* wrapStreamWithWatchdog(source, upstreamSignal, timeoutMs = DEFAU
5126
5284
  }
5127
5285
  //#endregion
5128
5286
  //#region src/host/responses-mapper.ts
5287
+ /**
5288
+ * Base64 image payload one Codex request may carry.
5289
+ *
5290
+ * THIS ROUTE HAD NO BOUND AT ALL, which is what made a long session fail
5291
+ * intermittently (issue #50). Every turn re-inlines every image still in
5292
+ * history, so the request body grew with the number of images ever read and
5293
+ * nothing ever trimmed it. Past roughly 4 MB the transport itself gives up: the
5294
+ * report's field logs show 18/18 successes in the 3–4 MB band and 3/10 in the
5295
+ * 4–5 MB band, and a synthetic probe against the subscription endpoint returned
5296
+ * `NGHTTP2_ENHANCE_YOUR_CALM` / `NGHTTP2_INTERNAL_ERROR` on every ≥4 MB body
5297
+ * while ≤3 MB always passed.
5298
+ *
5299
+ * The NUMBER is this route's own, derived from those measurements rather than
5300
+ * borrowed. Kimi's 1.5 MB figure is sized for Kimi's 2 MB request limit and
5301
+ * would silently drop images this endpoint accepts; MiniMax's 16 MB is sized
5302
+ * for a 64 MB body ceiling this route does not have. 2 MiB of image data keeps
5303
+ * the whole body inside the band that never failed, even after the
5304
+ * conversation text, tool schemas and system prompt are added. It is a
5305
+ * transport property of this deployment, not a documented API limit, so
5306
+ * `DSH_CODEX_MAX_IMAGE_BYTES` overrides it.
5307
+ *
5308
+ * It bounds the IMAGE contribution only. A request whose text alone exceeds the
5309
+ * transport is still refused by the transport, and no image budget can fix that.
5310
+ */
5311
+ const DEFAULT_MAX_REQUEST_IMAGE_BYTES$1 = 2 * 1024 * 1024;
5312
+ /**
5313
+ * Image budget this process enforces, honouring the deployment override.
5314
+ *
5315
+ * Parsed as bytes and ignored when it is not a positive finite number, so a
5316
+ * typo cannot silently disable the guard by producing `NaN`.
5317
+ */
5318
+ function maxRequestImageBytes$1(env = process.env) {
5319
+ const raw = (env.DSH_CODEX_MAX_IMAGE_BYTES ?? "").trim();
5320
+ if (raw === "") return DEFAULT_MAX_REQUEST_IMAGE_BYTES$1;
5321
+ const parsed = Number(raw);
5322
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_REQUEST_IMAGE_BYTES$1;
5323
+ return Math.floor(parsed);
5324
+ }
5325
+ /**
5326
+ * Placeholder left where an over-budget image was removed. It says the picture
5327
+ * is gone rather than leaving a blank the model would answer about anyway.
5328
+ */
5329
+ const OMITTED_IMAGE_TEXT$5 = "[image omitted to keep the request within its size limit; older images are omitted first. If this image is still needed, read its file again when a path is available; otherwise ask the user to attach it again.]";
5330
+ /**
5331
+ * Replace the oldest images with that placeholder once the built request would
5332
+ * carry more than `maxBytes` of base64 image data.
5333
+ *
5334
+ * WHY THIS RUNS ON THE BUILT INPUT rather than on `options.messages` like the
5335
+ * other routes. Two image forms reach this wire and only one of them is a
5336
+ * message block: a pasted attachment arrives as `{ type: 'image', attachment }`,
5337
+ * but the tool-read images in issue #50 arrive as local markdown links
5338
+ * (`![](/describe-image/raw/sha256:…)`) that `mapUserText` fetches and inlines
5339
+ * while building. A message-level pass cannot see the second form at all, and
5340
+ * the shared `offloadOldestRequestImages` additionally does not recurse into
5341
+ * `tool-result` content — so measuring here, after every form has become an
5342
+ * `input_image` item, is the one place that bounds what actually goes out.
5343
+ *
5344
+ * The oldest occurrences go first and durable history is untouched; only the
5345
+ * request about to be sent changes. A retry after a model fallback reuses the
5346
+ * bounded input instead of re-inflating it.
5347
+ *
5348
+ * @param input - the payload's `input` items, mutated in place.
5349
+ * @param maxBytes - base64 image budget for the whole request.
5350
+ * @returns how many images were replaced.
5351
+ */
5352
+ function offloadOldestInputImages(input, maxBytes = maxRequestImageBytes$1()) {
5353
+ const retained = [];
5354
+ for (const item of input) {
5355
+ if (!Array.isArray(item.content)) continue;
5356
+ for (const [index, block] of item.content.entries()) {
5357
+ if (record$3(block) === null || block.type !== "input_image") continue;
5358
+ const url = block.image_url;
5359
+ if (typeof url !== "string") continue;
5360
+ retained.push({
5361
+ content: item.content,
5362
+ index,
5363
+ bytes: url.length
5364
+ });
5365
+ }
5366
+ }
5367
+ let total = retained.reduce((sum, entry) => sum + entry.bytes, 0);
5368
+ if (total <= maxBytes) return 0;
5369
+ let omitted = 0;
5370
+ for (const entry of retained) {
5371
+ if (total <= maxBytes) break;
5372
+ entry.content[entry.index] = {
5373
+ type: "input_text",
5374
+ text: OMITTED_IMAGE_TEXT$5
5375
+ };
5376
+ total -= entry.bytes;
5377
+ omitted += 1;
5378
+ }
5379
+ return omitted;
5380
+ }
5129
5381
  function hiddenSandboxControlToolNames(options) {
5130
5382
  const retryTools = recentSandboxRetryToolNames(options.messages);
5131
5383
  return new Set(options.tools?.filter((tool) => hasSandboxControls(tool.parameters) && !retryTools.has(tool.name)).map((tool) => tool.name) ?? []);
@@ -5222,6 +5474,7 @@ async function buildResponsesPayload(options, attachments, localRawImages = {},
5222
5474
  });
5223
5475
  if (message.role === "assistant") appendMissingToolCalls(input, knownToolCalls, message, normalizeCallId);
5224
5476
  }
5477
+ offloadOldestInputImages(input);
5225
5478
  const payload = {
5226
5479
  model: options.model,
5227
5480
  input,
@@ -5787,7 +6040,8 @@ var ResponsesClient = class {
5787
6040
  });
5788
6041
  } catch (cause) {
5789
6042
  if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
5790
- throw new LlmError("Codex could not be reached.", "NETWORK", { cause });
6043
+ const chain = errorChain(cause).trim();
6044
+ throw new LlmError(`Codex request failed: ${chain === "" ? "the connection failed before any response arrived" : chain.slice(0, 500)}`, "NETWORK", { cause });
5791
6045
  }
5792
6046
  }
5793
6047
  };
@@ -6239,6 +6493,72 @@ function acceptIdentity(current, incoming) {
6239
6493
  return typeof incoming === "string" && incoming.length > 0 ? incoming : current;
6240
6494
  }
6241
6495
  //#endregion
6496
+ //#region src/shared/hub-contracts.ts
6497
+ /**
6498
+ * Wire contracts for the subscription hub's overview route.
6499
+ *
6500
+ * The overview is the settings page's opening screen: one card per provider
6501
+ * line, annotated with how many accounts it holds and whether it is enabled.
6502
+ * Everything here is a non-secret fact a settings card may render; no
6503
+ * credential, quota figure, or upstream call ever crosses this boundary.
6504
+ */
6505
+ /** The aggregated, read-only overview the hub's opening screen renders. */
6506
+ const HUB_OVERVIEW_PATH = `${ROUTE_PREFIX$5}/hub/overview`;
6507
+ //#endregion
6508
+ //#region src/host/hub-overview.ts
6509
+ function registerHubOverviewRoutes(ctx, sources) {
6510
+ const handler = async (request, response) => {
6511
+ if (request.method !== "GET") {
6512
+ response.writeHead(405, {
6513
+ allow: "GET",
6514
+ "content-type": "application/json; charset=utf-8"
6515
+ });
6516
+ response.end(JSON.stringify({
6517
+ ok: false,
6518
+ error: {
6519
+ code: "bad-request",
6520
+ message: "Method not allowed."
6521
+ }
6522
+ }));
6523
+ return;
6524
+ }
6525
+ const envelope = {
6526
+ ok: true,
6527
+ value: { providers: (await Promise.allSettled(sources.map(async (source) => ({
6528
+ id: source.id,
6529
+ providerId: source.providerId,
6530
+ canToggle: source.canToggle,
6531
+ ...await source.read()
6532
+ })))).map((result, index) => {
6533
+ if (result.status === "fulfilled") return result.value;
6534
+ const source = sources[index];
6535
+ return {
6536
+ id: source.id,
6537
+ providerId: source.providerId,
6538
+ canToggle: source.canToggle,
6539
+ enabled: !source.canToggle,
6540
+ accountCount: 0,
6541
+ authenticated: false,
6542
+ enabledModelCount: null,
6543
+ totalModelCount: null,
6544
+ error: true
6545
+ };
6546
+ }) }
6547
+ };
6548
+ response.writeHead(200, {
6549
+ "content-type": "application/json; charset=utf-8",
6550
+ "cache-control": "no-store",
6551
+ "x-content-type-options": "nosniff"
6552
+ });
6553
+ response.end(JSON.stringify(envelope));
6554
+ };
6555
+ return ctx.webServer.register({
6556
+ kind: "exact",
6557
+ path: HUB_OVERVIEW_PATH,
6558
+ handler
6559
+ });
6560
+ }
6561
+ //#endregion
6242
6562
  //#region src/host/common/same-origin.ts
6243
6563
  /**
6244
6564
  * Whether one mutation request comes from this application's own document.
@@ -6346,9 +6666,9 @@ var UsageService = class {
6346
6666
  };
6347
6667
  const now = this.now();
6348
6668
  if (!force && !this.invalidated && this.cache !== null && now - this.cache.fetchedAt < 6e4 && this.cacheIdentityRevision === this.oauth.currentIdentityRevision()) return this.fromCache(false);
6349
- let credentials;
6669
+ let selection;
6350
6670
  try {
6351
- credentials = await this.oauth.credentials(false, USAGE_CREDENTIAL_ACCESS);
6671
+ selection = await this.oauth.credentialSelection(false, USAGE_CREDENTIAL_ACCESS);
6352
6672
  } catch (error) {
6353
6673
  const reason = error instanceof Error && error.message !== "" ? ` (${error.message})` : "";
6354
6674
  return this.failure({
@@ -6356,7 +6676,8 @@ var UsageService = class {
6356
6676
  message: `ChatGPT credentials could not be refreshed.${reason}`
6357
6677
  });
6358
6678
  }
6359
- const accountKey = identityKey(credentials);
6679
+ const credentials = selection.credentials;
6680
+ const accountKey = this.snapshotKeyFor(selection.accountId, credentials);
6360
6681
  if (this.cache !== null && this.cache.accountKey !== accountKey) this.clear();
6361
6682
  if (!force && !this.invalidated && this.cache !== null && now - this.cache.fetchedAt < 6e4) return this.fromCache(false);
6362
6683
  if (this.cache !== null && now - this.lastUpstreamAt < 15e3) return this.fromCache(this.invalidated || now - this.cache.fetchedAt >= 6e4);
@@ -6384,9 +6705,9 @@ var UsageService = class {
6384
6705
  * The rejection is absorbed here: this refresh has no caller to receive it,
6385
6706
  * and {@link fromCache} keeps serving the snapshot it failed to replace.
6386
6707
  */
6387
- startBackgroundRefresh(credentials, accountKey) {
6708
+ startBackgroundRefresh(credentials, snapshotKey) {
6388
6709
  if (this.inFlight !== null) return;
6389
- const task = this.refreshUpstream(credentials, accountKey).finally(() => {
6710
+ const task = this.refreshUpstream(credentials, snapshotKey).finally(() => {
6390
6711
  if (this.inFlight === task) this.inFlight = null;
6391
6712
  });
6392
6713
  this.inFlight = task;
@@ -6403,8 +6724,8 @@ var UsageService = class {
6403
6724
  * cooldown rather than forever, so a missing upstream field cannot strand an
6404
6725
  * account permanently.
6405
6726
  */
6406
- blockedUntilFor(credentials, now) {
6407
- const snapshot = this.snapshots.get(identityKey(credentials));
6727
+ blockedUntilFor(accountId, credentials, now) {
6728
+ const snapshot = this.snapshots.get(this.snapshotKeyFor(accountId, credentials));
6408
6729
  if (snapshot === void 0) return void 0;
6409
6730
  let reopen;
6410
6731
  for (const bucket of snapshot.usage.buckets) {
@@ -6417,6 +6738,32 @@ var UsageService = class {
6417
6738
  }
6418
6739
  return reopen !== void 0 && reopen > now ? reopen : void 0;
6419
6740
  }
6741
+ /**
6742
+ * The newest quota snapshot held for one account, if any.
6743
+ *
6744
+ * Quota follows the account, so the settings card shows each account its own
6745
+ * progress. The pool asks here with the row id it addresses the account by —
6746
+ * the identity that survives a token refresh — while its credentials answer
6747
+ * for a composition without a pool. This reads the remembered map only, never
6748
+ * the network, because opening the settings page must not cost one upstream
6749
+ * request per pooled account.
6750
+ */
6751
+ snapshotFor(accountId, credentials) {
6752
+ return this.snapshots.get(this.snapshotKeyFor(accountId, credentials));
6753
+ }
6754
+ /**
6755
+ * Key one account's remembered snapshot is filed under.
6756
+ *
6757
+ * A pool row id when the caller names one, prefixed so it can never be
6758
+ * confused with the credential-derived key, and otherwise the credential
6759
+ * identity — which is all a composition without a pool has. Two pooled accounts
6760
+ * that state neither an account id nor an email would otherwise collapse onto
6761
+ * one key and show each other's numbers, so the row id is the answer wherever
6762
+ * a pool is in charge.
6763
+ */
6764
+ snapshotKeyFor(accountId, credentials) {
6765
+ return accountId === void 0 || accountId === "" ? identityKey(credentials) : `row:${accountId}`;
6766
+ }
6420
6767
  /** Remember the newest snapshot for one account, bounded to the accounts in use. */
6421
6768
  rememberSnapshot(accountKey, usage, fetchedAt) {
6422
6769
  this.snapshots.set(accountKey, {
@@ -6436,8 +6783,8 @@ var UsageService = class {
6436
6783
  this.invalidated = false;
6437
6784
  }
6438
6785
  /** Drop the remembered snapshot of one account, after its window is known to have reset. */
6439
- forgetSnapshot(credentials) {
6440
- this.snapshots.delete(identityKey(credentials));
6786
+ forgetSnapshot(accountId, credentials) {
6787
+ this.snapshots.delete(this.snapshotKeyFor(accountId, credentials));
6441
6788
  }
6442
6789
  async consumeResetCredit() {
6443
6790
  if (this.resetConsumeInFlight !== null) return this.resetConsumeInFlight;
@@ -6448,12 +6795,17 @@ var UsageService = class {
6448
6795
  }
6449
6796
  async consumeResetCreditUpstream() {
6450
6797
  let credentials;
6798
+ let accountId;
6451
6799
  try {
6452
- credentials = await this.oauth.credentials(false, USAGE_CREDENTIAL_ACCESS);
6800
+ const selection = await this.oauth.credentialSelection(false, USAGE_CREDENTIAL_ACCESS);
6801
+ credentials = selection.credentials;
6802
+ accountId = selection.accountId;
6453
6803
  let creditsResponse = await this.fetchResetCredits(credentials);
6454
6804
  if (creditsResponse.status === 401) {
6455
6805
  await creditsResponse.body?.cancel().catch(() => void 0);
6456
- credentials = await this.oauth.credentials(true, USAGE_CREDENTIAL_ACCESS);
6806
+ const renewed = await this.oauth.credentialSelection(true, USAGE_CREDENTIAL_ACCESS);
6807
+ credentials = renewed.credentials;
6808
+ accountId = renewed.accountId;
6457
6809
  creditsResponse = await this.fetchResetCredits(credentials);
6458
6810
  }
6459
6811
  if (!creditsResponse.ok) {
@@ -6484,7 +6836,9 @@ var UsageService = class {
6484
6836
  });
6485
6837
  if (consumeResponse.status === 401) {
6486
6838
  await consumeResponse.body?.cancel().catch(() => void 0);
6487
- credentials = await this.oauth.credentials(true, USAGE_CREDENTIAL_ACCESS);
6839
+ const renewed = await this.oauth.credentialSelection(true, USAGE_CREDENTIAL_ACCESS);
6840
+ credentials = renewed.credentials;
6841
+ accountId = renewed.accountId;
6488
6842
  consumeResponse = await this.fetchFn(CODEX_RESET_CREDITS_CONSUME_URL, {
6489
6843
  method: "POST",
6490
6844
  headers: {
@@ -6508,7 +6862,7 @@ var UsageService = class {
6508
6862
  }
6509
6863
  await consumeResponse.body?.cancel().catch(() => void 0);
6510
6864
  this.clear();
6511
- const refreshed = await this.refreshUpstream(credentials, identityKey(credentials));
6865
+ const refreshed = await this.refreshUpstream(credentials, this.snapshotKeyFor(accountId, credentials));
6512
6866
  if (refreshed.error !== void 0) throw new UsageServiceError({
6513
6867
  code: "quota-failed",
6514
6868
  message: "The reset credit was used, but usage could not be refreshed."
@@ -6535,16 +6889,17 @@ var UsageService = class {
6535
6889
  checkedAt: Math.floor(this.now() / 1e3)
6536
6890
  };
6537
6891
  }
6538
- async refreshUpstream(initialCredentials, initialAccountKey) {
6892
+ async refreshUpstream(initialCredentials, initialSnapshotKey) {
6539
6893
  this.lastUpstreamAt = this.now();
6540
6894
  try {
6541
6895
  let credentials = initialCredentials;
6542
- let accountKey = initialAccountKey;
6896
+ let snapshotKey = initialSnapshotKey;
6543
6897
  let response = await this.fetch(credentials);
6544
6898
  if (response.status === 401) {
6545
6899
  await response.body?.cancel().catch(() => void 0);
6546
- credentials = await this.oauth.credentials(true, USAGE_CREDENTIAL_ACCESS);
6547
- accountKey = identityKey(credentials);
6900
+ const renewed = await this.oauth.credentialSelection(true, USAGE_CREDENTIAL_ACCESS);
6901
+ credentials = renewed.credentials;
6902
+ snapshotKey = this.snapshotKeyFor(renewed.accountId, credentials);
6548
6903
  response = await this.fetch(credentials);
6549
6904
  }
6550
6905
  if (response.status === 429) {
@@ -6577,10 +6932,10 @@ var UsageService = class {
6577
6932
  this.cache = {
6578
6933
  usage,
6579
6934
  fetchedAt: this.now(),
6580
- accountKey
6935
+ accountKey: snapshotKey
6581
6936
  };
6582
6937
  this.cacheIdentityRevision = this.oauth.currentIdentityRevision();
6583
- this.rememberSnapshot(accountKey, usage, this.cache.fetchedAt);
6938
+ this.rememberSnapshot(snapshotKey, usage, this.cache.fetchedAt);
6584
6939
  this.invalidated = false;
6585
6940
  return this.fromCache(false);
6586
6941
  } catch (error) {
@@ -7919,7 +8274,7 @@ var FileModelSettingsStore$2 = class {
7919
8274
  */
7920
8275
  /** Default loopback port the Antigravity CLI's own flow uses. */
7921
8276
  const DEFAULT_CALLBACK_PORT$2 = 51121;
7922
- let cachedQuota$1;
8277
+ let cachedQuota;
7923
8278
  let quotaFetchInFlight = null;
7924
8279
  /** Bumped by every cache clear so an in-flight fetch cannot publish stale state. */
7925
8280
  let quotaCacheEpoch$2 = 0;
@@ -8096,7 +8451,7 @@ function parseQuotaSummary(data) {
8096
8451
  for (const bucket of rawBuckets) {
8097
8452
  if (typeof bucket !== "object" || bucket === null) continue;
8098
8453
  const bRec = bucket;
8099
- const remaining = typeof bRec.remainingFraction === "number" ? Math.max(0, Math.min(1, bRec.remainingFraction)) : 0;
8454
+ const remaining = typeof bRec.remainingFraction === "number" && Number.isFinite(bRec.remainingFraction) ? Math.max(0, Math.min(1, bRec.remainingFraction)) : null;
8100
8455
  buckets.push({
8101
8456
  bucketId: String(bRec.bucketId || bRec.displayName || "limit"),
8102
8457
  displayName: String(bRec.displayName || bRec.bucketId || "Limit"),
@@ -8159,7 +8514,7 @@ function mergeCatalogSelection(stored, storedCatalog) {
8159
8514
  return stored.filter((id) => known.has(id));
8160
8515
  }
8161
8516
  async function fetchAccountQuota(store = new FileCredentialStore$2(), modelSettings, fetchFn = fetch, force = false) {
8162
- if (!force && cachedQuota$1 && Date.now() - (cachedQuota$1.fetchedAt || 0) < 12e4) return cachedQuota$1;
8517
+ if (!force && cachedQuota && Date.now() - (cachedQuota.fetchedAt || 0) < 12e4) return cachedQuota;
8163
8518
  if (quotaFetchInFlight) return quotaFetchInFlight;
8164
8519
  const epoch = quotaCacheEpoch$2;
8165
8520
  const request = (async () => {
@@ -8195,7 +8550,7 @@ async function fetchAccountQuota(store = new FileCredentialStore$2(), modelSetti
8195
8550
  fetchedAt: Date.now()
8196
8551
  };
8197
8552
  if (epoch !== quotaCacheEpoch$2) return snapshot;
8198
- cachedQuota$1 = snapshot;
8553
+ cachedQuota = snapshot;
8199
8554
  if (modelSettings && catalogModels.length > 0) {
8200
8555
  const current = await modelSettings.read();
8201
8556
  const mergedEnabled = mergeCatalogSelection(current.enabledModelIds, current.catalogModels);
@@ -8211,11 +8566,11 @@ async function fetchAccountQuota(store = new FileCredentialStore$2(), modelSetti
8211
8566
  }
8212
8567
  }
8213
8568
  function getCachedQuota() {
8214
- return cachedQuota$1;
8569
+ return cachedQuota;
8215
8570
  }
8216
8571
  function clearCachedQuota() {
8217
8572
  quotaCacheEpoch$2 += 1;
8218
- cachedQuota$1 = void 0;
8573
+ cachedQuota = void 0;
8219
8574
  quotaFetchInFlight = null;
8220
8575
  }
8221
8576
  //#endregion
@@ -9595,6 +9950,54 @@ function createPoolCredentialBackend(filePath) {
9595
9950
  throw new Error("Antigravity pool encrypted storage requires Windows, macOS, or Linux.");
9596
9951
  }
9597
9952
  /**
9953
+ * One bucket's reset moment as Unix milliseconds, when it parses as a date.
9954
+ *
9955
+ * The service states it as an ISO-ish string; anything `Date.parse` refuses is
9956
+ * dropped rather than guessed at, because a wrong instant on the card is worse
9957
+ * than none.
9958
+ */
9959
+ function resetTimeMs(resetTime) {
9960
+ if (resetTime === void 0 || resetTime === "") return null;
9961
+ const parsed = Date.parse(resetTime);
9962
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
9963
+ }
9964
+ /**
9965
+ * One cached Antigravity snapshot as the shared account card renders it.
9966
+ *
9967
+ * One window per bucket. The service states what is LEFT, and the card draws what
9968
+ * is SPENT, so the used share is the complement. A bucket whose fraction is
9969
+ * `null` (never measured) or outside 0-1 is dropped rather than drawn as 0% or
9970
+ * 100%: "not stated" and "nothing used" are different claims and only one of
9971
+ * them is true. A stated 0 is a real measurement — the allowance is exhausted —
9972
+ * and is published as 100% used.
9973
+ *
9974
+ * `windowDurationMins` is deliberately absent. The bucket's `window` field is
9975
+ * carried through this line's cache verbatim and nothing in the line interprets
9976
+ * it as a duration, so naming a window from it would be a guess; the card names
9977
+ * each window from the label below instead.
9978
+ */
9979
+ function antigravityPoolQuota(quota) {
9980
+ if (quota === void 0) return void 0;
9981
+ const nameCounts = /* @__PURE__ */ new Map();
9982
+ for (const group of quota.groups) for (const bucket of group.buckets) {
9983
+ const name = bucket.displayName.trim();
9984
+ if (name === "") continue;
9985
+ nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
9986
+ }
9987
+ const windows = [];
9988
+ for (const group of quota.groups) {
9989
+ const groupName = group.displayName.trim();
9990
+ for (const bucket of group.buckets) {
9991
+ const remaining = bucket.remainingFraction;
9992
+ if (remaining === null || !Number.isFinite(remaining) || remaining < 0 || remaining > 1) continue;
9993
+ const name = bucket.displayName.trim();
9994
+ const label = (name === "" || (nameCounts.get(name) ?? 0) > 1) && groupName !== "" ? name === "" ? groupName : groupName + " · " + name : name;
9995
+ windows.push(quotaWindow$1(label, (1 - remaining) * 100, { resetsAt: resetTimeMs(bucket.resetTime) }));
9996
+ }
9997
+ }
9998
+ return poolQuota(quota.fetchedAt, windows);
9999
+ }
10000
+ /**
9598
10001
  * Antigravity's account pool.
9599
10002
  *
9600
10003
  * The storage, eligibility, rotation and cooldown rules live in the shared
@@ -9658,7 +10061,11 @@ var AccountPoolStore = class {
9658
10061
  ...base,
9659
10062
  ...projectId === void 0 ? {} : { projectId },
9660
10063
  ...email === void 0 ? {} : { email },
9661
- ...account.planLabel === void 0 ? {} : { planLabel: account.planLabel }
10064
+ ...account.planLabel === void 0 ? {} : { planLabel: account.planLabel },
10065
+ ...account.isPrimary === true ? (() => {
10066
+ const quota = antigravityPoolQuota(getCachedQuota());
10067
+ return quota === void 0 ? {} : { quota };
10068
+ })() : {}
9662
10069
  };
9663
10070
  },
9664
10071
  emptyMessage: "未登录 Antigravity 账号,请在「设置 → Antigravity」中添加并登录账号。",
@@ -12569,7 +12976,41 @@ function whoami(apiKey, options = {}) {
12569
12976
  async function verifyApiKey(apiKey, options = {}) {
12570
12977
  return parseWhoami(await whoami(apiKey, options), { authenticatedAt: Date.now() });
12571
12978
  }
12572
- let cachedQuotaEntry;
12979
+ /**
12980
+ * One newest snapshot PER ACCOUNT.
12981
+ *
12982
+ * Quota follows the account: rotation decides which key spends the next
12983
+ * request, so a single slot only ever described whichever key was read last.
12984
+ * The pool's card draws every account its own reading, so each account keeps
12985
+ * its own entry; past the cap the oldest read is evicted.
12986
+ *
12987
+ * This map is a memory read only. Nothing here fetches — the settings card
12988
+ * builds its account list from it, and that must never cost one upstream
12989
+ * request per pooled key.
12990
+ */
12991
+ const cachedQuotaEntries$1 = /* @__PURE__ */ new Map();
12992
+ /** How many accounts' snapshots are remembered; mirrors the Codex usage service. */
12993
+ const QUOTA_CACHE_LIMIT$1 = 20;
12994
+ /** Key of a snapshot whose caller named no account. */
12995
+ const UNNAMED_ACCOUNT_KEY$1 = "";
12996
+ function quotaCacheKey(accountId) {
12997
+ return accountId ?? UNNAMED_ACCOUNT_KEY$1;
12998
+ }
12999
+ /** The entry read most recently, whichever account it belongs to. */
13000
+ function newestQuotaEntry$2() {
13001
+ let newest;
13002
+ for (const entry of cachedQuotaEntries$1.values()) if (newest === void 0 || entry.fetchedAt > newest.fetchedAt) newest = entry;
13003
+ return newest;
13004
+ }
13005
+ /** Remember one account's newest snapshot, bounded to the accounts in use. */
13006
+ function rememberQuotaEntry(entry) {
13007
+ cachedQuotaEntries$1.set(quotaCacheKey(entry.accountId), entry);
13008
+ while (cachedQuotaEntries$1.size > QUOTA_CACHE_LIMIT$1) {
13009
+ const oldest = [...cachedQuotaEntries$1.entries()].sort((left, right) => left[1].fetchedAt - right[1].fetchedAt)[0];
13010
+ if (oldest === void 0) break;
13011
+ cachedQuotaEntries$1.delete(oldest[0]);
13012
+ }
13013
+ }
12573
13014
  let quotaInFlight$2 = null;
12574
13015
  let quotaInFlightAccountId$1;
12575
13016
  let quotaCacheEpoch$1 = 0;
@@ -12578,21 +13019,22 @@ let quotaCacheEpoch$1 = 0;
12578
13019
  * Kept with a no-argument signature for backward compatibility.
12579
13020
  */
12580
13021
  function getCachedQuota$2() {
12581
- return cachedQuotaEntry?.quota;
13022
+ return newestQuotaEntry$2()?.quota;
12582
13023
  }
12583
13024
  /**
12584
13025
  * The cached snapshot only when it belongs to this account.
12585
13026
  *
12586
13027
  * Passing no account keeps the pre-pool behavior of returning whatever was
12587
- * fetched last.
13028
+ * fetched last. This is the lookup the account pool calls while it builds its
13029
+ * summaries: a pure memory read, never a fetch.
12588
13030
  */
12589
- function getCachedQuotaFor$1(accountId) {
12590
- if (accountId === void 0 || accountId === null) return cachedQuotaEntry?.quota;
12591
- return cachedQuotaEntry?.accountId === accountId ? cachedQuotaEntry.quota : void 0;
13031
+ function getCachedQuotaFor$3(accountId) {
13032
+ if (accountId === void 0 || accountId === null) return newestQuotaEntry$2()?.quota;
13033
+ return cachedQuotaEntries$1.get(accountId)?.quota;
12592
13034
  }
12593
13035
  function clearCachedQuota$2() {
12594
13036
  quotaCacheEpoch$1 += 1;
12595
- cachedQuotaEntry = void 0;
13037
+ cachedQuotaEntries$1.clear();
12596
13038
  quotaInFlight$2 = null;
12597
13039
  quotaInFlightAccountId$1 = void 0;
12598
13040
  }
@@ -12607,8 +13049,8 @@ async function fetchAccountQuota$2(store = new FileCredentialStore$1(), fetchFnO
12607
13049
  const fetchFn = typeof fetchFnOrOptions === "function" ? fetchFnOrOptions : fetchFnOrOptions?.fetchFn ?? fetch;
12608
13050
  const effectiveForce = isOptionsObj ? fetchFnOrOptions.force ?? force : force;
12609
13051
  const effectiveAccountId = isOptionsObj ? fetchFnOrOptions.accountId ?? accountId : accountId;
12610
- const sameAccount = effectiveAccountId === void 0 || cachedQuotaEntry?.accountId === effectiveAccountId;
12611
- if (!effectiveForce && sameAccount && cachedQuotaEntry && Date.now() - cachedQuotaEntry.fetchedAt < 12e4) return cachedQuotaEntry.quota;
13052
+ const cached = getCachedQuotaFor$3(effectiveAccountId);
13053
+ if (!effectiveForce && cached !== void 0 && Date.now() - (cached.fetchedAt || 0) < 12e4) return cached;
12612
13054
  if (quotaInFlight$2 !== null && (effectiveAccountId === void 0 || quotaInFlightAccountId$1 === effectiveAccountId)) return quotaInFlight$2;
12613
13055
  const epoch = quotaCacheEpoch$1;
12614
13056
  const request = (async () => {
@@ -12664,11 +13106,11 @@ async function fetchAccountQuota$2(store = new FileCredentialStore$1(), fetchFnO
12664
13106
  sources
12665
13107
  };
12666
13108
  if (epoch !== quotaCacheEpoch$1) return snapshot;
12667
- cachedQuotaEntry = {
13109
+ rememberQuotaEntry({
12668
13110
  accountId: effectiveAccountId,
12669
13111
  quota: snapshot,
12670
13112
  fetchedAt: snapshot.fetchedAt
12671
- };
13113
+ });
12672
13114
  return snapshot;
12673
13115
  })();
12674
13116
  quotaInFlight$2 = request;
@@ -14758,6 +15200,34 @@ function processLine$1(line, state, wire) {
14758
15200
  }
14759
15201
  //#endregion
14760
15202
  //#region src/host/command-code/account-pool.ts
15203
+ /**
15204
+ * One cached snapshot as the shared account card renders it.
15205
+ *
15206
+ * A usage window states its own consumed share, so it maps straight across; a
15207
+ * window whose share nobody measured is dropped rather than drawn at 0, which
15208
+ * would claim an unspent window no reading supports.
15209
+ *
15210
+ * Exported for the pool's regression tests; the card only reaches it through
15211
+ * the summary hook.
15212
+ */
15213
+ function commandCodePoolQuota(cached) {
15214
+ return poolQuota(cached.fetchedAt, cached.windows.map((window) => quotaWindow$1(window.label, window.usedPercent, {
15215
+ windowDurationMins: window.windowDurationMins,
15216
+ resetsAt: window.resetsAt
15217
+ })));
15218
+ }
15219
+ /**
15220
+ * This key's own newest snapshot, ready for the account card.
15221
+ *
15222
+ * Quota follows the account, so the pool reports what the line already
15223
+ * remembered for THIS account id. The lookup is a pure memory read: the card
15224
+ * builds its account list here, and that must never cost one upstream request
15225
+ * per pooled key.
15226
+ */
15227
+ function quotaFor$1(accountId) {
15228
+ const cached = getCachedQuotaFor$3(accountId);
15229
+ return cached === void 0 ? void 0 : commandCodePoolQuota(cached);
15230
+ }
14761
15231
  /** Encrypted pool file this line owns; the pre-pool credential file stays the mirror. */
14762
15232
  function commandCodePoolPath() {
14763
15233
  return path.join(dshHomeDir(), "storages", "command-code-pool.json");
@@ -14895,7 +15365,11 @@ var CommandCodeAccountPool = class extends AccountPoolCore {
14895
15365
  ...account.userName === void 0 ? {} : { userName: account.userName },
14896
15366
  ...account.organizationName === void 0 ? {} : { organizationName: account.organizationName },
14897
15367
  ...account.planId === void 0 ? {} : { planId: account.planId },
14898
- ...account.userId === void 0 ? {} : { userId: account.userId }
15368
+ ...account.userId === void 0 ? {} : { userId: account.userId },
15369
+ ...(() => {
15370
+ const quota = quotaFor$1(account.id);
15371
+ return quota === void 0 ? {} : { quota };
15372
+ })()
14899
15373
  };
14900
15374
  },
14901
15375
  emptyMessage: "Not signed in to Command Code. 请在「设置 → 订阅服务 → Command Code」中添加账号或粘贴 API Key。",
@@ -15450,7 +15924,7 @@ async function getCommandCodeWebStatus(store, modelSettings, preferences, option
15450
15924
  const catalog = live.length > 0 ? live : fallbackCatalog$1();
15451
15925
  const enabled = settings.enabled !== false;
15452
15926
  const models = buildModelOptions$2(catalog, resolveEnabledModelIds$4(settings.enabledModelIds, catalog, enabled), settings.contextWindowOverrides);
15453
- const quota = getCachedQuotaFor$1(activeAccount?.id ?? null);
15927
+ const quota = getCachedQuotaFor$3(activeAccount?.id ?? null);
15454
15928
  const activeAccountView = activePoolAccount === void 0 ? null : {
15455
15929
  userId: activePoolAccount.userId ?? activePoolAccount.credentials.userId ?? null,
15456
15930
  userName: activePoolAccount.userName ?? activePoolAccount.credentials.userName ?? null,
@@ -15523,7 +15997,7 @@ function registerCommandCodeRoutes(ctx, store, modelSettings, preferences, optio
15523
15997
  if (method !== "GET") return sendMethodNotAllowed$5(response);
15524
15998
  const credentials = await store.read();
15525
15999
  const targetId = await activeAccountId();
15526
- const cached = getCachedQuotaFor$1(targetId);
16000
+ const cached = getCachedQuotaFor$3(targetId);
15527
16001
  if ((credentials !== null || targetId !== void 0) && (cached === void 0 || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
15528
16002
  const refresh = () => fetchAccountQuota$2(quotaStore, fetchFn, false, targetId);
15529
16003
  if (cached === void 0) await quotaRefresh.run(refresh);
@@ -19336,19 +19810,89 @@ function buildModelOptions$1(catalog, enabledModelIds, contextWindowOverrides) {
19336
19810
  };
19337
19811
  });
19338
19812
  }
19339
- let quotaCache$1 = null;
19340
- let quotaAccountId;
19813
+ /**
19814
+ * How many accounts' snapshots are remembered.
19815
+ *
19816
+ * Mirrors the bound the ChatGPT line's usage service applies to its own
19817
+ * per-account map: room for every account a user is likely to pool, and no way
19818
+ * for a long-lived process to accumulate one snapshot per account it has seen.
19819
+ */
19820
+ const QUOTA_ACCOUNT_LIMIT$1 = 20;
19821
+ /** Key of a snapshot whose caller named no account: the single-credential read. */
19822
+ const UNNAMED_ACCOUNT_KEY = "";
19823
+ /**
19824
+ * One newest snapshot PER ACCOUNT, keyed by the account id the caller stated.
19825
+ *
19826
+ * Quota follows the account: rotation decides which account spends the next
19827
+ * request, so a single slot could only ever describe whichever account was read
19828
+ * last. The pool's card draws every account its own reading, so each account
19829
+ * keeps its own entry; past the cap the oldest read is evicted.
19830
+ *
19831
+ * This map is a memory read only. Nothing here fetches — the settings card
19832
+ * builds its account list from it, and that must never cost one upstream
19833
+ * request per pooled account.
19834
+ */
19835
+ const quotaCache$1 = /* @__PURE__ */ new Map();
19836
+ /**
19837
+ * Key of the entry written most recently.
19838
+ *
19839
+ * The pre-pool card asks "whatever was fetched last"; the last WRITE answers
19840
+ * that exactly, where a scan by `fetchedAt` would break a tie between two
19841
+ * accounts read inside the same millisecond by map order.
19842
+ */
19843
+ let quotaLastKey$1 = UNNAMED_ACCOUNT_KEY;
19844
+ function quotaCacheKeyFor(accountId) {
19845
+ return accountId ?? UNNAMED_ACCOUNT_KEY;
19846
+ }
19847
+ /** The entry read most recently, whichever account it belongs to. */
19848
+ function newestQuotaEntry$1() {
19849
+ return quotaCache$1.get(quotaLastKey$1);
19850
+ }
19851
+ /** Remember one account's newest snapshot, bounded to the accounts in use. */
19852
+ function rememberQuota$1(accountId, snapshot) {
19853
+ const key = quotaCacheKeyFor(accountId);
19854
+ quotaCache$1.set(key, snapshot);
19855
+ quotaLastKey$1 = key;
19856
+ while (quotaCache$1.size > QUOTA_ACCOUNT_LIMIT$1) {
19857
+ const oldest = [...quotaCache$1.entries()].sort((left, right) => left[1].fetchedAt - right[1].fetchedAt)[0];
19858
+ if (oldest === void 0) break;
19859
+ quotaCache$1.delete(oldest[0]);
19860
+ if (quotaLastKey$1 === oldest[0]) quotaLastKey$1 = UNNAMED_ACCOUNT_KEY;
19861
+ }
19862
+ }
19863
+ /**
19864
+ * The snapshot read most recently, whichever account it belongs to.
19865
+ *
19866
+ * Kept for the pre-pool single-credential card, which has no account id to ask
19867
+ * with. A pooled caller asks {@link getCachedQuotaFor} instead.
19868
+ */
19341
19869
  function getCachedQuota$3() {
19342
- return quotaCache$1;
19870
+ return newestQuotaEntry$1() ?? null;
19343
19871
  }
19344
- /** The cached snapshot only when it belongs to this account. */
19345
- function getCachedQuotaFor(accountId) {
19346
- if (accountId === void 0 || accountId === null) return quotaCache$1;
19347
- return quotaAccountId === accountId ? quotaCache$1 : null;
19872
+ /**
19873
+ * The cached snapshot only when it belongs to this account.
19874
+ *
19875
+ * Passing no account keeps the pre-pool behavior of returning whatever was
19876
+ * fetched last. Passing one answers that account's own newest snapshot, or null
19877
+ * when this line has never read it — null is "never read", which is a different
19878
+ * statement from a snapshot whose windows are all at zero.
19879
+ */
19880
+ function getCachedQuotaFor$2(accountId) {
19881
+ if (accountId === void 0 || accountId === null) return newestQuotaEntry$1() ?? null;
19882
+ return cachedQuotaFor$1(accountId);
19883
+ }
19884
+ /**
19885
+ * The newest snapshot held for one account, for display only.
19886
+ *
19887
+ * The pool asks here while it builds the account list for the settings card, so
19888
+ * this is a pure memory read: it never fetches.
19889
+ */
19890
+ function cachedQuotaFor$1(accountId) {
19891
+ return quotaCache$1.get(accountId) ?? null;
19348
19892
  }
19349
19893
  function clearCachedQuota$3() {
19350
- quotaCache$1 = null;
19351
- quotaAccountId = void 0;
19894
+ quotaCache$1.clear();
19895
+ quotaLastKey$1 = UNNAMED_ACCOUNT_KEY;
19352
19896
  }
19353
19897
  /**
19354
19898
  * Display name and nominal length for each window key the service reports.
@@ -19595,8 +20139,8 @@ function parsePlanLevel(payload) {
19595
20139
  * account's real usage.
19596
20140
  */
19597
20141
  async function fetchAccountQuota$3(store, options = {}) {
19598
- const sameAccount = options.accountId === void 0 || quotaAccountId === options.accountId;
19599
- if (options.force !== true && sameAccount && quotaCache$1 !== null && Date.now() - quotaCache$1.fetchedAt < 12e4) return quotaCache$1;
20142
+ const cached = quotaCache$1.get(quotaCacheKeyFor(options.accountId));
20143
+ if (options.force !== true && cached !== void 0 && Date.now() - cached.fetchedAt < 12e4) return cached;
19600
20144
  const fetchFn = options.fetchFn ?? fetch;
19601
20145
  const pooled = options.credentials !== void 0;
19602
20146
  let credentials;
@@ -19645,8 +20189,7 @@ async function fetchAccountQuota$3(store, options = {}) {
19645
20189
  fetchedAt: Date.now(),
19646
20190
  sources: [openAIUrl(USAGES_PATH, credentials.region)]
19647
20191
  };
19648
- quotaCache$1 = snapshot;
19649
- quotaAccountId = options.accountId;
20192
+ rememberQuota$1(options.accountId, snapshot);
19650
20193
  return snapshot;
19651
20194
  }
19652
20195
  /**
@@ -22547,6 +23090,34 @@ function parseKimiCodePoolData(value) {
22547
23090
  return result;
22548
23091
  }
22549
23092
  /**
23093
+ * One account's own newest quota snapshot, in the shape the account card draws.
23094
+ *
23095
+ * The snapshot comes from 'client.ts''s per-account cache keyed by the pool
23096
+ * account id, and this is where this line's own numbers are translated into the
23097
+ * shared shape. Every judgement below trades a possibly-nice-looking bar for an
23098
+ * honest one:
23099
+ *
23100
+ * - a window whose consumed share nobody measured is DROPPED (`quotaWindow`
23101
+ * returns null), because "0% used" and "not stated" are different claims. This
23102
+ * parser only mints a window once it has a ratio, so a window that states none
23103
+ * never reaches here at all — the drop is the second line of defence;
23104
+ * - `resetsAt` is already Unix MILLISECONDS on this line, so it is passed
23105
+ * through and normalized rather than multiplied;
23106
+ * - the read time is the snapshot's own `fetchedAt`, which this line always
23107
+ * sets. A snapshot with no read time is not a snapshot, and `poolQuota`
23108
+ * reports that as undefined.
23109
+ *
23110
+ * A null snapshot means "this account was never read", which the card renders as
23111
+ * exactly that instead of drawing a zero.
23112
+ */
23113
+ function kimiCodePoolQuota(snapshot) {
23114
+ if (snapshot === null) return void 0;
23115
+ return poolQuota(snapshot.fetchedAt, snapshot.windows.map((window) => quotaWindow$1(window.label, window.usedPercent, {
23116
+ windowDurationMins: window.windowDurationMins,
23117
+ resetsAt: window.resetsAt
23118
+ })));
23119
+ }
23120
+ /**
22550
23121
  * Kimi Code's account pool.
22551
23122
  *
22552
23123
  * The credential is an OAuth pair whose refresh token rotates, so the pool owns
@@ -22625,6 +23196,7 @@ var KimiCodeAccountPool = class extends AccountPoolCore {
22625
23196
  const nickname = account.nickname ?? account.credentials.nickname;
22626
23197
  const planName = account.planName ?? account.credentials.planName;
22627
23198
  const region = account.region ?? account.credentials.region;
23199
+ const quota = kimiCodePoolQuota(cachedQuotaFor$1(account.id));
22628
23200
  return {
22629
23201
  ...base,
22630
23202
  ...email === void 0 ? {} : { email },
@@ -22634,7 +23206,8 @@ var KimiCodeAccountPool = class extends AccountPoolCore {
22634
23206
  planLabel: planName
22635
23207
  },
22636
23208
  ...region === void 0 ? {} : { region },
22637
- ...account.userId === void 0 ? {} : { userId: account.userId }
23209
+ ...account.userId === void 0 ? {} : { userId: account.userId },
23210
+ ...quota === void 0 ? {} : { quota }
22638
23211
  };
22639
23212
  },
22640
23213
  emptyMessage: "Not signed in to Kimi Code. Sign in from Settings > Kimi Code.",
@@ -22748,7 +23321,7 @@ async function getKimiCodeWebStatus(store, modelSettings, preferences, options =
22748
23321
  const catalog = live.length > 0 ? live : fallbackCatalog();
22749
23322
  const enabled = settings.enabled !== false;
22750
23323
  const models = buildModelOptions$1(catalog, resolveEnabledModelIds$3(settings.enabledModelIds, catalog, enabled), settings.contextWindowOverrides);
22751
- const quota = getCachedQuotaFor(activeAccount?.id ?? null);
23324
+ const quota = getCachedQuotaFor$2(activeAccount?.id ?? null);
22752
23325
  const account = quota?.account ?? (active === null || active === void 0 ? null : accountFromCredentials$2(active));
22753
23326
  return {
22754
23327
  enabled,
@@ -22882,7 +23455,7 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
22882
23455
  if (path === "" || path === "status") {
22883
23456
  if (method !== "GET") return sendMethodNotAllowed$3(response);
22884
23457
  const poolActive = await activeAccount();
22885
- const cached = getCachedQuotaFor(poolActive.id ?? null);
23458
+ const cached = getCachedQuotaFor$2(poolActive.id ?? null);
22886
23459
  if (poolActive.credentials !== void 0 && (cached === null || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
22887
23460
  const refresh = async () => {
22888
23461
  const credentials = await effectiveCredentials(poolActive);
@@ -26725,29 +27298,75 @@ const QUOTA_FAILURE_CACHE_MS = 10 * 6e4;
26725
27298
  const QUOTA_TIMEOUT_MS = 8e3;
26726
27299
  let quotaHostInForce = null;
26727
27300
  /**
26728
- * The last usage snapshot, plus the last failure to have one.
27301
+ * How many accounts' snapshots are remembered.
27302
+ *
27303
+ * Bounded like the ChatGPT line's own per-account map: the pool accepts a
27304
+ * handful of accounts, and a long-lived process must not accumulate one entry
27305
+ * per id it has ever seen. The oldest WRITE is evicted, which is also the read
27306
+ * least likely to be looked at again.
27307
+ */
27308
+ const QUOTA_SNAPSHOT_ACCOUNT_LIMIT = 20;
27309
+ /**
27310
+ * The newest read, whichever account it belonged to.
26729
27311
  *
26730
- * `value` and `reason` are tracked SEPARATELY from the last GOOD value on
26731
- * purpose. A read that fails carries no information about the previous one, so
26732
- * replacing a good snapshot with `null` turned every transient refusal into a
26733
- * visible disappearance of the quota box; `quotaLastGood` lets the card keep
26734
- * showing the last known numbers while saying they are old.
27312
+ * Kept beside the per-account map because the page-level block and the
27313
+ * "is a read even worth starting" test are about the line's latest reading,
27314
+ * not about one account.
26735
27315
  */
26736
27316
  let quotaSnapshot = null;
26737
- /** The last snapshot that actually parsed, kept across failures. */
26738
- let quotaLastGood = null;
27317
+ /** Per-account snapshots, so an account is never shown the numbers read for another. */
27318
+ const quotaByAccount = /* @__PURE__ */ new Map();
26739
27319
  /** How long a refused credential is remembered before asking again. */
26740
27320
  const QUOTA_REJECTED_CACHE_MS = 30 * 6e4;
26741
27321
  /** Forget the cached usage snapshot (a sign-out, a sign-in, a test). */
26742
27322
  function clearCachedQuota$4() {
26743
27323
  quotaSnapshot = null;
26744
- quotaLastGood = null;
27324
+ quotaByAccount.clear();
26745
27325
  quotaHostInForce = null;
26746
27326
  }
26747
27327
  /** The last usage snapshot without touching the network. */
26748
27328
  function getCachedQuota$4() {
26749
27329
  return quotaSnapshot?.value ?? null;
26750
27330
  }
27331
+ /**
27332
+ * The cached snapshot only when it belongs to this account, without touching the
27333
+ * network.
27334
+ *
27335
+ * Quota follows the account: a rotation decides whose credential is spent next,
27336
+ * so the snapshot a read produced describes the row it was made for. The pool
27337
+ * asks here while it builds the settings card's account list, which must never
27338
+ * wait on — or trigger — an upstream read.
27339
+ *
27340
+ * An absent, empty or null id keeps the pre-pool behaviour and answers with the
27341
+ * newest read, which is what the page-level block wants.
27342
+ */
27343
+ function getCachedQuotaFor$1(accountId) {
27344
+ if (accountId === void 0 || accountId === null || accountId === "") return getCachedQuota$4();
27345
+ return quotaByAccount.get(accountId)?.value ?? null;
27346
+ }
27347
+ /** The entry a read for this account should consult, when there is one. */
27348
+ function quotaEntryFor(accountId) {
27349
+ if (accountId === void 0 || accountId === "") return quotaSnapshot;
27350
+ return quotaByAccount.get(accountId) ?? null;
27351
+ }
27352
+ /**
27353
+ * Remember one read against the account it was made for.
27354
+ *
27355
+ * Re-inserting an id moves it to the newest position, so the map's first key is
27356
+ * always the least recently written entry and eviction is a single delete.
27357
+ */
27358
+ function rememberQuotaSnapshot(entry) {
27359
+ quotaSnapshot = entry;
27360
+ const accountId = entry.accountId;
27361
+ if (accountId === void 0 || accountId === "") return;
27362
+ quotaByAccount.delete(accountId);
27363
+ quotaByAccount.set(accountId, entry);
27364
+ while (quotaByAccount.size > QUOTA_SNAPSHOT_ACCOUNT_LIMIT) {
27365
+ const oldest = quotaByAccount.keys().next().value;
27366
+ if (oldest === void 0) break;
27367
+ quotaByAccount.delete(oldest);
27368
+ }
27369
+ }
26751
27370
  /** Why the last read produced no snapshot, or null when it produced one. */
26752
27371
  function getQuotaUnavailable() {
26753
27372
  return quotaSnapshot?.reason ?? null;
@@ -26869,9 +27488,10 @@ function parseTokenPlanQuota(payload, now = Date.now()) {
26869
27488
  async function fetchTokenPlanQuota(credentials, options = {}) {
26870
27489
  const fetchFn = options.fetchFn ?? fetch;
26871
27490
  const now = Date.now();
26872
- if (options.force !== true && quotaSnapshot !== null) {
26873
- const ttl = quotaSnapshot.reason === "credential-not-accepted" ? QUOTA_REJECTED_CACHE_MS : quotaSnapshot.reason === "token-expired" && quotaSnapshot.value === null ? QUOTA_FAILURE_CACHE_MS : quotaSnapshot.value === null ? QUOTA_FAILURE_CACHE_MS : QUOTA_CACHE_MS;
26874
- if (now - quotaSnapshot.at < ttl) return quotaSnapshot.value;
27491
+ const cached = quotaEntryFor(options.accountId);
27492
+ if (options.force !== true && cached !== null) {
27493
+ const ttl = cached.reason === "credential-not-accepted" ? QUOTA_REJECTED_CACHE_MS : cached.reason === "token-expired" && cached.value === null ? QUOTA_FAILURE_CACHE_MS : cached.value === null ? QUOTA_FAILURE_CACHE_MS : QUOTA_CACHE_MS;
27494
+ if (now - cached.at < ttl) return cached.value;
26875
27495
  }
26876
27496
  const candidates = quotaHostCandidates(credentials.region);
26877
27497
  const ordered = quotaHostInForce === null ? candidates : [quotaHostInForce, ...candidates.filter((host) => host !== quotaHostInForce)];
@@ -26911,19 +27531,19 @@ async function fetchTokenPlanQuota(credentials, options = {}) {
26911
27531
  });
26912
27532
  }
26913
27533
  if (value === null && reason === null) reason = authRefused ? "token-expired" : "unreachable";
26914
- if (value !== null) quotaLastGood = {
26915
- value,
26916
- at: Date.now()
26917
- };
26918
- else if (quotaLastGood !== null) {
26919
- value = quotaLastGood.value;
26920
- reason = "stale";
27534
+ if (value === null) {
27535
+ const previous = quotaEntryFor(options.accountId);
27536
+ if (previous !== null && previous.value !== null) {
27537
+ value = previous.value;
27538
+ reason = "stale";
27539
+ }
26921
27540
  }
26922
- quotaSnapshot = {
27541
+ rememberQuotaSnapshot({
27542
+ accountId: options.accountId,
26923
27543
  at: Date.now(),
26924
27544
  value,
26925
27545
  reason
26926
- };
27546
+ });
26927
27547
  return value;
26928
27548
  }
26929
27549
  /**
@@ -27961,6 +28581,38 @@ function parseMinimaxCodePoolData(value) {
27961
28581
  return result;
27962
28582
  }
27963
28583
  /**
28584
+ * Nominal length of the two windows the Token Plan reports, in minutes.
28585
+ *
28586
+ * The line's own card names these two keys (`quotaWindowInterval` = 5 小时窗口,
28587
+ * `quotaWindowWeekly` = 每周窗口), and 5 hours / 7 days is what those words mean,
28588
+ * so the shared account card can name an unlabeled window from its length
28589
+ * instead of showing a generic one. The same two numbers label the equivalent
28590
+ * windows on the Command Code and Kimi Code lines.
28591
+ */
28592
+ const WINDOW_MINUTES = {
28593
+ interval: 300,
28594
+ weekly: 10080
28595
+ };
28596
+ /**
28597
+ * One cached read as the shared account card renders it.
28598
+ *
28599
+ * Every window is labeled through its LENGTH rather than with a string: the
28600
+ * canonical window names live in the client's dictionary, and the host has no
28601
+ * business shipping display text. A window whose consumed share nobody measured
28602
+ * is dropped by {@link quotaWindow} — a null percentage is not 0%, it is
28603
+ * "not stated", and a bar would lie about it. An `unlimited` window states no
28604
+ * consumed share at all and the per-account shape has no way to say
28605
+ * "unlimited", so it is dropped too rather than drawn as an empty bar.
28606
+ */
28607
+ function minimaxCodePoolQuota(quota) {
28608
+ if (quota === null || quota === void 0) return void 0;
28609
+ const windows = quota.windows === void 0 || quota.windows.length === 0 ? [quotaWindow$1("", quota.usedPercent, { resetsAt: quota.resetsAtMs })] : quota.windows.map((window) => quotaWindow$1("", window.usedPercent, {
28610
+ windowDurationMins: WINDOW_MINUTES[window.key],
28611
+ resetsAt: window.resetsAtMs
28612
+ }));
28613
+ return poolQuota(quota.fetchedAtMs, windows);
28614
+ }
28615
+ /**
27964
28616
  * MiniMax Code's account pool.
27965
28617
  *
27966
28618
  * It owns rotation and the write-back, exactly like the sibling lines, with one
@@ -28041,7 +28693,11 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
28041
28693
  ...planName === void 0 ? {} : {
28042
28694
  planName,
28043
28695
  planLabel: planName
28044
- }
28696
+ },
28697
+ ...(() => {
28698
+ const quota = minimaxCodePoolQuota(getCachedQuotaFor$1(account.id));
28699
+ return quota === void 0 ? {} : { quota };
28700
+ })()
28045
28701
  };
28046
28702
  },
28047
28703
  liveCredentialsFor: (account) => this.nativeAuthority(account),
@@ -28128,6 +28784,34 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
28128
28784
  });
28129
28785
  }
28130
28786
  /**
28787
+ * The account a usage read belongs to, together with the credential to read it
28788
+ * with.
28789
+ *
28790
+ * Authored here rather than in the routes because it is provider-shaped: the
28791
+ * routes need a credential and the ACCOUNT it was read for, so a snapshot can
28792
+ * be remembered against the row whose numbers it describes. The selection is
28793
+ * the core's own rule for a caller that names no account — the active one, then
28794
+ * the primary, then the first — repeated in these three lines because the core
28795
+ * exposes it only through `getFreshCredential`, which returns the credential
28796
+ * alone; the credential (and its single-flight refresh, write-back and
28797
+ * bookkeeping) still comes from the core, so no rotation rule lives here.
28798
+ *
28799
+ * No upstream request is made for the selection itself, and none of this moves
28800
+ * the rotation: reading quota is not serving a request.
28801
+ */
28802
+ async getQuotaReadTarget(fetchFn = fetch) {
28803
+ const data = await this.read();
28804
+ const target = data.accounts.find((account) => account.id === data.activeAccountId) ?? data.accounts.find((account) => account.isPrimary) ?? data.accounts[0];
28805
+ if (target === void 0) {
28806
+ await this.getFreshCredential(void 0, fetchFn);
28807
+ throw new Error("MiniMax Code 号池中没有账号。");
28808
+ }
28809
+ return {
28810
+ accountId: target.id,
28811
+ credentials: await this.getFreshCredential(target.id, fetchFn)
28812
+ };
28813
+ }
28814
+ /**
28131
28815
  * Pick the account for the next request.
28132
28816
  *
28133
28817
  * The returned credential carries its own region and record key, so a caller
@@ -28461,8 +29145,12 @@ function registerMinimaxCodeRoutes(ctx, store = new MinimaxCodeCredentialStore()
28461
29145
  * network, and the status route reads the snapshot through the cache instead.
28462
29146
  */
28463
29147
  const refreshQuota = async (force) => {
28464
- const credentials = await (options.accountPool === void 0 ? ensureAccessToken$2(store, { fetchFn }).catch(() => null) : options.accountPool.getFreshCredential(void 0, fetchFn).catch(() => null));
28465
- if (credentials === null) return null;
29148
+ const target = await (options.accountPool === void 0 ? ensureAccessToken$2(store, { fetchFn }).then((credentials) => ({
29149
+ accountId: void 0,
29150
+ credentials
29151
+ })).catch(() => null) : options.accountPool.getQuotaReadTarget(fetchFn).catch(() => null));
29152
+ if (target === null) return null;
29153
+ const credentials = target.credentials;
28466
29154
  const readToken = credentials.refreshToken;
28467
29155
  const isCredentialStale = async () => {
28468
29156
  const live = await store.readFresh().catch(() => null);
@@ -28472,10 +29160,11 @@ function registerMinimaxCodeRoutes(ctx, store = new MinimaxCodeCredentialStore()
28472
29160
  fetchFn,
28473
29161
  force,
28474
29162
  isCredentialStale,
29163
+ ...target.accountId === void 0 ? {} : { accountId: target.accountId },
28475
29164
  renewCredential: async () => await (options.accountPool === void 0 ? ensureAccessToken$2(store, {
28476
29165
  fetchFn,
28477
29166
  force: true
28478
- }) : options.accountPool.renewCredential(void 0, fetchFn)).catch(() => null)
29167
+ }) : options.accountPool.renewCredential(target.accountId, fetchFn)).catch(() => null)
28479
29168
  }).catch(() => null);
28480
29169
  };
28481
29170
  /**
@@ -30540,6 +31229,9 @@ const CATALOG_CACHE_TTL_MS$1 = 1800 * 1e3;
30540
31229
  * every cache miss.
30541
31230
  */
30542
31231
  const catalogSnapshotScopesLoaded = /* @__PURE__ */ new Set();
31232
+ function getCachedCatalog$1() {
31233
+ return cachedCatalog?.models ?? [];
31234
+ }
30543
31235
  function clearCachedCatalog$1() {
30544
31236
  cachedCatalog = void 0;
30545
31237
  catalogCacheEpoch += 1;
@@ -30731,16 +31423,64 @@ function billingMeters(billing) {
30731
31423
  }
30732
31424
  return meters;
30733
31425
  }
30734
- let cachedQuota;
31426
+ /**
31427
+ * One newest snapshot PER ACCOUNT, keyed by the snapshot's own `account.id` —
31428
+ * the same public key the pool addresses an account with.
31429
+ *
31430
+ * Quota follows the account: rotation decides which account spends the next
31431
+ * request, so a single slot only ever described whichever account was read
31432
+ * last. The pool's card draws every account its own reading, so each account
31433
+ * keeps its own entry; past the cap the oldest read is evicted.
31434
+ *
31435
+ * This map is a memory read only. Nothing here fetches — the settings card
31436
+ * builds its account list from it, and that must never cost one upstream
31437
+ * request per pooled account.
31438
+ */
31439
+ const cachedQuotaEntries = /* @__PURE__ */ new Map();
31440
+ /** How many accounts' snapshots are remembered; mirrors the Codex usage service. */
31441
+ const QUOTA_CACHE_LIMIT = 20;
31442
+ /** The entry read most recently, whichever account it belongs to. */
31443
+ function newestCachedQuota() {
31444
+ let newest;
31445
+ for (const snapshot of cachedQuotaEntries.values()) if (newest === void 0 || (snapshot.fetchedAt || 0) > (newest.fetchedAt || 0)) newest = snapshot;
31446
+ return newest;
31447
+ }
31448
+ /** Remember one account's newest snapshot, bounded to the accounts in use. */
31449
+ function rememberCachedQuota(snapshot) {
31450
+ cachedQuotaEntries.set(snapshot.account.id, snapshot);
31451
+ while (cachedQuotaEntries.size > QUOTA_CACHE_LIMIT) {
31452
+ const oldest = [...cachedQuotaEntries.entries()].sort((left, right) => (left[1].fetchedAt || 0) - (right[1].fetchedAt || 0))[0];
31453
+ if (oldest === void 0) break;
31454
+ cachedQuotaEntries.delete(oldest[0]);
31455
+ }
31456
+ }
30735
31457
  let quotaInFlight$1 = null;
30736
31458
  let quotaInFlightAccountId = null;
30737
31459
  let quotaCacheEpoch = 0;
31460
+ /**
31461
+ * The snapshot read most recently, whichever account it belongs to.
31462
+ *
31463
+ * Kept with a no-argument signature for backward compatibility; the page-level
31464
+ * facts use {@link getCachedQuotaFor} instead, because they describe one
31465
+ * account and must never be another account's numbers.
31466
+ */
30738
31467
  function getCachedQuota$5() {
30739
- return cachedQuota;
31468
+ return newestCachedQuota();
31469
+ }
31470
+ /**
31471
+ * The snapshot remembered for one account, if it was read before.
31472
+ *
31473
+ * This is the lookup the account pool calls while it builds its summaries: a
31474
+ * pure memory read, never a fetch. Passing no account keeps the pre-pool
31475
+ * behavior of returning whatever was fetched last.
31476
+ */
31477
+ function getCachedQuotaFor(accountId) {
31478
+ if (accountId === void 0 || accountId === null) return newestCachedQuota();
31479
+ return cachedQuotaEntries.get(accountId);
30740
31480
  }
30741
31481
  function clearCachedQuota$5() {
30742
31482
  quotaCacheEpoch += 1;
30743
- cachedQuota = void 0;
31483
+ cachedQuotaEntries.clear();
30744
31484
  quotaInFlight$1 = null;
30745
31485
  quotaInFlightAccountId = null;
30746
31486
  }
@@ -30757,7 +31497,8 @@ async function fetchAccountQuota$4(store = new FileCredentialStore$4(), fetchFn
30757
31497
  });
30758
31498
  if (credentials === null) throw new Error(`Not signed in to ${PROVIDER_NAME$1}.`);
30759
31499
  const requestedAccountId = workBuddyAccountId(credentials);
30760
- if (!force && cachedQuota && cachedQuota.account.id === requestedAccountId && Date.now() - (cachedQuota.fetchedAt || 0) < 12e4) return cachedQuota;
31500
+ const cached = cachedQuotaEntries.get(requestedAccountId);
31501
+ if (!force && cached !== void 0 && Date.now() - (cached.fetchedAt || 0) < 12e4) return cached;
30761
31502
  if (quotaInFlight$1 && quotaInFlightAccountId === requestedAccountId) return quotaInFlight$1;
30762
31503
  const epoch = quotaCacheEpoch;
30763
31504
  const request = (async () => {
@@ -30793,7 +31534,7 @@ async function fetchAccountQuota$4(store = new FileCredentialStore$4(), fetchFn
30793
31534
  sources: ["billing/meter/get-user-resource"]
30794
31535
  };
30795
31536
  if (epoch !== quotaCacheEpoch) return snapshot;
30796
- cachedQuota = snapshot;
31537
+ rememberCachedQuota(snapshot);
30797
31538
  return snapshot;
30798
31539
  })();
30799
31540
  quotaInFlight$1 = request;
@@ -31108,7 +31849,7 @@ async function getWorkBuddyWebStatus(store, modelSettings, preferences, options
31108
31849
  const catalog = live.length > 0 ? live : FALLBACK_MODELS$1;
31109
31850
  const available = modelsForRegion(credentials?.region ?? "intl", catalog).map((model) => model.id);
31110
31851
  const models = buildModelOptions(catalog, available, resolveEnabledModelIds$2(settings.enabledModelIds, available, enabled), settings.contextWindowOverrides);
31111
- const quota = getCachedQuota$5();
31852
+ const quota = getCachedQuotaFor(credentials === null ? null : accountFromCredentials$1(credentials).id);
31112
31853
  const poolData = options.accountPool === void 0 ? null : await options.accountPool.read().catch(() => null);
31113
31854
  const poolAccounts = options.accountPool === void 0 ? [] : await options.accountPool.listAccounts().catch(() => []);
31114
31855
  const hiddenIds = new Set(settings.hiddenAccountIds);
@@ -31167,12 +31908,10 @@ function registerWorkBuddyRoutes(ctx, store, modelSettings, preferences, options
31167
31908
  accountId: settings.selectedAccountId,
31168
31909
  hiddenAccountIds: settings.hiddenAccountIds
31169
31910
  });
31170
- const cached = getCachedQuota$5();
31171
- const quotaMatches = cached?.account.id === (credentials === null ? void 0 : accountFromCredentials$1(credentials).id);
31172
- if (credentials !== null && (!quotaMatches || cached === void 0 || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
31173
- if (!quotaMatches) clearCachedQuota$5();
31911
+ const cached = getCachedQuotaFor(credentials === null ? null : accountFromCredentials$1(credentials).id);
31912
+ if (credentials !== null && (cached === void 0 || Date.now() - (cached.fetchedAt || 0) > 12e4)) {
31174
31913
  const refresh = () => fetchAccountQuota$4(store, fetchFn, false, settings.selectedAccountId, settings.hiddenAccountIds);
31175
- if (cached === void 0 || !quotaMatches) await quotaRefresh.run(refresh);
31914
+ if (cached === void 0) await quotaRefresh.run(refresh);
31176
31915
  else quotaRefresh.start(refresh);
31177
31916
  }
31178
31917
  return sendJson$1(response, 200, {
@@ -32658,6 +33397,47 @@ function defaultAliasFor(credentials, position) {
32658
33397
  return credentials.nickname || credentials.uin || credentials.uid || `账号 ${position}`;
32659
33398
  }
32660
33399
  /**
33400
+ * A meter's consumed share, from whichever fraction this line measured.
33401
+ *
33402
+ * `usedFraction` is the DTO's own "share consumed"; `remainingFraction` is the
33403
+ * same statement read from the other end, and is the one the rest of the card
33404
+ * renders. A meter that states neither is dropped (null) rather than drawn at
33405
+ * 0, which would claim a full window no reading supports.
33406
+ */
33407
+ function meterUsedPercent(meter) {
33408
+ if (typeof meter.usedFraction === "number" && Number.isFinite(meter.usedFraction)) return meter.usedFraction * 100;
33409
+ if (typeof meter.remainingFraction === "number" && Number.isFinite(meter.remainingFraction)) return (1 - meter.remainingFraction) * 100;
33410
+ return null;
33411
+ }
33412
+ /**
33413
+ * One cached snapshot as the shared account card renders it.
33414
+ *
33415
+ * Only what the DTO states is published: every meter that measured a share
33416
+ * becomes a window, and a meter that measured none is dropped rather than drawn
33417
+ * at 0 — which would claim an unspent allowance no reading supports.
33418
+ *
33419
+ * Exported for the pool's regression tests; the card only reaches it through
33420
+ * the summary hook.
33421
+ */
33422
+ function workBuddyPoolQuota(snapshot) {
33423
+ const windows = snapshot.meters.map((meter) => quotaWindow$1(meter.label, meterUsedPercent(meter), { resetsAt: meter.resetsAt }));
33424
+ if (!snapshot.meters.some((meter) => meter.id === "cycle") && snapshot.cycleCredits !== null && snapshot.cycleCredits > 0 && snapshot.cycleUsedCredits !== null) windows.push(quotaWindow$1(snapshot.packageName ?? "Billing cycle", snapshot.cycleUsedCredits / snapshot.cycleCredits * 100, { resetsAt: snapshot.cycleEndsAt }));
33425
+ return poolQuota(snapshot.fetchedAt, windows);
33426
+ }
33427
+ /**
33428
+ * This account's own newest snapshot, ready for the account card.
33429
+ *
33430
+ * Quota follows the account, so the pool reports the snapshot the line already
33431
+ * remembered for THIS account id. The lookup is a pure memory read: the card
33432
+ * builds its account list here, and that must never cost one upstream request
33433
+ * per pooled account. Undefined means exactly that — never read — which is not
33434
+ * the same statement as "nothing used".
33435
+ */
33436
+ function quotaFor(accountId) {
33437
+ const snapshot = getCachedQuotaFor(accountId);
33438
+ return snapshot === void 0 ? void 0 : workBuddyPoolQuota(snapshot);
33439
+ }
33440
+ /**
32661
33441
  * WorkBuddy's account pool.
32662
33442
  *
32663
33443
  * Desktop accounts are adopted into the pool so they can be scheduled and
@@ -32718,7 +33498,11 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
32718
33498
  ...account.credentials.domain === "" ? {} : { domain: account.credentials.domain },
32719
33499
  ...account.credentials.backend === "" ? {} : { backend: account.credentials.backend },
32720
33500
  ...account.credentials.accountType === void 0 ? {} : { accountType: account.credentials.accountType },
32721
- ...account.credentials.expiresAt > 0 ? {} : { expiresAt: void 0 }
33501
+ ...account.credentials.expiresAt > 0 ? {} : { expiresAt: void 0 },
33502
+ ...(() => {
33503
+ const quota = quotaFor(account.id);
33504
+ return quota === void 0 ? {} : { quota };
33505
+ })()
32722
33506
  }),
32723
33507
  preferAccountId: () => this.resolveSelectionId(options.selection?.().selectedAccountId ?? null),
32724
33508
  emptyMessage: `Not signed in to ${PROVIDER_NAME$1}. Sign in with the CodeBuddy desktop client, or add an account from Settings > WorkBuddy.`,
@@ -34727,6 +35511,10 @@ const catalogInFlight = /* @__PURE__ */ new Map();
34727
35511
  function clearCachedCatalog() {
34728
35512
  catalogCache = null;
34729
35513
  }
35514
+ /** The listing currently held, or an empty array before the first load. */
35515
+ function getCachedCatalog() {
35516
+ return catalogCache?.models ?? [];
35517
+ }
34730
35518
  /**
34731
35519
  * Read one entry of the live listing.
34732
35520
  *
@@ -35022,22 +35810,108 @@ function parseQuotaHeaders(headers) {
35022
35810
  }
35023
35811
  };
35024
35812
  }
35025
- let quotaCache = null;
35813
+ /**
35814
+ * Cache key of one account's snapshot.
35815
+ *
35816
+ * A pool row id when the caller names one — that is the identity that survives a
35817
+ * token refresh and the one the account card asks with — and otherwise the
35818
+ * credential's token tail, which is all a single-credential process has. The
35819
+ * `acc:` prefix keeps the two apart, so no row id can ever be answered with an
35820
+ * entry written under a token tail.
35821
+ */
35822
+ function quotaEntryKey(accountId, credentials) {
35823
+ return accountId === void 0 || accountId === "" ? accountKeyFor(credentials) : `acc:${accountId}`;
35824
+ }
35825
+ /**
35826
+ * How many accounts' snapshots are remembered.
35827
+ *
35828
+ * Mirrors the bound the ChatGPT line's usage service applies to its own
35829
+ * per-account map: room for every account a user is likely to pool, and no way
35830
+ * for a long-lived process to accumulate one snapshot per token it has seen.
35831
+ */
35832
+ const QUOTA_ACCOUNT_LIMIT = 20;
35833
+ /**
35834
+ * The newest snapshot of each account, keyed by {@link quotaEntryKey}.
35835
+ *
35836
+ * ONE ENTRY PER ACCOUNT rather than one slot for the line: quota follows the
35837
+ * account, and rotation decides which account spends the next request, so a
35838
+ * single slot could only ever describe whichever account was read last. This is
35839
+ * a memory read only — the settings card builds its account list from it, and
35840
+ * that must never cost one upstream request per pooled account.
35841
+ */
35842
+ const quotaCache = /* @__PURE__ */ new Map();
35843
+ /**
35844
+ * Key of the entry written most recently.
35845
+ *
35846
+ * The pre-pool card asks "whatever was read last", and a header reading that
35847
+ * names no account may only merge into the snapshot it was read beside. Both
35848
+ * need the last WRITE rather than a scan: two accounts can be written inside one
35849
+ * millisecond, and a tie would otherwise be broken by map order.
35850
+ */
35851
+ let quotaLastKey = null;
35026
35852
  const quotaInFlight = /* @__PURE__ */ new Map();
35027
- /** Drop the cached snapshot. Called on sign-out and after a credential change. */
35853
+ /** Drop every cached snapshot. Called on sign-out and after a credential change. */
35028
35854
  function clearCachedQuota$1() {
35029
- quotaCache = null;
35855
+ quotaCache.clear();
35856
+ quotaLastKey = null;
35857
+ }
35858
+ /** The entry written most recently, whichever account it belongs to. */
35859
+ function newestQuotaEntry() {
35860
+ return quotaLastKey === null ? null : quotaCache.get(quotaLastKey) ?? null;
35861
+ }
35862
+ /**
35863
+ * Remember one account's newest snapshot, bounded to the accounts in use.
35864
+ *
35865
+ * The oldest entry is measured by {@link QuotaCacheEntry.observedAt} rather than
35866
+ * by `fetchedAt`: a snapshot kept warm by response headers has no full read
35867
+ * behind it at all, and it is still the newest thing known about that account.
35868
+ */
35869
+ function rememberQuota(entry) {
35870
+ quotaCache.set(entry.key, entry);
35871
+ quotaLastKey = entry.key;
35872
+ while (quotaCache.size > QUOTA_ACCOUNT_LIMIT) {
35873
+ const oldest = [...quotaCache.entries()].sort((left, right) => left[1].observedAt - right[1].observedAt)[0];
35874
+ if (oldest === void 0) break;
35875
+ quotaCache.delete(oldest[0]);
35876
+ if (quotaLastKey === oldest[0]) quotaLastKey = null;
35877
+ }
35030
35878
  }
35031
35879
  /**
35032
35880
  * The cached snapshot, optionally only when it belongs to this credential.
35033
35881
  *
35034
35882
  * With one stored account the argument is unnecessary; it exists so a later
35035
35883
  * pool chunk cannot render one account's usage under another account's name.
35884
+ * Given a credential the answer is THAT account's own newest snapshot, which is
35885
+ * what keeps two pooled accounts apart: a later read of a different account
35886
+ * cannot displace it. Without one the pre-pool answer is kept — whatever was
35887
+ * written last — because the single-account card has no identity to ask with.
35036
35888
  */
35037
35889
  function getCachedQuota$1(credentials) {
35038
- if (quotaCache === null) return null;
35039
- if (credentials === void 0) return quotaCache.quota;
35040
- return quotaCache.key === accountKeyFor(credentials) ? quotaCache.quota : null;
35890
+ if (credentials === void 0) return newestQuotaEntry()?.quota ?? null;
35891
+ return cachedQuotaFor(credentials);
35892
+ }
35893
+ /**
35894
+ * The newest snapshot held for one credential's account, for display only.
35895
+ *
35896
+ * The token-tail form, kept for a composition with a single stored credential.
35897
+ * A pooled caller asks {@link cachedQuotaForPool} with the row id instead: a
35898
+ * token is rotated by every refresh, so an entry filed under a tail is lost the
35899
+ * moment the account renews.
35900
+ */
35901
+ function cachedQuotaFor(credentials) {
35902
+ return quotaCache.get(accountKeyFor(credentials))?.quota ?? null;
35903
+ }
35904
+ /**
35905
+ * The newest snapshot held for one pool row, for display only.
35906
+ *
35907
+ * The pool asks here while it builds the account list for the settings card, so
35908
+ * this is a pure memory read: it never fetches. Null means nothing was ever read
35909
+ * for this account, which is a different statement from "nothing used". A row id
35910
+ * that is undefined falls back to the credential's own tail, which is what a
35911
+ * composition without a pool has.
35912
+ */
35913
+ function cachedQuotaForPool(accountId, credentials) {
35914
+ return quotaCache.get(quotaEntryKey(accountId, credentials))?.quota ?? null;
35041
35915
  }
35042
35916
  /**
35043
35917
  * Read the account's usage meters.
@@ -35055,12 +35929,13 @@ function getCachedQuota$1(credentials) {
35055
35929
  * up staring at a card that claims they are fine.
35056
35930
  */
35057
35931
  function fetchAccountQuota$1(credentials, options = {}) {
35058
- const key = accountKeyFor(credentials);
35932
+ const key = quotaEntryKey(options.accountId, credentials);
35059
35933
  const at = now();
35060
- if (options.force !== true && quotaCache !== null && quotaCache.key === key) {
35061
- const warm = at - quotaCache.observedAt < QUOTA_CACHE_TTL_MS;
35062
- const freshFullRead = quotaCache.fullReadAt !== null && at - quotaCache.fullReadAt < 3e5;
35063
- if (warm && freshFullRead) return Promise.resolve(quotaCache.quota);
35934
+ const cached = quotaCache.get(key);
35935
+ if (options.force !== true && cached !== void 0) {
35936
+ const warm = at - cached.observedAt < QUOTA_CACHE_TTL_MS;
35937
+ const freshFullRead = cached.fullReadAt !== null && at - cached.fullReadAt < 3e5;
35938
+ if (warm && freshFullRead) return Promise.resolve(cached.quota);
35064
35939
  }
35065
35940
  const pending = quotaInFlight.get(key);
35066
35941
  if (pending !== void 0) return pending;
@@ -35075,9 +35950,9 @@ function fetchAccountQuota$1(credentials, options = {}) {
35075
35950
  return run;
35076
35951
  }
35077
35952
  async function performQuotaFetch(credentials, options) {
35078
- const key = accountKeyFor(credentials);
35953
+ const key = quotaEntryKey(options.accountId, credentials);
35079
35954
  const fetchFn = options.fetchFn ?? fetch;
35080
- const previous = quotaCache !== null && quotaCache.key === key ? quotaCache.quota : null;
35955
+ const previous = quotaCache.get(key)?.quota ?? null;
35081
35956
  let response;
35082
35957
  try {
35083
35958
  response = await fetchFn("https://api.anthropic.com/api/oauth/usage", {
@@ -35115,12 +35990,12 @@ async function performQuotaFetch(credentials, options) {
35115
35990
  status: null,
35116
35991
  representativeClaim: null
35117
35992
  };
35118
- quotaCache = {
35993
+ rememberQuota({
35119
35994
  quota,
35120
35995
  key,
35121
35996
  fullReadAt: at,
35122
35997
  observedAt: at
35123
- };
35998
+ });
35124
35999
  return quota;
35125
36000
  }
35126
36001
  /**
@@ -39747,6 +40622,32 @@ async function refreshClaudeCredential(credentials, fetchFn) {
39747
40622
  };
39748
40623
  }
39749
40624
  /**
40625
+ * One account's own newest quota snapshot, in the shape the account card draws.
40626
+ *
40627
+ * The snapshot comes from 'client.ts''s per-account cache, and this is where
40628
+ * this line's own numbers are translated into the shared shape. Every judgement
40629
+ * below trades a possibly-nice-looking bar for an honest one:
40630
+ *
40631
+ * - a window whose consumed share nobody measured is DROPPED (`quotaWindow`
40632
+ * returns null), because "0% used" and "not stated" are different claims;
40633
+ * - the share falls back to `100 - remainingPercent` only when that field was
40634
+ * stated; when the payload stated NEITHER share there is nothing to draw;
40635
+ * - the read time is the full read's own `fetchedAt`, or — for a snapshot that
40636
+ * only response headers ever kept warm, which performed no full read — the
40637
+ * `observedAt` instant those numbers arrived. Either way it is when this line
40638
+ * saw the numbers, never when upstream served them.
40639
+ *
40640
+ * A null snapshot means "this account was never read", which `poolQuota` reports
40641
+ * as undefined so the card can say exactly that instead of drawing a zero.
40642
+ */
40643
+ function claudePoolQuota(snapshot) {
40644
+ if (snapshot === null) return void 0;
40645
+ return poolQuota(snapshot.fetchedAt ?? snapshot.observedAt, snapshot.windows.map((window) => quotaWindow$1(window.label, window.usedPercent ?? (window.remainingPercent === null ? null : 100 - window.remainingPercent), {
40646
+ windowDurationMins: window.windowMinutes,
40647
+ resetsAt: typeof window.resetsAt === "string" ? Date.parse(window.resetsAt) : null
40648
+ })));
40649
+ }
40650
+ /**
39750
40651
  * The Claude subscription account pool.
39751
40652
  *
39752
40653
  * Read the module comment first: it carries the two rules this class exists to
@@ -39844,6 +40745,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
39844
40745
  const subscriptionType = account.subscriptionType ?? credentials.subscriptionType;
39845
40746
  const adopted = account.adopted === true;
39846
40747
  const expired = adopted && adoptedCredentialExpired(credentials);
40748
+ const quota = claudePoolQuota(cachedQuotaForPool(account.id, credentials));
39847
40749
  return {
39848
40750
  ...base,
39849
40751
  ...email === void 0 ? {} : { email },
@@ -39855,6 +40757,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
39855
40757
  source: account.source,
39856
40758
  removable: !adopted,
39857
40759
  ...account.sourcePath === void 0 ? {} : { sourcePath: account.sourcePath },
40760
+ ...quota === void 0 ? {} : { quota },
39858
40761
  ...expired ? {
39859
40762
  authStatus: "expired",
39860
40763
  authFailedReason: ADOPTED_CREDENTIAL_EXPIRED_HINT
@@ -40421,7 +41324,7 @@ async function getClaudeWebStatus(store, modelSettings, preferences, options = {
40421
41324
  const catalog = live.length > 0 ? live : FALLBACK_MODELS;
40422
41325
  const available = catalog.map((model) => model.id);
40423
41326
  const models = buildClaudeModelOptions(catalog, available, resolveEnabledModelIds(settings.enabledModelIds, available, enabled), settings.contextWindowOverrides);
40424
- const quota = credential === void 0 ? getCachedQuota$1() : getCachedQuota$1(credential);
41327
+ const quota = credential === void 0 ? getCachedQuota$1() : cachedQuotaForPool(active.id, credential);
40425
41328
  const poolData = options.accountPool === void 0 ? null : await options.accountPool.read().catch(() => null);
40426
41329
  const poolAccounts = options.accountPool === void 0 ? [] : await options.accountPool.listAccounts().catch(() => []);
40427
41330
  const presence = await claudeCodeCredentialPresence(options.adoptPaths ?? claudeCodeCredentialPaths()).catch(() => ({
@@ -40555,13 +41458,14 @@ function registerClaudeRoutes(ctx, store, modelSettings, preferences, options =
40555
41458
  if (path === "" || path === "status") {
40556
41459
  if (method !== "GET") return sendMethodNotAllowed(response);
40557
41460
  const active = await activeAccount();
40558
- const cached = active.credentials === void 0 ? null : getCachedQuota$1(active.credentials);
41461
+ const cached = active.credentials === void 0 ? null : cachedQuotaForPool(active.id, active.credentials);
40559
41462
  const stale = cached === null || Date.now() - (cached.fetchedAt || 0) > 12e4;
40560
41463
  if (active.credentials !== void 0 && stale) {
40561
41464
  const credentials = active.credentials;
40562
41465
  const refresh = () => fetchAccountQuota$1(credentials, {
40563
41466
  fetchFn,
40564
- signal: quotaAbort.signal
41467
+ signal: quotaAbort.signal,
41468
+ accountId: active.id
40565
41469
  });
40566
41470
  if (cached === null) await quotaRefresh.run(refresh);
40567
41471
  else quotaRefresh.start(refresh);
@@ -40683,7 +41587,8 @@ function registerClaudeRoutes(ctx, store, modelSettings, preferences, options =
40683
41587
  await fetchAccountQuota$1(credentials, {
40684
41588
  fetchFn,
40685
41589
  force: method === "POST",
40686
- signal: quotaAbort.signal
41590
+ signal: quotaAbort.signal,
41591
+ accountId: active.id
40687
41592
  });
40688
41593
  } catch (error) {
40689
41594
  quotaError = error instanceof Error ? error.message : String(error);
@@ -44299,39 +45204,24 @@ function apply(ctx, pluginConfig = {}) {
44299
45204
  provider,
44300
45205
  limits: providerLimits
44301
45206
  })]));
44302
- const antigravityAdapter = new AntigravityAdapter(antigravityStore, antigravityModelSettings, antigravityPreferences, {
44303
- fetchFn: modelFetch.antigravity,
44304
- attachments: ctx.attachments
44305
- }, antigravityAccountPool);
44306
- let antigravityRegistration;
44307
- let antigravityConflict = null;
44308
- const claimAntigravityRoute = () => {
44309
- if (antigravityRegistration !== void 0) return;
44310
- try {
44311
- antigravityRegistration = ctx.llm.registerAdapter([PROVIDER_ID$6], antigravityAdapter);
44312
- if (antigravityConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] Antigravity route "${PROVIDER_ID$6}" is now served by this plugin`);
44313
- antigravityConflict = null;
44314
- } catch (error) {
44315
- antigravityConflict = error instanceof Error ? error.message : String(error);
44316
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$6}" is already owned by another adapter; Antigravity models keep being served by that one until its configuration is removed (${antigravityConflict})`);
44317
- }
44318
- };
44319
- claimAntigravityRoute();
44320
- const antigravityRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44321
- claimAntigravityRoute();
44322
- }) : void 0;
45207
+ const antigravityClaim = claimProviderRoute(ctx, {
45208
+ providerId: PROVIDER_ID$6,
45209
+ label: PROVIDER_NAME$6,
45210
+ adapter: new AntigravityAdapter(antigravityStore, antigravityModelSettings, antigravityPreferences, {
45211
+ fetchFn: modelFetch.antigravity,
45212
+ attachments: ctx.attachments
45213
+ }, antigravityAccountPool)
45214
+ });
44323
45215
  const disposeAntigravityRoutes = registerAntigravityRoutes(ctx, antigravityStore, antigravityModelSettings, antigravityPreferences, proxyFetch, antigravityAccountPool);
44324
45216
  const disposeCommandCodeRoutes = registerCommandCodeRoutes(ctx, commandCodeStore, commandCodeModelSettings, commandCodePreferences, {
44325
45217
  fetchFn: proxyFetch,
44326
- serving: () => commandCodeRegistration !== void 0,
44327
- conflict: () => commandCodeConflict
45218
+ serving: () => commandCodeClaim.serving(),
45219
+ conflict: () => commandCodeClaim.conflict()
44328
45220
  }, commandCodeAccountPool);
44329
45221
  const commandCodeAdapter = new CommandCodeAdapter(commandCodeStore, commandCodeModelSettings, commandCodePreferences, {
44330
45222
  fetchFn: modelFetch["command-code"],
44331
45223
  attachments: ctx.attachments
44332
45224
  }, commandCodeAccountPool);
44333
- let commandCodeRegistration;
44334
- let commandCodeConflict = null;
44335
45225
  const ollamaStore = new FileCredentialStore$5();
44336
45226
  const ollamaModelSettings = new FileModelSettingsStore$5();
44337
45227
  const ollamaAccountPool = new OllamaAccountPool({ store: ollamaStore });
@@ -44339,92 +45229,48 @@ function apply(ctx, pluginConfig = {}) {
44339
45229
  fetchFn: modelFetch.ollama,
44340
45230
  attachments: ctx.attachments
44341
45231
  }, ollamaAccountPool);
44342
- let ollamaRegistration;
44343
- let ollamaConflict = null;
44344
- const kimiCodeAdapter = new KimiCodeAdapter(kimiCodeStore, kimiCodeModelSettings, kimiCodePreferences, {
44345
- fetchFn: modelFetch["kimi-code"],
44346
- attachments: ctx.attachments,
44347
- videos: { readVideo: async (ref) => ({
44348
- data: await readVideoBytes(ref),
44349
- mediaType: ref.mediaType
44350
- }) }
44351
- }, kimiCodeAccountPool);
44352
- let kimiCodeRegistration;
44353
- let kimiCodeConflict = null;
44354
- const claimKimiCodeRoute = () => {
44355
- if (kimiCodeRegistration !== void 0) return;
44356
- try {
44357
- kimiCodeRegistration = ctx.llm.registerAdapter([PROVIDER_ID$3], kimiCodeAdapter);
44358
- if (kimiCodeConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME$3} route "${PROVIDER_ID$3}" is now served by this plugin`);
44359
- kimiCodeConflict = null;
44360
- } catch (error) {
44361
- kimiCodeConflict = error instanceof Error ? error.message : String(error);
44362
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$3}" is already owned by another adapter; ${PROVIDER_NAME$3} models keep being served by that one until its configuration is removed (${kimiCodeConflict})`);
44363
- }
44364
- };
44365
- claimKimiCodeRoute();
44366
- const kimiCodeRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44367
- claimKimiCodeRoute();
44368
- }) : void 0;
44369
- const claimOllamaRoute = () => {
44370
- if (ollamaRegistration !== void 0) return;
44371
- try {
44372
- ollamaRegistration = ctx.llm.registerAdapter([PROVIDER_ID$4], ollamaAdapter);
44373
- if (ollamaConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME$4} route "${PROVIDER_ID$4}" is now served by this plugin`);
44374
- ollamaConflict = null;
44375
- } catch (error) {
44376
- ollamaConflict = error instanceof Error ? error.message : String(error);
44377
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$4}" is already owned by another adapter; ${PROVIDER_NAME$4} models keep being served by that one until its configuration is removed (${ollamaConflict})`);
44378
- }
44379
- };
44380
- claimOllamaRoute();
45232
+ const kimiCodeClaim = claimProviderRoute(ctx, {
45233
+ providerId: PROVIDER_ID$3,
45234
+ label: PROVIDER_NAME$3,
45235
+ adapter: new KimiCodeAdapter(kimiCodeStore, kimiCodeModelSettings, kimiCodePreferences, {
45236
+ fetchFn: modelFetch["kimi-code"],
45237
+ attachments: ctx.attachments,
45238
+ videos: { readVideo: async (ref) => ({
45239
+ data: await readVideoBytes(ref),
45240
+ mediaType: ref.mediaType
45241
+ }) }
45242
+ }, kimiCodeAccountPool)
45243
+ });
45244
+ const ollamaClaim = claimProviderRoute(ctx, {
45245
+ providerId: PROVIDER_ID$4,
45246
+ label: PROVIDER_NAME$4,
45247
+ adapter: ollamaAdapter,
45248
+ watch: false
45249
+ });
44381
45250
  const disposeOllamaRoutes = registerOllamaRoutes(ctx, {
44382
45251
  accountPool: ollamaAccountPool,
44383
45252
  modelSettings: ollamaModelSettings,
44384
45253
  fetchFn: proxyFetch
44385
45254
  });
44386
- const claimCommandCodeRoute = () => {
44387
- if (commandCodeRegistration !== void 0) return;
44388
- try {
44389
- commandCodeRegistration = ctx.llm.registerAdapter([PROVIDER_ID$5], commandCodeAdapter);
44390
- if (commandCodeConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME$5} route "${PROVIDER_ID$5}" is now served by this plugin`);
44391
- commandCodeConflict = null;
44392
- } catch (error) {
44393
- commandCodeConflict = error instanceof Error ? error.message : String(error);
44394
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$5}" is already owned by another adapter; ${PROVIDER_NAME$5} models keep being served by that one until its configuration is removed (${commandCodeConflict})`);
44395
- }
44396
- };
44397
- claimCommandCodeRoute();
44398
- const commandCodeRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44399
- claimCommandCodeRoute();
44400
- }) : void 0;
45255
+ const commandCodeClaim = claimProviderRoute(ctx, {
45256
+ providerId: PROVIDER_ID$5,
45257
+ label: PROVIDER_NAME$5,
45258
+ adapter: commandCodeAdapter
45259
+ });
44401
45260
  const disposeKimiCodeRoutes = registerKimiCodeRoutes(ctx, kimiCodeStore, kimiCodeModelSettings, kimiCodePreferences, {
44402
45261
  fetchFn: proxyFetch,
44403
- serving: () => kimiCodeRegistration !== void 0,
44404
- conflict: () => kimiCodeConflict
45262
+ serving: () => kimiCodeClaim.serving(),
45263
+ conflict: () => kimiCodeClaim.conflict()
44405
45264
  }, kimiCodeAccountPool);
44406
- const minimaxCodeAdapter = new MinimaxCodeAdapter(minimaxCodeStore, {
44407
- fetchFn: modelFetch["minimax-code"],
44408
- attachments: ctx.attachments,
44409
- accountPool: minimaxCodeAccountPool
44410
- }, minimaxCodeModelSettings, minimaxCodePreferences);
44411
- let minimaxCodeRegistration;
44412
- let minimaxCodeConflict = null;
44413
- const claimMinimaxCodeRoute = () => {
44414
- if (minimaxCodeRegistration !== void 0) return;
44415
- try {
44416
- minimaxCodeRegistration = ctx.llm.registerAdapter([PROVIDER_ID$2], minimaxCodeAdapter);
44417
- if (minimaxCodeConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME$2} route "${PROVIDER_ID$2}" is now served by this plugin`);
44418
- minimaxCodeConflict = null;
44419
- } catch (error) {
44420
- minimaxCodeConflict = error instanceof Error ? error.message : String(error);
44421
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$2}" is already owned by another adapter; ${PROVIDER_NAME$2} models keep being served by that one until its configuration is removed (${minimaxCodeConflict})`);
44422
- }
44423
- };
44424
- claimMinimaxCodeRoute();
44425
- const minimaxCodeRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44426
- claimMinimaxCodeRoute();
44427
- }) : void 0;
45265
+ const minimaxCodeClaim = claimProviderRoute(ctx, {
45266
+ providerId: PROVIDER_ID$2,
45267
+ label: PROVIDER_NAME$2,
45268
+ adapter: new MinimaxCodeAdapter(minimaxCodeStore, {
45269
+ fetchFn: modelFetch["minimax-code"],
45270
+ attachments: ctx.attachments,
45271
+ accountPool: minimaxCodeAccountPool
45272
+ }, minimaxCodeModelSettings, minimaxCodePreferences)
45273
+ });
44428
45274
  let minimaxCodeAppVersion;
44429
45275
  try {
44430
45276
  const manifest = createRequire(import.meta.url)("../package.json");
@@ -44448,33 +45294,20 @@ function apply(ctx, pluginConfig = {}) {
44448
45294
  minimaxCodeCheckinTick();
44449
45295
  const disposeMinimaxCodeRoutes = registerMinimaxCodeRoutes(ctx, minimaxCodeStore, {
44450
45296
  fetchFn: proxyFetch,
44451
- serving: () => minimaxCodeRegistration !== void 0,
44452
- conflict: () => minimaxCodeConflict,
45297
+ serving: () => minimaxCodeClaim.serving(),
45298
+ conflict: () => minimaxCodeClaim.conflict(),
44453
45299
  accountPool: minimaxCodeAccountPool,
44454
45300
  checkin: minimaxCodeCheckin
44455
45301
  }, minimaxCodeModelSettings, minimaxCodePreferences);
44456
- const workBuddyAdapter = new WorkBuddyAdapter(workBuddyStore, workBuddyModelSettings, workBuddyPreferences, {
44457
- fetchFn: modelFetch.workbuddy,
44458
- attachments: ctx.attachments,
44459
- accountPool: workBuddyAccountPool
45302
+ const workBuddyClaim = claimProviderRoute(ctx, {
45303
+ providerId: PROVIDER_ID$1,
45304
+ label: PROVIDER_NAME$1,
45305
+ adapter: new WorkBuddyAdapter(workBuddyStore, workBuddyModelSettings, workBuddyPreferences, {
45306
+ fetchFn: modelFetch.workbuddy,
45307
+ attachments: ctx.attachments,
45308
+ accountPool: workBuddyAccountPool
45309
+ })
44460
45310
  });
44461
- let workBuddyRegistration;
44462
- let workBuddyConflict = null;
44463
- const claimWorkBuddyRoute = () => {
44464
- if (workBuddyRegistration !== void 0) return;
44465
- try {
44466
- workBuddyRegistration = ctx.llm.registerAdapter([PROVIDER_ID$1], workBuddyAdapter);
44467
- if (workBuddyConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME$1} route "${PROVIDER_ID$1}" is now served by this plugin`);
44468
- workBuddyConflict = null;
44469
- } catch (error) {
44470
- workBuddyConflict = error instanceof Error ? error.message : String(error);
44471
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID$1}" is already owned by another adapter; ${PROVIDER_NAME$1} models keep being served by that one until its configuration is removed (${workBuddyConflict})`);
44472
- }
44473
- };
44474
- claimWorkBuddyRoute();
44475
- const workBuddyRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44476
- claimWorkBuddyRoute();
44477
- }) : void 0;
44478
45311
  const workBuddyCheckin = new WorkBuddyCheckinService(workBuddyStore, {
44479
45312
  fetchFn: proxyFetch,
44480
45313
  settings: () => workBuddyPreferences.status().checkin,
@@ -44490,37 +45323,24 @@ function apply(ctx, pluginConfig = {}) {
44490
45323
  checkinTick();
44491
45324
  const disposeWorkBuddyRoutes = registerWorkBuddyRoutes(ctx, workBuddyStore, workBuddyModelSettings, workBuddyPreferences, {
44492
45325
  fetchFn: proxyFetch,
44493
- serving: () => workBuddyRegistration !== void 0,
44494
- conflict: () => workBuddyConflict,
45326
+ serving: () => workBuddyClaim.serving(),
45327
+ conflict: () => workBuddyClaim.conflict(),
44495
45328
  accountPool: workBuddyAccountPool,
44496
45329
  checkin: workBuddyCheckin
44497
45330
  });
44498
- const claudeAdapter = new ClaudeAdapter(claudeStore, claudeModelSettings, claudePreferences, {
44499
- fetchFn: modelFetch.claude,
44500
- attachments: ctx.attachments,
44501
- accountPool: claudeAccountPool
45331
+ const claudeClaim = claimProviderRoute(ctx, {
45332
+ providerId: PROVIDER_ID,
45333
+ label: PROVIDER_NAME,
45334
+ adapter: new ClaudeAdapter(claudeStore, claudeModelSettings, claudePreferences, {
45335
+ fetchFn: modelFetch.claude,
45336
+ attachments: ctx.attachments,
45337
+ accountPool: claudeAccountPool
45338
+ })
44502
45339
  });
44503
- let claudeRegistration;
44504
- let claudeConflict = null;
44505
- const claimClaudeRoute = () => {
44506
- if (claudeRegistration !== void 0) return;
44507
- try {
44508
- claudeRegistration = ctx.llm.registerAdapter([PROVIDER_ID], claudeAdapter);
44509
- if (claudeConflict !== null) ctx.logger.info(`[dsh-chatgpt-subscription] ${PROVIDER_NAME} route "${PROVIDER_ID}" is now served by this plugin`);
44510
- claudeConflict = null;
44511
- } catch (error) {
44512
- claudeConflict = error instanceof Error ? error.message : String(error);
44513
- ctx.logger.warn(`[dsh-chatgpt-subscription] provider route "${PROVIDER_ID}" is already owned by another adapter; ${PROVIDER_NAME} models keep being served by that one until its configuration is removed (${claudeConflict})`);
44514
- }
44515
- };
44516
- claimClaudeRoute();
44517
- const claudeRouteWatch = typeof ctx.on === "function" ? ctx.on("llm/adapters-updated", () => {
44518
- claimClaudeRoute();
44519
- }) : void 0;
44520
45340
  const disposeClaudeRoutes = registerClaudeRoutes(ctx, claudeStore, claudeModelSettings, claudePreferences, {
44521
45341
  fetchFn: proxyFetch,
44522
- serving: () => claudeRegistration !== void 0,
44523
- conflict: () => claudeConflict,
45342
+ serving: () => claudeClaim.serving(),
45343
+ conflict: () => claudeClaim.conflict(),
44524
45344
  accountPool: claudeAccountPool
44525
45345
  });
44526
45346
  workBuddyAccountPool.syncDesktopAccounts().catch(() => void 0);
@@ -44531,7 +45351,11 @@ function apply(ctx, pluginConfig = {}) {
44531
45351
  pool: codexAccountPool
44532
45352
  });
44533
45353
  const usage = new UsageService(oauth, { fetchFn: proxyFetch });
44534
- codexAccountPool.setQuotaBlockedUntil((account, now) => usage.blockedUntilFor(account.credentials, now));
45354
+ codexAccountPool.setQuotaBlockedUntil((account, now) => usage.blockedUntilFor(account.id, account.credentials, now));
45355
+ codexAccountPool.setQuotaSnapshot((account) => {
45356
+ const snapshot = usage.snapshotFor(account.id, account.credentials);
45357
+ return snapshot === void 0 ? void 0 : codexAccountQuota(snapshot);
45358
+ });
44535
45359
  const adapter = new CodexChatGptAdapter(new ResponsesClient(oauth, ctx.attachments, {
44536
45360
  fetchFn: modelFetch["codex-chatgpt"],
44537
45361
  accountPool: codexAccountPool,
@@ -44587,6 +45411,145 @@ function apply(ctx, pluginConfig = {}) {
44587
45411
  });
44588
45412
  };
44589
45413
  const disposeRoutes = registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, readRouteAudit, codexAccountPool, fetchConfiguration);
45414
+ const routable = (accounts) => accounts.some((account) => account.authStatus === void 0 || account.authStatus === "ok");
45415
+ const disposeHubOverview = registerHubOverviewRoutes(ctx, [
45416
+ {
45417
+ id: "chatgpt",
45418
+ providerId: PROVIDER_ID$7,
45419
+ canToggle: true,
45420
+ read: async () => {
45421
+ const current = preferences.status();
45422
+ const accounts = await codexAccountPool.listAccounts().catch(() => []);
45423
+ return {
45424
+ enabled: current.enabled !== false,
45425
+ accountCount: accounts.length,
45426
+ authenticated: routable(accounts),
45427
+ enabledModelCount: current.visibleModelIds.length,
45428
+ totalModelCount: CODEX_MODEL_CATALOG.length
45429
+ };
45430
+ }
45431
+ },
45432
+ {
45433
+ id: "antigravity",
45434
+ providerId: PROVIDER_ID$6,
45435
+ canToggle: true,
45436
+ read: async () => {
45437
+ const current = antigravityPreferences.status();
45438
+ const accounts = await antigravityAccountPool.listAccounts().catch(() => []);
45439
+ const total = catalogTotal((await antigravityModelSettings.read()).catalogModels.length);
45440
+ return {
45441
+ enabled: current.enabled !== false,
45442
+ accountCount: accounts.length,
45443
+ authenticated: routable(accounts),
45444
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45445
+ totalModelCount: total
45446
+ };
45447
+ }
45448
+ },
45449
+ {
45450
+ id: "command-code",
45451
+ providerId: PROVIDER_ID$5,
45452
+ canToggle: true,
45453
+ read: async () => {
45454
+ const current = commandCodePreferences.status();
45455
+ const accounts = await commandCodeAccountPool.listAccounts().catch(() => []);
45456
+ const total = catalogTotal((await commandCodeModelSettings.read()).catalogModels.length);
45457
+ return {
45458
+ enabled: current.enabled !== false,
45459
+ accountCount: accounts.length,
45460
+ authenticated: routable(accounts),
45461
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45462
+ totalModelCount: total
45463
+ };
45464
+ }
45465
+ },
45466
+ {
45467
+ id: "kimi-code",
45468
+ providerId: PROVIDER_ID$3,
45469
+ canToggle: true,
45470
+ read: async () => {
45471
+ const current = kimiCodePreferences.status();
45472
+ const accounts = await kimiCodeAccountPool.listAccounts().catch(() => []);
45473
+ const total = catalogTotal((await kimiCodeModelSettings.read()).catalogModels.length);
45474
+ return {
45475
+ enabled: current.enabled !== false,
45476
+ accountCount: accounts.length,
45477
+ authenticated: routable(accounts),
45478
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45479
+ totalModelCount: total
45480
+ };
45481
+ }
45482
+ },
45483
+ {
45484
+ id: "workbuddy",
45485
+ providerId: PROVIDER_ID$1,
45486
+ canToggle: true,
45487
+ read: async () => {
45488
+ const current = workBuddyPreferences.status();
45489
+ const accounts = await workBuddyAccountPool.listAccounts().catch(() => []);
45490
+ const total = catalogTotal(getCachedCatalog$1().length);
45491
+ return {
45492
+ enabled: current.enabled !== false,
45493
+ accountCount: accounts.length,
45494
+ authenticated: routable(accounts),
45495
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45496
+ totalModelCount: total
45497
+ };
45498
+ }
45499
+ },
45500
+ {
45501
+ id: "minimax-code",
45502
+ providerId: PROVIDER_ID$2,
45503
+ canToggle: true,
45504
+ read: async () => {
45505
+ const current = minimaxCodePreferences.status();
45506
+ const accounts = await minimaxCodeAccountPool.listAccounts().catch(() => []);
45507
+ const total = catalogTotal(catalogSize());
45508
+ return {
45509
+ enabled: current.enabled !== false,
45510
+ accountCount: accounts.length,
45511
+ authenticated: routable(accounts),
45512
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45513
+ totalModelCount: total
45514
+ };
45515
+ }
45516
+ },
45517
+ {
45518
+ id: "claude",
45519
+ providerId: PROVIDER_ID,
45520
+ canToggle: true,
45521
+ read: async () => {
45522
+ const current = claudePreferences.status();
45523
+ const accounts = await claudeAccountPool.listAccounts().catch(() => []);
45524
+ const total = catalogTotal(getCachedCatalog().length);
45525
+ return {
45526
+ enabled: current.enabled !== false,
45527
+ accountCount: accounts.length,
45528
+ authenticated: routable(accounts),
45529
+ enabledModelCount: total === null ? null : Math.min(current.enabledModelIds?.length ?? 0, total),
45530
+ totalModelCount: total
45531
+ };
45532
+ }
45533
+ },
45534
+ {
45535
+ id: "ollama",
45536
+ providerId: PROVIDER_ID$4,
45537
+ canToggle: false,
45538
+ read: async () => {
45539
+ const accounts = await ollamaAccountPool.listAccounts().catch(() => []);
45540
+ const current = ollamaModelSettings.status();
45541
+ const total = catalogTotal(current.catalogModels.length);
45542
+ const enabled = current.enabledModelIds.length === 0 ? total : current.enabledModelIds.length;
45543
+ return {
45544
+ enabled: true,
45545
+ accountCount: accounts.length,
45546
+ authenticated: routable(accounts),
45547
+ enabledModelCount: total === null ? null : Math.min(enabled ?? 0, total),
45548
+ totalModelCount: total
45549
+ };
45550
+ }
45551
+ }
45552
+ ]);
44590
45553
  const disposeAdapter = ctx.llm.registerAdapter([PROVIDER_ID$7], adapter);
44591
45554
  const disposeImageTool = ctx.tools.register(createCodexImageTool(oauth, ctx.attachments, { fetchFn: proxyFetch }));
44592
45555
  const disposeVideoTool = ctx.tools.register(createKimiVideoTool(ctx, { fetchFn: proxyFetch }));
@@ -44613,48 +45576,28 @@ function apply(ctx, pluginConfig = {}) {
44613
45576
  disposeVideoTool();
44614
45577
  disposeAdapter();
44615
45578
  disposeRoutes();
45579
+ disposeHubOverview();
44616
45580
  disposeAntigravityRoutes();
44617
- releaseHandle(antigravityRouteWatch);
44618
- antigravityRegistration?.();
44619
- antigravityRegistration = void 0;
45581
+ antigravityClaim.dispose();
44620
45582
  disposeOllamaRoutes();
44621
- ollamaRegistration?.();
44622
- ollamaRegistration = void 0;
45583
+ ollamaClaim.dispose();
44623
45584
  disposeCommandCodeRoutes();
44624
- releaseHandle(commandCodeRouteWatch);
44625
- commandCodeRegistration?.();
44626
- commandCodeRegistration = void 0;
45585
+ commandCodeClaim.dispose();
44627
45586
  disposeKimiCodeRoutes();
44628
- releaseHandle(kimiCodeRouteWatch);
44629
- kimiCodeRegistration?.();
44630
- kimiCodeRegistration = void 0;
45587
+ kimiCodeClaim.dispose();
44631
45588
  disposeMinimaxCodeRoutes();
44632
- releaseHandle(minimaxCodeRouteWatch);
44633
- minimaxCodeRegistration?.();
44634
- minimaxCodeRegistration = void 0;
45589
+ minimaxCodeClaim.dispose();
44635
45590
  clearInterval(minimaxCodeCheckinTimer);
44636
45591
  clearInterval(checkinTimer);
44637
45592
  disposeWorkBuddyRoutes();
44638
- releaseHandle(workBuddyRouteWatch);
44639
- workBuddyRegistration?.();
44640
- workBuddyRegistration = void 0;
45593
+ workBuddyClaim.dispose();
44641
45594
  disposeClaudeRoutes();
44642
- releaseHandle(claudeRouteWatch);
44643
- claudeRegistration?.();
44644
- claudeRegistration = void 0;
45595
+ claudeClaim.dispose();
44645
45596
  oauth.dispose();
44646
45597
  proxyManager.dispose();
44647
45598
  };
44648
45599
  }, "dsh-chatgpt-subscription: adapter, routes, and lifecycle");
44649
45600
  }
44650
- /** Cordis event handles are either a disposer function or a disposable object. */
44651
- function releaseHandle(handle) {
44652
- if (typeof handle === "function") {
44653
- handle();
44654
- return;
44655
- }
44656
- handle?.dispose?.();
44657
- }
44658
45601
  function localWebServerBaseUrl(host, port) {
44659
45602
  return `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
44660
45603
  }