@henryqw/pi-multi-codex 1.0.2 → 1.0.4
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 +14 -16
- package/extensions/multi-codex.ts +25 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|

|
|
@@ -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
|
|
|
@@ -36,6 +36,8 @@ A numbered slot is one Codex account position in Pi.
|
|
|
36
36
|
## Flow
|
|
37
37
|
|
|
38
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.
|
|
39
41
|
- After HTTP 429, failover can use authenticated, registered, scope-allowed, untried slots with stale or missing quota.
|
|
40
42
|
- Failover skips known active five-hour blocks. It ranks fresh known quota first, then unranked slots by slot number.
|
|
41
43
|
- Routing preserves the model ID.
|
|
@@ -45,24 +47,20 @@ A numbered slot is one Codex account position in Pi.
|
|
|
45
47
|
|
|
46
48
|
## Config
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
Package-owned: `~/.pi/agent/config/pi-multi-codex/config.json`
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
```
|
|
52
|
+
| Name | Description | Values | Default |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| `autoSwitchOn429` | Switches to another eligible slot after an HTTP 429. | Boolean. | `true` |
|
|
55
55
|
|
|
56
|
-
The config must contain only
|
|
56
|
+
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.
|
|
57
57
|
|
|
58
58
|
## State and storage
|
|
59
59
|
|
|
60
60
|
The extension maintains a generated, credential-free quota cache at `~/.pi/agent/config/pi-multi-codex/usage.json`.
|
|
61
61
|
|
|
62
|
-
##
|
|
62
|
+
## Limits and recovery
|
|
63
63
|
|
|
64
64
|
The extension reads `auth.json`. It never writes or refreshes credentials.
|
|
65
65
|
|
|
66
|
-
## Limits and recovery
|
|
67
|
-
|
|
68
66
|
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,
|
|
@@ -258,22 +258,11 @@ async function saveState(state: UsageState, signal?: AbortSignal): Promise<void>
|
|
|
258
258
|
signal?.throwIfAborted();
|
|
259
259
|
if (state.untrusted) return;
|
|
260
260
|
const file = cachePath();
|
|
261
|
-
const directory = extensionConfigDir("pi-multi-codex");
|
|
262
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
263
261
|
const data = `${JSON.stringify({
|
|
264
262
|
slots: [...state.slots.values()].sort((a, b) => a.slot - b.slot),
|
|
265
263
|
locks: [...state.locks.values()].sort((a, b) => a.slot - b.slot),
|
|
266
264
|
})}\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
|
-
}
|
|
265
|
+
await writePrivateTextFileAtomically(file, data, { signal });
|
|
277
266
|
}
|
|
278
267
|
|
|
279
268
|
async function withCacheMutex<T>(operation: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
@@ -835,6 +824,8 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
835
824
|
let sessionContext: ExtensionContext | undefined;
|
|
836
825
|
let automaticOpen = false;
|
|
837
826
|
let automaticCandidate: Model<any> | undefined;
|
|
827
|
+
let preserveModelChoice = false;
|
|
828
|
+
let selectingAutomatically = false;
|
|
838
829
|
let autoSwitchOn429 = true;
|
|
839
830
|
let configWarned = false;
|
|
840
831
|
let providerRequest: { provider?: string; status?: number } | undefined;
|
|
@@ -921,8 +912,17 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
921
912
|
if (ctx) ctx.ui.setStatus?.("pi-multi-codex", footerText(ctx));
|
|
922
913
|
};
|
|
923
914
|
|
|
915
|
+
const setModelAutomatically = async (model: Model<any>): Promise<boolean> => {
|
|
916
|
+
selectingAutomatically = true;
|
|
917
|
+
try {
|
|
918
|
+
return await pi.setModel(model);
|
|
919
|
+
} finally {
|
|
920
|
+
selectingAutomatically = false;
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
|
|
924
924
|
const updatePendingCandidate = (ctx: ExtensionContext): void => {
|
|
925
|
-
if (!
|
|
925
|
+
if (!ctx.model || !isManagedProvider(ctx.model.provider)) return;
|
|
926
926
|
const slot = selectFreshSlot(ctx, ctx.model);
|
|
927
927
|
automaticCandidate = slot ? { ...ctx.model, provider: providerForSlot(slot) } : ctx.model;
|
|
928
928
|
};
|
|
@@ -941,6 +941,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
941
941
|
sessionContext = ctx;
|
|
942
942
|
triedSlots.clear();
|
|
943
943
|
providerRequest = undefined;
|
|
944
|
+
preserveModelChoice = false;
|
|
944
945
|
try {
|
|
945
946
|
autoSwitchOn429 = configStore.loadSync().value.autoSwitchOn429;
|
|
946
947
|
} catch {
|
|
@@ -962,14 +963,17 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
962
963
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
963
964
|
triedSlots.clear();
|
|
964
965
|
providerRequest = undefined;
|
|
965
|
-
if (!automaticOpen) return;
|
|
966
966
|
const model = ctx.model;
|
|
967
|
+
const slot = slotForProvider(model?.provider ?? "");
|
|
968
|
+
const identity = slot ? currentIdentity(slot) : undefined;
|
|
969
|
+
const snapshot = slot ? quota.snapshot(slot) : undefined;
|
|
970
|
+
if (!automaticOpen && (preserveModelChoice || !identity || snapshot?.accountHash !== identity.accountHash || !isFiveHourLimited(snapshot, Date.now()))) return;
|
|
967
971
|
updatePendingCandidate(ctx);
|
|
968
972
|
// Close before await: no timer, refresh, queued turn, or provider event can route later.
|
|
969
973
|
automaticOpen = false;
|
|
970
974
|
const candidate = automaticCandidate;
|
|
971
975
|
if (!model || !candidate || !isManagedProvider(model.provider) || candidate.id !== model.id || candidate.provider === model.provider) return;
|
|
972
|
-
await
|
|
976
|
+
await setModelAutomatically(candidate);
|
|
973
977
|
updateFooter(ctx);
|
|
974
978
|
});
|
|
975
979
|
|
|
@@ -1002,7 +1006,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
1002
1006
|
syncSlots();
|
|
1003
1007
|
for (const slot of failoverSlots(ctx, model)) {
|
|
1004
1008
|
const provider = providerForSlot(slot);
|
|
1005
|
-
if (!await
|
|
1009
|
+
if (!await setModelAutomatically({ ...model, provider })) continue;
|
|
1006
1010
|
ctx.ui.notify(`Codex #${failedSlot} returned HTTP 429. Retrying with Codex #${slot}.`, "warning");
|
|
1007
1011
|
updateFooter(ctx);
|
|
1008
1012
|
return { message: { ...event.message, errorMessage: `HTTP 429: ${event.message.errorMessage ?? "Codex request was rate limited."}` } };
|
|
@@ -1018,6 +1022,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
1018
1022
|
|
|
1019
1023
|
pi.on("agent_start", (_event, ctx) => {
|
|
1020
1024
|
automaticOpen = false;
|
|
1025
|
+
preserveModelChoice = false;
|
|
1021
1026
|
if (!sessionHasAgentWork(ctx)) pi.appendEntry(AGENT_STARTED_ENTRY);
|
|
1022
1027
|
});
|
|
1023
1028
|
pi.on("agent_settled", () => {
|
|
@@ -1032,8 +1037,9 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
1032
1037
|
quota.stop();
|
|
1033
1038
|
});
|
|
1034
1039
|
pi.on("model_select", (_event, ctx) => {
|
|
1035
|
-
// Explicit selector choice wins over
|
|
1040
|
+
// Explicit selector choice wins over routing at the next agent boundary.
|
|
1036
1041
|
automaticOpen = false;
|
|
1042
|
+
if (!selectingAutomatically) preserveModelChoice = true;
|
|
1037
1043
|
syncSlots();
|
|
1038
1044
|
quota.requestRefresh();
|
|
1039
1045
|
updateFooter(ctx);
|
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.4",
|
|
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": {
|