@henryqw/pi-multi-codex 1.0.2 → 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,6 +1,6 @@
1
1
  # `@henryqw/pi-multi-codex`
2
2
 
3
- Add multiple ChatGPT Codex OAuth accounts and start new Pi work on the eligible slot with the most weekly quota. Optional automatic switching can retry an HTTP 429 on another eligible slot.
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
6
  ![Flowchart separating fresh-quota startup ranking from broader HTTP 429 failover](./docs/codex-routing-flow.svg)
@@ -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.
@@ -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.2",
3
+ "version": "1.0.3",
4
4
  "description": "Use multiple ChatGPT Codex OAuth accounts in Pi.",
5
5
  "keywords": [
6
6
  "pi-package",