@henryqw/pi-multi-codex 1.0.1 → 1.0.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 +17 -11
- package/extensions/multi-codex.ts +22 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
# `@henryqw/pi-multi-codex`
|
|
2
2
|
|
|
3
|
-
Add multiple ChatGPT Codex OAuth accounts and start
|
|
3
|
+
Add multiple ChatGPT Codex OAuth accounts and start Pi work on the eligible slot with the most weekly quota. The extension switches away from active five-hour blocks and can retry an HTTP 429 on another eligible slot.
|
|
4
4
|
|
|
5
5
|

|
|
6
|
-
|
|
7
|
-
## Why
|
|
8
|
-
|
|
9
|
-
- **Created for**: Pi users who work across more than one Codex subscription.
|
|
10
|
-
- **Advantage**: Quota-aware routing starts with the eligible slot that has the most remaining seven-day quota. With automatic switching enabled, Pi can retry an HTTP 429 response on another eligible slot.
|
|
6
|
+

|
|
11
7
|
|
|
12
8
|
## Install
|
|
13
9
|
|
|
@@ -15,7 +11,7 @@ Add multiple ChatGPT Codex OAuth accounts and start new Pi work on the slot with
|
|
|
15
11
|
pi install npm:@henryqw/pi-multi-codex
|
|
16
12
|
```
|
|
17
13
|
|
|
18
|
-
##
|
|
14
|
+
## Works with
|
|
19
15
|
|
|
20
16
|
| Package | Why |
|
|
21
17
|
| --- | --- |
|
|
@@ -37,17 +33,17 @@ Run `/login` and authenticate `OpenAI Codex` for slot 1 first. Run `/codex-add`,
|
|
|
37
33
|
|
|
38
34
|
A numbered slot is one Codex account position in Pi.
|
|
39
35
|
|
|
40
|
-
|
|
36
|
+
## Flow
|
|
41
37
|
|
|
42
|
-
- The extension reads `auth.json`. It never writes or refreshes credentials.
|
|
43
38
|
- Before the first agent start, only fresh quota snapshots enter startup ranking. The slot with the most seven-day quota wins.
|
|
39
|
+
- An explicit model selection always wins for the next agent start.
|
|
40
|
+
- Before later agent starts, a known active five-hour block switches to the eligible fresh slot with the most seven-day quota.
|
|
44
41
|
- After HTTP 429, failover can use authenticated, registered, scope-allowed, untried slots with stale or missing quota.
|
|
45
42
|
- Failover skips known active five-hour blocks. It ranks fresh known quota first, then unranked slots by slot number.
|
|
46
43
|
- Routing preserves the model ID.
|
|
47
44
|
- During one agent run, each eligible slot is tried at most once after HTTP 429 responses.
|
|
48
45
|
- Automatic retry stops when no untried eligible slot remains.
|
|
49
46
|
- The footer shows the active slot's fresh quota or five-hour block.
|
|
50
|
-
- Scoped sessions can switch only to exact scoped aliases.
|
|
51
47
|
|
|
52
48
|
## Config
|
|
53
49
|
|
|
@@ -61,4 +57,14 @@ Automatic HTTP 429 switching is on by default. To disable it, create `~/.pi/agen
|
|
|
61
57
|
|
|
62
58
|
The config must contain only `autoSwitchOn429` as a boolean. Invalid config is preserved and disables automatic switching.
|
|
63
59
|
|
|
64
|
-
|
|
60
|
+
## State and storage
|
|
61
|
+
|
|
62
|
+
The extension maintains a generated, credential-free quota cache at `~/.pi/agent/config/pi-multi-codex/usage.json`.
|
|
63
|
+
|
|
64
|
+
## Data, cost, and privacy
|
|
65
|
+
|
|
66
|
+
The extension reads `auth.json`. It never writes or refreshes credentials.
|
|
67
|
+
|
|
68
|
+
## Limits and recovery
|
|
69
|
+
|
|
70
|
+
Scoped sessions can switch only to exact scoped aliases.
|
|
@@ -835,6 +835,8 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
835
835
|
let sessionContext: ExtensionContext | undefined;
|
|
836
836
|
let automaticOpen = false;
|
|
837
837
|
let automaticCandidate: Model<any> | undefined;
|
|
838
|
+
let preserveModelChoice = false;
|
|
839
|
+
let selectingAutomatically = false;
|
|
838
840
|
let autoSwitchOn429 = true;
|
|
839
841
|
let configWarned = false;
|
|
840
842
|
let providerRequest: { provider?: string; status?: number } | undefined;
|
|
@@ -921,8 +923,17 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
921
923
|
if (ctx) ctx.ui.setStatus?.("pi-multi-codex", footerText(ctx));
|
|
922
924
|
};
|
|
923
925
|
|
|
926
|
+
const setModelAutomatically = async (model: Model<any>): Promise<boolean> => {
|
|
927
|
+
selectingAutomatically = true;
|
|
928
|
+
try {
|
|
929
|
+
return await pi.setModel(model);
|
|
930
|
+
} finally {
|
|
931
|
+
selectingAutomatically = false;
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
924
935
|
const updatePendingCandidate = (ctx: ExtensionContext): void => {
|
|
925
|
-
if (!
|
|
936
|
+
if (!ctx.model || !isManagedProvider(ctx.model.provider)) return;
|
|
926
937
|
const slot = selectFreshSlot(ctx, ctx.model);
|
|
927
938
|
automaticCandidate = slot ? { ...ctx.model, provider: providerForSlot(slot) } : ctx.model;
|
|
928
939
|
};
|
|
@@ -941,6 +952,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
941
952
|
sessionContext = ctx;
|
|
942
953
|
triedSlots.clear();
|
|
943
954
|
providerRequest = undefined;
|
|
955
|
+
preserveModelChoice = false;
|
|
944
956
|
try {
|
|
945
957
|
autoSwitchOn429 = configStore.loadSync().value.autoSwitchOn429;
|
|
946
958
|
} catch {
|
|
@@ -962,14 +974,17 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
962
974
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
963
975
|
triedSlots.clear();
|
|
964
976
|
providerRequest = undefined;
|
|
965
|
-
if (!automaticOpen) return;
|
|
966
977
|
const model = ctx.model;
|
|
978
|
+
const slot = slotForProvider(model?.provider ?? "");
|
|
979
|
+
const identity = slot ? currentIdentity(slot) : undefined;
|
|
980
|
+
const snapshot = slot ? quota.snapshot(slot) : undefined;
|
|
981
|
+
if (!automaticOpen && (preserveModelChoice || !identity || snapshot?.accountHash !== identity.accountHash || !isFiveHourLimited(snapshot, Date.now()))) return;
|
|
967
982
|
updatePendingCandidate(ctx);
|
|
968
983
|
// Close before await: no timer, refresh, queued turn, or provider event can route later.
|
|
969
984
|
automaticOpen = false;
|
|
970
985
|
const candidate = automaticCandidate;
|
|
971
986
|
if (!model || !candidate || !isManagedProvider(model.provider) || candidate.id !== model.id || candidate.provider === model.provider) return;
|
|
972
|
-
await
|
|
987
|
+
await setModelAutomatically(candidate);
|
|
973
988
|
updateFooter(ctx);
|
|
974
989
|
});
|
|
975
990
|
|
|
@@ -1002,7 +1017,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
1002
1017
|
syncSlots();
|
|
1003
1018
|
for (const slot of failoverSlots(ctx, model)) {
|
|
1004
1019
|
const provider = providerForSlot(slot);
|
|
1005
|
-
if (!await
|
|
1020
|
+
if (!await setModelAutomatically({ ...model, provider })) continue;
|
|
1006
1021
|
ctx.ui.notify(`Codex #${failedSlot} returned HTTP 429. Retrying with Codex #${slot}.`, "warning");
|
|
1007
1022
|
updateFooter(ctx);
|
|
1008
1023
|
return { message: { ...event.message, errorMessage: `HTTP 429: ${event.message.errorMessage ?? "Codex request was rate limited."}` } };
|
|
@@ -1018,6 +1033,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
1018
1033
|
|
|
1019
1034
|
pi.on("agent_start", (_event, ctx) => {
|
|
1020
1035
|
automaticOpen = false;
|
|
1036
|
+
preserveModelChoice = false;
|
|
1021
1037
|
if (!sessionHasAgentWork(ctx)) pi.appendEntry(AGENT_STARTED_ENTRY);
|
|
1022
1038
|
});
|
|
1023
1039
|
pi.on("agent_settled", () => {
|
|
@@ -1032,8 +1048,9 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
1032
1048
|
quota.stop();
|
|
1033
1049
|
});
|
|
1034
1050
|
pi.on("model_select", (_event, ctx) => {
|
|
1035
|
-
// Explicit selector choice wins over
|
|
1051
|
+
// Explicit selector choice wins over routing at the next agent boundary.
|
|
1036
1052
|
automaticOpen = false;
|
|
1053
|
+
if (!selectingAutomatically) preserveModelChoice = true;
|
|
1037
1054
|
syncSlots();
|
|
1038
1055
|
quota.requestRefresh();
|
|
1039
1056
|
updateFooter(ctx);
|