@slkiser/opencode-quota 2.6.2 → 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 +225 -168
- package/dist/data/modelsdev-pricing.min.json +51 -1
- 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 +32 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/cursor-detection.d.ts +17 -0
- package/dist/lib/cursor-detection.d.ts.map +1 -0
- package/dist/lib/cursor-detection.js +169 -0
- package/dist/lib/cursor-detection.js.map +1 -0
- package/dist/lib/cursor-pricing.d.ts +34 -0
- package/dist/lib/cursor-pricing.d.ts.map +1 -0
- package/dist/lib/cursor-pricing.js +133 -0
- package/dist/lib/cursor-pricing.js.map +1 -0
- package/dist/lib/cursor-usage.d.ts +32 -0
- package/dist/lib/cursor-usage.d.ts.map +1 -0
- package/dist/lib/cursor-usage.js +127 -0
- package/dist/lib/cursor-usage.js.map +1 -0
- package/dist/lib/entries.d.ts +5 -0
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/entries.js +0 -6
- package/dist/lib/entries.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/provider-metadata.d.ts.map +1 -1
- package/dist/lib/provider-metadata.js +4 -0
- package/dist/lib/provider-metadata.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-stats.d.ts +1 -1
- package/dist/lib/quota-stats.d.ts.map +1 -1
- package/dist/lib/quota-stats.js +26 -14
- package/dist/lib/quota-stats.js.map +1 -1
- package/dist/lib/quota-status.d.ts +5 -0
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +59 -2
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/token-cost.d.ts +10 -0
- package/dist/lib/token-cost.d.ts.map +1 -0
- package/dist/lib/token-cost.js +16 -0
- package/dist/lib/token-cost.js.map +1 -0
- package/dist/lib/types.d.ts +18 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +5 -0
- package/dist/lib/types.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +382 -128
- package/dist/plugin.js.map +1 -1
- package/dist/providers/cursor.d.ts +3 -0
- package/dist/providers/cursor.d.ts.map +1 -0
- package/dist/providers/cursor.js +138 -0
- package/dist/providers/cursor.js.map +1 -0
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +2 -0
- package/dist/providers/registry.js.map +1 -1
- package/package.json +4 -1
package/dist/plugin.js
CHANGED
|
@@ -16,12 +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, isCursorProviderId } from "./lib/cursor-pricing.js";
|
|
25
26
|
import { parseOptionalJsonArgs, parseQuotaBetweenArgs, startOfLocalDayMs, startOfNextLocalDayMs, formatYmd, } from "./lib/command-parsing.js";
|
|
26
27
|
import { handled } from "./lib/command-handled.js";
|
|
27
28
|
import { renderCommandHeading } from "./lib/format-utils.js";
|
|
@@ -30,7 +31,7 @@ const TOKEN_REPORT_COMMANDS = [
|
|
|
30
31
|
{
|
|
31
32
|
id: "tokens_today",
|
|
32
33
|
template: "/tokens_today",
|
|
33
|
-
description: "Token +
|
|
34
|
+
description: "Token + deterministic cost summary for today (calendar day, local timezone).",
|
|
34
35
|
title: "Tokens used (Today) (/tokens_today)",
|
|
35
36
|
metadataTitle: "Tokens used (Today)",
|
|
36
37
|
kind: "today",
|
|
@@ -38,7 +39,7 @@ const TOKEN_REPORT_COMMANDS = [
|
|
|
38
39
|
{
|
|
39
40
|
id: "tokens_daily",
|
|
40
41
|
template: "/tokens_daily",
|
|
41
|
-
description: "Token +
|
|
42
|
+
description: "Token + deterministic cost summary for the last 24 hours (rolling).",
|
|
42
43
|
title: "Tokens used (Last 24 Hours) (/tokens_daily)",
|
|
43
44
|
metadataTitle: "Tokens used (Last 24 Hours)",
|
|
44
45
|
kind: "rolling",
|
|
@@ -47,7 +48,7 @@ const TOKEN_REPORT_COMMANDS = [
|
|
|
47
48
|
{
|
|
48
49
|
id: "tokens_weekly",
|
|
49
50
|
template: "/tokens_weekly",
|
|
50
|
-
description: "Token +
|
|
51
|
+
description: "Token + deterministic cost summary for the last 7 days (rolling).",
|
|
51
52
|
title: "Tokens used (Last 7 Days) (/tokens_weekly)",
|
|
52
53
|
metadataTitle: "Tokens used (Last 7 Days)",
|
|
53
54
|
kind: "rolling",
|
|
@@ -56,7 +57,7 @@ const TOKEN_REPORT_COMMANDS = [
|
|
|
56
57
|
{
|
|
57
58
|
id: "tokens_monthly",
|
|
58
59
|
template: "/tokens_monthly",
|
|
59
|
-
description: "Token +
|
|
60
|
+
description: "Token + deterministic cost summary for the last 30 days (rolling).",
|
|
60
61
|
title: "Tokens used (Last 30 Days) (/tokens_monthly)",
|
|
61
62
|
metadataTitle: "Tokens used (Last 30 Days)",
|
|
62
63
|
kind: "rolling",
|
|
@@ -65,7 +66,7 @@ const TOKEN_REPORT_COMMANDS = [
|
|
|
65
66
|
{
|
|
66
67
|
id: "tokens_all",
|
|
67
68
|
template: "/tokens_all",
|
|
68
|
-
description: "Token +
|
|
69
|
+
description: "Token + deterministic cost summary for all locally saved OpenCode history.",
|
|
69
70
|
title: "Tokens used (All Time) (/tokens_all)",
|
|
70
71
|
metadataTitle: "Tokens used (All Time)",
|
|
71
72
|
kind: "all",
|
|
@@ -75,7 +76,7 @@ const TOKEN_REPORT_COMMANDS = [
|
|
|
75
76
|
{
|
|
76
77
|
id: "tokens_session",
|
|
77
78
|
template: "/tokens_session",
|
|
78
|
-
description: "Token +
|
|
79
|
+
description: "Token + deterministic cost summary for current session only.",
|
|
79
80
|
title: "Tokens used (Current Session) (/tokens_session)",
|
|
80
81
|
metadataTitle: "Tokens used (Current Session)",
|
|
81
82
|
kind: "session",
|
|
@@ -83,7 +84,7 @@ const TOKEN_REPORT_COMMANDS = [
|
|
|
83
84
|
{
|
|
84
85
|
id: "tokens_between",
|
|
85
86
|
template: "/tokens_between",
|
|
86
|
-
description: "Token + cost report between two YYYY-MM-DD dates (local timezone, inclusive).",
|
|
87
|
+
description: "Token + deterministic cost report between two YYYY-MM-DD dates (local timezone, inclusive).",
|
|
87
88
|
titleForRange: (startYmd, endYmd) => {
|
|
88
89
|
return `Tokens used (${formatYmd(startYmd)} .. ${formatYmd(endYmd)}) (/tokens_between)`;
|
|
89
90
|
},
|
|
@@ -106,7 +107,7 @@ function isTokenReportCommand(cmd) {
|
|
|
106
107
|
// =============================================================================
|
|
107
108
|
// Plugin Implementation
|
|
108
109
|
// =============================================================================
|
|
109
|
-
const
|
|
110
|
+
const LIVE_LOCAL_USAGE_PROVIDER_IDS = new Set(["qwen-code", "alibaba-coding-plan", "cursor"]);
|
|
110
111
|
/**
|
|
111
112
|
* Main plugin export
|
|
112
113
|
*/
|
|
@@ -152,20 +153,45 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
152
153
|
let lastSessionTokenError;
|
|
153
154
|
const providerFetchCache = new Map();
|
|
154
155
|
function getQuotaCommandCache() {
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
globalThis.__opencodeQuotaCommandCache = quotaCache;
|
|
156
|
+
const existing = globalThis.__opencodeQuotaCommandCache;
|
|
157
|
+
if (existing instanceof Map) {
|
|
158
|
+
return existing;
|
|
159
159
|
}
|
|
160
|
+
const quotaCache = new Map();
|
|
161
|
+
globalThis.__opencodeQuotaCommandCache = quotaCache;
|
|
160
162
|
return quotaCache;
|
|
161
163
|
}
|
|
162
164
|
function clearQuotaCommandCache() {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
169
195
|
}
|
|
170
196
|
function asRecord(value) {
|
|
171
197
|
return value && typeof value === "object" ? value : null;
|
|
@@ -210,14 +236,18 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
210
236
|
const style = ctx.config.toastStyle ?? "classic";
|
|
211
237
|
const googleModels = ctx.config.googleModels.join(",");
|
|
212
238
|
const alibabaCodingPlanTier = ctx.config.alibabaCodingPlanTier;
|
|
239
|
+
const cursorPlan = ctx.config.cursorPlan;
|
|
240
|
+
const cursorIncludedApiUsd = ctx.config.cursorIncludedApiUsd ?? "";
|
|
241
|
+
const cursorBillingCycleStartDay = ctx.config.cursorBillingCycleStartDay ?? "";
|
|
213
242
|
const onlyCurrentModel = ctx.config.onlyCurrentModel ? "yes" : "no";
|
|
214
243
|
const currentModel = ctx.config.currentModel ?? "";
|
|
215
|
-
|
|
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}`;
|
|
216
246
|
}
|
|
217
247
|
async function fetchProviderWithCache(params) {
|
|
218
248
|
const { provider, ctx, ttlMs } = params;
|
|
219
|
-
//
|
|
220
|
-
if (
|
|
249
|
+
// Live local-usage providers should update per completion for accurate local reports.
|
|
250
|
+
if (LIVE_LOCAL_USAGE_PROVIDER_IDS.has(provider.id)) {
|
|
221
251
|
return await provider.fetch(ctx);
|
|
222
252
|
}
|
|
223
253
|
const cacheKey = makeProviderFetchCacheKey(provider.id, ctx);
|
|
@@ -255,30 +285,67 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
255
285
|
});
|
|
256
286
|
return promise;
|
|
257
287
|
}
|
|
258
|
-
|
|
288
|
+
function makeProviderFetchFailure(provider) {
|
|
289
|
+
return {
|
|
290
|
+
attempted: true,
|
|
291
|
+
entries: [],
|
|
292
|
+
errors: [
|
|
293
|
+
{
|
|
294
|
+
label: getQuotaProviderDisplayLabel(provider.id),
|
|
295
|
+
message: "Failed to read quota data",
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async function fetchProviderResults(params) {
|
|
301
|
+
const settled = await Promise.allSettled(params.providers.map((provider) => fetchProviderWithCache({
|
|
302
|
+
provider,
|
|
303
|
+
ctx: params.ctx,
|
|
304
|
+
ttlMs: params.ttlMs,
|
|
305
|
+
})));
|
|
306
|
+
return settled.map((result, index) => result.status === "fulfilled"
|
|
307
|
+
? result.value
|
|
308
|
+
: makeProviderFetchFailure(params.providers[index]));
|
|
309
|
+
}
|
|
310
|
+
function getExplicitNoDataMessage(provider) {
|
|
311
|
+
if (provider.id === "cursor") {
|
|
312
|
+
return "No local usage yet";
|
|
313
|
+
}
|
|
314
|
+
return "Not configured";
|
|
315
|
+
}
|
|
316
|
+
function isProviderEnabled(providerId) {
|
|
317
|
+
return config.enabledProviders === "auto" || config.enabledProviders.includes(providerId);
|
|
318
|
+
}
|
|
319
|
+
async function shouldBypassToastCacheForLiveLocalUsage(params) {
|
|
320
|
+
const { trigger, sessionID } = params;
|
|
259
321
|
if (trigger !== "question")
|
|
260
322
|
return false;
|
|
261
|
-
const
|
|
323
|
+
const currentSession = params.sessionMeta ?? (await getSessionModelMeta(sessionID));
|
|
324
|
+
const currentModel = currentSession.modelID;
|
|
262
325
|
if (isQwenCodeModelId(currentModel)) {
|
|
263
326
|
const plan = await resolveQwenLocalPlanCached();
|
|
264
|
-
return
|
|
265
|
-
(config.enabledProviders === "auto" || config.enabledProviders.includes("qwen-code")));
|
|
327
|
+
return plan.state === "qwen_free" && isProviderEnabled("qwen-code");
|
|
266
328
|
}
|
|
267
329
|
if (isAlibabaModelId(currentModel)) {
|
|
268
330
|
const plan = await resolveAlibabaCodingPlanAuthCached({
|
|
269
331
|
maxAgeMs: DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS,
|
|
270
332
|
fallbackTier: config.alibabaCodingPlanTier,
|
|
271
333
|
});
|
|
272
|
-
return
|
|
273
|
-
|
|
274
|
-
|
|
334
|
+
return plan.state === "configured" && isProviderEnabled("alibaba-coding-plan");
|
|
335
|
+
}
|
|
336
|
+
if (isCursorProviderId(currentSession.providerID) || isCursorModelId(currentModel)) {
|
|
337
|
+
return isProviderEnabled("cursor");
|
|
275
338
|
}
|
|
276
339
|
return false;
|
|
277
340
|
}
|
|
278
|
-
async function shouldBypassQuotaCommandCache(sessionID) {
|
|
341
|
+
async function shouldBypassQuotaCommandCache(sessionID, sessionMeta) {
|
|
279
342
|
if (config.debug || !sessionID)
|
|
280
343
|
return config.debug;
|
|
281
|
-
return await
|
|
344
|
+
return await shouldBypassToastCacheForLiveLocalUsage({
|
|
345
|
+
trigger: "question",
|
|
346
|
+
sessionID,
|
|
347
|
+
sessionMeta,
|
|
348
|
+
});
|
|
282
349
|
}
|
|
283
350
|
async function refreshConfig() {
|
|
284
351
|
if (configInFlight)
|
|
@@ -287,11 +354,15 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
287
354
|
try {
|
|
288
355
|
configMeta = createLoadConfigMeta();
|
|
289
356
|
config = await loadConfig(typedClient, configMeta);
|
|
357
|
+
setPricingSnapshotAutoRefresh(config.pricingSnapshot.autoRefresh);
|
|
358
|
+
setPricingSnapshotSelection(config.pricingSnapshot.source);
|
|
290
359
|
configLoaded = true;
|
|
291
360
|
}
|
|
292
361
|
catch {
|
|
293
362
|
// Leave configLoaded=false so we can retry on next trigger.
|
|
294
363
|
config = DEFAULT_CONFIG;
|
|
364
|
+
setPricingSnapshotAutoRefresh(DEFAULT_CONFIG.pricingSnapshot.autoRefresh);
|
|
365
|
+
setPricingSnapshotSelection(DEFAULT_CONFIG.pricingSnapshot.source);
|
|
295
366
|
}
|
|
296
367
|
finally {
|
|
297
368
|
configInFlight = null;
|
|
@@ -301,7 +372,10 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
301
372
|
}
|
|
302
373
|
async function kickPricingRefresh(params) {
|
|
303
374
|
try {
|
|
304
|
-
const refreshPromise = maybeRefreshPricingSnapshot({
|
|
375
|
+
const refreshPromise = maybeRefreshPricingSnapshot({
|
|
376
|
+
reason: params.reason,
|
|
377
|
+
snapshotSelection: config.pricingSnapshot.source,
|
|
378
|
+
});
|
|
305
379
|
const guardedRefreshPromise = refreshPromise.catch(() => undefined);
|
|
306
380
|
if (!params.maxWaitMs || params.maxWaitMs <= 0) {
|
|
307
381
|
void guardedRefreshPromise;
|
|
@@ -340,6 +414,11 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
340
414
|
enabledProviders: config.enabledProviders,
|
|
341
415
|
minIntervalMs: config.minIntervalMs,
|
|
342
416
|
googleModels: config.googleModels,
|
|
417
|
+
cursorPlan: config.cursorPlan,
|
|
418
|
+
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
419
|
+
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
420
|
+
pricingSnapshotSource: config.pricingSnapshot.source,
|
|
421
|
+
pricingSnapshotAutoRefresh: config.pricingSnapshot.autoRefresh,
|
|
343
422
|
showOnIdle: config.showOnIdle,
|
|
344
423
|
showOnQuestion: config.showOnQuestion,
|
|
345
424
|
showOnCompact: config.showOnCompact,
|
|
@@ -387,22 +466,38 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
387
466
|
}
|
|
388
467
|
}
|
|
389
468
|
/**
|
|
390
|
-
* Get the current model from the active session.
|
|
469
|
+
* Get the current model metadata from the active session.
|
|
391
470
|
*
|
|
392
471
|
* Only uses session-scoped model lookup. Does NOT fall back to
|
|
393
472
|
* client.config.get() because that returns the global/default model
|
|
394
473
|
* which can be stale across sessions.
|
|
395
474
|
*/
|
|
396
|
-
async function
|
|
475
|
+
async function getSessionModelMeta(sessionID) {
|
|
397
476
|
if (!sessionID)
|
|
398
|
-
return
|
|
477
|
+
return {};
|
|
399
478
|
try {
|
|
400
479
|
const sessionResp = await typedClient.session.get({ path: { id: sessionID } });
|
|
401
|
-
return
|
|
480
|
+
return {
|
|
481
|
+
modelID: sessionResp.data?.modelID,
|
|
482
|
+
providerID: sessionResp.data?.providerID,
|
|
483
|
+
};
|
|
402
484
|
}
|
|
403
485
|
catch {
|
|
486
|
+
return {};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function getCurrentModel(sessionID) {
|
|
490
|
+
if (!sessionID)
|
|
404
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;
|
|
405
497
|
}
|
|
498
|
+
if (!params.currentModel)
|
|
499
|
+
return false;
|
|
500
|
+
return params.provider.matchesCurrentModel ? params.provider.matchesCurrentModel(params.currentModel) : true;
|
|
406
501
|
}
|
|
407
502
|
function formatDebugInfo(params) {
|
|
408
503
|
const availability = params.availability
|
|
@@ -423,6 +518,99 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
423
518
|
`available=${availability}`,
|
|
424
519
|
].join("\n");
|
|
425
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
|
+
}
|
|
426
614
|
async function fetchQuotaMessage(trigger, sessionID) {
|
|
427
615
|
// Ensure we have loaded config at least once. If load fails, we keep trying
|
|
428
616
|
// on subsequent triggers.
|
|
@@ -449,21 +637,28 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
449
637
|
: null;
|
|
450
638
|
}
|
|
451
639
|
let currentModel;
|
|
640
|
+
let currentProviderID;
|
|
452
641
|
if (config.onlyCurrentModel) {
|
|
453
|
-
|
|
642
|
+
const currentSession = await getSessionModelMeta(sessionID);
|
|
643
|
+
currentModel = currentSession.modelID;
|
|
644
|
+
currentProviderID = currentSession.providerID;
|
|
454
645
|
}
|
|
455
646
|
const ctx = {
|
|
456
647
|
client: typedClient,
|
|
457
648
|
config: {
|
|
458
649
|
googleModels: config.googleModels,
|
|
459
650
|
alibabaCodingPlanTier: config.alibabaCodingPlanTier,
|
|
651
|
+
cursorPlan: config.cursorPlan,
|
|
652
|
+
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
653
|
+
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
460
654
|
toastStyle: config.toastStyle,
|
|
461
655
|
onlyCurrentModel: config.onlyCurrentModel,
|
|
462
656
|
currentModel,
|
|
657
|
+
currentProviderID,
|
|
463
658
|
},
|
|
464
659
|
};
|
|
465
|
-
const filtered = config.onlyCurrentModel && currentModel
|
|
466
|
-
? providers.filter((p) =>
|
|
660
|
+
const filtered = config.onlyCurrentModel && (currentModel || isCursorProviderId(currentProviderID))
|
|
661
|
+
? providers.filter((p) => matchesProviderCurrentSelection({ provider: p, currentModel, currentProviderID }))
|
|
467
662
|
: providers;
|
|
468
663
|
// availability checks are cheap, do them in parallel
|
|
469
664
|
const avail = await Promise.all(filtered.map(async (p) => ({ p, ok: await p.isAvailable(ctx) })));
|
|
@@ -479,11 +674,11 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
479
674
|
})
|
|
480
675
|
: null;
|
|
481
676
|
}
|
|
482
|
-
const results = await
|
|
483
|
-
|
|
677
|
+
const results = await fetchProviderResults({
|
|
678
|
+
providers: active,
|
|
484
679
|
ctx,
|
|
485
680
|
ttlMs: config.minIntervalMs,
|
|
486
|
-
})
|
|
681
|
+
});
|
|
487
682
|
const entries = results.flatMap((r) => r.entries);
|
|
488
683
|
const errors = results.flatMap((r) => r.errors);
|
|
489
684
|
const attemptedAny = results.some((r) => r.attempted);
|
|
@@ -498,7 +693,7 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
498
693
|
if (!result.attempted && result.entries.length === 0 && result.errors.length === 0) {
|
|
499
694
|
errors.push({
|
|
500
695
|
label: getQuotaProviderDisplayLabel(provider.id),
|
|
501
|
-
message:
|
|
696
|
+
message: getExplicitNoDataMessage(provider),
|
|
502
697
|
});
|
|
503
698
|
hasExplicitProviderIssues = true;
|
|
504
699
|
}
|
|
@@ -609,7 +804,7 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
609
804
|
}
|
|
610
805
|
const bypassMessageCache = config.debug
|
|
611
806
|
? true
|
|
612
|
-
: await
|
|
807
|
+
: await shouldBypassToastCacheForLiveLocalUsage({ trigger, sessionID });
|
|
613
808
|
const message = bypassMessageCache
|
|
614
809
|
? await fetchQuotaMessage(trigger, sessionID)
|
|
615
810
|
: await getOrFetchWithCacheControl(async () => {
|
|
@@ -642,50 +837,40 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
642
837
|
});
|
|
643
838
|
}
|
|
644
839
|
}
|
|
645
|
-
async function fetchQuotaCommandBody(trigger,
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (!config.enabled)
|
|
649
|
-
return null;
|
|
650
|
-
const allProviders = getProviders();
|
|
651
|
-
const isAutoMode = config.enabledProviders === "auto";
|
|
652
|
-
const providers = isAutoMode
|
|
653
|
-
? allProviders
|
|
654
|
-
: allProviders.filter((p) => config.enabledProviders.includes(p.id));
|
|
655
|
-
if (!isAutoMode && providers.length === 0)
|
|
840
|
+
async function fetchQuotaCommandBody(trigger, params = {}) {
|
|
841
|
+
const selection = await resolveQuotaCommandSelection(params);
|
|
842
|
+
if (!selection)
|
|
656
843
|
return null;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
currentModel = await getCurrentModel(sessionID);
|
|
660
|
-
}
|
|
661
|
-
const ctx = {
|
|
662
|
-
client: typedClient,
|
|
663
|
-
config: {
|
|
664
|
-
googleModels: config.googleModels,
|
|
665
|
-
alibabaCodingPlanTier: config.alibabaCodingPlanTier,
|
|
666
|
-
// Always format /quota in grouped mode for a more dashboard-like look.
|
|
667
|
-
toastStyle: "grouped",
|
|
668
|
-
onlyCurrentModel: config.onlyCurrentModel,
|
|
669
|
-
currentModel,
|
|
670
|
-
},
|
|
671
|
-
};
|
|
672
|
-
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) })));
|
|
673
846
|
const active = avail.filter((x) => x.ok).map((x) => x.p);
|
|
674
847
|
if (active.length === 0)
|
|
675
848
|
return null;
|
|
676
|
-
const results = await
|
|
677
|
-
|
|
849
|
+
const results = await fetchProviderResults({
|
|
850
|
+
providers: active,
|
|
678
851
|
ctx,
|
|
679
852
|
ttlMs: config.minIntervalMs,
|
|
680
|
-
})
|
|
853
|
+
});
|
|
681
854
|
const entries = results.flatMap((r) => r.entries);
|
|
682
855
|
const errors = results.flatMap((r) => r.errors);
|
|
856
|
+
if (!isAutoMode) {
|
|
857
|
+
for (let i = 0; i < active.length; i++) {
|
|
858
|
+
const provider = active[i];
|
|
859
|
+
const result = results[i];
|
|
860
|
+
if (!result.attempted && result.entries.length === 0 && result.errors.length === 0) {
|
|
861
|
+
errors.push({
|
|
862
|
+
label: getQuotaProviderDisplayLabel(provider.id),
|
|
863
|
+
message: getExplicitNoDataMessage(provider),
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
683
868
|
// Fetch session tokens if enabled and sessionID is available
|
|
684
869
|
let sessionTokens;
|
|
685
|
-
if (config.showSessionTokens && sessionID) {
|
|
870
|
+
if (config.showSessionTokens && params.sessionID) {
|
|
686
871
|
const stResult = await fetchSessionTokensForDisplay({
|
|
687
872
|
enabled: config.showSessionTokens,
|
|
688
|
-
sessionID,
|
|
873
|
+
sessionID: params.sessionID,
|
|
689
874
|
});
|
|
690
875
|
sessionTokens = stResult.sessionTokens;
|
|
691
876
|
// Update diagnostics state: clear on success (no error returned), set on failure
|
|
@@ -719,7 +904,9 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
719
904
|
if (!config.enabled)
|
|
720
905
|
return null;
|
|
721
906
|
await kickPricingRefresh({ reason: "status", maxWaitMs: 750 });
|
|
722
|
-
const
|
|
907
|
+
const currentSession = await getSessionModelMeta(params.sessionID);
|
|
908
|
+
const currentModel = currentSession.modelID;
|
|
909
|
+
const currentProviderID = currentSession.providerID;
|
|
723
910
|
const sessionModelLookup = !params.sessionID
|
|
724
911
|
? "no_session"
|
|
725
912
|
: currentModel
|
|
@@ -735,6 +922,11 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
735
922
|
config: {
|
|
736
923
|
googleModels: config.googleModels,
|
|
737
924
|
alibabaCodingPlanTier: config.alibabaCodingPlanTier,
|
|
925
|
+
cursorPlan: config.cursorPlan,
|
|
926
|
+
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
927
|
+
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
928
|
+
currentModel,
|
|
929
|
+
currentProviderID,
|
|
738
930
|
},
|
|
739
931
|
});
|
|
740
932
|
}
|
|
@@ -746,8 +938,8 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
746
938
|
// In auto mode, a provider is effectively "enabled" if it's available.
|
|
747
939
|
enabled: isAutoMode ? ok : config.enabledProviders.includes(p.id),
|
|
748
940
|
available: ok,
|
|
749
|
-
matchesCurrentModel:
|
|
750
|
-
? p
|
|
941
|
+
matchesCurrentModel: currentModel || isCursorProviderId(currentProviderID)
|
|
942
|
+
? matchesProviderCurrentSelection({ provider: p, currentModel, currentProviderID })
|
|
751
943
|
: undefined,
|
|
752
944
|
};
|
|
753
945
|
}));
|
|
@@ -759,6 +951,10 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
759
951
|
configPaths: configMeta.paths,
|
|
760
952
|
enabledProviders: config.enabledProviders,
|
|
761
953
|
alibabaCodingPlanTier: config.alibabaCodingPlanTier,
|
|
954
|
+
cursorPlan: config.cursorPlan,
|
|
955
|
+
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
956
|
+
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
957
|
+
pricingSnapshotSource: config.pricingSnapshot.source,
|
|
762
958
|
onlyCurrentModel: config.onlyCurrentModel,
|
|
763
959
|
currentModel,
|
|
764
960
|
sessionModelLookup,
|
|
@@ -775,6 +971,42 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
775
971
|
generatedAtMs: params.generatedAtMs,
|
|
776
972
|
});
|
|
777
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
|
+
}
|
|
778
1010
|
// Return hook implementations
|
|
779
1011
|
return {
|
|
780
1012
|
// Register built-in slash commands (in addition to /tool quota_*)
|
|
@@ -790,6 +1022,10 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
790
1022
|
template: "/quota_status",
|
|
791
1023
|
description: "Diagnostics for toast + pricing + local storage (includes unknown pricing report).",
|
|
792
1024
|
};
|
|
1025
|
+
cfg.command["pricing_refresh"] = {
|
|
1026
|
+
template: "/pricing_refresh",
|
|
1027
|
+
description: "Refresh the local runtime pricing snapshot from models.dev.",
|
|
1028
|
+
};
|
|
793
1029
|
// Register token report commands (/tokens_*)
|
|
794
1030
|
for (const spec of TOKEN_REPORT_COMMANDS) {
|
|
795
1031
|
cfg.command[spec.id] = {
|
|
@@ -802,7 +1038,10 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
802
1038
|
try {
|
|
803
1039
|
const cmd = input.command;
|
|
804
1040
|
const sessionID = input.sessionID;
|
|
805
|
-
const isQuotaCommand = cmd === "quota" ||
|
|
1041
|
+
const isQuotaCommand = cmd === "quota" ||
|
|
1042
|
+
cmd === "quota_status" ||
|
|
1043
|
+
cmd === "pricing_refresh" ||
|
|
1044
|
+
isTokenReportCommand(cmd);
|
|
806
1045
|
if (isQuotaCommand && !configLoaded) {
|
|
807
1046
|
await refreshConfig();
|
|
808
1047
|
}
|
|
@@ -811,30 +1050,45 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
811
1050
|
}
|
|
812
1051
|
if (cmd === "quota") {
|
|
813
1052
|
const generatedAtMs = Date.now();
|
|
814
|
-
// Separate cache for /quota so it doesn't pollute the toast cache.
|
|
815
|
-
const quotaCache = getQuotaCommandCache();
|
|
816
1053
|
const now = generatedAtMs;
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
(
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
+
})();
|
|
838
1092
|
if (!body) {
|
|
839
1093
|
// Provide an actionable message instead of a generic "unavailable".
|
|
840
1094
|
if (!configLoaded) {
|
|
@@ -844,30 +1098,7 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
844
1098
|
await injectRawOutput(sessionID, "Quota disabled in config (enabled: false)");
|
|
845
1099
|
}
|
|
846
1100
|
else {
|
|
847
|
-
|
|
848
|
-
const allProvs = getProviders();
|
|
849
|
-
const ctx = {
|
|
850
|
-
client: typedClient,
|
|
851
|
-
config: {
|
|
852
|
-
googleModels: config.googleModels,
|
|
853
|
-
alibabaCodingPlanTier: config.alibabaCodingPlanTier,
|
|
854
|
-
},
|
|
855
|
-
};
|
|
856
|
-
const avail = await Promise.all(allProvs.map(async (p) => {
|
|
857
|
-
try {
|
|
858
|
-
return { id: p.id, ok: await p.isAvailable(ctx) };
|
|
859
|
-
}
|
|
860
|
-
catch {
|
|
861
|
-
return { id: p.id, ok: false };
|
|
862
|
-
}
|
|
863
|
-
}));
|
|
864
|
-
const availableIds = avail.filter((x) => x.ok).map((x) => x.id);
|
|
865
|
-
if (availableIds.length === 0) {
|
|
866
|
-
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.");
|
|
867
|
-
}
|
|
868
|
-
else {
|
|
869
|
-
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.`);
|
|
870
|
-
}
|
|
1101
|
+
await injectRawOutput(sessionID, await buildQuotaCommandUnavailableMessage(quotaRequestContext));
|
|
871
1102
|
}
|
|
872
1103
|
handled();
|
|
873
1104
|
}
|
|
@@ -878,6 +1109,24 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
878
1109
|
await injectRawOutput(sessionID, `${heading}\n\n${body}`);
|
|
879
1110
|
handled();
|
|
880
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
|
+
}
|
|
881
1130
|
const untilMs = Date.now();
|
|
882
1131
|
// Handle token report commands (/tokens_*)
|
|
883
1132
|
if (isTokenReportCommand(cmd)) {
|
|
@@ -1029,7 +1278,8 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
1029
1278
|
if (!config.enabled)
|
|
1030
1279
|
return;
|
|
1031
1280
|
if (isSuccessfulQuestionExecution(output)) {
|
|
1032
|
-
const
|
|
1281
|
+
const sessionMeta = await getSessionModelMeta(input.sessionID);
|
|
1282
|
+
const model = sessionMeta.modelID;
|
|
1033
1283
|
try {
|
|
1034
1284
|
if (isQwenCodeModelId(model)) {
|
|
1035
1285
|
const plan = await resolveQwenLocalPlanCached();
|
|
@@ -1048,11 +1298,15 @@ export const QuotaToastPlugin = async ({ client }) => {
|
|
|
1048
1298
|
clearQuotaCommandCache();
|
|
1049
1299
|
}
|
|
1050
1300
|
}
|
|
1301
|
+
else if (isCursorProviderId(sessionMeta.providerID) || isCursorModelId(model)) {
|
|
1302
|
+
clearQuotaCommandCache();
|
|
1303
|
+
}
|
|
1051
1304
|
}
|
|
1052
1305
|
catch (err) {
|
|
1053
1306
|
await log("Failed to record local request-plan quota completion", {
|
|
1054
1307
|
error: err instanceof Error ? err.message : String(err),
|
|
1055
1308
|
model,
|
|
1309
|
+
providerID: sessionMeta.providerID,
|
|
1056
1310
|
});
|
|
1057
1311
|
}
|
|
1058
1312
|
}
|