oc-codex-multi-auth 6.20.0 → 6.21.0
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 +15 -12
- package/config/README.md +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +293 -66
- package/dist/index.js.map +1 -1
- package/dist/lib/account-display.d.ts.map +1 -1
- package/dist/lib/account-display.js +3 -1
- package/dist/lib/account-display.js.map +1 -1
- package/dist/lib/accounts/persistence.d.ts +3 -1
- package/dist/lib/accounts/persistence.d.ts.map +1 -1
- package/dist/lib/accounts/persistence.js +27 -3
- package/dist/lib/accounts/persistence.js.map +1 -1
- package/dist/lib/accounts/stale-state.d.ts +1 -0
- package/dist/lib/accounts/stale-state.d.ts.map +1 -1
- package/dist/lib/accounts/stale-state.js +30 -0
- package/dist/lib/accounts/stale-state.js.map +1 -1
- package/dist/lib/accounts/warm-recovery.d.ts +8 -0
- package/dist/lib/accounts/warm-recovery.d.ts.map +1 -0
- package/dist/lib/accounts/warm-recovery.js +70 -0
- package/dist/lib/accounts/warm-recovery.js.map +1 -0
- package/dist/lib/accounts/warm-request.d.ts +2 -0
- package/dist/lib/accounts/warm-request.d.ts.map +1 -1
- package/dist/lib/accounts/warm-request.js +4 -2
- package/dist/lib/accounts/warm-request.js.map +1 -1
- package/dist/lib/accounts.d.ts +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +2 -2
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/circuit-breaker.d.ts +5 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -1
- package/dist/lib/circuit-breaker.js +23 -6
- package/dist/lib/circuit-breaker.js.map +1 -1
- package/dist/lib/codex-reset.d.ts +7 -1
- package/dist/lib/codex-reset.d.ts.map +1 -1
- package/dist/lib/codex-reset.js +9 -2
- package/dist/lib/codex-reset.js.map +1 -1
- package/dist/lib/codex-usage.d.ts +3 -1
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +35 -2
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/config.d.ts +2 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +38 -14
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/parallel-probe.d.ts.map +1 -1
- package/dist/lib/parallel-probe.js +2 -1
- package/dist/lib/parallel-probe.js.map +1 -1
- package/dist/lib/quota-notifications.d.ts.map +1 -1
- package/dist/lib/quota-notifications.js +10 -1
- package/dist/lib/quota-notifications.js.map +1 -1
- package/dist/lib/request/fetch-helpers.js +22 -18
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/response-handler.d.ts.map +1 -1
- package/dist/lib/request/response-handler.js +85 -43
- package/dist/lib/request/response-handler.js.map +1 -1
- package/dist/lib/rotation.d.ts +1 -1
- package/dist/lib/storage/load-save.d.ts +1 -0
- package/dist/lib/storage/load-save.d.ts.map +1 -1
- package/dist/lib/storage/load-save.js +11 -0
- package/dist/lib/storage/load-save.js.map +1 -1
- package/dist/lib/storage/normalize.d.ts.map +1 -1
- package/dist/lib/storage/normalize.js +38 -2
- package/dist/lib/storage/normalize.js.map +1 -1
- package/dist/lib/storage/state.d.ts +1 -0
- package/dist/lib/storage/state.d.ts.map +1 -1
- package/dist/lib/storage/state.js +12 -0
- package/dist/lib/storage/state.js.map +1 -1
- package/dist/lib/tools/codex-limits.d.ts.map +1 -1
- package/dist/lib/tools/codex-limits.js +13 -1
- package/dist/lib/tools/codex-limits.js.map +1 -1
- package/dist/lib/tools/codex-reset.d.ts.map +1 -1
- package/dist/lib/tools/codex-reset.js +40 -1
- package/dist/lib/tools/codex-reset.js.map +1 -1
- package/dist/lib/tools/codex-warm.d.ts +2 -1
- package/dist/lib/tools/codex-warm.d.ts.map +1 -1
- package/dist/lib/tools/codex-warm.js +48 -5
- package/dist/lib/tools/codex-warm.js.map +1 -1
- package/dist/lib/tools/index.d.ts +1 -1
- package/dist/lib/tools/index.d.ts.map +1 -1
- package/dist/lib/tui-quota-cache.d.ts.map +1 -1
- package/dist/lib/tui-quota-cache.js +3 -1
- package/dist/lib/tui-quota-cache.js.map +1 -1
- package/dist/tui.d.ts +4 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +24 -14
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/scripts/install-oc-codex-multi-auth-core.js +35 -5
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Use it when you want OpenCode to run Codex-style coding workflows from your own
|
|
|
20
20
|
## What You Get
|
|
21
21
|
|
|
22
22
|
- OpenCode plugin support for ChatGPT Plus/Pro OAuth and Codex/GPT-5 coding workflows
|
|
23
|
-
- GPT-6 Astra
|
|
23
|
+
- GPT-6 Astra, GPT-5.6 Sol/Terra/Luna, and the Daybreak Blue/Red cyber tiers on the responses-lite request path, plus GPT-5.5, GPT-5.5 Fast, GPT-5.4 Mini, GPT-5.4 Nano, GPT-5.1, and Codex model templates
|
|
24
24
|
- Routing for the Daybreak-gated cyber tiers (`gpt-daybreak-blue-latest`, `gpt-daybreak-red-latest`, `gpt-5.6-cyber`), deliberately kept out of the shipped templates since they need program approval
|
|
25
25
|
- Compact modern OpenCode config with 13 base families and 59 variant presets; explicit legacy selector IDs when needed
|
|
26
26
|
- Stateless Codex-compatible request handling with `store: false` and `reasoning.encrypted_content`
|
|
@@ -50,7 +50,7 @@ Use it when you want OpenCode to run Codex-style coding workflows from your own
|
|
|
50
50
|
| `oc-codex-multi-auth` | npm CLI; explicit install modes manage OpenCode provider/TUI config, while `update` only clears the managed package cache. Also runs standalone commands: `doctor`, `status`, `list`, `limits`, `dashboard`, `health`, `diag`, `warm` |
|
|
51
51
|
| OpenCode plugin entry (`index.ts`) | auth loader, OAuth login modes, provider fetch pipeline, account rotation, retry/failover, and `codex-*` tool registry |
|
|
52
52
|
| OpenCode TUI plugin (`tui.ts`) | prompt quota status, quota details, shared quota cache, and active-account-aware display |
|
|
53
|
-
| 24 `codex-*` tools | setup, help, status, list, switch, warm, limits, health, metrics, doctor, dashboard, pool, backup, keychain, diagnostics, and
|
|
53
|
+
| 24 `codex-*` tools | setup, help, status, list, switch, warm, limits, health, metrics, doctor, dashboard, pool, backup, keychain, diagnostics, and repair actions |
|
|
54
54
|
|
|
55
55
|
The plugin does not replace OpenCode. OpenCode remains the host; this package installs provider/TUI config and supplies the OAuth-backed Codex request pipeline that OpenCode calls.
|
|
56
56
|
|
|
@@ -128,7 +128,7 @@ opencode debug config
|
|
|
128
128
|
opencode auth login
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
The default installer only normalizes the plugin entry in `~/.config/opencode/opencode.json`, enables the TUI status plugin in `~/.config/opencode/tui.json`, and clears the cached plugin copy. Catalog modes
|
|
131
|
+
The default installer only normalizes the plugin entry in `~/.config/opencode/opencode.json`, enables the TUI status plugin in `~/.config/opencode/tui.json`, and clears the cached plugin copy. Catalog modes also merge their selected `provider.openai` definitions. Changed config files are backed up before writing.
|
|
132
132
|
|
|
133
133
|
### Standalone CLI (no agent / no token cost)
|
|
134
134
|
|
|
@@ -225,10 +225,10 @@ If browser launch is blocked, use the alternate login paths in [docs/getting-sta
|
|
|
225
225
|
| `codex-status` | Which account, model family, and routing state are active? |
|
|
226
226
|
| `codex-limits` | What quota or rate-limit state is visible now? |
|
|
227
227
|
| `codex-reset` | Do I have a banked rate-limit reset credit, and how do I redeem it? |
|
|
228
|
-
| `codex-dashboard` |
|
|
228
|
+
| `codex-dashboard` | What does a read-only snapshot of account eligibility, retry budgets, and refresh queue health show? |
|
|
229
229
|
| `codex-pool` | Which accounts are preferred for each model, and how do I change them? |
|
|
230
230
|
|
|
231
|
-
Most of these also run as a **direct CLI** with no agent
|
|
231
|
+
Most of these also run as a **direct CLI** with no agent or model involvement, so there is no token cost. Examples are `oc-codex-multi-auth warm`, `oc-codex-multi-auth status`, or `npx -y oc-codex-multi-auth@latest warm`. Use `oc-codex-multi-auth warm` to open every enabled account's usage window at the start of a session and stagger the rolling quota cooldowns. Add `--json` for scriptable output.
|
|
232
232
|
|
|
233
233
|
### Account management
|
|
234
234
|
|
|
@@ -238,7 +238,7 @@ Most of these also run as a **direct CLI** with no agent/model involvement (no t
|
|
|
238
238
|
| `codex-tag` | How do I group accounts with tags? |
|
|
239
239
|
| `codex-note` | How do I attach a private note to an account? |
|
|
240
240
|
| `codex-remove` | How do I remove a saved account safely? |
|
|
241
|
-
| `codex-refresh` | How do I refresh
|
|
241
|
+
| `codex-refresh` | How do I refresh the OAuth tokens of every saved account to verify they are still valid? |
|
|
242
242
|
|
|
243
243
|
### Diagnostics and backup
|
|
244
244
|
|
|
@@ -266,7 +266,7 @@ Most of these also run as a **direct CLI** with no agent/model involvement (no t
|
|
|
266
266
|
- TUI quota status follows the account/workspace used by the latest request
|
|
267
267
|
- Business workspace memberships and Personal accounts keep separate usage and quota windows. Business members sharing one workspace are distinguished by their member/seat identity, so their usage is not collapsed into one row.
|
|
268
268
|
- An account identifies itself by its own ChatGPT email and the last 6 characters of its account id, with the email masked when `maskEmail` is on. The OAuth id_token also lists the API-platform organizations the login belongs to; those are not ChatGPT workspaces and are never used to name an account, so logging in clears a label left behind by one. A label you set with `codex-label` is always kept.
|
|
269
|
-
- The ChatGPT plan (`Free`, `Plus`, `Pro`, `Business`, `Business Premium`) is read from the access token, refreshed on every token refresh, and shown by `codex-list` and `codex-status`. `codex-limits` and the TUI read the plan live from the usage endpoint and name it the same way. An unrecognized plan is reported verbatim rather than renamed.
|
|
269
|
+
- The ChatGPT plan (`Free`, `Plus`, `Pro`, `Business`, `Business Premium`, `Enterprise`) is read from the access token, refreshed on every token refresh, and shown by `codex-list` and `codex-status`. `codex-limits` and the TUI read the plan live from the usage endpoint and name it the same way. An unrecognized plan is reported verbatim rather than renamed.
|
|
270
270
|
|
|
271
271
|
---
|
|
272
272
|
|
|
@@ -280,10 +280,10 @@ Most of these also run as a **direct CLI** with no agent/model involvement (no t
|
|
|
280
280
|
| Plugin config | `~/.opencode/openai-codex-auth-config.json` |
|
|
281
281
|
| Global account storage | `~/.opencode/oc-codex-multi-auth-accounts.json` |
|
|
282
282
|
| Per-project accounts | `~/.opencode/projects/<project-key>/oc-codex-multi-auth-accounts.json` |
|
|
283
|
-
| Flagged accounts |
|
|
283
|
+
| Flagged accounts | `oc-codex-multi-auth-flagged-accounts.json`, written beside the active accounts file (per-project path when `perProjectAccounts` is on) |
|
|
284
284
|
| Backups | `~/.opencode/backups/` or `~/.opencode/projects/<project-key>/backups/` |
|
|
285
285
|
| Logs | `~/.opencode/logs/codex-plugin/` |
|
|
286
|
-
| TUI quota cache | OpenCode state
|
|
286
|
+
| TUI quota cache | OpenCode state dir plus `oc-codex-multi-auth-tui-quota.json`, else `$OPENCODE_STATE_DIR/oc-codex-multi-auth-tui-quota.json` or `~/.local/state/opencode/oc-codex-multi-auth-tui-quota.json` |
|
|
287
287
|
|
|
288
288
|
Per-project storage is enabled by default. The plugin walks up from the current directory to find a project root, then stores account pools under the project-specific key. If no project root is found, it falls back to global storage.
|
|
289
289
|
|
|
@@ -430,6 +430,9 @@ Selected runtime/environment overrides:
|
|
|
430
430
|
| `ENABLE_PLUGIN_REQUEST_LOGGING=1` | Enable request metadata logs |
|
|
431
431
|
| `CODEX_PLUGIN_LOG_BODIES=1` | Include raw request/response bodies in logs; sensitive |
|
|
432
432
|
| `CODEX_KEYCHAIN=1` | Opt in to OS-native keychain account storage |
|
|
433
|
+
| `CODEX_AUTH_QUOTA_NOTIFICATIONS=1` | Enable desktop quota notifications (macOS only) |
|
|
434
|
+
| `CODEX_AUTH_AUTO_PROTECT_CREDITS=0/1` | Disable/enable the quota guard that keeps rotation off paid Credits after a spent subscription window (default on) |
|
|
435
|
+
| `CODEX_AUTH_QUOTA_NOTIFICATIONS_INTERVAL_MS=<ms>` | Override the quota poll interval (default 1800000, minimum 30000) |
|
|
433
436
|
|
|
434
437
|
Boolean env overrides are truthy only for the literal string `"1"`.
|
|
435
438
|
|
|
@@ -467,9 +470,9 @@ Set `CODEX_KEYCHAIN=1` to store account pools in the OS keychain instead:
|
|
|
467
470
|
Manage the backend from OpenCode:
|
|
468
471
|
|
|
469
472
|
```text
|
|
470
|
-
codex-keychain status
|
|
471
|
-
codex-keychain migrate
|
|
472
|
-
codex-keychain rollback
|
|
473
|
+
codex-keychain command="status"
|
|
474
|
+
codex-keychain command="migrate"
|
|
475
|
+
codex-keychain command="rollback"
|
|
473
476
|
```
|
|
474
477
|
|
|
475
478
|
If the keychain is unavailable, the plugin logs a warning and falls back to JSON storage for that operation. Credentials are never silently deleted.
|
package/config/README.md
CHANGED
|
@@ -87,7 +87,7 @@ If your OpenCode runtime supports global compaction tuning, you can also set val
|
|
|
87
87
|
|
|
88
88
|
- Served over the **responses-lite** path. Astra's catalog entry landed in openai/codex commit `ed391d4d` (2026-09-03) and reads `use_responses_lite: true`, `tool_mode: "code_mode_only"`, `multi_agent_version: "v2"`, so the shape is read rather than inferred. The `CODEX_AUTH_ASTRA_RESPONSES_LITE` switch that 6.17.0 carried while this was unverifiable has been removed.
|
|
89
89
|
- Rollout gate: Astra reached a limited set of organizations first, so accounts outside it auto-fallback
|
|
90
|
-
`gpt-6-astra → gpt-5.6-sol → gpt-5.6-terra → gpt-5.6-luna → gpt-5.5`
|
|
90
|
+
`gpt-6-astra → gpt-5.6-sol → gpt-5.6-terra → gpt-5.6-luna → gpt-5.5 → gpt-5.2`
|
|
91
91
|
(disable with `CODEX_AUTH_DISABLE_GPT6_AUTO_FALLBACK=1`).
|
|
92
92
|
- Efforts are low through `ultra`, per OpenAI's Codex model list. `ultra` is sent as `max` on the wire, as with 5.6.
|
|
93
93
|
- Bare `gpt-6` is a plugin-side alias. `gpt-6-astra-pro` is not a Codex-routable id and collapses onto `gpt-6-astra`.
|
|
@@ -95,21 +95,21 @@ If your OpenCode runtime supports global compaction tuning, you can also set val
|
|
|
95
95
|
## Cyber tier notes (Daybreak-gated)
|
|
96
96
|
|
|
97
97
|
- `gpt-daybreak-blue-latest` (defensive) and `gpt-daybreak-red-latest` (cyber-permissive, for authorized security research) are catalog-verified cyber-specialty models, served over the responses-lite path. `gpt-5.6-cyber` is OpenAI's published alias fronting them, and belongs to the 5.6 generation rather than GPT-6.
|
|
98
|
-
- All three need Daybreak program approval, and Blue/Red are `visibility: "hide"` in the catalog, so they are **not** in the shipped templates
|
|
99
|
-
- None has a fallback chain on purpose
|
|
98
|
+
- All three need Daybreak program approval, and Blue/Red are `visibility: "hide"` in the catalog, so they are **not** in the shipped templates, the same policy as `gpt-5.3-codex-spark` below. The plugin routes them fully; entitled users add the ids by hand.
|
|
99
|
+
- None has a fallback chain on purpose, because a cyber-specialty request must not be silently answered by a general model.
|
|
100
100
|
|
|
101
101
|
## GPT-5.6 notes
|
|
102
102
|
|
|
103
103
|
- Served over the **responses-lite** path (`use_responses_lite`).
|
|
104
104
|
- Preview entitlement: accounts without access auto-fallback
|
|
105
|
-
`gpt-5.6-sol → gpt-5.6-terra → gpt-5.6-luna → gpt-5.5`
|
|
105
|
+
`gpt-5.6-sol → gpt-5.6-terra → gpt-5.6-luna → gpt-5.5 → gpt-5.2`
|
|
106
106
|
(disable with `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1`).
|
|
107
107
|
- Default client identity for every responses-lite model (5.6, GPT-6 Astra, Daybreak) is host/opencode (`originator: opencode`); other families default to Codex CLI identity.
|
|
108
108
|
- `ultra` is accepted as a client-side alias and sent as `max` on the wire (no subagent orchestration in this plugin).
|
|
109
109
|
|
|
110
110
|
## Spark model note
|
|
111
111
|
|
|
112
|
-
The templates intentionally do **not** include `gpt-5.3-codex-spark` by default. Spark is often entitlement-gated at the account/workspace level, so shipping it by default causes avoidable startup failures for many users.
|
|
112
|
+
The templates intentionally do **not** include `gpt-5.3-codex-spark` by default. Spark is a distinct backend model id, not an alias of `gpt-5-codex`, and it is often entitlement-gated at the account/workspace level, so shipping it by default causes avoidable startup failures for many users.
|
|
113
113
|
|
|
114
114
|
If your workspace is entitled, you can add Spark model IDs manually.
|
|
115
115
|
|
|
@@ -147,12 +147,12 @@ Current defaults are strict entitlement handling except for common default-selec
|
|
|
147
147
|
- set `unsupportedCodexPolicy: "fallback"` (or `CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback`) to enable the full fallback chain for manual/legacy selectors
|
|
148
148
|
- `fallbackToGpt52OnUnsupportedGpt53: true` keeps the legacy `gpt-5.3-codex -> gpt-5.2-codex` edge inside fallback mode
|
|
149
149
|
- user-typed `gpt-5.5-pro*` is canonicalized to `gpt-5.5` before fallback because GPT-5.5 Pro is ChatGPT-only, not a Codex-routable model; `gpt-6-astra-pro*` is canonicalized to `gpt-6-astra` for the same reason
|
|
150
|
-
- legacy Codex
|
|
150
|
+
- legacy Codex selector `gpt-5.1-codex` normalizes to canonical `gpt-5-codex`, while `gpt-5.2-codex` and `gpt-5.3-codex` route as distinct backend model IDs; if canonical Codex or a gated model fails entitlement, default auto-fallback can retry through `gpt-5.6-terra`, `gpt-5.5`, then `gpt-5.2`
|
|
151
151
|
- set `CODEX_AUTH_DISABLE_GPT6_AUTO_FALLBACK=1` to disable GPT-6 Astra auto-fallback
|
|
152
152
|
- set `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1` to disable GPT-5.6 auto-fallback
|
|
153
153
|
- set `CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1` to disable GPT-5.5 auto-fallback
|
|
154
154
|
- set `CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1` to disable canonical Codex/GPT-5.4-family auto-fallback
|
|
155
|
-
- `gpt-5.4-pro -> gpt-5.
|
|
155
|
+
- `gpt-5.4-pro -> gpt-5.6-terra -> gpt-5.5 -> gpt-5.2` applies when you manually select `gpt-5.4-pro`
|
|
156
156
|
- `unsupportedCodexFallbackChain` lets you override fallback order per model
|
|
157
157
|
|
|
158
158
|
Default chains when generic fallback policy is enabled (and empty override map):
|
|
@@ -163,7 +163,7 @@ Default chains when generic fallback policy is enabled (and empty override map):
|
|
|
163
163
|
- `gpt-5-codex → gpt-5.6-terra → gpt-5.5 → gpt-5.2`
|
|
164
164
|
|
|
165
165
|
> GPT-5.4 and GPT-5.4 Mini were retired from Codex on 2026-08-31; the catalog marks both `visibility: "hide"` and names their replacements (`gpt-5.4` -> `gpt-5.6-terra`, `gpt-5.4-mini` -> `gpt-5.6-luna`), and `gpt-5.4-nano` has no catalog entry. The default chains therefore end at live models rather than leading with retired ones.
|
|
166
|
-
- `gpt-5.4-pro → gpt-5.
|
|
166
|
+
- `gpt-5.4-pro → gpt-5.6-terra → gpt-5.5 → gpt-5.2` (if you manually select `gpt-5.4-pro`)
|
|
167
167
|
- `gpt-5.3-codex → gpt-5-codex → gpt-5.2-codex`
|
|
168
168
|
- `gpt-5.3-codex-spark → gpt-5-codex → gpt-5.3-codex → gpt-5.2-codex` (only if Spark IDs are added manually)
|
|
169
169
|
- `gpt-5.2-codex → gpt-5-codex`
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAYH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AA6U/D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAg4I/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
|
|
24
24
|
*/
|
|
25
25
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
26
|
+
import { watchFile, unwatchFile } from "node:fs";
|
|
27
|
+
import { consumeLastWrittenAccountsDigest } from "./lib/storage/load-save.js";
|
|
28
|
+
import { subscribeToStoragePathChanges } from "./lib/storage/state.js";
|
|
29
|
+
import { isKeychainOptInEnabled } from "./lib/storage/keychain.js";
|
|
30
|
+
import { AnyAccountStorageSchema } from "./lib/schemas.js";
|
|
31
|
+
import { registerCleanup, unregisterCleanup } from "./lib/shutdown.js";
|
|
26
32
|
import { createHash } from "node:crypto";
|
|
27
33
|
import { homedir } from "node:os";
|
|
28
34
|
import { join } from "node:path";
|
|
@@ -833,6 +839,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
833
839
|
.filter((entry) => entry.length > 0)));
|
|
834
840
|
};
|
|
835
841
|
const supportsInteractiveMenus = () => {
|
|
842
|
+
if (process.env.FORCE_INTERACTIVE_MODE === "1")
|
|
843
|
+
return true;
|
|
836
844
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
837
845
|
return false;
|
|
838
846
|
if (process.env.OPENCODE_TUI === "1")
|
|
@@ -841,6 +849,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
841
849
|
return false;
|
|
842
850
|
if (process.env.TERM_PROGRAM === "opencode")
|
|
843
851
|
return false;
|
|
852
|
+
if (process.env.ELECTRON_RUN_AS_NODE === "1")
|
|
853
|
+
return false;
|
|
844
854
|
return true;
|
|
845
855
|
};
|
|
846
856
|
const promptAccountIndexSelection = async (ui, storage, title) => {
|
|
@@ -1112,22 +1122,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1112
1122
|
logDebug(`[${PLUGIN_NAME}] Startup preflight skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
1113
1123
|
}
|
|
1114
1124
|
};
|
|
1115
|
-
const invalidateAccountManagerCache = () => {
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
// pointing at a stale instance. Flush first (best-effort, in the
|
|
1119
|
-
// background) so any queued debounced save is not silently dropped.
|
|
1125
|
+
const invalidateAccountManagerCache = (clearedSnapshots) => {
|
|
1126
|
+
// Retire before flushing: keep disk membership authoritative while
|
|
1127
|
+
// publishing queued rate-limit evidence through the volatile merge.
|
|
1120
1128
|
const previous = cachedAccountManager;
|
|
1121
1129
|
cachedAccountManager = null;
|
|
1122
1130
|
accountManagerPromise = null;
|
|
1123
1131
|
if (previous) {
|
|
1132
|
+
previous.disposeShutdownHandler(false, clearedSnapshots);
|
|
1124
1133
|
void previous
|
|
1125
1134
|
.flushPendingSave()
|
|
1126
1135
|
.catch((error) => {
|
|
1127
1136
|
logWarn(`Failed to flush pending save while invalidating account manager: ${error instanceof Error ? error.message : String(error)}`);
|
|
1128
|
-
})
|
|
1129
|
-
.finally(() => {
|
|
1130
|
-
previous.disposeShutdownHandler();
|
|
1131
1137
|
});
|
|
1132
1138
|
}
|
|
1133
1139
|
};
|
|
@@ -1160,6 +1166,142 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1160
1166
|
logWarn(`Failed to reload account manager: ${error instanceof Error ? error.message : String(error)}`);
|
|
1161
1167
|
}
|
|
1162
1168
|
};
|
|
1169
|
+
let watchedAccountsPath;
|
|
1170
|
+
let observedAccountsDigest;
|
|
1171
|
+
let accountsReloadTimer;
|
|
1172
|
+
let accountsWatcherDisposed = false;
|
|
1173
|
+
let accountsWatchGeneration = 0;
|
|
1174
|
+
let unsubscribeAccountsPath;
|
|
1175
|
+
const stopAccountsWatcher = () => {
|
|
1176
|
+
accountsWatchGeneration += 1;
|
|
1177
|
+
if (watchedAccountsPath)
|
|
1178
|
+
unwatchFile(watchedAccountsPath, onAccountsStatChanged);
|
|
1179
|
+
watchedAccountsPath = undefined;
|
|
1180
|
+
observedAccountsDigest = undefined;
|
|
1181
|
+
clearTimeout(accountsReloadTimer);
|
|
1182
|
+
accountsReloadTimer = undefined;
|
|
1183
|
+
};
|
|
1184
|
+
const disposeAccountsWatcher = () => {
|
|
1185
|
+
accountsWatcherDisposed = true;
|
|
1186
|
+
unsubscribeAccountsPath?.();
|
|
1187
|
+
stopAccountsWatcher();
|
|
1188
|
+
unregisterCleanup(disposeAccountsWatcher);
|
|
1189
|
+
};
|
|
1190
|
+
const readAccountsDigest = async (path) => {
|
|
1191
|
+
try {
|
|
1192
|
+
const content = await readFile(path, "utf8");
|
|
1193
|
+
if (!AnyAccountStorageSchema.safeParse(JSON.parse(content)).success)
|
|
1194
|
+
return;
|
|
1195
|
+
return createHash("sha256").update(content).digest("hex");
|
|
1196
|
+
}
|
|
1197
|
+
catch {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
const reloadForExternalAccountsChange = async (path, generation, attempt = 0, retired) => {
|
|
1202
|
+
const digest = await readAccountsDigest(path);
|
|
1203
|
+
if (generation !== accountsWatchGeneration || !digest || path !== getStoragePath())
|
|
1204
|
+
return;
|
|
1205
|
+
if (digest === consumeLastWrittenAccountsDigest(path))
|
|
1206
|
+
return;
|
|
1207
|
+
const previous = cachedAccountManager;
|
|
1208
|
+
try {
|
|
1209
|
+
// A null cache means an invalidation retired the incumbent; the
|
|
1210
|
+
// reload still must adopt the external change. Bailing here would
|
|
1211
|
+
// strand it: onAccountsFileChanged already marked this digest
|
|
1212
|
+
// observed, and an in-flight load started before the write can
|
|
1213
|
+
// still resolve and install a pre-change snapshot whose
|
|
1214
|
+
// full-membership save then deletes the imported accounts.
|
|
1215
|
+
if (previous) {
|
|
1216
|
+
if (previous !== retired) {
|
|
1217
|
+
previous.disposeShutdownHandler(true);
|
|
1218
|
+
retired = previous;
|
|
1219
|
+
}
|
|
1220
|
+
await previous.flushPendingSave();
|
|
1221
|
+
}
|
|
1222
|
+
const reloaded = await AccountManager.loadFromDisk();
|
|
1223
|
+
if (generation !== accountsWatchGeneration || accountsWatcherDisposed || path !== getStoragePath()) {
|
|
1224
|
+
reloaded.disposeShutdownHandler();
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const outgoing = cachedAccountManager;
|
|
1228
|
+
if (outgoing && outgoing !== retired) {
|
|
1229
|
+
// Another actor replaced the cached manager while this reload
|
|
1230
|
+
// was in flight (concurrent fetch reload or tool mutation).
|
|
1231
|
+
// Retire the incumbent with the same external-reload
|
|
1232
|
+
// semantics, or its queued membership save can clobber the
|
|
1233
|
+
// external change this reload is about to adopt.
|
|
1234
|
+
outgoing.disposeShutdownHandler(true);
|
|
1235
|
+
retired = outgoing;
|
|
1236
|
+
}
|
|
1237
|
+
cachedAccountManager = reloaded;
|
|
1238
|
+
accountManagerPromise = Promise.resolve(reloaded);
|
|
1239
|
+
observedAccountsDigest = digest;
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
logWarn("Could not reload externally updated account storage");
|
|
1243
|
+
if (attempt < 2 && generation === accountsWatchGeneration && !accountsWatcherDisposed) {
|
|
1244
|
+
accountsReloadTimer = setTimeout(() => {
|
|
1245
|
+
accountsReloadTimer = undefined;
|
|
1246
|
+
void reloadForExternalAccountsChange(path, generation, attempt + 1, retired);
|
|
1247
|
+
}, 1500);
|
|
1248
|
+
accountsReloadTimer.unref();
|
|
1249
|
+
}
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
logDebug("Reloaded cached account manager after external accounts file change");
|
|
1253
|
+
};
|
|
1254
|
+
const onAccountsFileChanged = async () => {
|
|
1255
|
+
if (accountsWatcherDisposed)
|
|
1256
|
+
return;
|
|
1257
|
+
if (watchedAccountsPath !== getStoragePath()) {
|
|
1258
|
+
await ensureAccountsWatcher();
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const path = watchedAccountsPath;
|
|
1262
|
+
if (!path)
|
|
1263
|
+
return;
|
|
1264
|
+
const generation = accountsWatchGeneration;
|
|
1265
|
+
const digest = await readAccountsDigest(path);
|
|
1266
|
+
if (generation !== accountsWatchGeneration || !digest || digest === observedAccountsDigest)
|
|
1267
|
+
return;
|
|
1268
|
+
observedAccountsDigest = digest;
|
|
1269
|
+
clearTimeout(accountsReloadTimer);
|
|
1270
|
+
accountsReloadTimer = undefined;
|
|
1271
|
+
if (digest === consumeLastWrittenAccountsDigest(path))
|
|
1272
|
+
return;
|
|
1273
|
+
accountsReloadTimer = setTimeout(() => {
|
|
1274
|
+
accountsReloadTimer = undefined;
|
|
1275
|
+
void reloadForExternalAccountsChange(path, generation);
|
|
1276
|
+
}, 500);
|
|
1277
|
+
accountsReloadTimer.unref();
|
|
1278
|
+
};
|
|
1279
|
+
const onAccountsStatChanged = () => {
|
|
1280
|
+
void onAccountsFileChanged();
|
|
1281
|
+
};
|
|
1282
|
+
const ensureAccountsWatcher = async () => {
|
|
1283
|
+
if (accountsWatcherDisposed)
|
|
1284
|
+
return;
|
|
1285
|
+
const path = getStoragePath();
|
|
1286
|
+
if (path === watchedAccountsPath)
|
|
1287
|
+
return;
|
|
1288
|
+
stopAccountsWatcher();
|
|
1289
|
+
if (isKeychainOptInEnabled())
|
|
1290
|
+
return;
|
|
1291
|
+
unsubscribeAccountsPath ??= subscribeToStoragePathChanges(() => {
|
|
1292
|
+
void ensureAccountsWatcher();
|
|
1293
|
+
});
|
|
1294
|
+
watchedAccountsPath = path;
|
|
1295
|
+
const generation = accountsWatchGeneration;
|
|
1296
|
+
const initialDigest = await readAccountsDigest(path);
|
|
1297
|
+
if (generation !== accountsWatchGeneration)
|
|
1298
|
+
return;
|
|
1299
|
+
observedAccountsDigest = initialDigest;
|
|
1300
|
+
// Stat polling follows the path across the storage writer's temp-file rename.
|
|
1301
|
+
watchFile(path, { interval: 1500, persistent: false }, onAccountsStatChanged);
|
|
1302
|
+
unregisterCleanup(disposeAccountsWatcher);
|
|
1303
|
+
registerCleanup(disposeAccountsWatcher);
|
|
1304
|
+
};
|
|
1163
1305
|
const persistAuthenticatedSelections = async (results, replaceAll) => {
|
|
1164
1306
|
try {
|
|
1165
1307
|
await persistAccountPool(results, replaceAll);
|
|
@@ -1192,6 +1334,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1192
1334
|
const { event } = input;
|
|
1193
1335
|
if (event.type === "server.instance.disposed") {
|
|
1194
1336
|
quotaMonitor.dispose();
|
|
1337
|
+
disposeAccountsWatcher();
|
|
1195
1338
|
return;
|
|
1196
1339
|
}
|
|
1197
1340
|
// Handle TUI account selection events
|
|
@@ -1346,7 +1489,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1346
1489
|
});
|
|
1347
1490
|
}
|
|
1348
1491
|
applyUiRuntimeFromConfig(pluginConfig);
|
|
1349
|
-
|
|
1492
|
+
let perProjectAccounts = getPerProjectAccounts(pluginConfig);
|
|
1493
|
+
let storageTransition;
|
|
1494
|
+
const activeFetches = new Set();
|
|
1350
1495
|
setStoragePath(perProjectAccounts ? process.cwd() : null);
|
|
1351
1496
|
const authFallback = auth.type === "oauth" ? auth : undefined;
|
|
1352
1497
|
// Prefer multi-account auth metadata when available, but still handle
|
|
@@ -1386,8 +1531,19 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1386
1531
|
}
|
|
1387
1532
|
});
|
|
1388
1533
|
}
|
|
1389
|
-
|
|
1390
|
-
cachedAccountManager
|
|
1534
|
+
const loadedManager = await accountManagerPromise;
|
|
1535
|
+
if (cachedAccountManager && cachedAccountManager !== loadedManager) {
|
|
1536
|
+
// An external reload replaced the cache while this load was
|
|
1537
|
+
// in flight. The loaded snapshot is stale; retiring it keeps
|
|
1538
|
+
// its debounced save from writing full membership over the
|
|
1539
|
+
// successor's state.
|
|
1540
|
+
loadedManager.disposeShutdownHandler();
|
|
1541
|
+
}
|
|
1542
|
+
else {
|
|
1543
|
+
cachedAccountManager = loadedManager;
|
|
1544
|
+
}
|
|
1545
|
+
const accountManager = cachedAccountManager ?? loadedManager;
|
|
1546
|
+
await ensureAccountsWatcher();
|
|
1391
1547
|
const refreshToken = authFallback?.refresh ?? "";
|
|
1392
1548
|
const needsPersist = refreshToken &&
|
|
1393
1549
|
!accountManager.hasRefreshToken(refreshToken);
|
|
@@ -1412,77 +1568,37 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1412
1568
|
global: providerConfig?.options || {},
|
|
1413
1569
|
models: providerConfig?.models || {},
|
|
1414
1570
|
};
|
|
1415
|
-
// Load plugin configuration and determine CODEX_MODE
|
|
1416
|
-
// Priority: CODEX_MODE env var > config file > default (true)
|
|
1417
|
-
const codexMode = getCodexMode(pluginConfig);
|
|
1418
|
-
const requestTransformMode = getRequestTransformMode(pluginConfig);
|
|
1419
|
-
const useLegacyRequestTransform = requestTransformMode === "legacy";
|
|
1420
|
-
const fastSessionEnabled = getFastSession(pluginConfig);
|
|
1421
|
-
const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
|
|
1422
|
-
const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
|
|
1423
|
-
const beginnerSafeMode = getBeginnerSafeMode(pluginConfig);
|
|
1424
|
-
beginnerSafeModeEnabled = beginnerSafeMode;
|
|
1425
|
-
const maskEmailEnabled = getCodexTuiMaskEmail(pluginConfig);
|
|
1426
|
-
const retryProfile = beginnerSafeMode
|
|
1427
|
-
? "conservative"
|
|
1428
|
-
: getRetryProfile(pluginConfig);
|
|
1429
|
-
const retryBudgetOverrides = beginnerSafeMode
|
|
1430
|
-
? {}
|
|
1431
|
-
: getRetryBudgetOverrides(pluginConfig);
|
|
1432
|
-
const retryBudgetLimits = resolveRetryBudgetLimits(retryProfile, retryBudgetOverrides);
|
|
1433
|
-
runtimeMetrics.retryProfile = retryProfile;
|
|
1434
|
-
runtimeMetrics.retryBudgetLimits = { ...retryBudgetLimits };
|
|
1435
|
-
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
1436
|
-
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
1437
|
-
const retryAllAccountsRateLimited = beginnerSafeMode
|
|
1438
|
-
? false
|
|
1439
|
-
: getRetryAllAccountsRateLimited(pluginConfig);
|
|
1440
|
-
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
1441
|
-
const retryAllAccountsMaxRetries = beginnerSafeMode
|
|
1442
|
-
? Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig))
|
|
1443
|
-
: getRetryAllAccountsMaxRetries(pluginConfig);
|
|
1444
|
-
const unsupportedCodexPolicy = getUnsupportedCodexPolicy(pluginConfig);
|
|
1445
|
-
const fallbackOnUnsupportedCodexModel = unsupportedCodexPolicy === "fallback";
|
|
1446
|
-
const fallbackToGpt52OnUnsupportedGpt53 = getFallbackToGpt52OnUnsupportedGpt53(pluginConfig);
|
|
1447
|
-
const unsupportedCodexFallbackChain = getUnsupportedCodexFallbackChain(pluginConfig);
|
|
1448
|
-
const toastDurationMs = getToastDurationMs(pluginConfig);
|
|
1449
|
-
const accountToastsEnabled = getAccountToastsEnabled(pluginConfig);
|
|
1450
|
-
const fetchTimeoutMs = getFetchTimeoutMs(pluginConfig);
|
|
1451
|
-
const streamStallTimeoutMs = getStreamStallTimeoutMs(pluginConfig);
|
|
1452
1571
|
const sessionRecoveryEnabled = getSessionRecovery(pluginConfig);
|
|
1572
|
+
beginnerSafeModeEnabled = getBeginnerSafeMode(pluginConfig);
|
|
1573
|
+
const initialRetryProfile = beginnerSafeModeEnabled ? "conservative" : getRetryProfile(pluginConfig);
|
|
1574
|
+
runtimeMetrics.retryProfile = initialRetryProfile;
|
|
1575
|
+
runtimeMetrics.retryBudgetLimits = resolveRetryBudgetLimits(initialRetryProfile, beginnerSafeModeEnabled ? {} : getRetryBudgetOverrides(pluginConfig));
|
|
1453
1576
|
const autoResumeEnabled = getAutoResume(pluginConfig);
|
|
1454
1577
|
const autoUpdateEnabled = getAutoUpdate(pluginConfig);
|
|
1455
|
-
|
|
1456
|
-
const emptyResponseRetryDelayMs = getEmptyResponseRetryDelayMs(pluginConfig);
|
|
1457
|
-
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
|
|
1458
|
-
const rotationStrategy = getRotationStrategy(pluginConfig);
|
|
1459
|
-
const effectiveUserConfig = fastSessionEnabled
|
|
1460
|
-
? applyFastSessionDefaults(userConfig)
|
|
1461
|
-
: userConfig;
|
|
1462
|
-
if (fastSessionEnabled) {
|
|
1578
|
+
if (getFastSession(pluginConfig)) {
|
|
1463
1579
|
logDebug("Fast session mode enabled", {
|
|
1464
1580
|
reasoningEffort: "none/low",
|
|
1465
1581
|
reasoningSummary: "auto",
|
|
1466
1582
|
textVerbosity: "low",
|
|
1467
|
-
fastSessionStrategy,
|
|
1468
|
-
fastSessionMaxInputItems,
|
|
1583
|
+
fastSessionStrategy: getFastSessionStrategy(pluginConfig),
|
|
1584
|
+
fastSessionMaxInputItems: getFastSessionMaxInputItems(pluginConfig),
|
|
1469
1585
|
});
|
|
1470
1586
|
}
|
|
1471
|
-
if (
|
|
1587
|
+
if (getBeginnerSafeMode(pluginConfig)) {
|
|
1472
1588
|
logInfo("Beginner safe mode enabled", {
|
|
1473
|
-
retryProfile,
|
|
1474
|
-
retryAllAccountsRateLimited,
|
|
1475
|
-
retryAllAccountsMaxRetries,
|
|
1589
|
+
retryProfile: "conservative",
|
|
1590
|
+
retryAllAccountsRateLimited: false,
|
|
1591
|
+
retryAllAccountsMaxRetries: Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig)),
|
|
1476
1592
|
});
|
|
1477
1593
|
}
|
|
1478
1594
|
const prewarmEnabled = process.env.CODEX_AUTH_PREWARM !== "0" &&
|
|
1479
1595
|
process.env.VITEST !== "true" &&
|
|
1480
1596
|
process.env.NODE_ENV !== "test";
|
|
1481
|
-
if (!startupPrewarmTriggered && prewarmEnabled &&
|
|
1597
|
+
if (!startupPrewarmTriggered && prewarmEnabled && getRequestTransformMode(pluginConfig) === "legacy") {
|
|
1482
1598
|
startupPrewarmTriggered = true;
|
|
1483
1599
|
const configuredModels = Object.keys(userConfig.models ?? {});
|
|
1484
1600
|
prewarmCodexInstructions(configuredModels);
|
|
1485
|
-
if (
|
|
1601
|
+
if (getCodexMode(pluginConfig)) {
|
|
1486
1602
|
prewarmOpenCodeCodexPrompt();
|
|
1487
1603
|
}
|
|
1488
1604
|
}
|
|
@@ -1515,10 +1631,79 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1515
1631
|
* @returns Response from Codex API
|
|
1516
1632
|
*/
|
|
1517
1633
|
async fetch(input, init) {
|
|
1634
|
+
let finishFetch;
|
|
1635
|
+
let pendingFetch;
|
|
1518
1636
|
try {
|
|
1519
|
-
|
|
1520
|
-
|
|
1637
|
+
const pluginConfig = loadPluginConfig();
|
|
1638
|
+
const currentPerProjectAccounts = getPerProjectAccounts(pluginConfig);
|
|
1639
|
+
while (storageTransition)
|
|
1640
|
+
await storageTransition;
|
|
1641
|
+
if (currentPerProjectAccounts !== perProjectAccounts) {
|
|
1642
|
+
storageTransition = (async () => {
|
|
1643
|
+
await Promise.all(activeFetches);
|
|
1644
|
+
// Drain the old pool's pending write before changing the global path.
|
|
1645
|
+
await cachedAccountManager?.flushPendingSave();
|
|
1646
|
+
setStoragePath(currentPerProjectAccounts ? process.cwd() : null);
|
|
1647
|
+
invalidateAccountManagerCache();
|
|
1648
|
+
perProjectAccounts = currentPerProjectAccounts;
|
|
1649
|
+
})().finally(() => { storageTransition = undefined; });
|
|
1650
|
+
await storageTransition;
|
|
1651
|
+
}
|
|
1652
|
+
pendingFetch = new Promise((resolve) => { finishFetch = resolve; });
|
|
1653
|
+
activeFetches.add(pendingFetch);
|
|
1654
|
+
if (!accountManagerPromise) {
|
|
1655
|
+
const pending = AccountManager.loadFromDisk();
|
|
1656
|
+
accountManagerPromise = pending;
|
|
1657
|
+
void pending.catch(() => {
|
|
1658
|
+
if (accountManagerPromise === pending)
|
|
1659
|
+
accountManagerPromise = null;
|
|
1660
|
+
});
|
|
1521
1661
|
}
|
|
1662
|
+
if (!cachedAccountManager) {
|
|
1663
|
+
const loadedManager = await accountManagerPromise;
|
|
1664
|
+
if (cachedAccountManager && cachedAccountManager !== loadedManager) {
|
|
1665
|
+
// External reload won the race while this load was in
|
|
1666
|
+
// flight: adopt the cache and retire the stale load.
|
|
1667
|
+
loadedManager.disposeShutdownHandler();
|
|
1668
|
+
}
|
|
1669
|
+
else if (!cachedAccountManager) {
|
|
1670
|
+
cachedAccountManager = loadedManager;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
const codexMode = getCodexMode(pluginConfig);
|
|
1674
|
+
const requestTransformMode = getRequestTransformMode(pluginConfig);
|
|
1675
|
+
const fastSessionEnabled = getFastSession(pluginConfig);
|
|
1676
|
+
const fastSessionStrategy = getFastSessionStrategy(pluginConfig);
|
|
1677
|
+
const fastSessionMaxInputItems = getFastSessionMaxInputItems(pluginConfig);
|
|
1678
|
+
const beginnerSafeMode = getBeginnerSafeMode(pluginConfig);
|
|
1679
|
+
beginnerSafeModeEnabled = beginnerSafeMode;
|
|
1680
|
+
const maskEmailEnabled = getCodexTuiMaskEmail(pluginConfig);
|
|
1681
|
+
const retryProfile = beginnerSafeMode ? "conservative" : getRetryProfile(pluginConfig);
|
|
1682
|
+
const retryBudgetOverrides = beginnerSafeMode ? {} : getRetryBudgetOverrides(pluginConfig);
|
|
1683
|
+
const retryBudgetLimits = resolveRetryBudgetLimits(retryProfile, retryBudgetOverrides);
|
|
1684
|
+
runtimeMetrics.retryProfile = retryProfile;
|
|
1685
|
+
runtimeMetrics.retryBudgetLimits = { ...retryBudgetLimits };
|
|
1686
|
+
const tokenRefreshSkewMs = getTokenRefreshSkewMs(pluginConfig);
|
|
1687
|
+
const rateLimitToastDebounceMs = getRateLimitToastDebounceMs(pluginConfig);
|
|
1688
|
+
const retryAllAccountsRateLimited = beginnerSafeMode ? false : getRetryAllAccountsRateLimited(pluginConfig);
|
|
1689
|
+
const retryAllAccountsMaxWaitMs = getRetryAllAccountsMaxWaitMs(pluginConfig);
|
|
1690
|
+
const retryAllAccountsMaxRetries = beginnerSafeMode
|
|
1691
|
+
? Math.min(1, getRetryAllAccountsMaxRetries(pluginConfig))
|
|
1692
|
+
: getRetryAllAccountsMaxRetries(pluginConfig);
|
|
1693
|
+
const unsupportedCodexPolicy = getUnsupportedCodexPolicy(pluginConfig);
|
|
1694
|
+
const fallbackOnUnsupportedCodexModel = unsupportedCodexPolicy === "fallback";
|
|
1695
|
+
const fallbackToGpt52OnUnsupportedGpt53 = getFallbackToGpt52OnUnsupportedGpt53(pluginConfig);
|
|
1696
|
+
const unsupportedCodexFallbackChain = getUnsupportedCodexFallbackChain(pluginConfig);
|
|
1697
|
+
const toastDurationMs = getToastDurationMs(pluginConfig);
|
|
1698
|
+
const accountToastsEnabled = getAccountToastsEnabled(pluginConfig);
|
|
1699
|
+
const fetchTimeoutMs = getFetchTimeoutMs(pluginConfig);
|
|
1700
|
+
const streamStallTimeoutMs = getStreamStallTimeoutMs(pluginConfig);
|
|
1701
|
+
const emptyResponseMaxRetries = getEmptyResponseMaxRetries(pluginConfig);
|
|
1702
|
+
const emptyResponseRetryDelayMs = getEmptyResponseRetryDelayMs(pluginConfig);
|
|
1703
|
+
const pidOffsetEnabled = getPidOffsetEnabled(pluginConfig);
|
|
1704
|
+
const rotationStrategy = getRotationStrategy(pluginConfig);
|
|
1705
|
+
const effectiveUserConfig = fastSessionEnabled ? applyFastSessionDefaults(userConfig) : userConfig;
|
|
1706
|
+
let accountManager = cachedAccountManager;
|
|
1522
1707
|
// Step 1: Extract and rewrite URL for Codex backend
|
|
1523
1708
|
const originalUrl = extractRequestUrl(input);
|
|
1524
1709
|
const url = openAIBaseURL ? originalUrl : rewriteUrlForCodex(originalUrl);
|
|
@@ -1656,6 +1841,8 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1656
1841
|
const startTime = Date.now();
|
|
1657
1842
|
const endTime = startTime + totalMs;
|
|
1658
1843
|
while (Date.now() < endTime) {
|
|
1844
|
+
if (cachedAccountManager !== accountManager)
|
|
1845
|
+
return;
|
|
1659
1846
|
if (abortSignal?.aborted) {
|
|
1660
1847
|
throw abortError();
|
|
1661
1848
|
}
|
|
@@ -1758,6 +1945,43 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1758
1945
|
const MAX_QUOTA_FALLBACK_SWITCHES = 3;
|
|
1759
1946
|
let quotaFallbackSwitches = 0;
|
|
1760
1947
|
while (true) {
|
|
1948
|
+
if (cachedAccountManager && cachedAccountManager !== accountManager) {
|
|
1949
|
+
accountManager = cachedAccountManager;
|
|
1950
|
+
}
|
|
1951
|
+
else if (!cachedAccountManager) {
|
|
1952
|
+
// Reload through the shared accountManagerPromise so
|
|
1953
|
+
// concurrent requests (and the startup path above)
|
|
1954
|
+
// share one load. Racing independent loadFromDisk()
|
|
1955
|
+
// calls here can orphan the slower manager while an
|
|
1956
|
+
// in-flight request still holds it, and an orphaned
|
|
1957
|
+
// manager is never retired, so its next save would
|
|
1958
|
+
// write full membership and drop externally imported
|
|
1959
|
+
// accounts.
|
|
1960
|
+
if (accountManagerPromise === null) {
|
|
1961
|
+
const pending = AccountManager.loadFromDisk();
|
|
1962
|
+
accountManagerPromise = pending;
|
|
1963
|
+
void pending.catch(() => {
|
|
1964
|
+
if (accountManagerPromise === pending)
|
|
1965
|
+
accountManagerPromise = null;
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
const reloaded = await accountManagerPromise;
|
|
1969
|
+
if (cachedAccountManager) {
|
|
1970
|
+
if (cachedAccountManager !== reloaded) {
|
|
1971
|
+
// The cache was repopulated while this load was in
|
|
1972
|
+
// flight (external reload after an invalidation).
|
|
1973
|
+
// The loaded manager is stale; retire it so its
|
|
1974
|
+
// debounced save cannot write full membership over
|
|
1975
|
+
// accounts imported after its snapshot was taken.
|
|
1976
|
+
reloaded.disposeShutdownHandler();
|
|
1977
|
+
}
|
|
1978
|
+
accountManager = cachedAccountManager;
|
|
1979
|
+
}
|
|
1980
|
+
else {
|
|
1981
|
+
cachedAccountManager = reloaded;
|
|
1982
|
+
accountManager = reloaded;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1761
1985
|
let accountCount = accountManager.getAccountCount();
|
|
1762
1986
|
const attempted = new Set();
|
|
1763
1987
|
// Diagnostics for the terminal error message below. The composite key keeps
|
|
@@ -2657,6 +2881,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
2657
2881
|
}
|
|
2658
2882
|
}
|
|
2659
2883
|
finally {
|
|
2884
|
+
if (pendingFetch)
|
|
2885
|
+
activeFetches.delete(pendingFetch);
|
|
2886
|
+
finishFetch?.();
|
|
2660
2887
|
clearCorrelationId();
|
|
2661
2888
|
}
|
|
2662
2889
|
},
|