@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.
@@ -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: { utilization: 23, resets_at: "2026-04-26T23:00:00Z" },
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: { used_percent: 10, reset_at: 1776880800, limit_window_seconds: 18000 },
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: { used_percent: 10, reset_at: 1776880800, limit_window_seconds: 18000 },
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: { used_percent: 10, reset_at: 1776880800, limit_window_seconds: 18000 },
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({ label: "Chat / month", usedPercent: 18 });
258
- expect(windows[1]).toMatchObject({ label: "Completions / month", usedPercent: 0 });
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(windows.find((w) => w.label === "Free Tool Calls / day")).toBeUndefined();
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(1, Math.round((resetAt.getTime() - approxStart.getTime()) / 1000));
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(extra.utilization ?? safePercent(usedDollars, limitDollars)),
83
- resetsAt: new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1),
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) return Math.max(0, 100 - Number(limit.percent_left));
99
- if (limit?.remaining_percent != null) return Math.max(0, 100 - Number(limit.remaining_percent));
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 = rateLimit.primary_window ?? rateLimit.primary ?? rateLimit.five_hour_limit ?? rateLimit.five_hour;
107
- const secondary = rateLimit.secondary_window ?? rateLimit.secondary ?? rateLimit.weekly_limit ?? rateLimit.weekly;
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(data?.quota_reset_date ?? data?.quota_reset_date_utc ?? data?.limited_user_reset_date);
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: overageCount > 0
213
- ? `+${overageCount} overage`
214
- : overagePermitted
215
- ? "overage allowed"
216
- : undefined,
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(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, 0, 0, 0));
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 : (8 - day); // Days until next Monday
258
- const monday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + daysUntilMonday, 0, 0, 0));
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(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0));
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 = data.rollingFiveHourLimit.max - data.rollingFiveHourLimit.remaining;
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(data.search.hourly.requests, data.search.hourly.limit),
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(data.freeToolCalls.requests, data.freeToolCalls.limit),
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
+ }
@@ -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
- | { success: true; data: { windows: QuotaWindow[]; provider: SupportedQuotaProvider } }
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(window: QuotaWindow, percent: number): RiskSeverity {
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 hours = Math.floor(totalMins / 60);
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`;