@slkiser/opencode-quota 2.7.1 → 2.9.1

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