@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 +15 -4
- package/package.json +1 -1
- package/src/extensions/command-quotas/command.ts +1 -1
- package/src/extensions/command-quotas/components/quotas-display.ts +6 -1
- package/src/extensions/command-quotas/provider-commands.test.ts +9 -0
- package/src/extensions/command-quotas/provider-commands.ts +6 -0
- package/src/extensions/usage-status/format-status.test.ts +1 -1
- package/src/extensions/usage-status/format-status.ts +12 -1
- package/src/lib/quotas.ts +3 -0
- package/src/providers/fetch.test.ts +60 -0
- package/src/providers/fetch.ts +31 -0
- package/src/providers/parse.test.ts +154 -0
- package/src/providers/providers.ts +109 -0
- package/src/types/quotas.ts +2 -1
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,
|
|
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
|
|
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
|
|
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
|
@@ -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,
|
|
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
|
-
|
|
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
|
});
|
|
@@ -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
|
-
|
|
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
|
+
});
|
package/src/providers/fetch.ts
CHANGED
|
@@ -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
|
+
}
|