@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 CHANGED
@@ -1,13 +1,9 @@
1
1
  # `@henryqw/pi-multi-codex`
2
2
 
3
- Add multiple ChatGPT Codex OAuth accounts and start new Pi work on the slot with the most weekly quota.
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
  ![Pi showing Codex account quotas and the active footer slot](./example.png)
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
+ ![Flowchart separating fresh-quota startup ranking from broader HTTP 429 failover](./docs/codex-routing-flow.svg)
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
- ## With
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
- ![Flowchart separating fresh-quota startup ranking from broader HTTP 429 failover](./docs/codex-routing-flow.svg)
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
- Generated credential-free quota cache: `~/.pi/agent/config/pi-multi-codex/usage.json`. The extension maintains it.
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 (!automaticOpen || !ctx.model || !isManagedProvider(ctx.model.provider)) return;
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 pi.setModel(candidate);
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 pi.setModel({ ...model, provider })) continue;
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 startup routing.
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-multi-codex",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Use multiple ChatGPT Codex OAuth accounts in Pi.",
5
5
  "keywords": [
6
6
  "pi-package",