@henryqw/pi-multi-codex 1.0.3 → 1.0.5
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 +13 -15
- package/extensions/multi-codex.ts +25 -20
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,11 +13,11 @@ pi install npm:@henryqw/pi-multi-codex
|
|
|
13
13
|
|
|
14
14
|
## Works with
|
|
15
15
|
|
|
16
|
-
| Package |
|
|
17
|
-
| --- | --- |
|
|
18
|
-
| [`@henryqw/pi-footer`](https://pi.henry.wang/extensions/pi-footer) | Improves
|
|
19
|
-
| [`@henryqw/pi-subagent`](https://pi.henry.wang/extensions/pi-subagent) | Improves
|
|
20
|
-
| [`@henryqw/pi-task-models`](https://pi.henry.wang/extensions/pi-task-models) | Improves
|
|
16
|
+
| Package | Relationship | Purpose |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| [`@henryqw/pi-footer`](https://pi.henry.wang/extensions/pi-footer) | Improves | Shows the active slot's quota or five-hour block in the footer. |
|
|
19
|
+
| [`@henryqw/pi-subagent`](https://pi.henry.wang/extensions/pi-subagent) | Improves | Isolated children keep Main's active Codex slot. |
|
|
20
|
+
| [`@henryqw/pi-task-models`](https://pi.henry.wang/extensions/pi-task-models) | Improves | Numbered slots share one profile route. |
|
|
21
21
|
|
|
22
22
|
## Use
|
|
23
23
|
|
|
@@ -25,6 +25,8 @@ Run `/login` and authenticate `OpenAI Codex` for slot 1 first. Run `/codex-add`,
|
|
|
25
25
|
|
|
26
26
|
`/codex-status` lists the new slot. It shows cached quota when available, or `unavailable` until the first successful snapshot.
|
|
27
27
|
|
|
28
|
+
The footer and `/codex-status` show the five-hour reset countdown for Free, Go, and Plus tiers. They show the seven-day reset countdown for Pro Lite and other tiers.
|
|
29
|
+
|
|
28
30
|
| Surface | Type | Purpose |
|
|
29
31
|
| --- | --- | --- |
|
|
30
32
|
| `/codex-add` | command | Create the next numbered slot, then authenticate that slot. |
|
|
@@ -47,24 +49,20 @@ A numbered slot is one Codex account position in Pi.
|
|
|
47
49
|
|
|
48
50
|
## Config
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
Package-owned: `~/.pi/agent/config/pi-multi-codex/config.json`
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
```
|
|
54
|
+
| Name | Description | Values | Default |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| `autoSwitchOn429` | Switches to another eligible slot after an HTTP 429. | Boolean. | `true` |
|
|
57
57
|
|
|
58
|
-
The config must contain only
|
|
58
|
+
Set `autoSwitchOn429` to `false` to disable automatic switching. The config must contain only this field as a boolean. Invalid config is preserved and disables automatic switching.
|
|
59
59
|
|
|
60
60
|
## State and storage
|
|
61
61
|
|
|
62
62
|
The extension maintains a generated, credential-free quota cache at `~/.pi/agent/config/pi-multi-codex/usage.json`.
|
|
63
63
|
|
|
64
|
-
##
|
|
64
|
+
## Limits and recovery
|
|
65
65
|
|
|
66
66
|
The extension reads `auth.json`. It never writes or refreshes credentials.
|
|
67
67
|
|
|
68
|
-
## Limits and recovery
|
|
69
|
-
|
|
70
68
|
Scoped sessions can switch only to exact scoped aliases.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
-
import { createConfigStore, extensionConfigDir } from "@henryqw/pi-config-store";
|
|
6
|
+
import { createConfigStore, extensionConfigDir, writePrivateTextFileAtomically } from "@henryqw/pi-config-store";
|
|
7
7
|
import { lock } from "proper-lockfile";
|
|
8
8
|
import {
|
|
9
9
|
createAssistantMessageEventStream,
|
|
@@ -54,6 +54,7 @@ type UsageSnapshot = {
|
|
|
54
54
|
fetchedAt?: number;
|
|
55
55
|
remaining?: number;
|
|
56
56
|
reset?: number;
|
|
57
|
+
fiveHourReset?: number;
|
|
57
58
|
limitedUntil?: number | null;
|
|
58
59
|
};
|
|
59
60
|
|
|
@@ -70,7 +71,7 @@ type UsageState = {
|
|
|
70
71
|
untrusted?: true;
|
|
71
72
|
};
|
|
72
73
|
|
|
73
|
-
type ParsedUsage = { remaining: number; reset: number; limitedUntil?: number; tier?: string };
|
|
74
|
+
type ParsedUsage = { remaining: number; reset: number; fiveHourReset?: number; limitedUntil?: number; tier?: string };
|
|
74
75
|
type Config = { autoSwitchOn429: boolean };
|
|
75
76
|
|
|
76
77
|
const NATIVE_PROVIDER_ID = "openai-codex";
|
|
@@ -187,9 +188,10 @@ function readSnapshot(value: unknown): UsageSnapshot | undefined {
|
|
|
187
188
|
typeof value.remaining !== "number" ||
|
|
188
189
|
!Number.isFinite(value.remaining) ||
|
|
189
190
|
!validTime(value.reset) ||
|
|
191
|
+
(value.fiveHourReset !== undefined && !validTime(value.fiveHourReset)) ||
|
|
190
192
|
(value.limitedUntil !== null && !validTime(value.limitedUntil))
|
|
191
193
|
)) return undefined;
|
|
192
|
-
if (!successful && value.limitedUntil !== undefined) return undefined;
|
|
194
|
+
if (!successful && (value.fiveHourReset !== undefined || value.limitedUntil !== undefined)) return undefined;
|
|
193
195
|
return {
|
|
194
196
|
slot: value.slot,
|
|
195
197
|
accountHash: value.accountHash,
|
|
@@ -200,6 +202,7 @@ function readSnapshot(value: unknown): UsageSnapshot | undefined {
|
|
|
200
202
|
fetchedAt: value.fetchedAt as number,
|
|
201
203
|
remaining: Math.max(0, Math.min(100, value.remaining as number)),
|
|
202
204
|
reset: value.reset as number,
|
|
205
|
+
...(validTime(value.fiveHourReset) ? { fiveHourReset: value.fiveHourReset } : {}),
|
|
203
206
|
limitedUntil: value.limitedUntil as number | null,
|
|
204
207
|
}
|
|
205
208
|
: {}),
|
|
@@ -258,22 +261,11 @@ async function saveState(state: UsageState, signal?: AbortSignal): Promise<void>
|
|
|
258
261
|
signal?.throwIfAborted();
|
|
259
262
|
if (state.untrusted) return;
|
|
260
263
|
const file = cachePath();
|
|
261
|
-
const directory = extensionConfigDir("pi-multi-codex");
|
|
262
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
263
264
|
const data = `${JSON.stringify({
|
|
264
265
|
slots: [...state.slots.values()].sort((a, b) => a.slot - b.slot),
|
|
265
266
|
locks: [...state.locks.values()].sort((a, b) => a.slot - b.slot),
|
|
266
267
|
})}\n`;
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
await writeFile(temporary, data, { encoding: "utf8", mode: 0o600, flag: "wx", signal });
|
|
270
|
-
signal?.throwIfAborted();
|
|
271
|
-
await rename(temporary, file);
|
|
272
|
-
signal?.throwIfAborted();
|
|
273
|
-
await chmod(file, 0o600);
|
|
274
|
-
} finally {
|
|
275
|
-
await rm(temporary, { force: true }).catch(() => undefined);
|
|
276
|
-
}
|
|
268
|
+
await writePrivateTextFileAtomically(file, data, { signal });
|
|
277
269
|
}
|
|
278
270
|
|
|
279
271
|
async function withCacheMutex<T>(operation: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
@@ -323,6 +315,13 @@ function isFiveHourLimited(snapshot: UsageSnapshot, now: number): boolean {
|
|
|
323
315
|
return validTime(snapshot.limitedUntil) && snapshot.limitedUntil > now;
|
|
324
316
|
}
|
|
325
317
|
|
|
318
|
+
function displayedReset(snapshot: UsageSnapshot, now: number): { label: "5h" | "7d"; time: number } {
|
|
319
|
+
if (["free", "go", "plus"].includes(snapshot.tier ?? "") && validTime(snapshot.fiveHourReset) && snapshot.fiveHourReset > now) {
|
|
320
|
+
return { label: "5h", time: snapshot.fiveHourReset };
|
|
321
|
+
}
|
|
322
|
+
return { label: "7d", time: snapshot.reset! };
|
|
323
|
+
}
|
|
324
|
+
|
|
326
325
|
function checkedRecently(snapshot: UsageSnapshot | undefined, identity: SlotIdentity, now: number): boolean {
|
|
327
326
|
return Boolean(
|
|
328
327
|
snapshot &&
|
|
@@ -352,7 +351,7 @@ function resetTime(window: JsonRecord, now: number): number | undefined {
|
|
|
352
351
|
return resetAfter === undefined || resetAfter < 0 ? undefined : now + resetAfter * 1000;
|
|
353
352
|
}
|
|
354
353
|
|
|
355
|
-
/** Extract seven-day quota and
|
|
354
|
+
/** Extract seven-day quota and the five-hour window. */
|
|
356
355
|
export function parseCodexUsage(value: unknown, now = Date.now()): ParsedUsage | undefined {
|
|
357
356
|
if (!isRecord(value)) return undefined;
|
|
358
357
|
const rateLimit = isRecord(value.rate_limit) ? value.rate_limit : value;
|
|
@@ -366,11 +365,13 @@ export function parseCodexUsage(value: unknown, now = Date.now()): ParsedUsage |
|
|
|
366
365
|
?? (windows.length === 1 && numberValue(windows[0].value.limit_window_seconds) === undefined ? windows[0] : undefined);
|
|
367
366
|
if (!sevenDay || sevenDay.reset <= now) return undefined;
|
|
368
367
|
const fiveHour = windows.find((window) => numberValue(window.value.limit_window_seconds) === 5 * 60 * 60);
|
|
369
|
-
const
|
|
368
|
+
const fiveHourReset = fiveHour && fiveHour.reset > now ? fiveHour.reset : undefined;
|
|
369
|
+
const limitedUntil = fiveHour && fiveHour.used >= 100 ? fiveHourReset : undefined;
|
|
370
370
|
const tier = validTier(value.plan_type) ? value.plan_type : validTier(value.tier) ? value.tier : undefined;
|
|
371
371
|
return {
|
|
372
372
|
remaining: Math.max(0, Math.min(100, 100 - sevenDay.used)),
|
|
373
373
|
reset: sevenDay.reset,
|
|
374
|
+
...(fiveHourReset ? { fiveHourReset } : {}),
|
|
374
375
|
...(limitedUntil ? { limitedUntil } : {}),
|
|
375
376
|
...(tier ? { tier } : {}),
|
|
376
377
|
};
|
|
@@ -609,6 +610,7 @@ class CodexQuotaStatus {
|
|
|
609
610
|
fetchedAt: checkedAt,
|
|
610
611
|
remaining: outcome.remaining,
|
|
611
612
|
reset: outcome.reset,
|
|
613
|
+
...(outcome.fiveHourReset && outcome.fiveHourReset > checkedAt ? { fiveHourReset: outcome.fiveHourReset } : {}),
|
|
612
614
|
limitedUntil: outcome.limitedUntil && outcome.limitedUntil > checkedAt ? outcome.limitedUntil : null,
|
|
613
615
|
});
|
|
614
616
|
} else {
|
|
@@ -691,7 +693,9 @@ class CodexQuotaStatus {
|
|
|
691
693
|
if (isFiveHourLimited(snapshot, now)) {
|
|
692
694
|
return `Codex slot ${slot}${tier}: five-hour limit reached, resets in ${formatDuration(snapshot.limitedUntil! - now)} (${status})`;
|
|
693
695
|
}
|
|
694
|
-
|
|
696
|
+
const display = displayedReset(snapshot, now);
|
|
697
|
+
const resetWindow = display.label === "5h" ? "five-hour" : "seven-day";
|
|
698
|
+
return `Codex slot ${slot}${tier}: ${formatPercent(snapshot.remaining)}% of the seven-day quota remaining; ${resetWindow} reset in ${formatDuration(display.time - now)} (${status})`;
|
|
695
699
|
});
|
|
696
700
|
}
|
|
697
701
|
}
|
|
@@ -914,7 +918,8 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
914
918
|
return ctx.ui.theme?.fg ? ctx.ui.theme.fg("error", text) : text;
|
|
915
919
|
}
|
|
916
920
|
if (!isFresh(snapshot, identity, now)) return `${prefix} · stale`;
|
|
917
|
-
const
|
|
921
|
+
const display = displayedReset(snapshot, now);
|
|
922
|
+
const text = `${prefix} · ${formatPercent(snapshot.remaining)}% · ${display.label} ${formatDuration(display.time - now)}`;
|
|
918
923
|
const color = snapshot.remaining >= 50 ? "success" : snapshot.remaining >= 25 ? "warning" : "error";
|
|
919
924
|
return ctx.ui.theme?.fg ? ctx.ui.theme.fg(color, text) : text;
|
|
920
925
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-multi-codex",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Use multiple ChatGPT Codex OAuth accounts in Pi.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"image": "https://raw.githubusercontent.com/HenryQW/pi-harness/main/extensions/pi-multi-codex/example.png"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@henryqw/pi-config-store": "^1.
|
|
49
|
+
"@henryqw/pi-config-store": "^1.1.0",
|
|
50
50
|
"proper-lockfile": "^4.1.2"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|