@latentminds/pi-quotas 0.2.6 → 0.3.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 +9 -2
- package/package.json +25 -5
- package/src/config.ts +43 -13
- package/src/extensions/command-quotas/command.ts +23 -1
- package/src/extensions/command-quotas/components/quotas-display.test.ts +29 -0
- package/src/extensions/command-quotas/components/quotas-display.ts +8 -2
- package/src/extensions/command-quotas/provider-commands.test.ts +9 -0
- package/src/extensions/command-quotas/provider-commands.ts +12 -0
- package/src/extensions/command-tokens/command.ts +102 -0
- package/src/extensions/command-tokens/index.ts +1 -0
- package/src/extensions/command-tokens/tokens-display.ts +357 -0
- package/src/extensions/quota-warnings/index.ts +36 -14
- package/src/extensions/token-status/index.ts +241 -0
- package/src/extensions/token-status/provider-detection.test.ts +30 -0
- package/src/extensions/usage-status/index.test.ts +182 -0
- package/src/extensions/usage-status/index.ts +104 -40
- package/src/lib/quotas.ts +12 -1
- package/src/lib/session-tokens.test.ts +137 -0
- package/src/lib/session-tokens.ts +399 -0
- package/src/providers/fetch.test.ts +31 -0
- package/src/providers/fetch.ts +167 -22
- package/src/providers/opencode-go-config.ts +127 -0
- package/src/providers/opencode-go.ts +183 -0
- package/src/providers/parse.test.ts +185 -7
- package/src/providers/providers.ts +229 -21
- package/src/types/quotas.ts +12 -3
- package/src/utils/quotas-severity.ts +14 -2
|
@@ -4,6 +4,8 @@ import { parseCodexUsage } from "./providers.js";
|
|
|
4
4
|
import { parseGitHubCopilotUsage } from "./providers.js";
|
|
5
5
|
import { parseOpenRouterUsage } from "./providers.js";
|
|
6
6
|
import { parseSyntheticUsage } from "./providers.js";
|
|
7
|
+
import { parseZaiUsage } from "./providers.js";
|
|
8
|
+
import { parseOpenCodeGoUsage } from "./providers.js";
|
|
7
9
|
|
|
8
10
|
describe("parseAnthropicUsage", () => {
|
|
9
11
|
it("maps oauth usage response into quota windows", () => {
|
|
@@ -63,7 +65,10 @@ describe("parseAnthropicUsage", () => {
|
|
|
63
65
|
five_hour: { utilization: 9, resets_at: "2026-04-22T09:00:00Z" },
|
|
64
66
|
seven_day: { utilization: 31, resets_at: "2026-04-23T23:00:00Z" },
|
|
65
67
|
seven_day_sonnet: { utilization: 8, resets_at: "2026-04-23T23:00:00Z" },
|
|
66
|
-
seven_day_omelette: {
|
|
68
|
+
seven_day_omelette: {
|
|
69
|
+
utilization: 23,
|
|
70
|
+
resets_at: "2026-04-26T23:00:00Z",
|
|
71
|
+
},
|
|
67
72
|
seven_day_opus: null,
|
|
68
73
|
});
|
|
69
74
|
|
|
@@ -140,7 +145,11 @@ describe("parseCodexUsage", () => {
|
|
|
140
145
|
const windows = parseCodexUsage({
|
|
141
146
|
plan_type: "team",
|
|
142
147
|
rate_limit: {
|
|
143
|
-
primary_window: {
|
|
148
|
+
primary_window: {
|
|
149
|
+
used_percent: 10,
|
|
150
|
+
reset_at: 1776880800,
|
|
151
|
+
limit_window_seconds: 18000,
|
|
152
|
+
},
|
|
144
153
|
},
|
|
145
154
|
credits: {
|
|
146
155
|
has_credits: true,
|
|
@@ -160,7 +169,11 @@ describe("parseCodexUsage", () => {
|
|
|
160
169
|
const windows = parseCodexUsage({
|
|
161
170
|
plan_type: "team",
|
|
162
171
|
rate_limit: {
|
|
163
|
-
primary_window: {
|
|
172
|
+
primary_window: {
|
|
173
|
+
used_percent: 10,
|
|
174
|
+
reset_at: 1776880800,
|
|
175
|
+
limit_window_seconds: 18000,
|
|
176
|
+
},
|
|
164
177
|
},
|
|
165
178
|
spend_control: { reached: true },
|
|
166
179
|
});
|
|
@@ -173,7 +186,11 @@ describe("parseCodexUsage", () => {
|
|
|
173
186
|
it("skips credits when no balance", () => {
|
|
174
187
|
const windows = parseCodexUsage({
|
|
175
188
|
rate_limit: {
|
|
176
|
-
primary_window: {
|
|
189
|
+
primary_window: {
|
|
190
|
+
used_percent: 10,
|
|
191
|
+
reset_at: 1776880800,
|
|
192
|
+
limit_window_seconds: 18000,
|
|
193
|
+
},
|
|
177
194
|
},
|
|
178
195
|
credits: { has_credits: false, balance: null },
|
|
179
196
|
});
|
|
@@ -254,8 +271,14 @@ describe("parseGitHubCopilotUsage", () => {
|
|
|
254
271
|
});
|
|
255
272
|
|
|
256
273
|
expect(windows).toHaveLength(2);
|
|
257
|
-
expect(windows[0]).toMatchObject({
|
|
258
|
-
|
|
274
|
+
expect(windows[0]).toMatchObject({
|
|
275
|
+
label: "Chat / month",
|
|
276
|
+
usedPercent: 18,
|
|
277
|
+
});
|
|
278
|
+
expect(windows[1]).toMatchObject({
|
|
279
|
+
label: "Completions / month",
|
|
280
|
+
usedPercent: 0,
|
|
281
|
+
});
|
|
259
282
|
});
|
|
260
283
|
});
|
|
261
284
|
|
|
@@ -477,7 +500,9 @@ describe("parseSyntheticUsage", () => {
|
|
|
477
500
|
expect(search!.limitValue).toBe(250);
|
|
478
501
|
|
|
479
502
|
// freeToolCalls with limit=0 is NOT shown
|
|
480
|
-
expect(
|
|
503
|
+
expect(
|
|
504
|
+
windows.find((w) => w.label === "Free Tool Calls / day"),
|
|
505
|
+
).toBeUndefined();
|
|
481
506
|
});
|
|
482
507
|
|
|
483
508
|
it("shows freeToolCalls when limit > 0", () => {
|
|
@@ -541,3 +566,156 @@ describe("parseSyntheticUsage", () => {
|
|
|
541
566
|
expect(windows).toHaveLength(0);
|
|
542
567
|
});
|
|
543
568
|
});
|
|
569
|
+
|
|
570
|
+
describe("parseOpenCodeGoUsage", () => {
|
|
571
|
+
it("parses rolling, weekly, and monthly windows", () => {
|
|
572
|
+
const windows = parseOpenCodeGoUsage({
|
|
573
|
+
rolling: {
|
|
574
|
+
usagePercent: 35,
|
|
575
|
+
resetInSec: 12000,
|
|
576
|
+
percentRemaining: 65,
|
|
577
|
+
resetTimeIso: "2026-05-18T22:00:00Z",
|
|
578
|
+
},
|
|
579
|
+
weekly: {
|
|
580
|
+
usagePercent: 62,
|
|
581
|
+
resetInSec: 500000,
|
|
582
|
+
percentRemaining: 38,
|
|
583
|
+
resetTimeIso: "2026-05-25T00:00:00Z",
|
|
584
|
+
},
|
|
585
|
+
monthly: {
|
|
586
|
+
usagePercent: 28,
|
|
587
|
+
resetInSec: 1200000,
|
|
588
|
+
percentRemaining: 72,
|
|
589
|
+
resetTimeIso: "2026-06-01T00:00:00Z",
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
expect(windows).toHaveLength(3);
|
|
594
|
+
|
|
595
|
+
expect(windows[0]).toMatchObject({
|
|
596
|
+
provider: "opencode-go",
|
|
597
|
+
label: "5h Rolling",
|
|
598
|
+
usedPercent: 35,
|
|
599
|
+
windowSeconds: 5 * 60 * 60,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
expect(windows[1]).toMatchObject({
|
|
603
|
+
provider: "opencode-go",
|
|
604
|
+
label: "Weekly",
|
|
605
|
+
usedPercent: 62,
|
|
606
|
+
windowSeconds: 7 * 24 * 60 * 60,
|
|
607
|
+
showPace: true,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
expect(windows[2]).toMatchObject({
|
|
611
|
+
provider: "opencode-go",
|
|
612
|
+
label: "Monthly",
|
|
613
|
+
usedPercent: 28,
|
|
614
|
+
windowSeconds: 30 * 24 * 60 * 60,
|
|
615
|
+
showPace: true,
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("handles partial windows", () => {
|
|
620
|
+
const windows = parseOpenCodeGoUsage({
|
|
621
|
+
rolling: {
|
|
622
|
+
usagePercent: 10,
|
|
623
|
+
resetInSec: 15000,
|
|
624
|
+
percentRemaining: 90,
|
|
625
|
+
resetTimeIso: "2026-05-18T21:00:00Z",
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
expect(windows).toHaveLength(1);
|
|
630
|
+
expect(windows[0].label).toBe("5h Rolling");
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("returns empty for no data", () => {
|
|
634
|
+
const windows = parseOpenCodeGoUsage({});
|
|
635
|
+
expect(windows).toHaveLength(0);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
describe("parseZaiUsage", () => {
|
|
640
|
+
it("maps token windows (5h/7d) and the monthly web-search count", () => {
|
|
641
|
+
const windows = parseZaiUsage({
|
|
642
|
+
data: {
|
|
643
|
+
level: "lite",
|
|
644
|
+
limits: [
|
|
645
|
+
{
|
|
646
|
+
type: "TIME_LIMIT",
|
|
647
|
+
unit: 5,
|
|
648
|
+
number: 1,
|
|
649
|
+
usage: 100,
|
|
650
|
+
currentValue: 25,
|
|
651
|
+
remaining: 75,
|
|
652
|
+
percentage: 25,
|
|
653
|
+
nextResetTime: 1785048370995,
|
|
654
|
+
usageDetails: [
|
|
655
|
+
{ modelCode: "search-prime", usage: 20 },
|
|
656
|
+
{ modelCode: "web-reader", usage: 5 },
|
|
657
|
+
],
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
type: "TOKENS_LIMIT",
|
|
661
|
+
unit: 3,
|
|
662
|
+
number: 5,
|
|
663
|
+
percentage: 8,
|
|
664
|
+
nextResetTime: 1782932874304,
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
type: "TOKENS_LIMIT",
|
|
668
|
+
unit: 6,
|
|
669
|
+
number: 1,
|
|
670
|
+
percentage: 52,
|
|
671
|
+
nextResetTime: 1783061170994,
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// Shortest window first: 5h → 7d → month
|
|
678
|
+
expect(windows).toHaveLength(3);
|
|
679
|
+
expect(windows[0]).toMatchObject({
|
|
680
|
+
provider: "zai",
|
|
681
|
+
label: "5h",
|
|
682
|
+
usedPercent: 8,
|
|
683
|
+
windowSeconds: 5 * 60 * 60,
|
|
684
|
+
usedValue: 8,
|
|
685
|
+
limitValue: 100,
|
|
686
|
+
});
|
|
687
|
+
expect(windows[1]).toMatchObject({
|
|
688
|
+
provider: "zai",
|
|
689
|
+
label: "7d",
|
|
690
|
+
usedPercent: 52,
|
|
691
|
+
windowSeconds: 7 * 24 * 60 * 60,
|
|
692
|
+
});
|
|
693
|
+
expect(windows[2]).toMatchObject({
|
|
694
|
+
provider: "zai",
|
|
695
|
+
label: "Web / month",
|
|
696
|
+
usedPercent: 25,
|
|
697
|
+
usedValue: 25,
|
|
698
|
+
limitValue: 100,
|
|
699
|
+
windowSeconds: 30 * 24 * 60 * 60,
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it("skips the monthly window when the entitlement is zero", () => {
|
|
704
|
+
const windows = parseZaiUsage({
|
|
705
|
+
data: {
|
|
706
|
+
limits: [
|
|
707
|
+
{ type: "TIME_LIMIT", unit: 5, number: 1, usage: 0, currentValue: 0, nextResetTime: 1785048370995 },
|
|
708
|
+
{ type: "TOKENS_LIMIT", unit: 3, number: 5, percentage: 0, nextResetTime: 1782932874304 },
|
|
709
|
+
],
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
expect(windows).toHaveLength(1);
|
|
713
|
+
expect(windows[0].label).toBe("5h");
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("returns empty array when there are no limits", () => {
|
|
717
|
+
expect(parseZaiUsage({})).toHaveLength(0);
|
|
718
|
+
expect(parseZaiUsage({ data: {} })).toHaveLength(0);
|
|
719
|
+
expect(parseZaiUsage({ data: { limits: [] } })).toHaveLength(0);
|
|
720
|
+
});
|
|
721
|
+
});
|
|
@@ -13,7 +13,10 @@ function parseDateish(value: unknown): Date {
|
|
|
13
13
|
function monthWindowSeconds(resetAt: Date): number {
|
|
14
14
|
const approxStart = new Date(resetAt);
|
|
15
15
|
approxStart.setMonth(approxStart.getMonth() - 1);
|
|
16
|
-
return Math.max(
|
|
16
|
+
return Math.max(
|
|
17
|
+
1,
|
|
18
|
+
Math.round((resetAt.getTime() - approxStart.getTime()) / 1000),
|
|
19
|
+
);
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function parseAnthropicUsage(data: any): QuotaWindow[] {
|
|
@@ -79,8 +82,14 @@ export function parseAnthropicUsage(data: any): QuotaWindow[] {
|
|
|
79
82
|
windows.push({
|
|
80
83
|
provider: "anthropic",
|
|
81
84
|
label: `Extra (${currency})`,
|
|
82
|
-
usedPercent: Number(
|
|
83
|
-
|
|
85
|
+
usedPercent: Number(
|
|
86
|
+
extra.utilization ?? safePercent(usedDollars, limitDollars),
|
|
87
|
+
),
|
|
88
|
+
resetsAt: new Date(
|
|
89
|
+
new Date().getFullYear(),
|
|
90
|
+
new Date().getMonth() + 1,
|
|
91
|
+
1,
|
|
92
|
+
),
|
|
84
93
|
windowSeconds: 30 * 24 * 60 * 60,
|
|
85
94
|
usedValue: usedDollars,
|
|
86
95
|
limitValue: limitDollars,
|
|
@@ -95,16 +104,26 @@ export function parseAnthropicUsage(data: any): QuotaWindow[] {
|
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
function percentLeftToUsedPercent(limit: any): number {
|
|
98
|
-
if (limit?.percent_left != null)
|
|
99
|
-
|
|
107
|
+
if (limit?.percent_left != null)
|
|
108
|
+
return Math.max(0, 100 - Number(limit.percent_left));
|
|
109
|
+
if (limit?.remaining_percent != null)
|
|
110
|
+
return Math.max(0, 100 - Number(limit.remaining_percent));
|
|
100
111
|
if (limit?.used_percent != null) return Number(limit.used_percent);
|
|
101
112
|
return 0;
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
export function parseCodexUsage(data: any): QuotaWindow[] {
|
|
105
116
|
const rateLimit = data?.rate_limit ?? data?.rate_limits ?? {};
|
|
106
|
-
const primary =
|
|
107
|
-
|
|
117
|
+
const primary =
|
|
118
|
+
rateLimit.primary_window ??
|
|
119
|
+
rateLimit.primary ??
|
|
120
|
+
rateLimit.five_hour_limit ??
|
|
121
|
+
rateLimit.five_hour;
|
|
122
|
+
const secondary =
|
|
123
|
+
rateLimit.secondary_window ??
|
|
124
|
+
rateLimit.secondary ??
|
|
125
|
+
rateLimit.weekly_limit ??
|
|
126
|
+
rateLimit.weekly;
|
|
108
127
|
|
|
109
128
|
const windows: QuotaWindow[] = [];
|
|
110
129
|
|
|
@@ -180,7 +199,11 @@ export function parseCodexUsage(data: any): QuotaWindow[] {
|
|
|
180
199
|
export function parseGitHubCopilotUsage(data: any): QuotaWindow[] {
|
|
181
200
|
const windows: QuotaWindow[] = [];
|
|
182
201
|
|
|
183
|
-
const resetAt = parseDateish(
|
|
202
|
+
const resetAt = parseDateish(
|
|
203
|
+
data?.quota_reset_date ??
|
|
204
|
+
data?.quota_reset_date_utc ??
|
|
205
|
+
data?.limited_user_reset_date,
|
|
206
|
+
);
|
|
184
207
|
const periodSeconds = monthWindowSeconds(resetAt);
|
|
185
208
|
|
|
186
209
|
const snapshots = data?.quota_snapshots;
|
|
@@ -209,11 +232,12 @@ export function parseGitHubCopilotUsage(data: any): QuotaWindow[] {
|
|
|
209
232
|
limitValue: entitlement,
|
|
210
233
|
showPace: true,
|
|
211
234
|
nextLabel: "Resets",
|
|
212
|
-
nextAmount:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
235
|
+
nextAmount:
|
|
236
|
+
overageCount > 0
|
|
237
|
+
? `+${overageCount} overage`
|
|
238
|
+
: overagePermitted
|
|
239
|
+
? "overage allowed"
|
|
240
|
+
: undefined,
|
|
217
241
|
});
|
|
218
242
|
}
|
|
219
243
|
return windows;
|
|
@@ -247,21 +271,41 @@ export function parseGitHubCopilotUsage(data: any): QuotaWindow[] {
|
|
|
247
271
|
// Helper functions for OpenRouter date calculations (UTC-based)
|
|
248
272
|
function calculateNextMidnightUTC(): Date {
|
|
249
273
|
const now = new Date();
|
|
250
|
-
const midnight = new Date(
|
|
274
|
+
const midnight = new Date(
|
|
275
|
+
Date.UTC(
|
|
276
|
+
now.getUTCFullYear(),
|
|
277
|
+
now.getUTCMonth(),
|
|
278
|
+
now.getUTCDate() + 1,
|
|
279
|
+
0,
|
|
280
|
+
0,
|
|
281
|
+
0,
|
|
282
|
+
),
|
|
283
|
+
);
|
|
251
284
|
return midnight;
|
|
252
285
|
}
|
|
253
286
|
|
|
254
287
|
function calculateNextMondayUTC(): Date {
|
|
255
288
|
const now = new Date();
|
|
256
289
|
const day = now.getUTCDay(); // 0 = Sunday, 1 = Monday, etc.
|
|
257
|
-
const daysUntilMonday = day === 0 ? 1 :
|
|
258
|
-
const monday = new Date(
|
|
290
|
+
const daysUntilMonday = day === 0 ? 1 : 8 - day; // Days until next Monday
|
|
291
|
+
const monday = new Date(
|
|
292
|
+
Date.UTC(
|
|
293
|
+
now.getUTCFullYear(),
|
|
294
|
+
now.getUTCMonth(),
|
|
295
|
+
now.getUTCDate() + daysUntilMonday,
|
|
296
|
+
0,
|
|
297
|
+
0,
|
|
298
|
+
0,
|
|
299
|
+
),
|
|
300
|
+
);
|
|
259
301
|
return monday;
|
|
260
302
|
}
|
|
261
303
|
|
|
262
304
|
function calculateNextMonthStartUTC(): Date {
|
|
263
305
|
const now = new Date();
|
|
264
|
-
return new Date(
|
|
306
|
+
return new Date(
|
|
307
|
+
Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0),
|
|
308
|
+
);
|
|
265
309
|
}
|
|
266
310
|
|
|
267
311
|
export function parseOpenRouterUsage(data: any): QuotaWindow[] {
|
|
@@ -364,7 +408,6 @@ export function parseSyntheticUsage(data: any): QuotaWindow[] {
|
|
|
364
408
|
|
|
365
409
|
// Weekly token/credit limit (primary window for paid plans)
|
|
366
410
|
if (data?.weeklyTokenLimit) {
|
|
367
|
-
const { weeklyTokenLimit } = data.weeklyTokenLimit;
|
|
368
411
|
const limitValue = parseCurrency(data.weeklyTokenLimit.maxCredits);
|
|
369
412
|
const remainingValue = parseCurrency(data.weeklyTokenLimit.remainingCredits);
|
|
370
413
|
windows.push({
|
|
@@ -385,7 +428,8 @@ export function parseSyntheticUsage(data: any): QuotaWindow[] {
|
|
|
385
428
|
|
|
386
429
|
// Rolling 5-hour request limit
|
|
387
430
|
if (data?.rollingFiveHourLimit && data.rollingFiveHourLimit.max > 0) {
|
|
388
|
-
const used =
|
|
431
|
+
const used =
|
|
432
|
+
data.rollingFiveHourLimit.max - data.rollingFiveHourLimit.remaining;
|
|
389
433
|
windows.push({
|
|
390
434
|
provider: "synthetic",
|
|
391
435
|
label: "Requests / 5h",
|
|
@@ -405,7 +449,10 @@ export function parseSyntheticUsage(data: any): QuotaWindow[] {
|
|
|
405
449
|
windows.push({
|
|
406
450
|
provider: "synthetic",
|
|
407
451
|
label: "Search / hour",
|
|
408
|
-
usedPercent: safePercent(
|
|
452
|
+
usedPercent: safePercent(
|
|
453
|
+
data.search.hourly.requests,
|
|
454
|
+
data.search.hourly.limit,
|
|
455
|
+
),
|
|
409
456
|
resetsAt: parseDateish(data.search.hourly.renewsAt),
|
|
410
457
|
windowSeconds: 60 * 60,
|
|
411
458
|
usedValue: data.search.hourly.requests,
|
|
@@ -421,7 +468,10 @@ export function parseSyntheticUsage(data: any): QuotaWindow[] {
|
|
|
421
468
|
windows.push({
|
|
422
469
|
provider: "synthetic",
|
|
423
470
|
label: "Free Tool Calls / day",
|
|
424
|
-
usedPercent: safePercent(
|
|
471
|
+
usedPercent: safePercent(
|
|
472
|
+
data.freeToolCalls.requests,
|
|
473
|
+
data.freeToolCalls.limit,
|
|
474
|
+
),
|
|
425
475
|
resetsAt: parseDateish(data.freeToolCalls.renewsAt),
|
|
426
476
|
windowSeconds: 24 * 60 * 60,
|
|
427
477
|
usedValue: data.freeToolCalls.requests,
|
|
@@ -434,3 +484,161 @@ export function parseSyntheticUsage(data: any): QuotaWindow[] {
|
|
|
434
484
|
|
|
435
485
|
return windows;
|
|
436
486
|
}
|
|
487
|
+
|
|
488
|
+
export function parseOpenCodeGoUsage(data: {
|
|
489
|
+
rolling?: {
|
|
490
|
+
usagePercent: number;
|
|
491
|
+
resetInSec: number;
|
|
492
|
+
percentRemaining: number;
|
|
493
|
+
resetTimeIso: string;
|
|
494
|
+
};
|
|
495
|
+
weekly?: {
|
|
496
|
+
usagePercent: number;
|
|
497
|
+
resetInSec: number;
|
|
498
|
+
percentRemaining: number;
|
|
499
|
+
resetTimeIso: string;
|
|
500
|
+
};
|
|
501
|
+
monthly?: {
|
|
502
|
+
usagePercent: number;
|
|
503
|
+
resetInSec: number;
|
|
504
|
+
percentRemaining: number;
|
|
505
|
+
resetTimeIso: string;
|
|
506
|
+
};
|
|
507
|
+
}): QuotaWindow[] {
|
|
508
|
+
const windows: QuotaWindow[] = [];
|
|
509
|
+
|
|
510
|
+
if (data.rolling) {
|
|
511
|
+
windows.push({
|
|
512
|
+
provider: "opencode-go",
|
|
513
|
+
label: "5h Rolling",
|
|
514
|
+
usedPercent: data.rolling.usagePercent,
|
|
515
|
+
resetsAt: new Date(data.rolling.resetTimeIso),
|
|
516
|
+
windowSeconds: 5 * 60 * 60,
|
|
517
|
+
usedValue: data.rolling.usagePercent,
|
|
518
|
+
limitValue: 100,
|
|
519
|
+
showPace: false,
|
|
520
|
+
nextLabel: "Resets",
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (data.weekly) {
|
|
525
|
+
windows.push({
|
|
526
|
+
provider: "opencode-go",
|
|
527
|
+
label: "Weekly",
|
|
528
|
+
usedPercent: data.weekly.usagePercent,
|
|
529
|
+
resetsAt: new Date(data.weekly.resetTimeIso),
|
|
530
|
+
windowSeconds: 7 * 24 * 60 * 60,
|
|
531
|
+
usedValue: data.weekly.usagePercent,
|
|
532
|
+
limitValue: 100,
|
|
533
|
+
showPace: true,
|
|
534
|
+
paceScale: 1 / 7,
|
|
535
|
+
nextLabel: "Resets",
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (data.monthly) {
|
|
540
|
+
windows.push({
|
|
541
|
+
provider: "opencode-go",
|
|
542
|
+
label: "Monthly",
|
|
543
|
+
usedPercent: data.monthly.usagePercent,
|
|
544
|
+
resetsAt: new Date(data.monthly.resetTimeIso),
|
|
545
|
+
windowSeconds: 30 * 24 * 60 * 60,
|
|
546
|
+
usedValue: data.monthly.usagePercent,
|
|
547
|
+
limitValue: 100,
|
|
548
|
+
showPace: true,
|
|
549
|
+
paceScale: 1,
|
|
550
|
+
nextLabel: "Resets",
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return windows;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Z.ai (Zhipu AI) GLM Coding Plan quotas.
|
|
558
|
+
//
|
|
559
|
+
// The quota endpoint returns { data: { limits: [...], level } }. Each entry in
|
|
560
|
+
// `limits` is either a TOKENS_LIMIT (token utilisation, reported as a bare
|
|
561
|
+
// `percentage` with no absolute used/limit counts) or a TIME_LIMIT (a monthly
|
|
562
|
+
// count window such as web searches, which does carry real used/limit counts).
|
|
563
|
+
//
|
|
564
|
+
// The window length is encoded as (unit, number). Observed values:
|
|
565
|
+
// unit 3 = HOUR (e.g. the rolling 5-hour session window)
|
|
566
|
+
// unit 6 = WEEK (e.g. the rolling 7-day weekly window)
|
|
567
|
+
// unit 5 = MONTH (TIME_LIMIT only, the monthly count window)
|
|
568
|
+
// Reset times are epoch milliseconds.
|
|
569
|
+
export function parseZaiUsage(data: any): QuotaWindow[] {
|
|
570
|
+
const collected: QuotaWindow[] = [];
|
|
571
|
+
|
|
572
|
+
const limits: any[] = data?.data?.limits ?? data?.limits ?? [];
|
|
573
|
+
if (!Array.isArray(limits)) return collected;
|
|
574
|
+
|
|
575
|
+
for (const entry of limits) {
|
|
576
|
+
if (!entry || typeof entry !== "object") continue;
|
|
577
|
+
|
|
578
|
+
// Token windows only expose a percentage, so — like Anthropic/Codex — we
|
|
579
|
+
// report usedValue as the percentage against a nominal limit of 100.
|
|
580
|
+
if (entry.type === "TOKENS_LIMIT") {
|
|
581
|
+
const unit = entry.unit;
|
|
582
|
+
const count = Number(entry.number ?? 1) || 1;
|
|
583
|
+
let label: string;
|
|
584
|
+
let windowSeconds: number;
|
|
585
|
+
|
|
586
|
+
switch (unit) {
|
|
587
|
+
case 3: // HOUR
|
|
588
|
+
label = `${count}h`;
|
|
589
|
+
windowSeconds = count * 60 * 60;
|
|
590
|
+
break;
|
|
591
|
+
case 4: // DAY (defensive — not observed, but handled)
|
|
592
|
+
label = `${count}d`;
|
|
593
|
+
windowSeconds = count * 24 * 60 * 60;
|
|
594
|
+
break;
|
|
595
|
+
case 6: // WEEK
|
|
596
|
+
label = `${count * 7}d`;
|
|
597
|
+
windowSeconds = count * 7 * 24 * 60 * 60;
|
|
598
|
+
break;
|
|
599
|
+
default:
|
|
600
|
+
// Unknown unit — still surface it so usage is never silently hidden.
|
|
601
|
+
label = "Tokens";
|
|
602
|
+
windowSeconds = 0;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
collected.push({
|
|
607
|
+
provider: "zai",
|
|
608
|
+
label,
|
|
609
|
+
usedPercent: Number(entry.percentage ?? 0),
|
|
610
|
+
resetsAt: parseDateish(entry.nextResetTime),
|
|
611
|
+
windowSeconds,
|
|
612
|
+
usedValue: Number(entry.percentage ?? 0),
|
|
613
|
+
limitValue: 100,
|
|
614
|
+
showPace: false,
|
|
615
|
+
nextLabel: "Resets",
|
|
616
|
+
});
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Monthly web-search / tool-call count window. Here z.ai gives real
|
|
621
|
+
// counts: `usage` is the entitlement, `currentValue` is what's been used.
|
|
622
|
+
if (entry.type === "TIME_LIMIT") {
|
|
623
|
+
const limit = Number(entry.usage ?? 0);
|
|
624
|
+
const used = Number(entry.currentValue ?? 0);
|
|
625
|
+
if (limit <= 0) continue;
|
|
626
|
+
|
|
627
|
+
collected.push({
|
|
628
|
+
provider: "zai",
|
|
629
|
+
label: "Web / month",
|
|
630
|
+
usedPercent: safePercent(used, limit),
|
|
631
|
+
resetsAt: parseDateish(entry.nextResetTime),
|
|
632
|
+
windowSeconds: 30 * 24 * 60 * 60,
|
|
633
|
+
usedValue: used,
|
|
634
|
+
limitValue: limit,
|
|
635
|
+
showPace: false,
|
|
636
|
+
nextLabel: "Resets",
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Shortest window first (5h → 7d → month), matching Anthropic/Codex order.
|
|
642
|
+
collected.sort((a, b) => a.windowSeconds - b.windowSeconds);
|
|
643
|
+
return collected;
|
|
644
|
+
}
|
package/src/types/quotas.ts
CHANGED
|
@@ -3,17 +3,26 @@ export type SupportedQuotaProvider =
|
|
|
3
3
|
| "openai-codex"
|
|
4
4
|
| "github-copilot"
|
|
5
5
|
| "openrouter"
|
|
6
|
-
| "synthetic"
|
|
6
|
+
| "synthetic"
|
|
7
|
+
| "zai"
|
|
8
|
+
| "opencode-go";
|
|
7
9
|
|
|
8
10
|
export type QuotasErrorKind =
|
|
9
11
|
| "cancelled"
|
|
10
12
|
| "timeout"
|
|
11
13
|
| "config"
|
|
12
14
|
| "http"
|
|
13
|
-
| "network"
|
|
15
|
+
| "network"
|
|
16
|
+
// The provider is not applicable for the stored credential type
|
|
17
|
+
// (e.g. a direct Anthropic API key has no OAuth subscription usage to
|
|
18
|
+
// report). Consumers should render this silently rather than as a warning.
|
|
19
|
+
| "not_applicable";
|
|
14
20
|
|
|
15
21
|
export type QuotasResult =
|
|
16
|
-
| {
|
|
22
|
+
| {
|
|
23
|
+
success: true;
|
|
24
|
+
data: { windows: QuotaWindow[]; provider: SupportedQuotaProvider };
|
|
25
|
+
}
|
|
17
26
|
| { success: false; error: { message: string; kind: QuotasErrorKind } };
|
|
18
27
|
|
|
19
28
|
export interface QuotaWindow {
|
|
@@ -53,7 +53,10 @@ export function getProjectedPercent(
|
|
|
53
53
|
return Math.max(0, (usedPercent / effectivePace) * 100);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function absoluteUsageSeverity(
|
|
56
|
+
function absoluteUsageSeverity(
|
|
57
|
+
window: QuotaWindow,
|
|
58
|
+
percent: number,
|
|
59
|
+
): RiskSeverity {
|
|
57
60
|
if (window.limited || percent >= 100) return "critical";
|
|
58
61
|
if (percent >= 90) return "high";
|
|
59
62
|
if (percent >= 80) return "warning";
|
|
@@ -139,8 +142,17 @@ export function formatTimeRemaining(date: Date): string {
|
|
|
139
142
|
const ms = date.getTime() - Date.now();
|
|
140
143
|
if (ms <= 0) return "now";
|
|
141
144
|
const totalMins = Math.ceil(ms / (1000 * 60));
|
|
142
|
-
const
|
|
145
|
+
const totalHours = Math.floor(totalMins / 60);
|
|
146
|
+
const days = Math.floor(totalHours / 24);
|
|
147
|
+
const hours = totalHours % 24;
|
|
143
148
|
const mins = totalMins % 60;
|
|
149
|
+
|
|
150
|
+
if (days >= 1) {
|
|
151
|
+
const parts: string[] = [`${days}d`];
|
|
152
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
153
|
+
if (mins > 0) parts.push(`${mins}m`);
|
|
154
|
+
return parts.join("");
|
|
155
|
+
}
|
|
144
156
|
if (hours >= 1) return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
|
|
145
157
|
const totalSecs = Math.ceil(ms / 1000);
|
|
146
158
|
return totalMins >= 1 ? `${totalMins}m` : `${totalSecs}s`;
|