@latentminds/pi-quotas 0.2.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentminds/pi-quotas",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
package/src/config.ts CHANGED
@@ -152,7 +152,7 @@ const FEATURE_META: Array<{
152
152
  {
153
153
  id: "providerCommands",
154
154
  label: "Provider quota commands",
155
- description: "Toggle `/anthropic:quotas`, `/codex:quotas`, and `/github:quotas`",
155
+ description: "Toggle `/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`, and `/synthetic:quotas`",
156
156
  },
157
157
  {
158
158
  id: "usageStatus",
@@ -34,5 +34,11 @@ export function getProviderCommandInfo(
34
34
  commandName: "openrouter:quotas",
35
35
  title: "OpenRouter Quotas",
36
36
  };
37
+ case "synthetic":
38
+ return {
39
+ provider,
40
+ commandName: "synthetic:quotas",
41
+ title: "Synthetic Quotas",
42
+ };
37
43
  }
38
44
  }
@@ -1,5 +1,7 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { formatWindowStatus, type WindowStatus } from "./format-status.js";
3
+ import type { SupportedQuotaProvider } from "../../types/quotas.js";
4
+ import { formatStatus, toWindowStatus } from "./index.js";
3
5
 
4
6
  // Minimal fake theme that just returns text with markers for color assertions
5
7
  function fakeTheme() {
@@ -11,6 +13,10 @@ function fakeTheme() {
11
13
  describe("formatWindowStatus", () => {
12
14
  const theme = fakeTheme() as any;
13
15
 
16
+ afterEach(() => {
17
+ vi.useRealTimers();
18
+ });
19
+
14
20
  it("shows remaining/limit for windows with known limits (GitHub premium)", () => {
15
21
  const w: WindowStatus = {
16
22
  label: "Premium / month",
@@ -101,4 +107,102 @@ describe("formatWindowStatus", () => {
101
107
  const result = formatWindowStatus(theme, w);
102
108
  expect(result).toContain("[dim]5h:");
103
109
  });
110
+
111
+ it("renders footer reset times with minute precision for every provider", () => {
112
+ vi.useFakeTimers();
113
+ vi.setSystemTime(new Date("2026-05-06T05:28:37Z"));
114
+
115
+ const providers: Array<{ provider: SupportedQuotaProvider; label: string }> = [
116
+ { provider: "anthropic", label: "5h" },
117
+ { provider: "openai-codex", label: "7d" },
118
+ { provider: "github-copilot", label: "Premium / month" },
119
+ { provider: "openrouter", label: "Monthly Budget" },
120
+ { provider: "synthetic", label: "Subscription" },
121
+ ];
122
+
123
+ for (const { provider, label } of providers) {
124
+ const status = toWindowStatus({
125
+ provider,
126
+ label,
127
+ usedPercent: 50,
128
+ resetsAt: new Date("2026-05-06T07:47:37Z"),
129
+ windowSeconds: 5 * 60 * 60,
130
+ usedValue: 50,
131
+ limitValue: 100,
132
+ });
133
+
134
+ const result = formatStatus({ ui: { theme } } as any, [status]);
135
+
136
+ expect(result).toContain("(↺in 2h19m)");
137
+ expect(result).not.toContain("(↺in 3h)");
138
+ }
139
+ });
140
+
141
+ it("omits footer reset tags for windows without a real reset time", () => {
142
+ const result = formatStatus(
143
+ { ui: { theme } } as any,
144
+ [
145
+ {
146
+ label: "Spend cap",
147
+ usedPercent: 0,
148
+ severity: "none",
149
+ resetsAt: null,
150
+ limited: false,
151
+ usedValue: 0,
152
+ limitValue: 1,
153
+ },
154
+ ],
155
+ );
156
+
157
+ expect(result).toContain("cap:");
158
+ expect(result).not.toContain("↺");
159
+ expect(result).not.toContain("soon");
160
+ });
161
+
162
+ it("maps sentinel reset dates to null before rendering status for non-reset provider windows", () => {
163
+ const windows: Array<{ provider: SupportedQuotaProvider; label: string; isCurrency?: boolean }> = [
164
+ { provider: "openai-codex", label: "Spend cap" },
165
+ { provider: "openai-codex", label: "Credits", isCurrency: true },
166
+ { provider: "openrouter", label: "Credits Remaining", isCurrency: true },
167
+ ];
168
+
169
+ for (const { provider, label, isCurrency } of windows) {
170
+ const status = toWindowStatus({
171
+ provider,
172
+ label,
173
+ usedPercent: 0,
174
+ resetsAt: new Date(0),
175
+ windowSeconds: 0,
176
+ usedValue: 0,
177
+ limitValue: 1,
178
+ limited: false,
179
+ isCurrency,
180
+ });
181
+
182
+ expect(status.resetsAt).toBeNull();
183
+ }
184
+ });
185
+
186
+ it("does not prefix elapsed reset times with in", () => {
187
+ vi.useFakeTimers();
188
+ vi.setSystemTime(new Date("2026-05-06T05:28:37Z"));
189
+
190
+ const result = formatStatus(
191
+ { ui: { theme } } as any,
192
+ [
193
+ {
194
+ label: "5h",
195
+ usedPercent: 100,
196
+ severity: "critical",
197
+ resetsAt: "2026-05-06T05:28:37Z",
198
+ limited: false,
199
+ usedValue: 100,
200
+ limitValue: 100,
201
+ },
202
+ ],
203
+ );
204
+
205
+ expect(result).toContain("(↺now)");
206
+ expect(result).not.toContain("(↺in now)");
207
+ });
104
208
  });
@@ -11,28 +11,47 @@ import {
11
11
  } from "../../config.js";
12
12
  import {
13
13
  fetchProviderQuotas,
14
- formatResetTime,
15
14
  isSupportedProvider,
16
15
  } from "../../lib/quotas.js";
17
16
  import {
18
17
  assessWindow,
18
+ formatTimeRemaining,
19
19
  } from "../../utils/quotas-severity.js";
20
+ import type { QuotaWindow } from "../../types/quotas.js";
20
21
  import { formatWindowStatus, type WindowStatus } from "./format-status.js";
21
22
 
22
23
  const EXTENSION_ID = "pi-quotas-usage";
23
24
  const REFRESH_INTERVAL_MS = 60_000;
24
25
 
25
- function formatStatus(ctx: ExtensionContext, windows: WindowStatus[]): string {
26
+ function formatFooterResetTime(resetsAt: string): string {
27
+ const remaining = formatTimeRemaining(new Date(resetsAt));
28
+ return remaining === "now" ? "now" : `in ${remaining}`;
29
+ }
30
+
31
+ export function formatStatus(ctx: Pick<ExtensionContext, "ui">, windows: WindowStatus[]): string {
26
32
  const theme = ctx.ui.theme;
27
33
  return windows
28
34
  .map((w) => {
29
35
  const core = formatWindowStatus(theme, w);
30
- const reset = w.resetsAt ? theme.fg("dim", ` (↺${formatResetTime(w.resetsAt)})`) : "";
36
+ const reset = w.resetsAt ? theme.fg("dim", ` (↺${formatFooterResetTime(w.resetsAt)})`) : "";
31
37
  return `${core}${reset}`;
32
38
  })
33
39
  .join(" ");
34
40
  }
35
41
 
42
+ export function toWindowStatus(window: QuotaWindow): WindowStatus {
43
+ return {
44
+ label: window.label,
45
+ usedPercent: window.usedPercent,
46
+ severity: assessWindow(window).severity,
47
+ resetsAt: window.resetsAt.getTime() > 0 ? window.resetsAt.toISOString() : null,
48
+ limited: window.limited ?? false,
49
+ isCurrency: window.isCurrency,
50
+ usedValue: window.usedValue,
51
+ limitValue: window.limitValue,
52
+ };
53
+ }
54
+
36
55
  function createStatusRefresher() {
37
56
  let refreshTimer: ReturnType<typeof setInterval> | undefined;
38
57
  let activeContext: ExtensionContext | undefined;
@@ -54,16 +73,7 @@ function createStatusRefresher() {
54
73
  ctx.ui.setStatus(EXTENSION_ID, ctx.ui.theme.fg("warning", "usage unavailable"));
55
74
  return;
56
75
  }
57
- const windows: WindowStatus[] = result.data.windows.map((window) => ({
58
- label: window.label,
59
- usedPercent: window.usedPercent,
60
- severity: assessWindow(window).severity,
61
- resetsAt: window.resetsAt.toISOString(),
62
- limited: window.limited ?? false,
63
- isCurrency: window.isCurrency,
64
- usedValue: window.usedValue,
65
- limitValue: window.limitValue,
66
- }));
76
+ const windows: WindowStatus[] = result.data.windows.map(toWindowStatus);
67
77
  lastStatus = windows;
68
78
  ctx.ui.setStatus(EXTENSION_ID, formatStatus(ctx, windows));
69
79
  } catch {
package/src/lib/quotas.ts CHANGED
@@ -7,6 +7,7 @@ export const SUPPORTED_PROVIDERS: SupportedQuotaProvider[] = [
7
7
  "openai-codex",
8
8
  "github-copilot",
9
9
  "openrouter",
10
+ "synthetic",
10
11
  ];
11
12
 
12
13
  export const PROVIDER_LABELS: Record<SupportedQuotaProvider, string> = {
@@ -14,6 +15,7 @@ export const PROVIDER_LABELS: Record<SupportedQuotaProvider, string> = {
14
15
  "openai-codex": "OpenAI Codex",
15
16
  "github-copilot": "GitHub Copilot",
16
17
  openrouter: "OpenRouter",
18
+ synthetic: "Synthetic",
17
19
  };
18
20
 
19
21
  const PROVIDER_TTLS_MS: Record<SupportedQuotaProvider, number> = {
@@ -21,6 +23,7 @@ const PROVIDER_TTLS_MS: Record<SupportedQuotaProvider, number> = {
21
23
  "openai-codex": 60_000,
22
24
  "github-copilot": 5 * 60_000,
23
25
  openrouter: 60_000,
26
+ synthetic: 60_000,
24
27
  };
25
28
 
26
29
  type CacheEntry = {
@@ -9,6 +9,7 @@ import {
9
9
  parseCodexUsage,
10
10
  parseGitHubCopilotUsage,
11
11
  parseOpenRouterUsage,
12
+ parseSyntheticUsage,
12
13
  } from "./providers.js";
13
14
 
14
15
  const FETCH_TIMEOUT_MS = 15_000;
@@ -268,9 +269,28 @@ export async function fetchOpenRouterQuotas(
268
269
  );
269
270
  }
270
271
 
272
+ export async function fetchSyntheticQuotas(
273
+ _authStorage: AuthStorage,
274
+ signal?: AbortSignal,
275
+ ): Promise<QuotasResult> {
276
+ const apiKey = process.env.SYNTHETIC_API_KEY;
277
+ if (!apiKey) return failure("No Synthetic API key found (set SYNTHETIC_API_KEY)", "config");
278
+
279
+ const result = await fetchJson(
280
+ "https://api.synthetic.new/v2/quotas",
281
+ {
282
+ headers: { Authorization: `Bearer ${apiKey}` },
283
+ },
284
+ signal,
285
+ );
286
+ if (!result.ok) return failure(result.message, result.kind);
287
+ return success("synthetic", parseSyntheticUsage(result.data));
288
+ }
289
+
271
290
  export const PROVIDER_FETCHERS = {
272
291
  anthropic: fetchAnthropicQuotas,
273
292
  "openai-codex": fetchCodexQuotas,
274
293
  "github-copilot": fetchGitHubCopilotQuotas,
275
294
  openrouter: fetchOpenRouterQuotas,
295
+ synthetic: fetchSyntheticQuotas,
276
296
  } as const;
@@ -352,3 +352,88 @@ export function parseOpenRouterUsage(data: any): QuotaWindow[] {
352
352
 
353
353
  return windows;
354
354
  }
355
+
356
+ export function parseSyntheticUsage(data: any): QuotaWindow[] {
357
+ const windows: QuotaWindow[] = [];
358
+
359
+ // Subscription (requests)
360
+ if (data?.subscription) {
361
+ windows.push({
362
+ provider: "synthetic",
363
+ label: "Subscription",
364
+ usedPercent: safePercent(data.subscription.requests, data.subscription.limit),
365
+ resetsAt: parseDateish(data.subscription.renewsAt),
366
+ windowSeconds: 30 * 24 * 60 * 60,
367
+ usedValue: data.subscription.requests,
368
+ limitValue: data.subscription.limit,
369
+ showPace: true,
370
+ nextLabel: "Resets",
371
+ });
372
+ }
373
+
374
+ // Search hourly
375
+ if (data?.search?.hourly) {
376
+ windows.push({
377
+ provider: "synthetic",
378
+ label: "Search / hour",
379
+ usedPercent: safePercent(data.search.hourly.requests, data.search.hourly.limit),
380
+ resetsAt: parseDateish(data.search.hourly.renewsAt),
381
+ windowSeconds: 60 * 60,
382
+ usedValue: data.search.hourly.requests,
383
+ limitValue: data.search.hourly.limit,
384
+ showPace: false,
385
+ nextLabel: "Resets",
386
+ });
387
+ }
388
+
389
+ // Free tool calls
390
+ if (data?.freeToolCalls) {
391
+ windows.push({
392
+ provider: "synthetic",
393
+ label: "Free Tools",
394
+ usedPercent: safePercent(data.freeToolCalls.requests, data.freeToolCalls.limit),
395
+ resetsAt: parseDateish(data.freeToolCalls.renewsAt),
396
+ windowSeconds: 30 * 24 * 60 * 60,
397
+ usedValue: data.freeToolCalls.requests,
398
+ limitValue: data.freeToolCalls.limit,
399
+ showPace: true,
400
+ nextLabel: "Resets",
401
+ });
402
+ }
403
+
404
+ // Weekly token limit
405
+ if (data?.weeklyTokenLimit) {
406
+ const maxCredits = parseFloat(data.weeklyTokenLimit.maxCredits) || 0;
407
+ const remainingCredits = parseFloat(data.weeklyTokenLimit.remainingCredits) || 0;
408
+ windows.push({
409
+ provider: "synthetic",
410
+ label: "Weekly Tokens",
411
+ usedPercent: data.weeklyTokenLimit.percentRemaining ?? safePercent(remainingCredits, maxCredits),
412
+ resetsAt: parseDateish(data.weeklyTokenLimit.nextRegenAt),
413
+ windowSeconds: 7 * 24 * 60 * 60,
414
+ usedValue: maxCredits - remainingCredits,
415
+ limitValue: maxCredits,
416
+ isCurrency: true,
417
+ showPace: true,
418
+ nextLabel: "Regen",
419
+ });
420
+ }
421
+
422
+ // Rolling 5-hour limit
423
+ if (data?.rollingFiveHourLimit) {
424
+ windows.push({
425
+ provider: "synthetic",
426
+ label: "5h Limit",
427
+ usedPercent: data.rollingFiveHourLimit.tickPercent,
428
+ resetsAt: parseDateish(data.rollingFiveHourLimit.nextTickAt),
429
+ windowSeconds: 5 * 60 * 60,
430
+ usedValue: data.rollingFiveHourLimit.max - data.rollingFiveHourLimit.remaining,
431
+ limitValue: data.rollingFiveHourLimit.max,
432
+ limited: data.rollingFiveHourLimit.limited,
433
+ showPace: false,
434
+ nextLabel: data.rollingFiveHourLimit.limited ? "Limited" : "Resets",
435
+ });
436
+ }
437
+
438
+ return windows;
439
+ }
@@ -2,7 +2,8 @@ export type SupportedQuotaProvider =
2
2
  | "anthropic"
3
3
  | "openai-codex"
4
4
  | "github-copilot"
5
- | "openrouter";
5
+ | "openrouter"
6
+ | "synthetic";
6
7
 
7
8
  export type QuotasErrorKind =
8
9
  | "cancelled"