@slkiser/opencode-quota 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +256 -561
  2. package/dist/lib/anthropic.js +1 -1
  3. package/dist/lib/anthropic.js.map +1 -1
  4. package/dist/lib/config-file-utils.d.ts +12 -0
  5. package/dist/lib/config-file-utils.d.ts.map +1 -1
  6. package/dist/lib/config-file-utils.js +23 -0
  7. package/dist/lib/config-file-utils.js.map +1 -1
  8. package/dist/lib/config.d.ts +16 -3
  9. package/dist/lib/config.d.ts.map +1 -1
  10. package/dist/lib/config.js +434 -216
  11. package/dist/lib/config.js.map +1 -1
  12. package/dist/lib/copilot.d.ts.map +1 -1
  13. package/dist/lib/copilot.js +3 -2
  14. package/dist/lib/copilot.js.map +1 -1
  15. package/dist/lib/entries.d.ts +1 -1
  16. package/dist/lib/entries.d.ts.map +1 -1
  17. package/dist/lib/format-utils.d.ts.map +1 -1
  18. package/dist/lib/format-utils.js +3 -2
  19. package/dist/lib/format-utils.js.map +1 -1
  20. package/dist/lib/format.d.ts.map +1 -1
  21. package/dist/lib/format.js +4 -2
  22. package/dist/lib/format.js.map +1 -1
  23. package/dist/lib/google-gemini-cli-companion.d.ts +29 -0
  24. package/dist/lib/google-gemini-cli-companion.d.ts.map +1 -0
  25. package/dist/lib/google-gemini-cli-companion.js +166 -0
  26. package/dist/lib/google-gemini-cli-companion.js.map +1 -0
  27. package/dist/lib/google-gemini-cli.d.ts +48 -0
  28. package/dist/lib/google-gemini-cli.d.ts.map +1 -0
  29. package/dist/lib/google-gemini-cli.js +404 -0
  30. package/dist/lib/google-gemini-cli.js.map +1 -0
  31. package/dist/lib/opencode-go.js +1 -1
  32. package/dist/lib/opencode-go.js.map +1 -1
  33. package/dist/lib/provider-metadata.d.ts +1 -1
  34. package/dist/lib/provider-metadata.d.ts.map +1 -1
  35. package/dist/lib/provider-metadata.js +19 -0
  36. package/dist/lib/provider-metadata.js.map +1 -1
  37. package/dist/lib/quota-render-data.d.ts +2 -0
  38. package/dist/lib/quota-render-data.d.ts.map +1 -1
  39. package/dist/lib/quota-render-data.js +2 -0
  40. package/dist/lib/quota-render-data.js.map +1 -1
  41. package/dist/lib/quota-runtime-context.d.ts +43 -0
  42. package/dist/lib/quota-runtime-context.d.ts.map +1 -0
  43. package/dist/lib/quota-runtime-context.js +61 -0
  44. package/dist/lib/quota-runtime-context.js.map +1 -0
  45. package/dist/lib/quota-status.d.ts +16 -0
  46. package/dist/lib/quota-status.d.ts.map +1 -1
  47. package/dist/lib/quota-status.js +63 -17
  48. package/dist/lib/quota-status.js.map +1 -1
  49. package/dist/lib/toast-format-grouped.d.ts.map +1 -1
  50. package/dist/lib/toast-format-grouped.js +5 -3
  51. package/dist/lib/toast-format-grouped.js.map +1 -1
  52. package/dist/lib/tui-config-diagnostics.d.ts +7 -2
  53. package/dist/lib/tui-config-diagnostics.d.ts.map +1 -1
  54. package/dist/lib/tui-config-diagnostics.js +27 -8
  55. package/dist/lib/tui-config-diagnostics.js.map +1 -1
  56. package/dist/lib/tui-runtime.d.ts.map +1 -1
  57. package/dist/lib/tui-runtime.js +24 -16
  58. package/dist/lib/tui-runtime.js.map +1 -1
  59. package/dist/lib/types.d.ts +37 -6
  60. package/dist/lib/types.d.ts.map +1 -1
  61. package/dist/lib/types.js.map +1 -1
  62. package/dist/plugin.d.ts.map +1 -1
  63. package/dist/plugin.js +419 -159
  64. package/dist/plugin.js.map +1 -1
  65. package/dist/providers/cursor.js +2 -2
  66. package/dist/providers/cursor.js.map +1 -1
  67. package/dist/providers/google-gemini-cli.d.ts +3 -0
  68. package/dist/providers/google-gemini-cli.d.ts.map +1 -0
  69. package/dist/providers/google-gemini-cli.js +83 -0
  70. package/dist/providers/google-gemini-cli.js.map +1 -0
  71. package/dist/providers/minimax-coding-plan.js +2 -2
  72. package/dist/providers/minimax-coding-plan.js.map +1 -1
  73. package/dist/providers/registry.d.ts.map +1 -1
  74. package/dist/providers/registry.js +2 -0
  75. package/dist/providers/registry.js.map +1 -1
  76. package/package.json +2 -1
package/dist/plugin.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Supports GitHub Copilot and Google (via opencode-antigravity-auth).
7
7
  */
8
8
  import { DEFAULT_CONFIG } from "./lib/types.js";
9
- import { loadConfig, createLoadConfigMeta } from "./lib/config.js";
9
+ import { createLoadConfigMeta } from "./lib/config.js";
10
10
  import { clearCache, getOrFetchWithCacheControl } from "./lib/cache.js";
11
11
  import { formatQuotaRows } from "./lib/format.js";
12
12
  import { formatQuotaCommand } from "./lib/quota-command-format.js";
@@ -28,6 +28,8 @@ import { renderCommandHeading } from "./lib/format-utils.js";
28
28
  import { sanitizeDisplayText } from "./lib/display-sanitize.js";
29
29
  import { ALL_WINDOWS_FORMAT_STYLE, SINGLE_WINDOW_PER_PROVIDER_FORMAT_STYLE, resolveQuotaFormatStyle, } from "./lib/quota-format-style.js";
30
30
  import { collectQuotaRenderData, collectQuotaStatusLiveProbes, matchesQuotaProviderCurrentSelection, resolveQuotaRenderSelection, } from "./lib/quota-render-data.js";
31
+ import { createQuotaProviderRuntimeContext, createQuotaRuntimeRequestContext, resolveQuotaRuntimeContext, } from "./lib/quota-runtime-context.js";
32
+ const DEFERRED_QUOTA_REFRESH_DELAYS_MS = [3_000, 15_000, 60_000, 300_000];
31
33
  /** All token report command specifications */
32
34
  const TOKEN_REPORT_COMMANDS = [
33
35
  {
@@ -158,8 +160,65 @@ export const QuotaToastPlugin = async ({ client }) => {
158
160
  let configLoaded = false;
159
161
  let configInFlight = null;
160
162
  let configMeta = createLoadConfigMeta();
163
+ let runtimeProviders = getProviders();
161
164
  // Track last session token error for /quota_status diagnostics
162
165
  let lastSessionTokenError;
166
+ const deferredQuotaRefreshes = new Map();
167
+ function getDeferredQuotaRefreshDelayMs(attempts) {
168
+ const index = Math.min(Math.max(0, attempts), DEFERRED_QUOTA_REFRESH_DELAYS_MS.length - 1);
169
+ return DEFERRED_QUOTA_REFRESH_DELAYS_MS[index];
170
+ }
171
+ function clearDeferredQuotaRefresh(sessionID) {
172
+ const state = deferredQuotaRefreshes.get(sessionID);
173
+ if (state?.timer) {
174
+ clearTimeout(state.timer);
175
+ }
176
+ deferredQuotaRefreshes.delete(sessionID);
177
+ }
178
+ function clearDeferredQuotaRefreshTimer(state) {
179
+ if (!state.timer)
180
+ return;
181
+ clearTimeout(state.timer);
182
+ state.timer = null;
183
+ }
184
+ function scheduleDeferredQuotaRefresh(params) {
185
+ let state = deferredQuotaRefreshes.get(params.sessionID);
186
+ if (!state) {
187
+ state = {
188
+ sessionID: params.sessionID,
189
+ attempts: 0,
190
+ reason: params.reason,
191
+ queuedAtMs: Date.now(),
192
+ timer: null,
193
+ inFlight: false,
194
+ };
195
+ deferredQuotaRefreshes.set(params.sessionID, state);
196
+ }
197
+ else {
198
+ if (params.incrementAttempts) {
199
+ state.attempts += 1;
200
+ }
201
+ state.reason = params.reason;
202
+ clearDeferredQuotaRefreshTimer(state);
203
+ }
204
+ const delayMs = getDeferredQuotaRefreshDelayMs(state.attempts);
205
+ state.timer = setTimeout(() => {
206
+ void runDeferredQuotaRefresh(params.sessionID);
207
+ }, delayMs);
208
+ state.timer.unref?.();
209
+ void log("Deferred quota refresh scheduled", {
210
+ sessionID: params.sessionID,
211
+ reason: params.reason,
212
+ attempts: state.attempts,
213
+ delayMs,
214
+ });
215
+ }
216
+ async function runDeferredQuotaRefresh(sessionID) {
217
+ const state = deferredQuotaRefreshes.get(sessionID);
218
+ if (!state || state.inFlight)
219
+ return;
220
+ await showQuotaToast(sessionID, "deferred.retry", { deferredRetry: true });
221
+ }
163
222
  function asRecord(value) {
164
223
  return value && typeof value === "object" ? value : null;
165
224
  }
@@ -224,13 +283,42 @@ export const QuotaToastPlugin = async ({ client }) => {
224
283
  }
225
284
  return false;
226
285
  }
286
+ function getPluginRuntimeRootHints() {
287
+ const cwd = process.cwd();
288
+ return {
289
+ workspaceRoot: cwd,
290
+ configRoot: cwd,
291
+ fallbackDirectory: cwd,
292
+ };
293
+ }
294
+ async function resolvePluginRuntimeContext(params = {}) {
295
+ if (!configLoaded) {
296
+ await refreshConfig();
297
+ }
298
+ return resolveQuotaRuntimeContext({
299
+ client: typedClient,
300
+ roots: getPluginRuntimeRootHints(),
301
+ config,
302
+ configMeta,
303
+ providers: runtimeProviders,
304
+ sessionID: params.sessionID,
305
+ sessionMeta: params.sessionMeta,
306
+ resolveSessionMeta: (sessionID) => getSessionModelMeta(sessionID),
307
+ includeSessionMeta: params.includeSessionMeta,
308
+ });
309
+ }
227
310
  async function refreshConfig() {
228
311
  if (configInFlight)
229
312
  return configInFlight;
230
313
  configInFlight = (async () => {
231
314
  try {
232
- configMeta = createLoadConfigMeta();
233
- config = await loadConfig(typedClient, configMeta);
315
+ const runtime = await resolveQuotaRuntimeContext({
316
+ client: typedClient,
317
+ roots: getPluginRuntimeRootHints(),
318
+ });
319
+ configMeta = runtime.configMeta;
320
+ config = runtime.config;
321
+ runtimeProviders = runtime.providers;
234
322
  setPricingSnapshotAutoRefresh(config.pricingSnapshot.autoRefresh);
235
323
  setPricingSnapshotSelection(config.pricingSnapshot.source);
236
324
  configLoaded = true;
@@ -239,6 +327,8 @@ export const QuotaToastPlugin = async ({ client }) => {
239
327
  catch {
240
328
  // Leave configLoaded=false so we can retry on next trigger.
241
329
  config = DEFAULT_CONFIG;
330
+ configMeta = createLoadConfigMeta();
331
+ runtimeProviders = getProviders();
242
332
  setPricingSnapshotAutoRefresh(DEFAULT_CONFIG.pricingSnapshot.autoRefresh);
243
333
  setPricingSnapshotSelection(DEFAULT_CONFIG.pricingSnapshot.source);
244
334
  }
@@ -394,11 +484,11 @@ export const QuotaToastPlugin = async ({ client }) => {
394
484
  }
395
485
  return "current session";
396
486
  }
397
- async function buildQuotaCommandUnavailableMessage(params = {}) {
487
+ async function buildQuotaCommandUnavailableMessage(runtime) {
398
488
  const selection = await resolveQuotaRenderSelection({
399
- client: typedClient,
400
- config,
401
- request: params,
489
+ client: runtime.client,
490
+ config: runtime.config,
491
+ request: createQuotaRuntimeRequestContext(runtime),
402
492
  });
403
493
  if (!selection) {
404
494
  return "Quota unavailable\n\nNo enabled quota providers are configured.\n\nRun /quota_status for diagnostics.";
@@ -461,177 +551,347 @@ export const QuotaToastPlugin = async ({ client }) => {
461
551
  function clearToastCacheForSession(params) {
462
552
  clearCache(buildToastCacheKey(params));
463
553
  }
464
- async function fetchQuotaMessage(params) {
554
+ function isProviderFetchFailureOnly(errors) {
555
+ return (errors.length > 0 && errors.every((error) => error.message === "Failed to read quota data"));
556
+ }
557
+ async function fetchQuotaMessageResult(params) {
465
558
  // Ensure we have loaded config at least once. If load fails, we keep trying
466
- // on subsequent triggers.
559
+ // on subsequent triggers and queue a deferred retry for toast paths.
467
560
  if (!configLoaded) {
468
561
  await refreshConfig();
469
562
  }
563
+ if (!configLoaded) {
564
+ return {
565
+ message: config.debug
566
+ ? formatDebugInfo({
567
+ trigger: params.trigger,
568
+ reason: "config load failed",
569
+ enabledProviders: config.enabledProviders,
570
+ })
571
+ : null,
572
+ cacheRenderedMessage: false,
573
+ retryable: true,
574
+ retryReason: "config_load_failed",
575
+ hasQuotaRows: false,
576
+ };
577
+ }
470
578
  if (!config.enabled) {
471
- return config.debug
472
- ? formatDebugInfo({ trigger: params.trigger, reason: "disabled", enabledProviders: [] })
473
- : null;
579
+ return {
580
+ message: config.debug
581
+ ? formatDebugInfo({ trigger: params.trigger, reason: "disabled", enabledProviders: [] })
582
+ : null,
583
+ cacheRenderedMessage: false,
584
+ retryable: false,
585
+ hasQuotaRows: false,
586
+ };
474
587
  }
475
588
  if (config.enabledProviders !== "auto" && config.enabledProviders.length === 0) {
476
- return config.debug
477
- ? formatDebugInfo({
478
- trigger: params.trigger,
479
- reason: "enabledProviders empty",
480
- enabledProviders: [],
481
- })
482
- : null;
589
+ return {
590
+ message: config.debug
591
+ ? formatDebugInfo({
592
+ trigger: params.trigger,
593
+ reason: "enabledProviders empty",
594
+ enabledProviders: [],
595
+ })
596
+ : null,
597
+ cacheRenderedMessage: false,
598
+ retryable: false,
599
+ hasQuotaRows: false,
600
+ };
483
601
  }
484
- const quotaRequestContext = {
602
+ const runtime = await resolvePluginRuntimeContext({
485
603
  sessionID: params.sessionID,
486
- sessionMeta: config.onlyCurrentModel && params.sessionID
487
- ? (params.sessionMeta ?? (await getSessionModelMeta(params.sessionID)))
488
- : undefined,
489
- };
604
+ sessionMeta: params.sessionMeta,
605
+ includeSessionMeta: (config) => config.onlyCurrentModel,
606
+ });
607
+ const runtimeConfig = runtime.config;
608
+ const quotaRequestContext = createQuotaRuntimeRequestContext(runtime);
490
609
  const quotaResult = await collectQuotaRenderData({
491
- client: typedClient,
492
- config,
610
+ client: runtime.client,
611
+ config: runtimeConfig,
493
612
  request: quotaRequestContext,
494
613
  surfaceExplicitProviderIssues: true,
495
- formatStyle: resolveQuotaFormatStyle(config.formatStyle),
614
+ formatStyle: resolveQuotaFormatStyle(runtimeConfig.formatStyle),
615
+ bypassProviderCache: params.bypassProviderCache,
496
616
  });
497
617
  const { selection, availability, active, attemptedAny, hasExplicitProviderIssues, data } = quotaResult;
498
- if (config.showSessionTokens && params.sessionID) {
618
+ if (runtimeConfig.showSessionTokens && params.sessionID) {
499
619
  lastSessionTokenError = quotaResult.sessionTokenError;
500
620
  }
501
621
  const currentModel = selection?.currentModel;
502
622
  const errors = data?.errors ?? [];
623
+ const hasProviderQuotaRows = Boolean(data?.entries.length);
624
+ const hasQuotaRows = Boolean(hasProviderQuotaRows || data?.sessionTokens);
625
+ const providerFetchFailureOnly = attemptedAny && isProviderFetchFailureOnly(errors);
626
+ const retryableAvailabilityFailure = active.length === 0 && availability.some((item) => !item.ok && item.error === true);
503
627
  if (active.length === 0 && !(hasExplicitProviderIssues && errors.length > 0)) {
504
- return config.debug
628
+ const message = runtimeConfig.debug
505
629
  ? formatDebugInfo({
506
630
  trigger: params.trigger,
507
631
  reason: "no enabled providers available",
508
632
  currentModel,
509
- enabledProviders: config.enabledProviders,
633
+ enabledProviders: runtimeConfig.enabledProviders,
510
634
  availability: availability.map((item) => ({
511
635
  id: item.provider.id,
512
636
  ok: item.ok,
513
637
  })),
514
638
  })
515
639
  : null;
640
+ const retryableNoProviders = selection?.isAutoMode === true || retryableAvailabilityFailure;
641
+ return {
642
+ message,
643
+ cacheRenderedMessage: false,
644
+ retryable: retryableNoProviders,
645
+ retryReason: retryableNoProviders ? "no_available_providers" : undefined,
646
+ hasQuotaRows: false,
647
+ };
516
648
  }
517
- if (data?.entries.length) {
649
+ if (hasQuotaRows) {
518
650
  const formatted = formatQuotaRows({
519
651
  version: "1.0.0",
520
- layout: config.layout,
521
- entries: data.entries,
522
- errors: data.errors,
523
- style: resolveQuotaFormatStyle(config.formatStyle),
524
- percentDisplayMode: config.percentDisplayMode,
525
- sessionTokens: data.sessionTokens,
652
+ layout: runtimeConfig.layout,
653
+ entries: data?.entries ?? [],
654
+ errors: data?.errors ?? [],
655
+ style: resolveQuotaFormatStyle(runtimeConfig.formatStyle),
656
+ percentDisplayMode: runtimeConfig.percentDisplayMode,
657
+ sessionTokens: data?.sessionTokens,
526
658
  });
527
- if (!config.debug)
528
- return formatted;
529
- const debugFooter = `\n\n[debug] src=${configMeta.source} providers=${config.enabledProviders === "auto" ? "(auto)" : config.enabledProviders.join(",") || "(none)"} avail=${availability
659
+ const retryableMaskedProviderFailure = !hasProviderQuotaRows && providerFetchFailureOnly;
660
+ if (!runtimeConfig.debug) {
661
+ return {
662
+ message: formatted,
663
+ cacheRenderedMessage: true,
664
+ retryable: retryableMaskedProviderFailure,
665
+ retryReason: retryableMaskedProviderFailure ? "provider_fetch_failed" : undefined,
666
+ hasQuotaRows: true,
667
+ };
668
+ }
669
+ const debugFooter = `\n\n[debug] src=${configMeta.source} providers=${runtimeConfig.enabledProviders === "auto" ? "(auto)" : runtimeConfig.enabledProviders.join(",") || "(none)"} avail=${availability
530
670
  .map((item) => `${item.provider.id}:${item.ok ? "ok" : "no"}`)
531
671
  .join(" ")}`;
532
- return formatted + debugFooter;
672
+ return {
673
+ message: formatted + debugFooter,
674
+ cacheRenderedMessage: false,
675
+ retryable: retryableMaskedProviderFailure,
676
+ retryReason: retryableMaskedProviderFailure ? "provider_fetch_failed" : undefined,
677
+ hasQuotaRows: true,
678
+ };
533
679
  }
534
680
  // Show errors even without entries when:
535
681
  // 1. showOnBothFail is enabled and at least one provider attempted (existing behavior)
536
682
  // 2. OR we're in explicit mode and have "Not configured"/"Unavailable" errors (new behavior)
537
- if ((config.showOnBothFail && attemptedAny && errors.length > 0) || hasExplicitProviderIssues) {
683
+ if ((runtimeConfig.showOnBothFail && attemptedAny && errors.length > 0) ||
684
+ hasExplicitProviderIssues) {
538
685
  const errorLines = errors.map((error) => `${error.label}: ${error.message}`).join("\n");
539
- if (!config.debug)
540
- return errorLines || "Quota unavailable";
541
- return ((errorLines || "Quota unavailable") +
542
- "\n\n" +
543
- formatDebugInfo({
686
+ const retryableFetchFailure = !hasExplicitProviderIssues && providerFetchFailureOnly;
687
+ const retryableFailure = retryableFetchFailure || retryableAvailabilityFailure;
688
+ const retryReason = retryableFetchFailure
689
+ ? "provider_fetch_failed"
690
+ : retryableAvailabilityFailure
691
+ ? "no_available_providers"
692
+ : undefined;
693
+ const message = !runtimeConfig.debug
694
+ ? errorLines || "Quota unavailable"
695
+ : (errorLines || "Quota unavailable") +
696
+ "\n\n" +
697
+ formatDebugInfo({
698
+ trigger: params.trigger,
699
+ reason: hasExplicitProviderIssues
700
+ ? "providers missing/unavailable"
701
+ : "all providers failed",
702
+ currentModel,
703
+ enabledProviders: runtimeConfig.enabledProviders,
704
+ availability: availability.map((item) => ({
705
+ id: item.provider.id,
706
+ ok: item.ok,
707
+ })),
708
+ });
709
+ return {
710
+ message,
711
+ cacheRenderedMessage: false,
712
+ retryable: retryableFailure,
713
+ retryReason,
714
+ hasQuotaRows: false,
715
+ };
716
+ }
717
+ const retryableNoData = providerFetchFailureOnly ||
718
+ (selection?.isAutoMode === true && active.length > 0 && errors.length === 0);
719
+ return {
720
+ message: runtimeConfig.debug
721
+ ? formatDebugInfo({
544
722
  trigger: params.trigger,
545
- reason: hasExplicitProviderIssues
546
- ? "providers missing/unavailable"
547
- : "all providers failed",
723
+ reason: "no entries",
548
724
  currentModel,
549
- enabledProviders: config.enabledProviders,
725
+ enabledProviders: runtimeConfig.enabledProviders,
550
726
  availability: availability.map((item) => ({
551
727
  id: item.provider.id,
552
728
  ok: item.ok,
553
729
  })),
554
- }));
730
+ })
731
+ : null,
732
+ cacheRenderedMessage: false,
733
+ retryable: retryableNoData,
734
+ retryReason: providerFetchFailureOnly
735
+ ? "provider_fetch_failed"
736
+ : retryableNoData
737
+ ? "no_reportable_data"
738
+ : undefined,
739
+ hasQuotaRows: false,
740
+ };
741
+ }
742
+ async function fetchQuotaMessage(params) {
743
+ const result = await fetchQuotaMessageResult(params);
744
+ return result.message;
745
+ }
746
+ async function reconcileDeferredQuotaRefresh(params) {
747
+ const existing = deferredQuotaRefreshes.get(params.sessionID);
748
+ if (!params.result.retryable) {
749
+ if (existing) {
750
+ clearDeferredQuotaRefresh(params.sessionID);
751
+ await log("Deferred quota refresh cleared", {
752
+ sessionID: params.sessionID,
753
+ trigger: params.trigger,
754
+ reason: params.result.hasQuotaRows ? "quota_rows_available" : "not_retryable",
755
+ });
756
+ }
757
+ return;
555
758
  }
556
- return config.debug
557
- ? formatDebugInfo({
558
- trigger: params.trigger,
559
- reason: "no entries",
560
- currentModel,
561
- enabledProviders: config.enabledProviders,
562
- availability: availability.map((item) => ({
563
- id: item.provider.id,
564
- ok: item.ok,
565
- })),
566
- })
567
- : null;
759
+ if (!params.result.retryReason) {
760
+ return;
761
+ }
762
+ scheduleDeferredQuotaRefresh({
763
+ sessionID: params.sessionID,
764
+ reason: params.result.retryReason,
765
+ incrementAttempts: params.consumedDeferredRetry,
766
+ });
568
767
  }
569
768
  /**
570
769
  * Show quota toast for a session
571
770
  */
572
- async function showQuotaToast(sessionID, trigger) {
771
+ async function showQuotaToast(sessionID, trigger, options = {}) {
573
772
  if (!configLoaded) {
574
773
  await refreshConfig();
575
774
  }
576
- // Check if subagent session
577
- if (await isSubagentSession(sessionID)) {
578
- await log("Skipping toast for subagent session", { sessionID, trigger });
579
- return;
580
- }
581
- // Get or fetch quota (with caching/throttling)
582
- // If debug is enabled, bypass caching so the toast reflects current state.
583
- function shouldCacheToastMessage(msg) {
584
- // Cache when we have any quota row (which always includes a "NN%" token).
585
- // Do not cache when output is only error rows (rendered as "label: message").
586
- const lines = msg.split("\n");
587
- return lines.some((l) => /\b\d{1,3}%\b/.test(l) && !/:\s/.test(l));
588
- }
589
- const sessionMeta = await getSessionModelMeta(sessionID);
590
- const bypassMessageCache = config.debug
591
- ? true
592
- : await shouldBypassToastCacheForLiveLocalUsage({ trigger, sessionID, sessionMeta });
593
- const toastCacheKey = buildToastCacheKey({ sessionID, sessionMeta });
594
- const message = bypassMessageCache
595
- ? await fetchQuotaMessage({ trigger, sessionID, sessionMeta })
596
- : await getOrFetchWithCacheControl(toastCacheKey, async () => {
597
- const msg = await fetchQuotaMessage({ trigger, sessionID, sessionMeta });
598
- const cache = msg ? shouldCacheToastMessage(msg) : true;
599
- return { message: msg, cache };
600
- }, config.minIntervalMs);
601
- if (!message) {
602
- await log("No quota message to display", { trigger });
603
- return;
604
- }
605
- if (!config.enableToast) {
606
- await log("Toast disabled (enableToast=false)", { trigger });
607
- return;
775
+ const pendingDeferred = deferredQuotaRefreshes.get(sessionID);
776
+ const consumedDeferredRetry = options.deferredRetry === true || Boolean(pendingDeferred);
777
+ if (pendingDeferred) {
778
+ if (pendingDeferred.inFlight && !options.deferredRetry) {
779
+ await log("Skipping duplicate deferred quota refresh", { sessionID, trigger });
780
+ return;
781
+ }
782
+ pendingDeferred.inFlight = true;
783
+ clearDeferredQuotaRefreshTimer(pendingDeferred);
608
784
  }
609
- // Show toast
610
785
  try {
611
- await typedClient.tui.showToast({
612
- body: {
613
- message: sanitizeDisplayText(message),
614
- variant: "info",
615
- duration: config.toastDurationMs,
616
- },
786
+ // Check if session is a subagent session
787
+ if (await isSubagentSession(sessionID)) {
788
+ if (consumedDeferredRetry) {
789
+ clearDeferredQuotaRefresh(sessionID);
790
+ }
791
+ await log("Skipping toast for subagent session", { sessionID, trigger });
792
+ return;
793
+ }
794
+ // Get or fetch quota (with caching/throttling)
795
+ // If debug is enabled, bypass caching so the toast reflects current state.
796
+ function shouldCacheToastMessage(msg) {
797
+ // Cache when we have any quota row (which always includes a "NN%" token).
798
+ // Do not cache when output is only error rows (rendered as "label: message").
799
+ const lines = msg.split("\n");
800
+ return lines.some((l) => /\b\d+%\b/.test(l) && !/:\s/.test(l));
801
+ }
802
+ const sessionMeta = await getSessionModelMeta(sessionID);
803
+ const bypassForLiveLocalUsage = await shouldBypassToastCacheForLiveLocalUsage({
804
+ trigger,
805
+ sessionID,
806
+ sessionMeta,
617
807
  });
618
- await log("Displayed quota toast", { message, trigger });
619
- }
620
- catch (err) {
621
- await log("Failed to show toast", {
622
- error: err instanceof Error ? err.message : String(err),
808
+ const bypassMessageCache = config.debug || consumedDeferredRetry || bypassForLiveLocalUsage;
809
+ const bypassProviderCache = consumedDeferredRetry || bypassForLiveLocalUsage;
810
+ const toastCacheKey = buildToastCacheKey({ sessionID, sessionMeta });
811
+ let fetchResult;
812
+ const fetchForToast = () => fetchQuotaMessageResult({
813
+ trigger,
814
+ sessionID,
815
+ sessionMeta,
816
+ bypassProviderCache,
623
817
  });
818
+ const message = bypassMessageCache
819
+ ? await (async () => {
820
+ fetchResult = await fetchForToast();
821
+ return fetchResult.message;
822
+ })()
823
+ : await (async () => {
824
+ const fetched = {};
825
+ const cachedMessage = await getOrFetchWithCacheControl(toastCacheKey, async () => {
826
+ const result = await fetchForToast();
827
+ fetched.result = result;
828
+ const cache = result.message
829
+ ? result.cacheRenderedMessage && shouldCacheToastMessage(result.message)
830
+ : result.cacheRenderedMessage;
831
+ return { message: result.message, cache };
832
+ }, config.minIntervalMs);
833
+ fetchResult = fetched.result;
834
+ return cachedMessage;
835
+ })();
836
+ if (fetchResult) {
837
+ await reconcileDeferredQuotaRefresh({
838
+ sessionID,
839
+ result: fetchResult,
840
+ consumedDeferredRetry,
841
+ trigger,
842
+ });
843
+ }
844
+ if (options.deferredRetry && fetchResult && !fetchResult.hasQuotaRows) {
845
+ await log("Deferred quota refresh did not produce reportable data", {
846
+ sessionID,
847
+ trigger,
848
+ retryable: fetchResult.retryable,
849
+ retryReason: fetchResult.retryReason,
850
+ });
851
+ return;
852
+ }
853
+ if (!message) {
854
+ await log("No quota message to display", { trigger });
855
+ return;
856
+ }
857
+ if (!config.enableToast) {
858
+ await log("Toast disabled (enableToast=false)", { trigger });
859
+ return;
860
+ }
861
+ // Show toast
862
+ try {
863
+ await typedClient.tui.showToast({
864
+ body: {
865
+ message: sanitizeDisplayText(message),
866
+ variant: "info",
867
+ duration: config.toastDurationMs,
868
+ },
869
+ });
870
+ await log("Displayed quota toast", { message, trigger });
871
+ }
872
+ catch (err) {
873
+ await log("Failed to show toast", {
874
+ error: err instanceof Error ? err.message : String(err),
875
+ });
876
+ }
877
+ }
878
+ finally {
879
+ const state = deferredQuotaRefreshes.get(sessionID);
880
+ if (state) {
881
+ state.inFlight = false;
882
+ }
624
883
  }
625
884
  }
626
- async function fetchQuotaCommandData(params = {}) {
885
+ async function fetchQuotaCommandData(runtime) {
886
+ const request = createQuotaRuntimeRequestContext(runtime);
627
887
  const quotaResult = await collectQuotaRenderData({
628
- client: typedClient,
629
- config,
630
- request: params,
888
+ client: runtime.client,
889
+ config: runtime.config,
890
+ request,
631
891
  surfaceExplicitProviderIssues: false,
632
892
  formatStyle: ALL_WINDOWS_FORMAT_STYLE,
633
893
  });
634
- if (config.showSessionTokens && params.sessionID) {
894
+ if (runtime.config.showSessionTokens && request.sessionID) {
635
895
  lastSessionTokenError = quotaResult.sessionTokenError;
636
896
  }
637
897
  return quotaResult.data;
@@ -656,11 +916,15 @@ export const QuotaToastPlugin = async ({ client }) => {
656
916
  });
657
917
  }
658
918
  async function buildStatusReport(params) {
659
- await refreshConfig();
660
- if (!config.enabled)
919
+ const runtime = await resolvePluginRuntimeContext({
920
+ sessionID: params.sessionID,
921
+ includeSessionMeta: true,
922
+ });
923
+ const runtimeConfig = runtime.config;
924
+ if (!runtimeConfig.enabled)
661
925
  return null;
662
926
  await kickPricingRefresh({ reason: "status", maxWaitMs: 750 });
663
- const currentSession = await getSessionModelMeta(params.sessionID);
927
+ const currentSession = runtime.session.sessionMeta ?? {};
664
928
  const currentModel = currentSession.modelID;
665
929
  const currentProviderID = currentSession.providerID;
666
930
  const sessionModelLookup = !params.sessionID
@@ -668,24 +932,13 @@ export const QuotaToastPlugin = async ({ client }) => {
668
932
  : currentModel
669
933
  ? "ok"
670
934
  : "not_found";
671
- const isAutoMode = config.enabledProviders === "auto";
672
- const providers = getProviders();
935
+ const isAutoMode = runtimeConfig.enabledProviders === "auto";
936
+ const providers = runtime.providers;
937
+ const providerContext = createQuotaProviderRuntimeContext(runtime);
673
938
  const availability = await Promise.all(providers.map(async (p) => {
674
939
  let ok = false;
675
940
  try {
676
- ok = await p.isAvailable({
677
- client: typedClient,
678
- config: {
679
- googleModels: config.googleModels,
680
- anthropicBinaryPath: config.anthropicBinaryPath,
681
- alibabaCodingPlanTier: config.alibabaCodingPlanTier,
682
- cursorPlan: config.cursorPlan,
683
- cursorIncludedApiUsd: config.cursorIncludedApiUsd,
684
- cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
685
- currentModel,
686
- currentProviderID,
687
- },
688
- });
941
+ ok = await p.isAvailable(providerContext);
689
942
  }
690
943
  catch {
691
944
  ok = false;
@@ -693,7 +946,7 @@ export const QuotaToastPlugin = async ({ client }) => {
693
946
  return {
694
947
  id: p.id,
695
948
  // In auto mode, a provider is effectively "enabled" if it's available.
696
- enabled: isAutoMode ? ok : config.enabledProviders.includes(p.id),
949
+ enabled: isAutoMode ? ok : runtimeConfig.enabledProviders.includes(p.id),
697
950
  available: ok,
698
951
  matchesCurrentModel: currentModel || isCursorProviderId(currentProviderID)
699
952
  ? matchesQuotaProviderCurrentSelection({
@@ -716,12 +969,9 @@ export const QuotaToastPlugin = async ({ client }) => {
716
969
  if (liveProbeProviders.length > 0) {
717
970
  try {
718
971
  providerLiveProbes = await collectQuotaStatusLiveProbes({
719
- client: typedClient,
720
- config,
721
- request: {
722
- sessionID: params.sessionID,
723
- sessionMeta: currentSession,
724
- },
972
+ client: runtime.client,
973
+ config: runtimeConfig,
974
+ request: createQuotaRuntimeRequestContext(runtime),
725
975
  formatStyle: SINGLE_WINDOW_PER_PROVIDER_FORMAT_STYLE,
726
976
  providers: liveProbeProviders,
727
977
  });
@@ -743,20 +993,23 @@ export const QuotaToastPlugin = async ({ client }) => {
743
993
  const refresh = params.refreshGoogleTokens
744
994
  ? await refreshGoogleTokensForAllAccounts({ skewMs: params.skewMs, force: params.force })
745
995
  : null;
746
- const tuiDiagnostics = await inspectTuiConfig();
996
+ const tuiDiagnostics = await inspectTuiConfig({ roots: runtime.roots });
747
997
  return await buildQuotaStatusReport({
748
998
  tuiDiagnostics,
749
- configSource: configMeta.source,
750
- configPaths: configMeta.paths,
751
- networkSettingSources: configMeta.networkSettingSources,
752
- enabledProviders: config.enabledProviders,
753
- anthropicBinaryPath: config.anthropicBinaryPath,
754
- alibabaCodingPlanTier: config.alibabaCodingPlanTier,
755
- cursorPlan: config.cursorPlan,
756
- cursorIncludedApiUsd: config.cursorIncludedApiUsd,
757
- cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
758
- pricingSnapshotSource: config.pricingSnapshot.source,
759
- onlyCurrentModel: config.onlyCurrentModel,
999
+ configSource: runtime.configMeta.source,
1000
+ configPaths: runtime.configMeta.paths,
1001
+ globalConfigPaths: runtime.configMeta.globalConfigPaths,
1002
+ workspaceConfigPaths: runtime.configMeta.workspaceConfigPaths,
1003
+ settingSources: runtime.configMeta.settingSources,
1004
+ configIssues: runtime.configMeta.configIssues,
1005
+ enabledProviders: runtimeConfig.enabledProviders,
1006
+ anthropicBinaryPath: runtimeConfig.anthropicBinaryPath,
1007
+ alibabaCodingPlanTier: runtimeConfig.alibabaCodingPlanTier,
1008
+ cursorPlan: runtimeConfig.cursorPlan,
1009
+ cursorIncludedApiUsd: runtimeConfig.cursorIncludedApiUsd,
1010
+ cursorBillingCycleStartDay: runtimeConfig.cursorBillingCycleStartDay,
1011
+ pricingSnapshotSource: runtimeConfig.pricingSnapshot.source,
1012
+ onlyCurrentModel: runtimeConfig.onlyCurrentModel,
760
1013
  currentModel,
761
1014
  sessionModelLookup,
762
1015
  providerAvailability: availability,
@@ -770,6 +1023,7 @@ export const QuotaToastPlugin = async ({ client }) => {
770
1023
  }
771
1024
  : { attempted: false },
772
1025
  sessionTokenError: lastSessionTokenError,
1026
+ geminiCliClient: typedClient,
773
1027
  generatedAtMs: params.generatedAtMs,
774
1028
  });
775
1029
  }
@@ -832,19 +1086,21 @@ export const QuotaToastPlugin = async ({ client }) => {
832
1086
  async function handleQuotaSlashCommand(input) {
833
1087
  const sessionID = input.sessionID;
834
1088
  const generatedAtMs = Date.now();
835
- const quotaRequestContext = {
1089
+ const sessionMeta = sessionID ? await getSessionModelMeta(sessionID) : undefined;
1090
+ const runtime = await resolvePluginRuntimeContext({
836
1091
  sessionID,
837
- sessionMeta: sessionID ? await getSessionModelMeta(sessionID) : undefined,
838
- };
839
- const reportData = await fetchQuotaCommandData(quotaRequestContext);
1092
+ sessionMeta,
1093
+ includeSessionMeta: (config) => config.onlyCurrentModel,
1094
+ });
1095
+ const reportData = await fetchQuotaCommandData(runtime);
840
1096
  if (!reportData) {
841
1097
  if (!configLoaded) {
842
1098
  return await injectCommandOutputAndHandle(sessionID, "Quota unavailable (config not loaded, try again)");
843
1099
  }
844
- if (!config.enabled) {
1100
+ if (!runtime.config.enabled) {
845
1101
  return await injectCommandOutputAndHandle(sessionID, "Quota disabled in config (enabled: false)");
846
1102
  }
847
- return await injectCommandOutputAndHandle(sessionID, await buildQuotaCommandUnavailableMessage(quotaRequestContext));
1103
+ return await injectCommandOutputAndHandle(sessionID, await buildQuotaCommandUnavailableMessage(runtime));
848
1104
  }
849
1105
  return await injectCommandOutputAndHandle(sessionID, formatQuotaCommand({
850
1106
  ...reportData,
@@ -1087,8 +1343,10 @@ export const QuotaToastPlugin = async ({ client }) => {
1087
1343
  if (!configLoaded) {
1088
1344
  await refreshConfig();
1089
1345
  }
1090
- if (!config.enabled)
1346
+ if (!config.enabled) {
1347
+ clearDeferredQuotaRefresh(sessionID);
1091
1348
  return;
1349
+ }
1092
1350
  if (event.type === "session.idle" && config.showOnIdle) {
1093
1351
  await showQuotaToast(sessionID, "session.idle");
1094
1352
  }
@@ -1103,8 +1361,10 @@ export const QuotaToastPlugin = async ({ client }) => {
1103
1361
  if (!configLoaded) {
1104
1362
  await refreshConfig();
1105
1363
  }
1106
- if (!config.enabled)
1364
+ if (!config.enabled) {
1365
+ clearDeferredQuotaRefresh(input.sessionID);
1107
1366
  return;
1367
+ }
1108
1368
  if (isSuccessfulQuestionExecution(output)) {
1109
1369
  const sessionMeta = await getSessionModelMeta(input.sessionID);
1110
1370
  const model = sessionMeta.modelID;