@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.
- package/README.md +182 -239
- 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/api-key-resolver.d.ts +11 -0
- package/dist/lib/api-key-resolver.d.ts.map +1 -1
- package/dist/lib/api-key-resolver.js +17 -2
- package/dist/lib/api-key-resolver.js.map +1 -1
- package/dist/lib/chutes-config.d.ts +3 -3
- package/dist/lib/chutes-config.d.ts.map +1 -1
- package/dist/lib/chutes-config.js +9 -6
- package/dist/lib/chutes-config.js.map +1 -1
- package/dist/lib/chutes.d.ts.map +1 -1
- package/dist/lib/chutes.js +3 -2
- package/dist/lib/chutes.js.map +1 -1
- package/dist/lib/config.d.ts +7 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +80 -28
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot.d.ts.map +1 -1
- package/dist/lib/copilot.js +4 -3
- package/dist/lib/copilot.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 +8 -3
- package/dist/lib/cursor-pricing.js.map +1 -1
- package/dist/lib/display-sanitize.d.ts +10 -0
- package/dist/lib/display-sanitize.d.ts.map +1 -0
- package/dist/lib/display-sanitize.js +17 -0
- package/dist/lib/display-sanitize.js.map +1 -0
- package/dist/lib/entries.d.ts +1 -0
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/env-template.d.ts +3 -2
- package/dist/lib/env-template.d.ts.map +1 -1
- package/dist/lib/env-template.js +6 -2
- package/dist/lib/env-template.js.map +1 -1
- package/dist/lib/firmware-config.d.ts +3 -3
- package/dist/lib/firmware-config.d.ts.map +1 -1
- package/dist/lib/firmware-config.js +9 -6
- package/dist/lib/firmware-config.js.map +1 -1
- package/dist/lib/firmware.d.ts.map +1 -1
- package/dist/lib/firmware.js +3 -11
- package/dist/lib/firmware.js.map +1 -1
- package/dist/lib/google-token-cache.js +2 -2
- package/dist/lib/google-token-cache.js.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/openai.d.ts.map +1 -1
- package/dist/lib/openai.js +3 -2
- package/dist/lib/openai.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/lib/zai.d.ts.map +1 -1
- package/dist/lib/zai.js +3 -2
- package/dist/lib/zai.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +303 -117
- 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,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
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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(
|
|
320
|
+
async function shouldBypassToastCacheForLiveLocalUsage(params) {
|
|
321
|
+
const { trigger, sessionID } = params;
|
|
294
322
|
if (trigger !== "question")
|
|
295
323
|
return false;
|
|
296
|
-
const
|
|
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(
|
|
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({
|
|
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
|
|
476
|
+
async function getSessionModelMeta(sessionID) {
|
|
435
477
|
if (!sessionID)
|
|
436
|
-
return
|
|
478
|
+
return {};
|
|
437
479
|
try {
|
|
438
480
|
const sessionResp = await typedClient.session.get({ path: { id: sessionID } });
|
|
439
|
-
return
|
|
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
|
-
|
|
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) =>
|
|
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,
|
|
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)
|
|
841
|
+
async function fetchQuotaCommandBody(trigger, params = {}) {
|
|
842
|
+
const selection = await resolveQuotaCommandSelection(params);
|
|
843
|
+
if (!selection)
|
|
697
844
|
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) })));
|
|
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
|
|
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:
|
|
810
|
-
? p
|
|
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" ||
|
|
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
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
(
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|