@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.
- package/README.md +256 -561
- package/dist/lib/anthropic.js +1 -1
- package/dist/lib/anthropic.js.map +1 -1
- package/dist/lib/config-file-utils.d.ts +12 -0
- package/dist/lib/config-file-utils.d.ts.map +1 -1
- package/dist/lib/config-file-utils.js +23 -0
- package/dist/lib/config-file-utils.js.map +1 -1
- package/dist/lib/config.d.ts +16 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +434 -216
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot.d.ts.map +1 -1
- package/dist/lib/copilot.js +3 -2
- package/dist/lib/copilot.js.map +1 -1
- package/dist/lib/entries.d.ts +1 -1
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/format-utils.d.ts.map +1 -1
- package/dist/lib/format-utils.js +3 -2
- package/dist/lib/format-utils.js.map +1 -1
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +4 -2
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/google-gemini-cli-companion.d.ts +29 -0
- package/dist/lib/google-gemini-cli-companion.d.ts.map +1 -0
- package/dist/lib/google-gemini-cli-companion.js +166 -0
- package/dist/lib/google-gemini-cli-companion.js.map +1 -0
- package/dist/lib/google-gemini-cli.d.ts +48 -0
- package/dist/lib/google-gemini-cli.d.ts.map +1 -0
- package/dist/lib/google-gemini-cli.js +404 -0
- package/dist/lib/google-gemini-cli.js.map +1 -0
- package/dist/lib/opencode-go.js +1 -1
- package/dist/lib/opencode-go.js.map +1 -1
- package/dist/lib/provider-metadata.d.ts +1 -1
- package/dist/lib/provider-metadata.d.ts.map +1 -1
- package/dist/lib/provider-metadata.js +19 -0
- package/dist/lib/provider-metadata.js.map +1 -1
- package/dist/lib/quota-render-data.d.ts +2 -0
- package/dist/lib/quota-render-data.d.ts.map +1 -1
- package/dist/lib/quota-render-data.js +2 -0
- package/dist/lib/quota-render-data.js.map +1 -1
- package/dist/lib/quota-runtime-context.d.ts +43 -0
- package/dist/lib/quota-runtime-context.d.ts.map +1 -0
- package/dist/lib/quota-runtime-context.js +61 -0
- package/dist/lib/quota-runtime-context.js.map +1 -0
- package/dist/lib/quota-status.d.ts +16 -0
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +63 -17
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/toast-format-grouped.d.ts.map +1 -1
- package/dist/lib/toast-format-grouped.js +5 -3
- package/dist/lib/toast-format-grouped.js.map +1 -1
- package/dist/lib/tui-config-diagnostics.d.ts +7 -2
- package/dist/lib/tui-config-diagnostics.d.ts.map +1 -1
- package/dist/lib/tui-config-diagnostics.js +27 -8
- package/dist/lib/tui-config-diagnostics.js.map +1 -1
- package/dist/lib/tui-runtime.d.ts.map +1 -1
- package/dist/lib/tui-runtime.js +24 -16
- package/dist/lib/tui-runtime.js.map +1 -1
- package/dist/lib/types.d.ts +37 -6
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +419 -159
- package/dist/plugin.js.map +1 -1
- package/dist/providers/cursor.js +2 -2
- package/dist/providers/cursor.js.map +1 -1
- package/dist/providers/google-gemini-cli.d.ts +3 -0
- package/dist/providers/google-gemini-cli.d.ts.map +1 -0
- package/dist/providers/google-gemini-cli.js +83 -0
- package/dist/providers/google-gemini-cli.js.map +1 -0
- package/dist/providers/minimax-coding-plan.js +2 -2
- package/dist/providers/minimax-coding-plan.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +2 -0
- package/dist/providers/registry.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
233
|
-
|
|
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(
|
|
487
|
+
async function buildQuotaCommandUnavailableMessage(runtime) {
|
|
398
488
|
const selection = await resolveQuotaRenderSelection({
|
|
399
|
-
client:
|
|
400
|
-
config,
|
|
401
|
-
request:
|
|
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
|
-
|
|
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
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
602
|
+
const runtime = await resolvePluginRuntimeContext({
|
|
485
603
|
sessionID: params.sessionID,
|
|
486
|
-
sessionMeta:
|
|
487
|
-
|
|
488
|
-
|
|
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:
|
|
492
|
-
config,
|
|
610
|
+
client: runtime.client,
|
|
611
|
+
config: runtimeConfig,
|
|
493
612
|
request: quotaRequestContext,
|
|
494
613
|
surfaceExplicitProviderIssues: true,
|
|
495
|
-
formatStyle: resolveQuotaFormatStyle(
|
|
614
|
+
formatStyle: resolveQuotaFormatStyle(runtimeConfig.formatStyle),
|
|
615
|
+
bypassProviderCache: params.bypassProviderCache,
|
|
496
616
|
});
|
|
497
617
|
const { selection, availability, active, attemptedAny, hasExplicitProviderIssues, data } = quotaResult;
|
|
498
|
-
if (
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
649
|
+
if (hasQuotaRows) {
|
|
518
650
|
const formatted = formatQuotaRows({
|
|
519
651
|
version: "1.0.0",
|
|
520
|
-
layout:
|
|
521
|
-
entries: data
|
|
522
|
-
errors: data
|
|
523
|
-
style: resolveQuotaFormatStyle(
|
|
524
|
-
percentDisplayMode:
|
|
525
|
-
sessionTokens: data
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
|
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 ((
|
|
683
|
+
if ((runtimeConfig.showOnBothFail && attemptedAny && errors.length > 0) ||
|
|
684
|
+
hasExplicitProviderIssues) {
|
|
538
685
|
const errorLines = errors.map((error) => `${error.label}: ${error.message}`).join("\n");
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
"
|
|
543
|
-
|
|
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:
|
|
546
|
-
? "providers missing/unavailable"
|
|
547
|
-
: "all providers failed",
|
|
723
|
+
reason: "no entries",
|
|
548
724
|
currentModel,
|
|
549
|
-
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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(
|
|
885
|
+
async function fetchQuotaCommandData(runtime) {
|
|
886
|
+
const request = createQuotaRuntimeRequestContext(runtime);
|
|
627
887
|
const quotaResult = await collectQuotaRenderData({
|
|
628
|
-
client:
|
|
629
|
-
config,
|
|
630
|
-
request
|
|
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 &&
|
|
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
|
|
660
|
-
|
|
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 =
|
|
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 =
|
|
672
|
-
const providers =
|
|
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 :
|
|
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:
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
1089
|
+
const sessionMeta = sessionID ? await getSessionModelMeta(sessionID) : undefined;
|
|
1090
|
+
const runtime = await resolvePluginRuntimeContext({
|
|
836
1091
|
sessionID,
|
|
837
|
-
sessionMeta
|
|
838
|
-
|
|
839
|
-
|
|
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(
|
|
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;
|