@ravshansbox/pi-quota 0.4.1 → 0.6.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 +5 -4
- package/index.ts +69 -18
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -12,13 +12,14 @@ Add `-l` to install it in project settings.
|
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
-
Pi loads the extension from `./index.ts` and shows the remaining quota for the provider backing the active model
|
|
15
|
+
Pi loads the extension from `./index.ts` and shows the remaining quota for the provider backing the active model below the editor.
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
17
|
+
- Calls the Anthropic usage endpoint once on session start, then reads the Anthropic quota from the rate limit headers of each Anthropic response
|
|
18
|
+
- Polls the OpenAI Codex usage endpoint on session start, then at every 10-minute wall-clock mark (`HH:00`, `HH:10`, `HH:20`, `HH:30`, `HH:40`, `HH:50`)
|
|
19
|
+
- Shows a widget below the editor for the provider backing the **active model**, and only when quota data for that provider has been polled; switching models re-renders it immediately
|
|
19
20
|
- The text stays compact, e.g. `93% 1h29m · 67% 3d13h`, where the 5-hour window comes first and the 7-day window second, and each window shows its remaining percentage followed by time until reset
|
|
20
21
|
- Available resets, when present, are appended as `· 2x 5h` (count, then time until the soonest expires)
|
|
21
|
-
- Both providers are
|
|
22
|
+
- Both providers are checked on session start whenever their credentials exist, so quota is already known when the active model changes
|
|
22
23
|
- Refreshes Anthropic and OpenAI Codex OAuth access tokens from `~/.pi/agent/auth.json` when needed, writes updated credentials back (re-reading the file first to avoid clobbering concurrent updates), and notifies on the first successful refresh per provider each session
|
|
23
24
|
- Appends poll and token-refresh errors to `~/.pi/agent/pi-quota.log`
|
|
24
25
|
|
package/index.ts
CHANGED
|
@@ -113,6 +113,31 @@ function formatResetTime(reset: Date): string {
|
|
|
113
113
|
return 'now';
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function parseAnthropicHeaderWindow(headers: Record<string, string>, name: string) {
|
|
117
|
+
const utilization = headers[`anthropic-ratelimit-unified-${name}-utilization`];
|
|
118
|
+
const reset = headers[`anthropic-ratelimit-unified-${name}-reset`];
|
|
119
|
+
return {
|
|
120
|
+
remaining: utilization === undefined ? null : clampPercent(100 - Number(utilization) * 100),
|
|
121
|
+
reset: reset === undefined ? null : new Date(Number(reset) * 1000),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseAnthropicHeaders(headers: Record<string, string>): QuotaState | null {
|
|
126
|
+
const fiveHour = parseAnthropicHeaderWindow(headers, '5h');
|
|
127
|
+
const sevenDay = parseAnthropicHeaderWindow(headers, '7d');
|
|
128
|
+
if (fiveHour.remaining === null && sevenDay.remaining === null) return null;
|
|
129
|
+
return {
|
|
130
|
+
provider: 'anthropic',
|
|
131
|
+
fiveHourRemaining: fiveHour.remaining,
|
|
132
|
+
fiveHourReset: fiveHour.reset,
|
|
133
|
+
sevenDayRemaining: sevenDay.remaining,
|
|
134
|
+
sevenDayReset: sevenDay.reset,
|
|
135
|
+
resetsAvailable: 0,
|
|
136
|
+
resetSoonestExpiry: null,
|
|
137
|
+
lastUpdated: new Date(),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
116
141
|
async function loadConfig(): Promise<QuotaConfig | null> {
|
|
117
142
|
try {
|
|
118
143
|
const settingsPath = join(getAgentDir(), 'settings.json');
|
|
@@ -168,6 +193,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
168
193
|
const states: QuotaState[] = [];
|
|
169
194
|
const refreshNotified = new Set<string>();
|
|
170
195
|
let checkTimer: ReturnType<typeof setTimeout> | null = null;
|
|
196
|
+
let anthropicPingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
171
197
|
let ctxRef: ExtensionContext | null = null;
|
|
172
198
|
let codexRedeemAttempted = false;
|
|
173
199
|
let cycleRunning = false;
|
|
@@ -210,7 +236,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
210
236
|
const line = text === undefined ? undefined : ctxRef.ui.theme.fg('muted', text);
|
|
211
237
|
|
|
212
238
|
ctxRef.ui.setWidget('pi-quota', line === undefined ? undefined : () => new Text(line, 0, 0), {
|
|
213
|
-
placement: '
|
|
239
|
+
placement: 'belowEditor',
|
|
214
240
|
});
|
|
215
241
|
}
|
|
216
242
|
|
|
@@ -272,14 +298,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
272
298
|
child.unref();
|
|
273
299
|
}
|
|
274
300
|
|
|
301
|
+
function scheduleAnthropicPing(state: QuotaState) {
|
|
302
|
+
if (anthropicPingTimer) clearTimeout(anthropicPingTimer);
|
|
303
|
+
anthropicPingTimer = null;
|
|
304
|
+
const now = Date.now();
|
|
305
|
+
const resets = [state.fiveHourReset, state.sevenDayReset]
|
|
306
|
+
.map((reset) => reset?.getTime())
|
|
307
|
+
.filter((time): time is number => time !== undefined && time > now);
|
|
308
|
+
if (resets.length === 0) return;
|
|
309
|
+
anthropicPingTimer = setTimeout(
|
|
310
|
+
() => pingProviderOnReset('anthropic'),
|
|
311
|
+
Math.min(...resets) - now,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
275
315
|
function updateState(parsed: QuotaState) {
|
|
316
|
+
if (parsed.provider === 'anthropic') scheduleAnthropicPing(parsed);
|
|
276
317
|
const existing = states.find((s) => s.provider === parsed.provider);
|
|
277
318
|
if (!existing) {
|
|
278
319
|
states.push(parsed);
|
|
279
320
|
return;
|
|
280
321
|
}
|
|
281
322
|
|
|
282
|
-
const shouldPing = shouldPingOnReset(existing, parsed);
|
|
323
|
+
const shouldPing = parsed.provider !== 'anthropic' && shouldPingOnReset(existing, parsed);
|
|
283
324
|
|
|
284
325
|
existing.fiveHourRemaining = parsed.fiveHourRemaining;
|
|
285
326
|
existing.fiveHourReset = parsed.fiveHourReset;
|
|
@@ -438,12 +479,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
438
479
|
}
|
|
439
480
|
}
|
|
440
481
|
|
|
441
|
-
async function pollQuotaStatus() {
|
|
482
|
+
async function pollQuotaStatus(includeAnthropic: boolean) {
|
|
442
483
|
try {
|
|
443
484
|
const auth = await loadAuth();
|
|
444
485
|
if (!auth) return;
|
|
445
486
|
|
|
446
|
-
const anthropicAuth = await ensureAnthropicAccess(auth);
|
|
487
|
+
const anthropicAuth = includeAnthropic ? await ensureAnthropicAccess(auth) : undefined;
|
|
447
488
|
if (anthropicAuth?.access) {
|
|
448
489
|
const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
|
|
449
490
|
headers: {
|
|
@@ -560,7 +601,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
560
601
|
|
|
561
602
|
if (result.code === 'reset') {
|
|
562
603
|
ctxRef?.ui.notify('pi-quota: saved reset redeemed successfully', 'info');
|
|
563
|
-
await pollQuotaStatus();
|
|
604
|
+
await pollQuotaStatus(false);
|
|
564
605
|
updateStatus();
|
|
565
606
|
} else {
|
|
566
607
|
logError(`Reset redeem returned code: ${result.code}`);
|
|
@@ -568,11 +609,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
568
609
|
}
|
|
569
610
|
}
|
|
570
611
|
|
|
571
|
-
async function runCycle() {
|
|
612
|
+
async function runCycle(includeAnthropic: boolean) {
|
|
572
613
|
if (cycleRunning) return;
|
|
573
614
|
cycleRunning = true;
|
|
574
615
|
try {
|
|
575
|
-
await pollQuotaStatus();
|
|
616
|
+
await pollQuotaStatus(includeAnthropic);
|
|
576
617
|
await tryAutoRedeemCodexReset();
|
|
577
618
|
updateStatus();
|
|
578
619
|
} catch (error) {
|
|
@@ -586,34 +627,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
586
627
|
const next = nextMarkAfter(new Date());
|
|
587
628
|
const delay = Math.max(0, next.getTime() - Date.now());
|
|
588
629
|
checkTimer = setTimeout(async () => {
|
|
589
|
-
await runCycle();
|
|
630
|
+
await runCycle(false);
|
|
590
631
|
scheduleCheck();
|
|
591
632
|
}, delay);
|
|
592
633
|
}
|
|
593
634
|
|
|
635
|
+
function stopTimers() {
|
|
636
|
+
if (checkTimer) clearTimeout(checkTimer);
|
|
637
|
+
if (anthropicPingTimer) clearTimeout(anthropicPingTimer);
|
|
638
|
+
checkTimer = null;
|
|
639
|
+
anthropicPingTimer = null;
|
|
640
|
+
}
|
|
641
|
+
|
|
594
642
|
pi.on('session_start', (_event, ctx) => {
|
|
595
|
-
|
|
596
|
-
clearTimeout(checkTimer);
|
|
597
|
-
checkTimer = null;
|
|
598
|
-
}
|
|
643
|
+
stopTimers();
|
|
599
644
|
states.length = 0;
|
|
600
645
|
refreshNotified.clear();
|
|
601
646
|
ctxRef = ctx;
|
|
602
647
|
codexRedeemAttempted = false;
|
|
603
648
|
|
|
604
|
-
void runCycle();
|
|
649
|
+
void runCycle(true);
|
|
605
650
|
scheduleCheck();
|
|
606
651
|
});
|
|
607
652
|
|
|
653
|
+
pi.on('after_provider_response', (event, ctx) => {
|
|
654
|
+
if (ctx.model?.provider !== modelProviderNames.anthropic) return;
|
|
655
|
+
const next = parseAnthropicHeaders(event.headers);
|
|
656
|
+
if (!next) return;
|
|
657
|
+
ctxRef = ctx;
|
|
658
|
+
updateState(next);
|
|
659
|
+
updateStatus();
|
|
660
|
+
});
|
|
661
|
+
|
|
608
662
|
pi.on('model_select', (_event, ctx) => {
|
|
609
663
|
ctxRef = ctx;
|
|
610
664
|
updateStatus();
|
|
611
665
|
});
|
|
612
666
|
|
|
613
|
-
pi.on('session_shutdown',
|
|
614
|
-
|
|
615
|
-
clearTimeout(checkTimer);
|
|
616
|
-
checkTimer = null;
|
|
617
|
-
}
|
|
667
|
+
pi.on('session_shutdown', () => {
|
|
668
|
+
stopTimers();
|
|
618
669
|
});
|
|
619
670
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ravshansbox/pi-quota",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Anthropic and OpenAI Codex quota status extension for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"lint:fix": "oxlint --fix",
|
|
29
29
|
"format": "oxfmt",
|
|
30
30
|
"format:check": "oxfmt --check",
|
|
31
|
-
"
|
|
31
|
+
"test": "node --test",
|
|
32
|
+
"check": "npm run typecheck && npm run lint && npm run format:check && npm test"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"@earendil-works/pi-coding-agent": "*",
|