@latentminds/pi-quotas 0.2.0 → 0.2.3
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 +6 -3
- package/package.json +1 -1
- package/src/config.ts +1 -1
- package/src/extensions/command-quotas/provider-commands.ts +6 -0
- package/src/extensions/usage-status/format-status.test.ts +145 -1
- package/src/extensions/usage-status/index.ts +54 -16
- package/src/lib/quotas.ts +3 -0
- package/src/providers/fetch.ts +20 -0
- package/src/providers/parse.test.ts +2 -0
- package/src/providers/providers.ts +85 -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, GitHub Copilot, and
|
|
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, OpenRouter, and Synthetic — directly in your Pi session.
|
|
4
4
|
|
|
5
5
|
## Screenshots
|
|
6
6
|
|
|
@@ -38,6 +38,7 @@ pi -e npm:@latentminds/pi-quotas
|
|
|
38
38
|
| `/codex:quotas` | OpenAI Codex quotas only |
|
|
39
39
|
| `/github:quotas` | GitHub Copilot quotas only |
|
|
40
40
|
| `/openrouter:quotas` | OpenRouter quotas only |
|
|
41
|
+
| `/synthetic:quotas` | Synthetic quotas only |
|
|
41
42
|
| `/quotas:settings` | Toggle individual features on or off |
|
|
42
43
|
|
|
43
44
|
## Features
|
|
@@ -58,7 +59,7 @@ Automatic notifications when projected usage is on track to exceed limits before
|
|
|
58
59
|
|
|
59
60
|
Use `/quotas:settings` to enable or disable:
|
|
60
61
|
- Combined `/quotas` command
|
|
61
|
-
- Per-provider commands (`/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`)
|
|
62
|
+
- Per-provider commands (`/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`, `/synthetic:quotas`)
|
|
62
63
|
- Footer status widget
|
|
63
64
|
- Quota warning notifications
|
|
64
65
|
|
|
@@ -72,6 +73,7 @@ Settings can be saved globally (`~/.pi/agent/extensions/quotas.json`) or per-pro
|
|
|
72
73
|
| OpenAI Codex | 5h, 7d, credits, spend cap | Rate-limit percentages; credit balance; spend-cap reached/OK |
|
|
73
74
|
| GitHub Copilot | Premium/chat/completions per month | Remaining/entitlement counts with overage indicators |
|
|
74
75
|
| OpenRouter | Monthly budget, daily/weekly/monthly usage | USD spending tracking with cents precision; optional per-key budget limits; UTC-based period resets |
|
|
76
|
+
| Synthetic | Subscription, search/hour, free tools, weekly tokens, 5h limit | Request counts and token budgets; rolling five-hour rate limit; weekly token regen |
|
|
75
77
|
|
|
76
78
|
## Credentials
|
|
77
79
|
|
|
@@ -81,8 +83,9 @@ pi-quotas reads existing Pi auth entries from `~/.pi/agent/auth.json`:
|
|
|
81
83
|
- `openai-codex` — Codex access token (also reads `~/.codex/auth.json` for the account ID)
|
|
82
84
|
- `github-copilot` — GitHub Copilot OAuth token (falls back to `gh auth token` if needed)
|
|
83
85
|
- `openrouter` — OpenRouter API key (Bearer token)
|
|
86
|
+
- `synthetic` — Synthetic API key (set the `SYNTHETIC_API_KEY` environment variable)
|
|
84
87
|
|
|
85
|
-
No additional setup is required - if Pi can use the provider, pi-quotas can check its quotas.
|
|
88
|
+
No additional setup is required - if Pi can use the provider, pi-quotas can check its quotas. For Synthetic, export `SYNTHETIC_API_KEY` in your shell or Pi environment.
|
|
86
89
|
|
|
87
90
|
## Requirements
|
|
88
91
|
|
package/package.json
CHANGED
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 `/
|
|
155
|
+
description: "Toggle `/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`, and `/synthetic:quotas`",
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
id: "usageStatus",
|
|
@@ -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, formatStatusForFooter, toStatusWindows, 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,142 @@ 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("clears the footer status when filtering removes all windows", () => {
|
|
187
|
+
expect(formatStatusForFooter({ ui: { theme } } as any, [])).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("filters Anthropic subscription windows from footer status while keeping extra usage", () => {
|
|
191
|
+
const windows = toStatusWindows([
|
|
192
|
+
{
|
|
193
|
+
provider: "anthropic",
|
|
194
|
+
label: "5h",
|
|
195
|
+
usedPercent: 10,
|
|
196
|
+
resetsAt: new Date("2026-05-06T07:47:37Z"),
|
|
197
|
+
windowSeconds: 5 * 60 * 60,
|
|
198
|
+
usedValue: 10,
|
|
199
|
+
limitValue: 100,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
provider: "anthropic",
|
|
203
|
+
label: "7d Sonnet",
|
|
204
|
+
usedPercent: 20,
|
|
205
|
+
resetsAt: new Date("2026-05-06T07:47:37Z"),
|
|
206
|
+
windowSeconds: 7 * 24 * 60 * 60,
|
|
207
|
+
usedValue: 20,
|
|
208
|
+
limitValue: 100,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
provider: "anthropic",
|
|
212
|
+
label: "Extra (USD)",
|
|
213
|
+
usedPercent: 30,
|
|
214
|
+
resetsAt: new Date("2026-06-01T00:00:00Z"),
|
|
215
|
+
windowSeconds: 30 * 24 * 60 * 60,
|
|
216
|
+
usedValue: 30,
|
|
217
|
+
limitValue: 100,
|
|
218
|
+
isCurrency: true,
|
|
219
|
+
},
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
expect(windows).toHaveLength(1);
|
|
223
|
+
expect(windows[0]).toMatchObject({ label: "Extra (USD)" });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("does not prefix elapsed reset times with in", () => {
|
|
227
|
+
vi.useFakeTimers();
|
|
228
|
+
vi.setSystemTime(new Date("2026-05-06T05:28:37Z"));
|
|
229
|
+
|
|
230
|
+
const result = formatStatus(
|
|
231
|
+
{ ui: { theme } } as any,
|
|
232
|
+
[
|
|
233
|
+
{
|
|
234
|
+
label: "5h",
|
|
235
|
+
usedPercent: 100,
|
|
236
|
+
severity: "critical",
|
|
237
|
+
resetsAt: "2026-05-06T05:28:37Z",
|
|
238
|
+
limited: false,
|
|
239
|
+
usedValue: 100,
|
|
240
|
+
limitValue: 100,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(result).toContain("(↺now)");
|
|
246
|
+
expect(result).not.toContain("(↺in now)");
|
|
247
|
+
});
|
|
104
248
|
});
|
|
@@ -11,28 +11,74 @@ 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
|
|
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", ` (↺${
|
|
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
|
+
const ANTHROPIC_SUBSCRIPTION_WINDOW_LABELS = new Set([
|
|
43
|
+
"5h",
|
|
44
|
+
"7d",
|
|
45
|
+
"7d Sonnet",
|
|
46
|
+
"7d Opus",
|
|
47
|
+
"7d Opus (legacy)",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function shouldShowInStatus(window: QuotaWindow): boolean {
|
|
51
|
+
return !(
|
|
52
|
+
window.provider === "anthropic" &&
|
|
53
|
+
ANTHROPIC_SUBSCRIPTION_WINDOW_LABELS.has(window.label)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function toWindowStatus(window: QuotaWindow): WindowStatus {
|
|
58
|
+
return {
|
|
59
|
+
label: window.label,
|
|
60
|
+
usedPercent: window.usedPercent,
|
|
61
|
+
severity: assessWindow(window).severity,
|
|
62
|
+
resetsAt: window.resetsAt.getTime() > 0 ? window.resetsAt.toISOString() : null,
|
|
63
|
+
limited: window.limited ?? false,
|
|
64
|
+
isCurrency: window.isCurrency,
|
|
65
|
+
usedValue: window.usedValue,
|
|
66
|
+
limitValue: window.limitValue,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function toStatusWindows(windows: QuotaWindow[]): WindowStatus[] {
|
|
71
|
+
return windows.filter(shouldShowInStatus).map(toWindowStatus);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatStatusForFooter(
|
|
75
|
+
ctx: Pick<ExtensionContext, "ui">,
|
|
76
|
+
windows: WindowStatus[],
|
|
77
|
+
): string | undefined {
|
|
78
|
+
if (windows.length === 0) return undefined;
|
|
79
|
+
return formatStatus(ctx, windows);
|
|
80
|
+
}
|
|
81
|
+
|
|
36
82
|
function createStatusRefresher() {
|
|
37
83
|
let refreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
38
84
|
let activeContext: ExtensionContext | undefined;
|
|
@@ -54,18 +100,10 @@ function createStatusRefresher() {
|
|
|
54
100
|
ctx.ui.setStatus(EXTENSION_ID, ctx.ui.theme.fg("warning", "usage unavailable"));
|
|
55
101
|
return;
|
|
56
102
|
}
|
|
57
|
-
const windows: WindowStatus[] = result.data.windows
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
resetsAt: window.resetsAt.toISOString(),
|
|
62
|
-
limited: window.limited ?? false,
|
|
63
|
-
isCurrency: window.isCurrency,
|
|
64
|
-
usedValue: window.usedValue,
|
|
65
|
-
limitValue: window.limitValue,
|
|
66
|
-
}));
|
|
67
|
-
lastStatus = windows;
|
|
68
|
-
ctx.ui.setStatus(EXTENSION_ID, formatStatus(ctx, windows));
|
|
103
|
+
const windows: WindowStatus[] = toStatusWindows(result.data.windows);
|
|
104
|
+
const status = formatStatusForFooter(ctx, windows);
|
|
105
|
+
lastStatus = status === undefined ? undefined : windows;
|
|
106
|
+
ctx.ui.setStatus(EXTENSION_ID, status);
|
|
69
107
|
} catch {
|
|
70
108
|
ctx.ui.setStatus(EXTENSION_ID, ctx.ui.theme.fg("warning", "usage unavailable"));
|
|
71
109
|
} finally {
|
|
@@ -104,7 +142,7 @@ function createStatusRefresher() {
|
|
|
104
142
|
},
|
|
105
143
|
renderLast(ctx: ExtensionContext): boolean {
|
|
106
144
|
if (!lastStatus || !ctx.hasUI) return false;
|
|
107
|
-
ctx.ui.setStatus(EXTENSION_ID,
|
|
145
|
+
ctx.ui.setStatus(EXTENSION_ID, formatStatusForFooter(ctx, lastStatus));
|
|
108
146
|
return true;
|
|
109
147
|
},
|
|
110
148
|
};
|
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 = {
|
package/src/providers/fetch.ts
CHANGED
|
@@ -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;
|
|
@@ -48,6 +48,8 @@ describe("parseAnthropicUsage", () => {
|
|
|
48
48
|
const extra = windows.find((w) => w.label === "Extra (AUD)");
|
|
49
49
|
expect(extra).toBeDefined();
|
|
50
50
|
expect(extra).toMatchObject({
|
|
51
|
+
provider: "anthropic",
|
|
52
|
+
label: "Extra (AUD)",
|
|
51
53
|
isCurrency: true,
|
|
52
54
|
usedPercent: 71.83,
|
|
53
55
|
usedValue: 215.48,
|
|
@@ -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
|
+
}
|