@slkiser/opencode-quota 2.7.1 → 2.9.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 +192 -213
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +14 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/cursor-detection.d.ts.map +1 -1
- package/dist/lib/cursor-detection.js +49 -5
- package/dist/lib/cursor-detection.js.map +1 -1
- package/dist/lib/cursor-pricing.d.ts +1 -0
- package/dist/lib/cursor-pricing.d.ts.map +1 -1
- package/dist/lib/cursor-pricing.js +5 -3
- package/dist/lib/cursor-pricing.js.map +1 -1
- package/dist/lib/entries.d.ts +1 -0
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/jsonc.d.ts +8 -1
- package/dist/lib/jsonc.d.ts.map +1 -1
- package/dist/lib/jsonc.js +12 -2
- package/dist/lib/jsonc.js.map +1 -1
- package/dist/lib/modelsdev-pricing.d.ts +8 -2
- package/dist/lib/modelsdev-pricing.d.ts.map +1 -1
- package/dist/lib/modelsdev-pricing.js +84 -29
- package/dist/lib/modelsdev-pricing.js.map +1 -1
- package/dist/lib/quota-stats-format.d.ts.map +1 -1
- package/dist/lib/quota-stats-format.js +30 -38
- package/dist/lib/quota-stats-format.js.map +1 -1
- package/dist/lib/quota-status.d.ts +2 -1
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +10 -1
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/types.d.ts +14 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +4 -0
- package/dist/lib/types.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +300 -115
- package/dist/plugin.js.map +1 -1
- package/dist/providers/cursor.d.ts.map +1 -1
- package/dist/providers/cursor.js +3 -1
- package/dist/providers/cursor.js.map +1 -1
- package/package.json +4 -1
package/dist/plugin.js
CHANGED
|
@@ -16,13 +16,13 @@ import { aggregateUsage } from "./lib/quota-stats.js";
|
|
|
16
16
|
import { fetchSessionTokensForDisplay } from "./lib/session-tokens.js";
|
|
17
17
|
import { formatQuotaStatsReport } from "./lib/quota-stats-format.js";
|
|
18
18
|
import { buildQuotaStatusReport } from "./lib/quota-status.js";
|
|
19
|
-
import { maybeRefreshPricingSnapshot } from "./lib/modelsdev-pricing.js";
|
|
19
|
+
import { getPricingSnapshotMeta, getPricingSnapshotSource, getRuntimePricingRefreshStatePath, getRuntimePricingSnapshotPath, maybeRefreshPricingSnapshot, setPricingSnapshotAutoRefresh, setPricingSnapshotSelection, } from "./lib/modelsdev-pricing.js";
|
|
20
20
|
import { refreshGoogleTokensForAllAccounts } from "./lib/google.js";
|
|
21
21
|
import { getQuotaProviderDisplayLabel } from "./lib/provider-metadata.js";
|
|
22
22
|
import { DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS, isAlibabaModelId, resolveAlibabaCodingPlanAuthCached, } from "./lib/alibaba-auth.js";
|
|
23
23
|
import { isQwenCodeModelId, resolveQwenLocalPlanCached, } from "./lib/qwen-auth.js";
|
|
24
24
|
import { recordAlibabaCodingPlanCompletion, recordQwenCompletion, } from "./lib/qwen-local-quota.js";
|
|
25
|
-
import { isCursorModelId } from "./lib/cursor-pricing.js";
|
|
25
|
+
import { isCursorModelId, isCursorProviderId } from "./lib/cursor-pricing.js";
|
|
26
26
|
import { parseOptionalJsonArgs, parseQuotaBetweenArgs, startOfLocalDayMs, startOfNextLocalDayMs, formatYmd, } from "./lib/command-parsing.js";
|
|
27
27
|
import { handled } from "./lib/command-handled.js";
|
|
28
28
|
import { renderCommandHeading } from "./lib/format-utils.js";
|
|
@@ -153,20 +153,45 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
153
153
|
let lastSessionTokenError;
|
|
154
154
|
const providerFetchCache = new Map();
|
|
155
155
|
function getQuotaCommandCache() {
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
globalThis.__opencodeQuotaCommandCache = quotaCache;
|
|
156
|
+
const existing = globalThis.__opencodeQuotaCommandCache;
|
|
157
|
+
if (existing instanceof Map) {
|
|
158
|
+
return existing;
|
|
160
159
|
}
|
|
160
|
+
const quotaCache = new Map();
|
|
161
|
+
globalThis.__opencodeQuotaCommandCache = quotaCache;
|
|
161
162
|
return quotaCache;
|
|
162
163
|
}
|
|
163
164
|
function clearQuotaCommandCache() {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
getQuotaCommandCache().clear();
|
|
166
|
+
}
|
|
167
|
+
function buildQuotaCommandCacheKey(params) {
|
|
168
|
+
const enabledProviders = config.enabledProviders === "auto" ? "auto" : config.enabledProviders.join(",");
|
|
169
|
+
const googleModels = config.googleModels.join(",");
|
|
170
|
+
const currentModel = config.onlyCurrentModel && params.sessionID ? params.sessionMeta?.modelID ?? "" : "";
|
|
171
|
+
const currentProviderID = config.onlyCurrentModel && params.sessionID ? params.sessionMeta?.providerID ?? "" : "";
|
|
172
|
+
return [
|
|
173
|
+
`sessionID=${params.sessionID ?? ""}`,
|
|
174
|
+
`showSessionTokens=${config.showSessionTokens ? "yes" : "no"}`,
|
|
175
|
+
`onlyCurrentModel=${config.onlyCurrentModel ? "yes" : "no"}`,
|
|
176
|
+
`enabledProviders=${enabledProviders}`,
|
|
177
|
+
`googleModels=${googleModels}`,
|
|
178
|
+
`alibabaTier=${config.alibabaCodingPlanTier}`,
|
|
179
|
+
`cursorPlan=${config.cursorPlan}`,
|
|
180
|
+
`cursorIncludedApiUsd=${config.cursorIncludedApiUsd ?? ""}`,
|
|
181
|
+
`cursorBillingCycleStartDay=${config.cursorBillingCycleStartDay ?? ""}`,
|
|
182
|
+
`currentModel=${currentModel}`,
|
|
183
|
+
`currentProviderID=${currentProviderID}`,
|
|
184
|
+
].join("|");
|
|
185
|
+
}
|
|
186
|
+
function pruneQuotaCommandCache(ttlMs, nowMs) {
|
|
187
|
+
const quotaCache = getQuotaCommandCache();
|
|
188
|
+
for (const [cacheKey, entry] of quotaCache.entries()) {
|
|
189
|
+
if (entry.inFlight)
|
|
190
|
+
continue;
|
|
191
|
+
if (entry.timestamp <= 0 || ttlMs <= 0 || nowMs - entry.timestamp >= ttlMs) {
|
|
192
|
+
quotaCache.delete(cacheKey);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
170
195
|
}
|
|
171
196
|
function asRecord(value) {
|
|
172
197
|
return value && typeof value === "object" ? value : null;
|
|
@@ -216,7 +241,8 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
216
241
|
const cursorBillingCycleStartDay = ctx.config.cursorBillingCycleStartDay ?? "";
|
|
217
242
|
const onlyCurrentModel = ctx.config.onlyCurrentModel ? "yes" : "no";
|
|
218
243
|
const currentModel = ctx.config.currentModel ?? "";
|
|
219
|
-
|
|
244
|
+
const currentProviderID = ctx.config.currentProviderID ?? "";
|
|
245
|
+
return `${providerId}|style=${style}|googleModels=${googleModels}|alibabaTier=${alibabaCodingPlanTier}|cursorPlan=${cursorPlan}|cursorIncludedApiUsd=${cursorIncludedApiUsd}|cursorBillingCycleStartDay=${cursorBillingCycleStartDay}|onlyCurrentModel=${onlyCurrentModel}|currentModel=${currentModel}|currentProviderID=${currentProviderID}`;
|
|
220
246
|
}
|
|
221
247
|
async function fetchProviderWithCache(params) {
|
|
222
248
|
const { provider, ctx, ttlMs } = params;
|
|
@@ -290,10 +316,12 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
290
316
|
function isProviderEnabled(providerId) {
|
|
291
317
|
return config.enabledProviders === "auto" || config.enabledProviders.includes(providerId);
|
|
292
318
|
}
|
|
293
|
-
async function shouldBypassToastCacheForLiveLocalUsage(
|
|
319
|
+
async function shouldBypassToastCacheForLiveLocalUsage(params) {
|
|
320
|
+
const { trigger, sessionID } = params;
|
|
294
321
|
if (trigger !== "question")
|
|
295
322
|
return false;
|
|
296
|
-
const
|
|
323
|
+
const currentSession = params.sessionMeta ?? (await getSessionModelMeta(sessionID));
|
|
324
|
+
const currentModel = currentSession.modelID;
|
|
297
325
|
if (isQwenCodeModelId(currentModel)) {
|
|
298
326
|
const plan = await resolveQwenLocalPlanCached();
|
|
299
327
|
return plan.state === "qwen_free" && isProviderEnabled("qwen-code");
|
|
@@ -305,15 +333,19 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
305
333
|
});
|
|
306
334
|
return plan.state === "configured" && isProviderEnabled("alibaba-coding-plan");
|
|
307
335
|
}
|
|
308
|
-
if (isCursorModelId(currentModel)) {
|
|
336
|
+
if (isCursorProviderId(currentSession.providerID) || isCursorModelId(currentModel)) {
|
|
309
337
|
return isProviderEnabled("cursor");
|
|
310
338
|
}
|
|
311
339
|
return false;
|
|
312
340
|
}
|
|
313
|
-
async function shouldBypassQuotaCommandCache(sessionID) {
|
|
341
|
+
async function shouldBypassQuotaCommandCache(sessionID, sessionMeta) {
|
|
314
342
|
if (config.debug || !sessionID)
|
|
315
343
|
return config.debug;
|
|
316
|
-
return await shouldBypassToastCacheForLiveLocalUsage(
|
|
344
|
+
return await shouldBypassToastCacheForLiveLocalUsage({
|
|
345
|
+
trigger: "question",
|
|
346
|
+
sessionID,
|
|
347
|
+
sessionMeta,
|
|
348
|
+
});
|
|
317
349
|
}
|
|
318
350
|
async function refreshConfig() {
|
|
319
351
|
if (configInFlight)
|
|
@@ -322,11 +354,15 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
322
354
|
try {
|
|
323
355
|
configMeta = createLoadConfigMeta();
|
|
324
356
|
config = await loadConfig(typedClient, configMeta);
|
|
357
|
+
setPricingSnapshotAutoRefresh(config.pricingSnapshot.autoRefresh);
|
|
358
|
+
setPricingSnapshotSelection(config.pricingSnapshot.source);
|
|
325
359
|
configLoaded = true;
|
|
326
360
|
}
|
|
327
361
|
catch {
|
|
328
362
|
// Leave configLoaded=false so we can retry on next trigger.
|
|
329
363
|
config = DEFAULT_CONFIG;
|
|
364
|
+
setPricingSnapshotAutoRefresh(DEFAULT_CONFIG.pricingSnapshot.autoRefresh);
|
|
365
|
+
setPricingSnapshotSelection(DEFAULT_CONFIG.pricingSnapshot.source);
|
|
330
366
|
}
|
|
331
367
|
finally {
|
|
332
368
|
configInFlight = null;
|
|
@@ -336,7 +372,10 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
336
372
|
}
|
|
337
373
|
async function kickPricingRefresh(params) {
|
|
338
374
|
try {
|
|
339
|
-
const refreshPromise = maybeRefreshPricingSnapshot({
|
|
375
|
+
const refreshPromise = maybeRefreshPricingSnapshot({
|
|
376
|
+
reason: params.reason,
|
|
377
|
+
snapshotSelection: config.pricingSnapshot.source,
|
|
378
|
+
});
|
|
340
379
|
const guardedRefreshPromise = refreshPromise.catch(() => undefined);
|
|
341
380
|
if (!params.maxWaitMs || params.maxWaitMs <= 0) {
|
|
342
381
|
void guardedRefreshPromise;
|
|
@@ -378,6 +417,8 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
378
417
|
cursorPlan: config.cursorPlan,
|
|
379
418
|
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
380
419
|
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
420
|
+
pricingSnapshotSource: config.pricingSnapshot.source,
|
|
421
|
+
pricingSnapshotAutoRefresh: config.pricingSnapshot.autoRefresh,
|
|
381
422
|
showOnIdle: config.showOnIdle,
|
|
382
423
|
showOnQuestion: config.showOnQuestion,
|
|
383
424
|
showOnCompact: config.showOnCompact,
|
|
@@ -425,22 +466,38 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
425
466
|
}
|
|
426
467
|
}
|
|
427
468
|
/**
|
|
428
|
-
* Get the current model from the active session.
|
|
469
|
+
* Get the current model metadata from the active session.
|
|
429
470
|
*
|
|
430
471
|
* Only uses session-scoped model lookup. Does NOT fall back to
|
|
431
472
|
* client.config.get() because that returns the global/default model
|
|
432
473
|
* which can be stale across sessions.
|
|
433
474
|
*/
|
|
434
|
-
async function
|
|
475
|
+
async function getSessionModelMeta(sessionID) {
|
|
435
476
|
if (!sessionID)
|
|
436
|
-
return
|
|
477
|
+
return {};
|
|
437
478
|
try {
|
|
438
479
|
const sessionResp = await typedClient.session.get({ path: { id: sessionID } });
|
|
439
|
-
return
|
|
480
|
+
return {
|
|
481
|
+
modelID: sessionResp.data?.modelID,
|
|
482
|
+
providerID: sessionResp.data?.providerID,
|
|
483
|
+
};
|
|
440
484
|
}
|
|
441
485
|
catch {
|
|
486
|
+
return {};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function getCurrentModel(sessionID) {
|
|
490
|
+
if (!sessionID)
|
|
442
491
|
return undefined;
|
|
492
|
+
return (await getSessionModelMeta(sessionID)).modelID;
|
|
493
|
+
}
|
|
494
|
+
function matchesProviderCurrentSelection(params) {
|
|
495
|
+
if (params.provider.id === "cursor" && isCursorProviderId(params.currentProviderID)) {
|
|
496
|
+
return true;
|
|
443
497
|
}
|
|
498
|
+
if (!params.currentModel)
|
|
499
|
+
return false;
|
|
500
|
+
return params.provider.matchesCurrentModel ? params.provider.matchesCurrentModel(params.currentModel) : true;
|
|
444
501
|
}
|
|
445
502
|
function formatDebugInfo(params) {
|
|
446
503
|
const availability = params.availability
|
|
@@ -461,6 +518,99 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
461
518
|
`available=${availability}`,
|
|
462
519
|
].join("\n");
|
|
463
520
|
}
|
|
521
|
+
async function resolveQuotaCommandSelection(params = {}) {
|
|
522
|
+
if (!configLoaded)
|
|
523
|
+
await refreshConfig();
|
|
524
|
+
if (!config.enabled)
|
|
525
|
+
return null;
|
|
526
|
+
const allProviders = getProviders();
|
|
527
|
+
const isAutoMode = config.enabledProviders === "auto";
|
|
528
|
+
const providers = isAutoMode
|
|
529
|
+
? allProviders
|
|
530
|
+
: allProviders.filter((p) => config.enabledProviders.includes(p.id));
|
|
531
|
+
if (!isAutoMode && providers.length === 0)
|
|
532
|
+
return null;
|
|
533
|
+
let currentModel;
|
|
534
|
+
let currentProviderID;
|
|
535
|
+
if (config.onlyCurrentModel && params.sessionID) {
|
|
536
|
+
const currentSession = params.sessionMeta ?? (await getSessionModelMeta(params.sessionID));
|
|
537
|
+
currentModel = currentSession.modelID;
|
|
538
|
+
currentProviderID = currentSession.providerID;
|
|
539
|
+
}
|
|
540
|
+
const ctx = {
|
|
541
|
+
client: typedClient,
|
|
542
|
+
config: {
|
|
543
|
+
googleModels: config.googleModels,
|
|
544
|
+
alibabaCodingPlanTier: config.alibabaCodingPlanTier,
|
|
545
|
+
cursorPlan: config.cursorPlan,
|
|
546
|
+
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
547
|
+
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
548
|
+
// Always format /quota in grouped mode for a more dashboard-like look.
|
|
549
|
+
toastStyle: "grouped",
|
|
550
|
+
onlyCurrentModel: config.onlyCurrentModel,
|
|
551
|
+
currentModel,
|
|
552
|
+
currentProviderID,
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
const filteringByCurrentSelection = config.onlyCurrentModel && Boolean(currentModel || isCursorProviderId(currentProviderID));
|
|
556
|
+
const filtered = filteringByCurrentSelection
|
|
557
|
+
? providers.filter((p) => matchesProviderCurrentSelection({ provider: p, currentModel, currentProviderID }))
|
|
558
|
+
: providers;
|
|
559
|
+
return {
|
|
560
|
+
isAutoMode,
|
|
561
|
+
providers,
|
|
562
|
+
filtered,
|
|
563
|
+
ctx,
|
|
564
|
+
currentModel,
|
|
565
|
+
currentProviderID,
|
|
566
|
+
filteringByCurrentSelection,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function describeQuotaCommandCurrentSelection(params) {
|
|
570
|
+
if (isCursorProviderId(params.currentProviderID)) {
|
|
571
|
+
return `current provider: ${params.currentProviderID}`;
|
|
572
|
+
}
|
|
573
|
+
if (params.currentModel) {
|
|
574
|
+
return `current model: ${params.currentModel}`;
|
|
575
|
+
}
|
|
576
|
+
return "current session";
|
|
577
|
+
}
|
|
578
|
+
async function buildQuotaCommandUnavailableMessage(params = {}) {
|
|
579
|
+
const selection = await resolveQuotaCommandSelection(params);
|
|
580
|
+
if (!selection) {
|
|
581
|
+
return "Quota unavailable\n\nNo enabled quota providers are configured.\n\nRun /quota_status for diagnostics.";
|
|
582
|
+
}
|
|
583
|
+
if (selection.filteringByCurrentSelection && selection.filtered.length === 0) {
|
|
584
|
+
const detail = describeQuotaCommandCurrentSelection({
|
|
585
|
+
currentModel: selection.currentModel,
|
|
586
|
+
currentProviderID: selection.currentProviderID,
|
|
587
|
+
});
|
|
588
|
+
return `Quota unavailable\n\nNo enabled quota providers matched the ${detail}.\n\nRun /quota_status for diagnostics.`;
|
|
589
|
+
}
|
|
590
|
+
const avail = await Promise.all(selection.filtered.map(async (p) => {
|
|
591
|
+
try {
|
|
592
|
+
return { id: p.id, ok: await p.isAvailable(selection.ctx) };
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return { id: p.id, ok: false };
|
|
596
|
+
}
|
|
597
|
+
}));
|
|
598
|
+
const availableIds = avail.filter((x) => x.ok).map((x) => x.id);
|
|
599
|
+
if (availableIds.length === 0) {
|
|
600
|
+
const scopedDetail = selection.filteringByCurrentSelection
|
|
601
|
+
? ` for the ${describeQuotaCommandCurrentSelection({
|
|
602
|
+
currentModel: selection.currentModel,
|
|
603
|
+
currentProviderID: selection.currentProviderID,
|
|
604
|
+
})}`
|
|
605
|
+
: "";
|
|
606
|
+
return (`Quota unavailable\n\nNo quota providers detected${scopedDetail}. ` +
|
|
607
|
+
"Make sure you are logged in to a supported provider (Copilot, OpenAI, etc.).\n\n" +
|
|
608
|
+
"Run /quota_status for diagnostics.");
|
|
609
|
+
}
|
|
610
|
+
return (`Quota unavailable\n\nProviders detected (${availableIds.join(", ")}) but returned no data. ` +
|
|
611
|
+
"This may be a temporary API error.\n\n" +
|
|
612
|
+
"Run /quota_status for diagnostics.");
|
|
613
|
+
}
|
|
464
614
|
async function fetchQuotaMessage(trigger, sessionID) {
|
|
465
615
|
// Ensure we have loaded config at least once. If load fails, we keep trying
|
|
466
616
|
// on subsequent triggers.
|
|
@@ -487,8 +637,11 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
487
637
|
: null;
|
|
488
638
|
}
|
|
489
639
|
let currentModel;
|
|
640
|
+
let currentProviderID;
|
|
490
641
|
if (config.onlyCurrentModel) {
|
|
491
|
-
|
|
642
|
+
const currentSession = await getSessionModelMeta(sessionID);
|
|
643
|
+
currentModel = currentSession.modelID;
|
|
644
|
+
currentProviderID = currentSession.providerID;
|
|
492
645
|
}
|
|
493
646
|
const ctx = {
|
|
494
647
|
client: typedClient,
|
|
@@ -501,10 +654,11 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
501
654
|
toastStyle: config.toastStyle,
|
|
502
655
|
onlyCurrentModel: config.onlyCurrentModel,
|
|
503
656
|
currentModel,
|
|
657
|
+
currentProviderID,
|
|
504
658
|
},
|
|
505
659
|
};
|
|
506
|
-
const filtered = config.onlyCurrentModel && currentModel
|
|
507
|
-
? providers.filter((p) =>
|
|
660
|
+
const filtered = config.onlyCurrentModel && (currentModel || isCursorProviderId(currentProviderID))
|
|
661
|
+
? providers.filter((p) => matchesProviderCurrentSelection({ provider: p, currentModel, currentProviderID }))
|
|
508
662
|
: providers;
|
|
509
663
|
// availability checks are cheap, do them in parallel
|
|
510
664
|
const avail = await Promise.all(filtered.map(async (p) => ({ p, ok: await p.isAvailable(ctx) })));
|
|
@@ -650,7 +804,7 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
650
804
|
}
|
|
651
805
|
const bypassMessageCache = config.debug
|
|
652
806
|
? true
|
|
653
|
-
: await shouldBypassToastCacheForLiveLocalUsage(trigger, sessionID);
|
|
807
|
+
: await shouldBypassToastCacheForLiveLocalUsage({ trigger, sessionID });
|
|
654
808
|
const message = bypassMessageCache
|
|
655
809
|
? await fetchQuotaMessage(trigger, sessionID)
|
|
656
810
|
: await getOrFetchWithCacheControl(async () => {
|
|
@@ -683,37 +837,12 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
683
837
|
});
|
|
684
838
|
}
|
|
685
839
|
}
|
|
686
|
-
async function fetchQuotaCommandBody(trigger,
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (!config.enabled)
|
|
690
|
-
return null;
|
|
691
|
-
const allProviders = getProviders();
|
|
692
|
-
const isAutoMode = config.enabledProviders === "auto";
|
|
693
|
-
const providers = isAutoMode
|
|
694
|
-
? allProviders
|
|
695
|
-
: allProviders.filter((p) => config.enabledProviders.includes(p.id));
|
|
696
|
-
if (!isAutoMode && providers.length === 0)
|
|
840
|
+
async function fetchQuotaCommandBody(trigger, params = {}) {
|
|
841
|
+
const selection = await resolveQuotaCommandSelection(params);
|
|
842
|
+
if (!selection)
|
|
697
843
|
return null;
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
currentModel = await getCurrentModel(sessionID);
|
|
701
|
-
}
|
|
702
|
-
const ctx = {
|
|
703
|
-
client: typedClient,
|
|
704
|
-
config: {
|
|
705
|
-
googleModels: config.googleModels,
|
|
706
|
-
alibabaCodingPlanTier: config.alibabaCodingPlanTier,
|
|
707
|
-
cursorPlan: config.cursorPlan,
|
|
708
|
-
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
709
|
-
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
710
|
-
// Always format /quota in grouped mode for a more dashboard-like look.
|
|
711
|
-
toastStyle: "grouped",
|
|
712
|
-
onlyCurrentModel: config.onlyCurrentModel,
|
|
713
|
-
currentModel,
|
|
714
|
-
},
|
|
715
|
-
};
|
|
716
|
-
const avail = await Promise.all(providers.map(async (p) => ({ p, ok: await p.isAvailable(ctx) })));
|
|
844
|
+
const { isAutoMode, ctx } = selection;
|
|
845
|
+
const avail = await Promise.all(selection.filtered.map(async (p) => ({ p, ok: await p.isAvailable(ctx) })));
|
|
717
846
|
const active = avail.filter((x) => x.ok).map((x) => x.p);
|
|
718
847
|
if (active.length === 0)
|
|
719
848
|
return null;
|
|
@@ -738,10 +867,10 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
738
867
|
}
|
|
739
868
|
// Fetch session tokens if enabled and sessionID is available
|
|
740
869
|
let sessionTokens;
|
|
741
|
-
if (config.showSessionTokens && sessionID) {
|
|
870
|
+
if (config.showSessionTokens && params.sessionID) {
|
|
742
871
|
const stResult = await fetchSessionTokensForDisplay({
|
|
743
872
|
enabled: config.showSessionTokens,
|
|
744
|
-
sessionID,
|
|
873
|
+
sessionID: params.sessionID,
|
|
745
874
|
});
|
|
746
875
|
sessionTokens = stResult.sessionTokens;
|
|
747
876
|
// Update diagnostics state: clear on success (no error returned), set on failure
|
|
@@ -775,7 +904,9 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
775
904
|
if (!config.enabled)
|
|
776
905
|
return null;
|
|
777
906
|
await kickPricingRefresh({ reason: "status", maxWaitMs: 750 });
|
|
778
|
-
const
|
|
907
|
+
const currentSession = await getSessionModelMeta(params.sessionID);
|
|
908
|
+
const currentModel = currentSession.modelID;
|
|
909
|
+
const currentProviderID = currentSession.providerID;
|
|
779
910
|
const sessionModelLookup = !params.sessionID
|
|
780
911
|
? "no_session"
|
|
781
912
|
: currentModel
|
|
@@ -795,6 +926,7 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
795
926
|
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
796
927
|
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
797
928
|
currentModel,
|
|
929
|
+
currentProviderID,
|
|
798
930
|
},
|
|
799
931
|
});
|
|
800
932
|
}
|
|
@@ -806,8 +938,8 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
806
938
|
// In auto mode, a provider is effectively "enabled" if it's available.
|
|
807
939
|
enabled: isAutoMode ? ok : config.enabledProviders.includes(p.id),
|
|
808
940
|
available: ok,
|
|
809
|
-
matchesCurrentModel:
|
|
810
|
-
? p
|
|
941
|
+
matchesCurrentModel: currentModel || isCursorProviderId(currentProviderID)
|
|
942
|
+
? matchesProviderCurrentSelection({ provider: p, currentModel, currentProviderID })
|
|
811
943
|
: undefined,
|
|
812
944
|
};
|
|
813
945
|
}));
|
|
@@ -822,6 +954,7 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
822
954
|
cursorPlan: config.cursorPlan,
|
|
823
955
|
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
824
956
|
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
957
|
+
pricingSnapshotSource: config.pricingSnapshot.source,
|
|
825
958
|
onlyCurrentModel: config.onlyCurrentModel,
|
|
826
959
|
currentModel,
|
|
827
960
|
sessionModelLookup,
|
|
@@ -838,6 +971,42 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
838
971
|
generatedAtMs: params.generatedAtMs,
|
|
839
972
|
});
|
|
840
973
|
}
|
|
974
|
+
function formatIsoTimestamp(timestampMs) {
|
|
975
|
+
return typeof timestampMs === "number" && Number.isFinite(timestampMs) && timestampMs > 0
|
|
976
|
+
? new Date(timestampMs).toISOString()
|
|
977
|
+
: "(none)";
|
|
978
|
+
}
|
|
979
|
+
function buildPricingRefreshCommandOutput(params) {
|
|
980
|
+
const meta = getPricingSnapshotMeta();
|
|
981
|
+
const activeSource = getPricingSnapshotSource();
|
|
982
|
+
const configuredSelection = config.pricingSnapshot.source;
|
|
983
|
+
const resultLabel = params.result.reason ??
|
|
984
|
+
params.result.state.lastResult ??
|
|
985
|
+
(params.result.updated ? "success" : "unknown");
|
|
986
|
+
const lines = [
|
|
987
|
+
renderCommandHeading({
|
|
988
|
+
title: "Pricing Refresh (/pricing_refresh)",
|
|
989
|
+
generatedAtMs: params.generatedAtMs,
|
|
990
|
+
}),
|
|
991
|
+
"",
|
|
992
|
+
"refresh:",
|
|
993
|
+
`- attempted: ${params.result.attempted ? "true" : "false"}`,
|
|
994
|
+
`- result: ${resultLabel}`,
|
|
995
|
+
`- runtime_snapshot_persisted: ${params.result.updated ? "true" : "false"}`,
|
|
996
|
+
];
|
|
997
|
+
if (params.result.error) {
|
|
998
|
+
lines.push(`- error: ${params.result.error}`);
|
|
999
|
+
}
|
|
1000
|
+
lines.push("");
|
|
1001
|
+
lines.push("pricing_snapshot:");
|
|
1002
|
+
lines.push(`- selection: configured=${configuredSelection} active=${activeSource}`);
|
|
1003
|
+
lines.push(`- active_snapshot: source=${meta.source} generated_at=${formatIsoTimestamp(meta.generatedAt)} units=${meta.units}`);
|
|
1004
|
+
lines.push(`- runtime_paths: snapshot=${getRuntimePricingSnapshotPath()} refresh_state=${getRuntimePricingRefreshStatePath()}`);
|
|
1005
|
+
if (configuredSelection === "bundled" && params.result.updated) {
|
|
1006
|
+
lines.push("- selection_note: runtime snapshot refreshed locally, but active reports remain pinned to bundled pricing");
|
|
1007
|
+
}
|
|
1008
|
+
return lines.join("\n");
|
|
1009
|
+
}
|
|
841
1010
|
// Return hook implementations
|
|
842
1011
|
return {
|
|
843
1012
|
// Register built-in slash commands (in addition to /tool quota_*)
|
|
@@ -853,6 +1022,10 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
853
1022
|
template: "/quota_status",
|
|
854
1023
|
description: "Diagnostics for toast + pricing + local storage (includes unknown pricing report).",
|
|
855
1024
|
};
|
|
1025
|
+
cfg.command["pricing_refresh"] = {
|
|
1026
|
+
template: "/pricing_refresh",
|
|
1027
|
+
description: "Refresh the local runtime pricing snapshot from models.dev.",
|
|
1028
|
+
};
|
|
856
1029
|
// Register token report commands (/tokens_*)
|
|
857
1030
|
for (const spec of TOKEN_REPORT_COMMANDS) {
|
|
858
1031
|
cfg.command[spec.id] = {
|
|
@@ -865,7 +1038,10 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
865
1038
|
try {
|
|
866
1039
|
const cmd = input.command;
|
|
867
1040
|
const sessionID = input.sessionID;
|
|
868
|
-
const isQuotaCommand = cmd === "quota" ||
|
|
1041
|
+
const isQuotaCommand = cmd === "quota" ||
|
|
1042
|
+
cmd === "quota_status" ||
|
|
1043
|
+
cmd === "pricing_refresh" ||
|
|
1044
|
+
isTokenReportCommand(cmd);
|
|
869
1045
|
if (isQuotaCommand && !configLoaded) {
|
|
870
1046
|
await refreshConfig();
|
|
871
1047
|
}
|
|
@@ -874,30 +1050,45 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
874
1050
|
}
|
|
875
1051
|
if (cmd === "quota") {
|
|
876
1052
|
const generatedAtMs = Date.now();
|
|
877
|
-
// Separate cache for /quota so it doesn't pollute the toast cache.
|
|
878
|
-
const quotaCache = getQuotaCommandCache();
|
|
879
1053
|
const now = generatedAtMs;
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
(
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1054
|
+
const quotaRequestContext = {
|
|
1055
|
+
sessionID,
|
|
1056
|
+
sessionMeta: sessionID ? await getSessionModelMeta(sessionID) : undefined,
|
|
1057
|
+
};
|
|
1058
|
+
const bypassCommandCache = await shouldBypassQuotaCommandCache(sessionID, quotaRequestContext.sessionMeta);
|
|
1059
|
+
const body = bypassCommandCache
|
|
1060
|
+
? await fetchQuotaCommandBody("command:/quota", quotaRequestContext)
|
|
1061
|
+
: await (async () => {
|
|
1062
|
+
const quotaCache = getQuotaCommandCache();
|
|
1063
|
+
pruneQuotaCommandCache(config.minIntervalMs, now);
|
|
1064
|
+
const cacheKey = buildQuotaCommandCacheKey(quotaRequestContext);
|
|
1065
|
+
const cachedEntry = quotaCache.get(cacheKey);
|
|
1066
|
+
if (cachedEntry?.timestamp &&
|
|
1067
|
+
now - cachedEntry.timestamp < config.minIntervalMs) {
|
|
1068
|
+
return cachedEntry.body;
|
|
1069
|
+
}
|
|
1070
|
+
const cacheEntry = cachedEntry ?? { body: "", timestamp: 0 };
|
|
1071
|
+
if (!cachedEntry) {
|
|
1072
|
+
quotaCache.set(cacheKey, cacheEntry);
|
|
1073
|
+
}
|
|
1074
|
+
return await (cacheEntry.inFlight ??
|
|
1075
|
+
(cacheEntry.inFlight = (async () => {
|
|
1076
|
+
try {
|
|
1077
|
+
const freshBody = await fetchQuotaCommandBody("command:/quota", quotaRequestContext);
|
|
1078
|
+
if (freshBody) {
|
|
1079
|
+
cacheEntry.body = freshBody;
|
|
1080
|
+
cacheEntry.timestamp = Date.now();
|
|
1081
|
+
}
|
|
1082
|
+
return freshBody;
|
|
1083
|
+
}
|
|
1084
|
+
finally {
|
|
1085
|
+
cacheEntry.inFlight = undefined;
|
|
1086
|
+
if (!cacheEntry.body && cacheEntry.timestamp <= 0) {
|
|
1087
|
+
quotaCache.delete(cacheKey);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
})()));
|
|
1091
|
+
})();
|
|
901
1092
|
if (!body) {
|
|
902
1093
|
// Provide an actionable message instead of a generic "unavailable".
|
|
903
1094
|
if (!configLoaded) {
|
|
@@ -907,33 +1098,7 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
907
1098
|
await injectRawOutput(sessionID, "Quota disabled in config (enabled: false)");
|
|
908
1099
|
}
|
|
909
1100
|
else {
|
|
910
|
-
|
|
911
|
-
const allProvs = getProviders();
|
|
912
|
-
const ctx = {
|
|
913
|
-
client: typedClient,
|
|
914
|
-
config: {
|
|
915
|
-
googleModels: config.googleModels,
|
|
916
|
-
alibabaCodingPlanTier: config.alibabaCodingPlanTier,
|
|
917
|
-
cursorPlan: config.cursorPlan,
|
|
918
|
-
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
919
|
-
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
920
|
-
},
|
|
921
|
-
};
|
|
922
|
-
const avail = await Promise.all(allProvs.map(async (p) => {
|
|
923
|
-
try {
|
|
924
|
-
return { id: p.id, ok: await p.isAvailable(ctx) };
|
|
925
|
-
}
|
|
926
|
-
catch {
|
|
927
|
-
return { id: p.id, ok: false };
|
|
928
|
-
}
|
|
929
|
-
}));
|
|
930
|
-
const availableIds = avail.filter((x) => x.ok).map((x) => x.id);
|
|
931
|
-
if (availableIds.length === 0) {
|
|
932
|
-
await injectRawOutput(sessionID, "Quota unavailable\n\nNo quota providers detected. Make sure you are logged in to a supported provider (Copilot, OpenAI, etc.).\n\nRun /quota_status for diagnostics.");
|
|
933
|
-
}
|
|
934
|
-
else {
|
|
935
|
-
await injectRawOutput(sessionID, `Quota unavailable\n\nProviders detected (${availableIds.join(", ")}) but returned no data. This may be a temporary API error.\n\nRun /quota_status for diagnostics.`);
|
|
936
|
-
}
|
|
1101
|
+
await injectRawOutput(sessionID, await buildQuotaCommandUnavailableMessage(quotaRequestContext));
|
|
937
1102
|
}
|
|
938
1103
|
handled();
|
|
939
1104
|
}
|
|
@@ -944,6 +1109,24 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
944
1109
|
await injectRawOutput(sessionID, `${heading}\n\n${body}`);
|
|
945
1110
|
handled();
|
|
946
1111
|
}
|
|
1112
|
+
if (cmd === "pricing_refresh") {
|
|
1113
|
+
const generatedAtMs = Date.now();
|
|
1114
|
+
if ((input.arguments ?? "").trim()) {
|
|
1115
|
+
await injectRawOutput(sessionID, "Invalid arguments for /pricing_refresh\n\nThis command does not accept arguments.\n\nUsage:\n/pricing_refresh");
|
|
1116
|
+
handled();
|
|
1117
|
+
}
|
|
1118
|
+
const result = await maybeRefreshPricingSnapshot({
|
|
1119
|
+
reason: "manual",
|
|
1120
|
+
force: true,
|
|
1121
|
+
snapshotSelection: config.pricingSnapshot.source,
|
|
1122
|
+
allowRefreshWhenSelectionBundled: true,
|
|
1123
|
+
});
|
|
1124
|
+
await injectRawOutput(sessionID, buildPricingRefreshCommandOutput({
|
|
1125
|
+
result,
|
|
1126
|
+
generatedAtMs,
|
|
1127
|
+
}));
|
|
1128
|
+
handled();
|
|
1129
|
+
}
|
|
947
1130
|
const untilMs = Date.now();
|
|
948
1131
|
// Handle token report commands (/tokens_*)
|
|
949
1132
|
if (isTokenReportCommand(cmd)) {
|
|
@@ -1095,7 +1278,8 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
1095
1278
|
if (!config.enabled)
|
|
1096
1279
|
return;
|
|
1097
1280
|
if (isSuccessfulQuestionExecution(output)) {
|
|
1098
|
-
const
|
|
1281
|
+
const sessionMeta = await getSessionModelMeta(input.sessionID);
|
|
1282
|
+
const model = sessionMeta.modelID;
|
|
1099
1283
|
try {
|
|
1100
1284
|
if (isQwenCodeModelId(model)) {
|
|
1101
1285
|
const plan = await resolveQwenLocalPlanCached();
|
|
@@ -1114,7 +1298,7 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
1114
1298
|
clearQuotaCommandCache();
|
|
1115
1299
|
}
|
|
1116
1300
|
}
|
|
1117
|
-
else if (isCursorModelId(model)) {
|
|
1301
|
+
else if (isCursorProviderId(sessionMeta.providerID) || isCursorModelId(model)) {
|
|
1118
1302
|
clearQuotaCommandCache();
|
|
1119
1303
|
}
|
|
1120
1304
|
}
|
|
@@ -1122,6 +1306,7 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
1122
1306
|
await log("Failed to record local request-plan quota completion", {
|
|
1123
1307
|
error: err instanceof Error ? err.message : String(err),
|
|
1124
1308
|
model,
|
|
1309
|
+
providerID: sessionMeta.providerID,
|
|
1125
1310
|
});
|
|
1126
1311
|
}
|
|
1127
1312
|
}
|