@latentminds/pi-quotas 0.1.1 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @latentminds/pi-quotas
2
2
 
3
- Quota monitoring for the [Pi coding agent](https://github.com/mariozechner/pi). Shows remaining usage and rate limits for Anthropic, OpenAI Codex, and GitHub Copilot — directly in your Pi session.
3
+ Quota monitoring for the [Pi coding agent](https://github.com/mariozechner/pi). Shows remaining usage and rate limits for Anthropic, OpenAI Codex, GitHub Copilot, and OpenRouter — directly in your Pi session.
4
4
 
5
5
  ## Screenshots
6
6
 
@@ -37,6 +37,7 @@ pi -e npm:@latentminds/pi-quotas
37
37
  | `/anthropic:quotas` | Anthropic quotas only |
38
38
  | `/codex:quotas` | OpenAI Codex quotas only |
39
39
  | `/github:quotas` | GitHub Copilot quotas only |
40
+ | `/openrouter:quotas` | OpenRouter quotas only |
40
41
  | `/quotas:settings` | Toggle individual features on or off |
41
42
 
42
43
  ## Features
@@ -47,7 +48,7 @@ Run `/quotas` to open a bordered TUI view showing all providers side by side, wi
47
48
 
48
49
  ### Footer status widget
49
50
 
50
- When your active model is from a supported provider, the Pi footer shows real-time quota headroom updated every 60 seconds and on each turn. Colours shift from green → amber → red as usage climbs.
51
+ When your active model is from a supported provider, the Pi footer shows real-time quota headroom - updated every 60 seconds and on each turn. Colours shift from green → amber → red as usage climbs.
51
52
 
52
53
  ### Quota warnings
53
54
 
@@ -57,7 +58,7 @@ Automatic notifications when projected usage is on track to exceed limits before
57
58
 
58
59
  Use `/quotas:settings` to enable or disable:
59
60
  - Combined `/quotas` command
60
- - Per-provider commands (`/anthropic:quotas`, `/codex:quotas`, `/github:quotas`)
61
+ - Per-provider commands (`/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`)
61
62
  - Footer status widget
62
63
  - Quota warning notifications
63
64
 
@@ -70,6 +71,7 @@ Settings can be saved globally (`~/.pi/agent/extensions/quotas.json`) or per-pro
70
71
  | Anthropic | 5h, 7d, per-model 7d, extra usage | Utilization percentages; optional overage budget in local currency |
71
72
  | OpenAI Codex | 5h, 7d, credits, spend cap | Rate-limit percentages; credit balance; spend-cap reached/OK |
72
73
  | GitHub Copilot | Premium/chat/completions per month | Remaining/entitlement counts with overage indicators |
74
+ | OpenRouter | Monthly budget, daily/weekly/monthly usage | USD spending tracking with cents precision; optional per-key budget limits; UTC-based period resets |
73
75
 
74
76
  ## Credentials
75
77
 
@@ -78,17 +80,26 @@ pi-quotas reads existing Pi auth entries from `~/.pi/agent/auth.json`:
78
80
  - `anthropic` — Anthropic OAuth token
79
81
  - `openai-codex` — Codex access token (also reads `~/.codex/auth.json` for the account ID)
80
82
  - `github-copilot` — GitHub Copilot OAuth token (falls back to `gh auth token` if needed)
83
+ - `openrouter` — OpenRouter API key (Bearer token)
81
84
 
82
- No additional setup is required if Pi can use the provider, pi-quotas can check its quotas.
85
+ No additional setup is required - if Pi can use the provider, pi-quotas can check its quotas.
83
86
 
84
87
  ## Requirements
85
88
 
86
89
  - [Pi](https://github.com/mariozechner/pi) >= 0.61.0
87
90
 
91
+ ## Changelog
92
+
93
+ See [CHANGELOG.md](CHANGELOG.md) for release notes and recent changes.
94
+
88
95
  ## License
89
96
 
90
97
  [MIT](LICENSE) © Latent Minds Pty Ltd
91
98
 
99
+ ## Acknowledgements
100
+
101
+ This project was inspired by [@aliou/pi-synthetic](https://www.npmjs.com/package/@aliou/pi-synthetic).
102
+
92
103
  <p align="center">
93
104
  <img src="docs/latent-minds@2x.png" alt="Latent Minds" width="320" />
94
105
  </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentminds/pi-quotas",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -65,7 +65,7 @@ async function openQuotaView(
65
65
 
66
66
  export function registerQuotasCommands(pi: ExtensionAPI): void {
67
67
  pi.registerCommand("quotas", {
68
- description: "Display remaining quotas for Anthropic, Codex, and GitHub Copilot",
68
+ description: "Display remaining quotas for Anthropic, Codex, GitHub Copilot, and OpenRouter",
69
69
  handler: async (_args, ctx) => {
70
70
  if (!configLoader.getConfig().quotasCommand) {
71
71
  ctx.ui.notify("/quotas is disabled. Re-enable it in /quotas:settings.", "warning");
@@ -162,7 +162,12 @@ export class QuotasComponent implements Component {
162
162
  // Format the usage string depending on window type
163
163
  let usedStr: string;
164
164
  if (window.isCurrency) {
165
- usedStr = `$${window.usedValue.toFixed(2)} / $${window.limitValue.toFixed(2)}`;
165
+ // Tracking-only windows have limitValue=0, show just usage
166
+ if (window.limitValue === 0) {
167
+ usedStr = `$${window.usedValue.toFixed(2)} used`;
168
+ } else {
169
+ usedStr = `$${window.usedValue.toFixed(2)} / $${window.limitValue.toFixed(2)}`;
170
+ }
166
171
  } else if (window.limitValue <= 1 && window.label === "Spend cap") {
167
172
  usedStr = window.limited ? "REACHED" : "OK";
168
173
  } else if (window.limitValue > 0 && window.limitValue !== 100) {
@@ -31,4 +31,13 @@ describe("getProviderCommandInfo", () => {
31
31
  title: "GitHub Copilot Quotas",
32
32
  });
33
33
  });
34
+
35
+ it("maps openrouter to openrouter:quotas", () => {
36
+ const info = getProviderCommandInfo("openrouter");
37
+ expect(info).toMatchObject<Partial<ProviderCommandInfo>>({
38
+ provider: "openrouter",
39
+ commandName: "openrouter:quotas",
40
+ title: "OpenRouter Quotas",
41
+ });
42
+ });
34
43
  });
@@ -28,5 +28,11 @@ export function getProviderCommandInfo(
28
28
  commandName: "github:quotas",
29
29
  title: "GitHub Copilot Quotas",
30
30
  };
31
+ case "openrouter":
32
+ return {
33
+ provider,
34
+ commandName: "openrouter:quotas",
35
+ title: "OpenRouter Quotas",
36
+ };
31
37
  }
32
38
  }
@@ -53,7 +53,7 @@ describe("formatWindowStatus", () => {
53
53
  limitValue: 300,
54
54
  };
55
55
  const result = formatWindowStatus(theme, w);
56
- expect(result).toContain("$215/$300");
56
+ expect(result).toContain("$215.00/$300.00");
57
57
  expect(result).toContain("[warning]");
58
58
  });
59
59
 
@@ -31,6 +31,12 @@ const SHORT_LABELS: Record<string, string> = {
31
31
  "Extra (USD)": "extra",
32
32
  "Extra (EUR)": "extra",
33
33
  "Extra (GBP)": "extra",
34
+ // OpenRouter labels
35
+ "Monthly Budget": "budget",
36
+ "Credits Remaining": "credits",
37
+ "Daily": "daily",
38
+ "Weekly": "weekly",
39
+ "Monthly": "monthly",
34
40
  };
35
41
 
36
42
  /**
@@ -66,7 +72,12 @@ export function formatWindowStatus(theme: ThemeLike, w: WindowStatus): string {
66
72
  if (w.label === "Spend cap") {
67
73
  valueText = theme.fg(color, w.limited ? "REACHED" : "OK");
68
74
  } else if (w.isCurrency && w.usedValue != null && w.limitValue != null) {
69
- valueText = theme.fg(color, `$${w.usedValue.toFixed(0)}/$${w.limitValue.toFixed(0)}`);
75
+ // Tracking-only windows have limitValue=0, show just usage
76
+ if (w.limitValue === 0) {
77
+ valueText = theme.fg(color, `$${w.usedValue.toFixed(2)} used`);
78
+ } else {
79
+ valueText = theme.fg(color, `$${w.usedValue.toFixed(2)}/$${w.limitValue.toFixed(2)}`);
80
+ }
70
81
  } else if (hasRealCounts(w)) {
71
82
  const remaining = Math.max(0, Math.round(w.limitValue! - w.usedValue!));
72
83
  valueText = theme.fg(color, `${remaining}/${w.limitValue}`);
package/src/lib/quotas.ts CHANGED
@@ -6,18 +6,21 @@ export const SUPPORTED_PROVIDERS: SupportedQuotaProvider[] = [
6
6
  "anthropic",
7
7
  "openai-codex",
8
8
  "github-copilot",
9
+ "openrouter",
9
10
  ];
10
11
 
11
12
  export const PROVIDER_LABELS: Record<SupportedQuotaProvider, string> = {
12
13
  anthropic: "Anthropic",
13
14
  "openai-codex": "OpenAI Codex",
14
15
  "github-copilot": "GitHub Copilot",
16
+ openrouter: "OpenRouter",
15
17
  };
16
18
 
17
19
  const PROVIDER_TTLS_MS: Record<SupportedQuotaProvider, number> = {
18
20
  anthropic: 5 * 60_000,
19
21
  "openai-codex": 60_000,
20
22
  "github-copilot": 5 * 60_000,
23
+ openrouter: 60_000,
21
24
  };
22
25
 
23
26
  type CacheEntry = {
@@ -3,6 +3,7 @@ import {
3
3
  fetchAnthropicQuotasWithToken,
4
4
  fetchCodexQuotasWithToken,
5
5
  fetchGitHubCopilotQuotasWithToken,
6
+ fetchOpenRouterQuotasWithToken,
6
7
  } from "./fetch.js";
7
8
 
8
9
  const originalFetch = globalThis.fetch;
@@ -135,3 +136,62 @@ describe("fetchGitHubCopilotQuotasWithToken", () => {
135
136
  expect(globalThis.fetch).toHaveBeenCalledTimes(2);
136
137
  });
137
138
  });
139
+
140
+ describe("fetchOpenRouterQuotasWithToken", () => {
141
+ it("returns config error when token missing", async () => {
142
+ const result = await fetchOpenRouterQuotasWithToken(undefined);
143
+ expect(result).toMatchObject({
144
+ success: false,
145
+ error: { kind: "config" },
146
+ });
147
+ });
148
+
149
+ it("fetches and parses OpenRouter key info with budget", async () => {
150
+ globalThis.fetch = vi.fn().mockResolvedValue(
151
+ new Response(
152
+ JSON.stringify({
153
+ data: {
154
+ label: "Test Key",
155
+ limit: 50,
156
+ limit_remaining: 35,
157
+ limit_reset: "monthly",
158
+ usage: 15,
159
+ usage_daily: 2.5,
160
+ usage_weekly: 12,
161
+ usage_monthly: 15,
162
+ byok_usage: 0,
163
+ byok_usage_daily: 0,
164
+ byok_usage_weekly: 0,
165
+ byok_usage_monthly: 0,
166
+ is_free_tier: false,
167
+ },
168
+ }),
169
+ { status: 200 },
170
+ ),
171
+ ) as any;
172
+
173
+ const result = await fetchOpenRouterQuotasWithToken("sk-or-test");
174
+ expect(result.success).toBe(true);
175
+ if (result.success) {
176
+ expect(result.data.provider).toBe("openrouter");
177
+ expect(result.data.windows).toHaveLength(4);
178
+ expect(result.data.windows[0]).toMatchObject({
179
+ label: "Monthly Budget",
180
+ usedValue: 15,
181
+ limitValue: 50,
182
+ });
183
+ }
184
+ });
185
+
186
+ it("handles HTTP error", async () => {
187
+ globalThis.fetch = vi.fn().mockResolvedValue(
188
+ new Response("Unauthorized", { status: 401 }),
189
+ ) as any;
190
+
191
+ const result = await fetchOpenRouterQuotasWithToken("bad-key");
192
+ expect(result).toMatchObject({
193
+ success: false,
194
+ error: { kind: "http" },
195
+ });
196
+ });
197
+ });
@@ -8,6 +8,7 @@ import {
8
8
  parseAnthropicUsage,
9
9
  parseCodexUsage,
10
10
  parseGitHubCopilotUsage,
11
+ parseOpenRouterUsage,
11
12
  } from "./providers.js";
12
13
 
13
14
  const FETCH_TIMEOUT_MS = 15_000;
@@ -238,8 +239,38 @@ export async function fetchGitHubCopilotQuotas(
238
239
  );
239
240
  }
240
241
 
242
+ export async function fetchOpenRouterQuotasWithToken(
243
+ accessToken: string | undefined,
244
+ signal?: AbortSignal,
245
+ ): Promise<QuotasResult> {
246
+ if (!accessToken) return failure("No OpenRouter API key found", "config");
247
+ const result = await fetchJson(
248
+ "https://openrouter.ai/api/v1/key",
249
+ {
250
+ headers: {
251
+ Authorization: `Bearer ${accessToken}`,
252
+ Accept: "application/json",
253
+ },
254
+ },
255
+ signal,
256
+ );
257
+ if (!result.ok) return failure(result.message, result.kind);
258
+ return success("openrouter", parseOpenRouterUsage(result.data));
259
+ }
260
+
261
+ export async function fetchOpenRouterQuotas(
262
+ authStorage: AuthStorage,
263
+ signal?: AbortSignal,
264
+ ): Promise<QuotasResult> {
265
+ return fetchOpenRouterQuotasWithToken(
266
+ await providerAccessToken(authStorage, "openrouter"),
267
+ signal,
268
+ );
269
+ }
270
+
241
271
  export const PROVIDER_FETCHERS = {
242
272
  anthropic: fetchAnthropicQuotas,
243
273
  "openai-codex": fetchCodexQuotas,
244
274
  "github-copilot": fetchGitHubCopilotQuotas,
275
+ openrouter: fetchOpenRouterQuotas,
245
276
  } as const;
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import { parseAnthropicUsage } from "./providers.js";
3
3
  import { parseCodexUsage } from "./providers.js";
4
4
  import { parseGitHubCopilotUsage } from "./providers.js";
5
+ import { parseOpenRouterUsage } from "./providers.js";
5
6
 
6
7
  describe("parseAnthropicUsage", () => {
7
8
  it("maps oauth usage response into quota windows", () => {
@@ -254,3 +255,156 @@ describe("parseGitHubCopilotUsage", () => {
254
255
  expect(windows[1]).toMatchObject({ label: "Completions / month", usedPercent: 0 });
255
256
  });
256
257
  });
258
+
259
+ describe("parseOpenRouterUsage", () => {
260
+ it("maps API response with monthly budget limit", () => {
261
+ const windows = parseOpenRouterUsage({
262
+ data: {
263
+ label: "Test Key",
264
+ limit: 50,
265
+ limit_remaining: 35,
266
+ limit_reset: "monthly",
267
+ usage: 15,
268
+ usage_daily: 2.5,
269
+ usage_weekly: 12,
270
+ usage_monthly: 15,
271
+ byok_usage: 0,
272
+ byok_usage_daily: 0,
273
+ byok_usage_weekly: 0,
274
+ byok_usage_monthly: 0,
275
+ creator_user_id: "user123",
276
+ include_byok_in_limit: false,
277
+ is_free_tier: false,
278
+ is_management_key: false,
279
+ is_provisioning_key: false,
280
+ },
281
+ });
282
+
283
+ // When limit is set, we get: Monthly Budget + Daily + Weekly + Monthly = 4 windows
284
+ expect(windows).toHaveLength(4);
285
+
286
+ // Monthly Budget window
287
+ const budget = windows.find((w) => w.label === "Monthly Budget");
288
+ expect(budget).toBeDefined();
289
+ expect(budget).toMatchObject({
290
+ provider: "openrouter",
291
+ label: "Monthly Budget",
292
+ usedPercent: 30, // 15/50 = 30%
293
+ isCurrency: true,
294
+ usedValue: 15,
295
+ limitValue: 50,
296
+ showPace: true,
297
+ });
298
+
299
+ // Daily, Weekly, Monthly usage windows
300
+ expect(windows.find((w) => w.label === "Daily")).toBeDefined();
301
+ expect(windows.find((w) => w.label === "Weekly")).toBeDefined();
302
+ expect(windows.find((w) => w.label === "Monthly")).toBeDefined();
303
+ });
304
+
305
+ it("maps unlimited key with remaining credits", () => {
306
+ const windows = parseOpenRouterUsage({
307
+ data: {
308
+ label: "Unlimited Key",
309
+ limit: null,
310
+ limit_remaining: 100,
311
+ limit_reset: null,
312
+ usage: 50,
313
+ usage_daily: 5,
314
+ usage_weekly: 20,
315
+ usage_monthly: 50,
316
+ byok_usage: 0,
317
+ byok_usage_daily: 0,
318
+ byok_usage_weekly: 0,
319
+ byok_usage_monthly: 0,
320
+ creator_user_id: "user123",
321
+ include_byok_in_limit: false,
322
+ is_free_tier: false,
323
+ is_management_key: false,
324
+ is_provisioning_key: false,
325
+ },
326
+ });
327
+
328
+ // When unlimited with limit_remaining: Credits Remaining + Daily + Weekly + Monthly = 4 windows
329
+ expect(windows).toHaveLength(4);
330
+
331
+ // Credits Remaining window
332
+ const remaining = windows.find((w) => w.label === "Credits Remaining");
333
+ expect(remaining).toBeDefined();
334
+ expect(remaining).toMatchObject({
335
+ provider: "openrouter",
336
+ label: "Credits Remaining",
337
+ usedPercent: 0,
338
+ isCurrency: true,
339
+ usedValue: 100,
340
+ limitValue: 100,
341
+ showPace: false,
342
+ });
343
+ });
344
+
345
+ it("handles zero usage", () => {
346
+ const windows = parseOpenRouterUsage({
347
+ data: {
348
+ label: "Test Key",
349
+ limit: 100,
350
+ limit_remaining: 100,
351
+ limit_reset: "monthly",
352
+ usage: 0,
353
+ usage_daily: 0,
354
+ usage_weekly: 0,
355
+ usage_monthly: 0,
356
+ byok_usage: 0,
357
+ byok_usage_daily: 0,
358
+ byok_usage_weekly: 0,
359
+ byok_usage_monthly: 0,
360
+ creator_user_id: "user123",
361
+ include_byok_in_limit: false,
362
+ is_free_tier: false,
363
+ is_management_key: false,
364
+ is_provisioning_key: false,
365
+ },
366
+ });
367
+
368
+ const budget = windows.find((w) => w.label === "Monthly Budget");
369
+ expect(budget).toBeDefined();
370
+ expect(budget!.usedPercent).toBe(0);
371
+ });
372
+
373
+ it("returns empty array when no data", () => {
374
+ const windows = parseOpenRouterUsage({});
375
+ expect(windows).toHaveLength(0);
376
+ });
377
+
378
+ it("handles missing limit_remaining for unlimited keys", () => {
379
+ const windows = parseOpenRouterUsage({
380
+ data: {
381
+ label: "Test Key",
382
+ limit: null,
383
+ limit_remaining: null,
384
+ limit_reset: null,
385
+ usage: 25,
386
+ usage_daily: 3,
387
+ usage_weekly: 10,
388
+ usage_monthly: 25,
389
+ byok_usage: 0,
390
+ byok_usage_daily: 0,
391
+ byok_usage_weekly: 0,
392
+ byok_usage_monthly: 0,
393
+ creator_user_id: "user123",
394
+ include_byok_in_limit: false,
395
+ is_free_tier: false,
396
+ is_management_key: false,
397
+ is_provisioning_key: false,
398
+ },
399
+ });
400
+
401
+ // Should not have "Credits Remaining" window if limit_remaining is null
402
+ const remaining = windows.find((w) => w.label === "Credits Remaining");
403
+ expect(remaining).toBeUndefined();
404
+
405
+ // But should still have usage tracking windows
406
+ expect(windows.find((w) => w.label === "Daily")).toBeDefined();
407
+ expect(windows.find((w) => w.label === "Weekly")).toBeDefined();
408
+ expect(windows.find((w) => w.label === "Monthly")).toBeDefined();
409
+ });
410
+ });
@@ -243,3 +243,112 @@ export function parseGitHubCopilotUsage(data: any): QuotaWindow[] {
243
243
 
244
244
  return windows;
245
245
  }
246
+
247
+ // Helper functions for OpenRouter date calculations (UTC-based)
248
+ function calculateNextMidnightUTC(): Date {
249
+ const now = new Date();
250
+ const midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, 0, 0, 0));
251
+ return midnight;
252
+ }
253
+
254
+ function calculateNextMondayUTC(): Date {
255
+ const now = new Date();
256
+ 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));
259
+ return monday;
260
+ }
261
+
262
+ function calculateNextMonthStartUTC(): Date {
263
+ const now = new Date();
264
+ return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0));
265
+ }
266
+
267
+ export function parseOpenRouterUsage(data: any): QuotaWindow[] {
268
+ const windows: QuotaWindow[] = [];
269
+ const keyData = data?.data;
270
+
271
+ if (!keyData) return windows;
272
+
273
+ const limit = keyData.limit;
274
+ const limitRemaining = keyData.limit_remaining;
275
+ const usageDaily = keyData.usage_daily ?? 0;
276
+ const usageWeekly = keyData.usage_weekly ?? 0;
277
+ const usageMonthly = keyData.usage_monthly ?? 0;
278
+
279
+ // Monthly budget window (if limit is set)
280
+ if (limit != null && limit > 0) {
281
+ const usedPercent = safePercent(usageMonthly, limit);
282
+ windows.push({
283
+ provider: "openrouter",
284
+ label: "Monthly Budget",
285
+ usedPercent,
286
+ resetsAt: calculateNextMonthStartUTC(),
287
+ windowSeconds: 30 * 24 * 60 * 60,
288
+ usedValue: usageMonthly,
289
+ limitValue: limit,
290
+ isCurrency: true,
291
+ showPace: true,
292
+ paceScale: 1,
293
+ nextLabel: "Resets",
294
+ });
295
+ } else if (limitRemaining != null && limitRemaining >= 0) {
296
+ // Unlimited key with remaining tracked
297
+ windows.push({
298
+ provider: "openrouter",
299
+ label: "Credits Remaining",
300
+ usedPercent: 0,
301
+ resetsAt: new Date(0),
302
+ windowSeconds: 0,
303
+ usedValue: limitRemaining,
304
+ limitValue: limitRemaining,
305
+ isCurrency: true,
306
+ showPace: false,
307
+ nextLabel: undefined,
308
+ });
309
+ }
310
+
311
+ // Daily usage window (tracking only)
312
+ windows.push({
313
+ provider: "openrouter",
314
+ label: "Daily",
315
+ usedPercent: 0,
316
+ resetsAt: calculateNextMidnightUTC(),
317
+ windowSeconds: 24 * 60 * 60,
318
+ usedValue: usageDaily,
319
+ limitValue: 0,
320
+ isCurrency: true,
321
+ showPace: false,
322
+ nextLabel: "UTC",
323
+ });
324
+
325
+ // Weekly usage window (tracking only)
326
+ windows.push({
327
+ provider: "openrouter",
328
+ label: "Weekly",
329
+ usedPercent: 0,
330
+ resetsAt: calculateNextMondayUTC(),
331
+ windowSeconds: 7 * 24 * 60 * 60,
332
+ usedValue: usageWeekly,
333
+ limitValue: 0,
334
+ isCurrency: true,
335
+ showPace: false,
336
+ nextLabel: "Week",
337
+ });
338
+
339
+ // Monthly usage window (tracking only)
340
+ windows.push({
341
+ provider: "openrouter",
342
+ label: "Monthly",
343
+ usedPercent: 0,
344
+ resetsAt: calculateNextMonthStartUTC(),
345
+ windowSeconds: 30 * 24 * 60 * 60,
346
+ usedValue: usageMonthly,
347
+ limitValue: 0,
348
+ isCurrency: true,
349
+ showPace: false,
350
+ nextLabel: "Month",
351
+ });
352
+
353
+ return windows;
354
+ }
@@ -1,7 +1,8 @@
1
1
  export type SupportedQuotaProvider =
2
2
  | "anthropic"
3
3
  | "openai-codex"
4
- | "github-copilot";
4
+ | "github-copilot"
5
+ | "openrouter";
5
6
 
6
7
  export type QuotasErrorKind =
7
8
  | "cancelled"