diff --git a/.husky/commit-msg b/.husky/commit-msg index 4efaef55..0df4d11b 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,6 +1,3 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - # Enforce Conventional Commits 1.0.0 subject line. # type(scope)!: description # Types align with common Conventional Commits usage and CONTRIBUTING.md. diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af2198..2312dc58 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx lint-staged diff --git a/AGENTS.md b/AGENTS.md index d5caab14..b2981888 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ Package version: see `package.json` (`version` field). | SSE to JSON | `lib/request/response-handler.ts` | stream parsing and empty-response detection | | Prompt templates | `lib/prompts/codex.ts`, `lib/prompts/opencode-codex.ts`, `lib/prompts/codex-opencode-bridge.ts` | model-family detection, Codex prompt cache, bridge prompts | | Config parsing | `lib/config.ts`, `lib/schemas.ts` | plugin config and environment overrides (bool env truthy only `"1"`) | -| Session recovery | `lib/recovery/`, `lib/recovery.ts` | recoverable error handling and auto-resume | +| Session recovery | `lib/recovery/`, `lib/recovery.ts` | recoverable error detection and TUI toast notifications; underlying auto-resume/repair engine exists in hook.ts | | Health monitoring | `lib/health.ts`, `lib/parallel-probe.ts` | account health status and concurrent probes | | Circuit breaker | `lib/circuit-breaker.ts` | failure isolation | | Public architecture | `docs/architecture.md` | user-facing architecture overview | @@ -77,7 +77,7 @@ Package version: see `package.json` (`version` field). - Do not treat `oc-chatgpt-multi-auth` as current except in migration/cleanup logic. - Do not expose account emails, access tokens, refresh tokens, or raw prompt/response bodies in normal diagnostics. - Do not silently delete JSON credentials when keychain operations fail. -- Do not document boolean env overrides as truthy for `"true"` / `"yes"` — only `"1"` is truthy. +- Do not document boolean env overrides as truthy for `"true"` or `"yes"`. Only `"1"` is truthy. ## COMMANDS @@ -103,7 +103,7 @@ oc-codex-multi-auth doctor ## NOTES -- OAuth callback: `http://127.0.0.1:1455/auth/callback`. +- OAuth redirect URI: `http://localhost:1455/auth/callback` (registered with the Codex OAuth client). The callback server binds both `127.0.0.1:1455` and `[::1]:1455`. - ChatGPT backend requires `store: false`, include `reasoning.encrypted_content`. - OpenCode config: `~/.config/opencode/opencode.json`. - OpenCode TUI config: `~/.config/opencode/tui.json`. @@ -111,12 +111,12 @@ oc-codex-multi-auth doctor - Plugin config: `~/.opencode/openai-codex-auth-config.json`. - Per-project accounts: `~/.opencode/projects//oc-codex-multi-auth-accounts.json`. - Global accounts: `~/.opencode/oc-codex-multi-auth-accounts.json`. -- Flagged accounts: `~/.opencode/oc-codex-multi-auth-flagged-accounts.json`. +- Flagged accounts: `oc-codex-multi-auth-flagged-accounts.json`, written beside the active accounts file (per project when `perProjectAccounts` is on). - Quota notification state: `oc-codex-multi-auth-quota-notifications.json`, written beside the active accounts file (per project when `perProjectAccounts` is on). - Request logs: `~/.opencode/logs/codex-plugin/` when logging is enabled. - Model catalog: 13 modern bases / 59 variants; legacy 59 explicit. - Bases: `gpt-6-astra`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5`, `gpt-5.5-fast`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.1-codex-max`, `gpt-5.1-codex`, `gpt-5.1-codex-mini`, `gpt-5.1`, `gpt-5-codex`. Routed but deliberately unshipped (Daybreak-gated, add by hand): `gpt-daybreak-blue-latest`, `gpt-daybreak-red-latest`, `gpt-5.6-cyber`. -- Prompt templates sync from Codex CLI GitHub releases with ETag caching; 5.6 and Daybreak instructions come from the Codex model catalog. `gpt-6-astra` is registered as a catalog slug but has no entry yet, so it reads the prompt file until openai/codex publishes one. +- Prompt templates sync from Codex CLI GitHub releases with ETag caching; 5.6 and Daybreak instructions come from the Codex model catalog. `gpt-6-astra` has a catalog entry, but its `base_instructions` is empty and the loader treats empty as absent, so Astra reads its prompt file until openai/codex publishes catalog text. - 5xx server errors trigger account rotation and health penalty like network errors. - API deprecation/sunset headers (RFC 8594) are logged as warnings. - StorageError preserves original stack traces via `cause` parameter. diff --git a/README.md b/README.md index 58c34461..11b4e230 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Use it when you want OpenCode to run Codex-style coding workflows from your own ## What You Get - OpenCode plugin support for ChatGPT Plus/Pro OAuth and Codex/GPT-5 coding workflows -- GPT-6 Astra and GPT-5.6 Sol/Terra/Luna (responses-lite path) plus GPT-5.5, GPT-5.5 Fast, GPT-5.4 Mini, GPT-5.4 Nano, GPT-5.1, and Codex model templates +- 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 - 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 - Compact modern OpenCode config with 13 base families and 59 variant presets; explicit legacy selector IDs when needed - 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 | `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` | | OpenCode plugin entry (`index.ts`) | auth loader, OAuth login modes, provider fetch pipeline, account rotation, retry/failover, and `codex-*` tool registry | | OpenCode TUI plugin (`tui.ts`) | prompt quota status, quota details, shared quota cache, and active-account-aware display | -| 24 `codex-*` tools | setup, help, status, list, switch, warm, limits, health, metrics, doctor, dashboard, pool, backup, keychain, diagnostics, and recovery actions | +| 24 `codex-*` tools | setup, help, status, list, switch, warm, limits, health, metrics, doctor, dashboard, pool, backup, keychain, diagnostics, and repair actions | 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. @@ -128,7 +128,7 @@ opencode debug config opencode auth login ``` -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 additionally merge their selected `provider.openai` definitions. Changed config files are backed up before writing. +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. ### Standalone CLI (no agent / no token cost) @@ -225,10 +225,10 @@ If browser launch is blocked, use the alternate login paths in [docs/getting-sta | `codex-status` | Which account, model family, and routing state are active? | | `codex-limits` | What quota or rate-limit state is visible now? | | `codex-reset` | Do I have a banked rate-limit reset credit, and how do I redeem it? | -| `codex-dashboard` | Can I manage accounts from one interactive surface? | +| `codex-dashboard` | What does a read-only snapshot of account eligibility, retry budgets, and refresh queue health show? | | `codex-pool` | Which accounts are preferred for each model, and how do I change them? | -Most of these also run as a **direct CLI** with no agent/model involvement (no token cost) — e.g. `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. +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. ### Account management @@ -238,7 +238,7 @@ Most of these also run as a **direct CLI** with no agent/model involvement (no t | `codex-tag` | How do I group accounts with tags? | | `codex-note` | How do I attach a private note to an account? | | `codex-remove` | How do I remove a saved account safely? | -| `codex-refresh` | How do I refresh or re-login an account? | +| `codex-refresh` | How do I refresh the OAuth tokens of every saved account to verify they are still valid? | ### Diagnostics and backup @@ -266,7 +266,7 @@ Most of these also run as a **direct CLI** with no agent/model involvement (no t - TUI quota status follows the account/workspace used by the latest request - 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. - 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. -- 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. +- 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. --- @@ -280,10 +280,10 @@ Most of these also run as a **direct CLI** with no agent/model involvement (no t | Plugin config | `~/.opencode/openai-codex-auth-config.json` | | Global account storage | `~/.opencode/oc-codex-multi-auth-accounts.json` | | Per-project accounts | `~/.opencode/projects//oc-codex-multi-auth-accounts.json` | -| Flagged accounts | `~/.opencode/oc-codex-multi-auth-flagged-accounts.json` | +| Flagged accounts | `oc-codex-multi-auth-flagged-accounts.json`, written beside the active accounts file (per-project path when `perProjectAccounts` is on) | | Backups | `~/.opencode/backups/` or `~/.opencode/projects//backups/` | | Logs | `~/.opencode/logs/codex-plugin/` | -| TUI quota cache | OpenCode state path plus `~/.opencode/oc-codex-multi-auth-tui-quota.json` fallback | +| 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` | 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. @@ -430,6 +430,9 @@ Selected runtime/environment overrides: | `ENABLE_PLUGIN_REQUEST_LOGGING=1` | Enable request metadata logs | | `CODEX_PLUGIN_LOG_BODIES=1` | Include raw request/response bodies in logs; sensitive | | `CODEX_KEYCHAIN=1` | Opt in to OS-native keychain account storage | +| `CODEX_AUTH_QUOTA_NOTIFICATIONS=1` | Enable desktop quota notifications (macOS only) | +| `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) | +| `CODEX_AUTH_QUOTA_NOTIFICATIONS_INTERVAL_MS=` | Override the quota poll interval (default 1800000, minimum 30000) | Boolean env overrides are truthy only for the literal string `"1"`. @@ -467,9 +470,9 @@ Set `CODEX_KEYCHAIN=1` to store account pools in the OS keychain instead: Manage the backend from OpenCode: ```text -codex-keychain status -codex-keychain migrate -codex-keychain rollback +codex-keychain command="status" +codex-keychain command="migrate" +codex-keychain command="rollback" ``` If the keychain is unavailable, the plugin logs a warning and falls back to JSON storage for that operation. Credentials are never silently deleted. diff --git a/config/README.md b/config/README.md index 14f2f46b..32afa53e 100644 --- a/config/README.md +++ b/config/README.md @@ -87,7 +87,7 @@ If your OpenCode runtime supports global compaction tuning, you can also set val - 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. - Rollout gate: Astra reached a limited set of organizations first, so accounts outside it auto-fallback - `gpt-6-astra → gpt-5.6-sol → gpt-5.6-terra → gpt-5.6-luna → gpt-5.5` + `gpt-6-astra → gpt-5.6-sol → gpt-5.6-terra → gpt-5.6-luna → gpt-5.5 → gpt-5.2` (disable with `CODEX_AUTH_DISABLE_GPT6_AUTO_FALLBACK=1`). - Efforts are low through `ultra`, per OpenAI's Codex model list. `ultra` is sent as `max` on the wire, as with 5.6. - 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 ## Cyber tier notes (Daybreak-gated) - `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. -- All three need Daybreak program approval, and Blue/Red are `visibility: "hide"` in the catalog, so they are **not** in the shipped templates — same policy as `gpt-5.3-codex-spark` below. The plugin routes them fully; entitled users add the ids by hand. -- None has a fallback chain on purpose — a cyber-specialty request must not be silently answered by a general model. +- 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. +- None has a fallback chain on purpose, because a cyber-specialty request must not be silently answered by a general model. ## GPT-5.6 notes - Served over the **responses-lite** path (`use_responses_lite`). - Preview entitlement: accounts without access auto-fallback - `gpt-5.6-sol → gpt-5.6-terra → gpt-5.6-luna → gpt-5.5` + `gpt-5.6-sol → gpt-5.6-terra → gpt-5.6-luna → gpt-5.5 → gpt-5.2` (disable with `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1`). - 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. - `ultra` is accepted as a client-side alias and sent as `max` on the wire (no subagent orchestration in this plugin). ## Spark model note -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. +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. If your workspace is entitled, you can add Spark model IDs manually. @@ -147,12 +147,12 @@ Current defaults are strict entitlement handling except for common default-selec - set `unsupportedCodexPolicy: "fallback"` (or `CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback`) to enable the full fallback chain for manual/legacy selectors - `fallbackToGpt52OnUnsupportedGpt53: true` keeps the legacy `gpt-5.3-codex -> gpt-5.2-codex` edge inside fallback mode - 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 -- legacy Codex selectors such as `gpt-5.2-codex`, `gpt-5.3-codex`, and `gpt-5.3-codex-spark` normalize to canonical `gpt-5-codex`; if that canonical Codex model is gated, the default auto-fallback can retry through `gpt-5.6-terra`, `gpt-5.5`, then `gpt-5.2` +- 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` - set `CODEX_AUTH_DISABLE_GPT6_AUTO_FALLBACK=1` to disable GPT-6 Astra auto-fallback - set `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1` to disable GPT-5.6 auto-fallback - set `CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1` to disable GPT-5.5 auto-fallback - set `CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1` to disable canonical Codex/GPT-5.4-family auto-fallback -- `gpt-5.4-pro -> gpt-5.4` remains available for older manual configs +- `gpt-5.4-pro -> gpt-5.6-terra -> gpt-5.5 -> gpt-5.2` applies when you manually select `gpt-5.4-pro` - `unsupportedCodexFallbackChain` lets you override fallback order per model 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): - `gpt-5-codex → gpt-5.6-terra → gpt-5.5 → gpt-5.2` > 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. -- `gpt-5.4-pro → gpt-5.4` (if you manually select `gpt-5.4-pro`) +- `gpt-5.4-pro → gpt-5.6-terra → gpt-5.5 → gpt-5.2` (if you manually select `gpt-5.4-pro`) - `gpt-5.3-codex → gpt-5-codex → gpt-5.2-codex` - `gpt-5.3-codex-spark → gpt-5-codex → gpt-5.3-codex → gpt-5.2-codex` (only if Spark IDs are added manually) - `gpt-5.2-codex → gpt-5-codex` diff --git a/docs/architecture.md b/docs/architecture.md index 825b7a66..b54d9c58 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,7 +12,7 @@ Public overview of how `oc-codex-multi-auth` installs config, handles ChatGPT Pl - OpenCode loads `dist/index.js` as the provider plugin entry. - The plugin registers **24** `codex-*` tools via **24 per-file factories** under `lib/tools/` (`codex-list`, `codex-switch`, `codex-warm`, and 21 others). - OpenCode loads `dist/tui.js` as the TUI plugin for active-session quota status. -- Request handling stays stateless for the ChatGPT-backed Codex API by enforcing `store: false` and preserving `reasoning.encrypted_content`. +- Request handling stays stateless for the ChatGPT-backed Codex API with `store: false`, `stream: true`, and `reasoning.encrypted_content`. Legacy transformation mode enforces all three unconditionally. Native mode carries them through the shipped config templates and the host payload. - GPT-6 Astra, the Daybreak cyber tiers and the GPT-5.6 tiers use the responses-lite request path; older models keep the classic shape. - Account, config, backup, log, and TUI quota state lives under `~/.opencode` and `~/.config/opencode`. - Per-project account pools are enabled by default under `~/.opencode/projects//...`. @@ -48,8 +48,8 @@ Standalone read/ops commands (no OpenCode agent loop required): `doctor`, `statu - account manager lifecycle and local account storage (V3) - request URL/body/header transformation (native or legacy, plus responses-lite for GPT-6 Astra, Daybreak and GPT-5.6) - health-aware account selection, `rotationStrategy`, and `modelAccountPools` -- retry budgets, circuit breaking, rate-limit backoff, and failover -- session recovery hooks and beginner-safe next-action guidance +- retry budgets, circuit breaking (per account, workspace identity, and model family), rate-limit backoff, and failover +- recoverable error detection with a recovery toast in the current plugin runtime, plus beginner-safe next-action guidance - `ToolContext` construction for the `codex-*` registry ### 3. Request pipeline @@ -77,19 +77,19 @@ oc-codex-multi-auth index.ts ChatGPT-backed Codex endpoint or configured OpenAI-compatible gateway ``` -The gateway override is fail-closed: remote gateways require HTTPS, literal loopback IPs are the only accepted HTTP targets, embedded credentials/query strings/fragments are rejected, and redirects are not followed. Any address in `127.0.0.0/8` and the IPv6 loopback `::1` count as loopback, so several local services can each hold their own address. Hostnames such as `localhost` are not trusted for cleartext OAuth transport because name resolution can be redirected. A rejected value fails the auth loader with a `[oc-codex-multi-auth]`-prefixed reason and an error toast rather than silently falling back to the default endpoint, and a gateway that answers with a 3xx yields a `502` naming the redirect origin instead of being retried as a malformed response. The gateway receives the same short-lived ChatGPT OAuth access token and account header as the default Codex endpoint, so setting `OPENAI_BASE_URL` alone does not activate the override. +The gateway override is fail-closed. Remote gateways require HTTPS, literal loopback IPs are the only accepted HTTP targets, embedded credentials/query strings/fragments are rejected, and redirects are not followed. Any address in `127.0.0.0/8` and the IPv6 loopback `::1` count as loopback, so several local services can each hold their own address. Hostnames such as `localhost` are not trusted for cleartext OAuth transport because name resolution can be redirected. A rejected value fails the auth loader with a `[oc-codex-multi-auth]`-prefixed reason and an error toast rather than silently falling back to the default endpoint, and a gateway that answers with a 3xx yields a `502` naming the redirect origin instead of being retried as a malformed response. The gateway receives the same short-lived ChatGPT OAuth access token and account header as the default Codex endpoint, so setting `OPENAI_BASE_URL` alone does not activate the override. -**Native mode** keeps the host payload shape whenever possible. **Legacy mode** applies compatibility rewrites for older OpenCode/AI SDK behavior, including filtering unsupported `item_reference` payloads and stripping IDs that cannot be used with `store: false`. +**Native mode** (default) keeps the host payload shape. It normalizes the model name, sets the backend instruction identity line, and upserts one `## Backend Model Identity` developer message naming the outgoing model, refreshed again when fallback changes the model. `store: false` and the `reasoning.encrypted_content` include are not added by the native transform. They ride the provider options in `opencode.json`, which the shipped config templates write. **Legacy mode** applies compatibility rewrites for older OpenCode/AI SDK behavior through `lib/request/request-transformer.ts`. It sets `store: false` and the include itself, filters unsupported `item_reference` payloads, and strips IDs that cannot be used with `store: false`. -**Responses-lite:** for `gpt-6-astra`, the Daybreak-gated cyber tiers (`gpt-daybreak-blue-latest`, `gpt-daybreak-red-latest`, `gpt-5.6-cyber`), `gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna`, the plugin reshapes the request the way Codex does: tool definitions move into `input` as a leading `additional_tools` developer item, Codex instructions follow as a developer message, top-level `instructions` is emptied, `tools` is omitted, `parallel_tool_calls` is forced off, image `detail` fields are stripped, and `x-openai-internal-codex-responses-lite: true` is sent. Lite reshape is applied per request attempt against the model actually being sent, so a sol → gpt-5.5 fallback re-serializes into the classic shape and keeps its tools. +**Responses-lite:** for `gpt-6-astra`, the Daybreak-gated cyber tiers (`gpt-daybreak-blue-latest`, `gpt-daybreak-red-latest`, `gpt-5.6-cyber`), `gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna`, the plugin reshapes the request the way Codex does: tool definitions move into `input` as a leading `additional_tools` developer item, Codex instructions follow as a developer message, top-level `instructions` is emptied, `tools` is omitted, `parallel_tool_calls` is forced off, `reasoning.context` is set to `all_turns`, image `detail` fields are stripped, and `x-openai-internal-codex-responses-lite: true` is sent. Lite reshape is applied per request attempt against the model actually being sent, so a sol → gpt-5.5 fallback re-serializes into the classic shape and keeps its tools. **Client identity:** by default every responses-lite model uses the host/opencode identity (`originator: opencode` with an `opencode/...` User-Agent). Other families default to the Codex CLI identity. Override with `CODEX_AUTH_CLIENT_IDENTITY`. **Auto-fallback (preview entitlement gates):** -- GPT-6 Astra: `gpt-6-astra` → `gpt-5.6-sol` → `gpt-5.6-terra` → `gpt-5.6-luna` → `gpt-5.5` (disable with `CODEX_AUTH_DISABLE_GPT6_AUTO_FALLBACK=1`) +- GPT-6 Astra: `gpt-6-astra` → `gpt-5.6-sol` → `gpt-5.6-terra` → `gpt-5.6-luna` → `gpt-5.5` → `gpt-5.2` (disable with `CODEX_AUTH_DISABLE_GPT6_AUTO_FALLBACK=1`) - Cyber tiers (`gpt-daybreak-blue-latest`, `gpt-daybreak-red-latest`, `gpt-5.6-cyber`): no chain. They fail loudly rather than silently answering from a general model. -- GPT-5.6: `gpt-5.6-sol` → `gpt-5.6-terra` → `gpt-5.6-luna` → `gpt-5.5` (disable with `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1`) +- GPT-5.6: `gpt-5.6-sol` → `gpt-5.6-terra` → `gpt-5.6-luna` → `gpt-5.5` → `gpt-5.2` (disable with `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1`) - GPT-5.5 / canonical Codex also have default auto-fallback, now through `gpt-5.6-terra` / `gpt-5.6-luna` / `gpt-5.2`; broader fallback chains require `unsupportedCodexPolicy: "fallback"`. 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. ### 4. Account rotation and model pools @@ -98,7 +98,7 @@ The gateway override is fail-closed: remote gateways require HTTPS, literal loop | Strategy | Behavior | | --- | --- | -| `hybrid` (default) | Stay on the current account while healthy; otherwise score-select the next | +| `hybrid` (default) | Stay on the current account while it is selectable; otherwise score candidates with `health*2 + tokens*5 + hoursSinceUsed*2.0` and take the best, falling back to the least-recently-used account when all are blocked | | `sticky` | Drain one account until rate-limited/cooling, then move to the lowest-indexed available account | | `round-robin` | Advance through accounts in order | @@ -125,9 +125,11 @@ Full catalog: [tools-and-cli.md](tools-and-cli.md). ### 7. Storage and sync -The storage layer uses V3 account files with migrations from older formats, atomic writes, keychain opt-in, import/export previews, flagged-account recovery, and per-project path resolution. Mutations flow through one transaction primitive that combines the process-local mutex with a distinct `proper-lockfile` lease on `.transaction.lock`. The existing `.lock` JSON sidecar remains advisory collision diagnostics. +The storage layer uses V3 account files with migrations from older formats, atomic writes, keychain opt-in, import/export previews, flagged-account recovery, and per-project path resolution. V1 pools migrate to V3 on load. V2 files are rejected with the typed `UNKNOWN_V2_FORMAT` recovery error, and versions above 3 with `UNSUPPORTED_SCHEMA_VERSION`. Mutations flow through one transaction primitive that combines the process-local mutex with a distinct `proper-lockfile` lease on `.transaction.lock`. The existing `.lock` JSON sidecar remains advisory collision diagnostics. -OAuth refresh uses a **second, independent** lease on `.refresh.lock`. That split is deliberate: the refresh lease serializes the provider exchange across processes — refresh tokens are single-use, so two concurrent exchanges of the same token would leave one process with `refresh_token_reused` — while the storage lease is only ever held for a local read or write. A refresh therefore opens two short storage transactions (an authoritative reload that either adopts a rotation another process already committed or reports the current token, then a durable commit) with the multi-second network round trip sitting *between* them rather than inside either. Unrelated writers (`codex-note`, `codex-tag`, account toggles, rotation stamps, TUI quota writes) never queue behind a network call. The two leases use distinct lock targets, because `proper-lockfile` keys its in-process registry by target path and would otherwise corrupt that registry when one lease nests inside the other. +The optional keychain backend uses the OS keychain service name `oc-codex-multi-auth`. The global pool is stored under the account key `accounts:global`, and a project pool under `accounts:`. Migrating a JSON pool into the keychain renames the original file to `.migrated-to-keychain.` and keeps it as the rollback artifact. + +OAuth refresh uses a **second, independent** lease on `.refresh.lock`. That split is deliberate. The refresh lease serializes the provider exchange across processes, because refresh tokens are single-use and two concurrent exchanges of the same token would leave one process with `refresh_token_reused`. The storage lease is only ever held for a local read or write. A refresh therefore opens two short storage transactions (an authoritative reload that either adopts a rotation another process already committed or reports the current token, then a durable commit) with the multi-second network round trip sitting *between* them rather than inside either. Unrelated writers (`codex-note`, `codex-tag`, account toggles, rotation stamps, TUI quota writes) never queue behind a network call. The two leases use distinct lock targets, because `proper-lockfile` keys its in-process registry by target path and would otherwise corrupt that registry when one lease nests inside the other. This guarantee is intentionally local-filesystem/same-host. A process that exits after the provider accepts a refresh token but before the replacement token is committed still requires reauthentication, and cross-host or unreliable network filesystems require an external coordinator. @@ -135,12 +137,13 @@ This guarantee is intentionally local-filesystem/same-host. A process that exits | --- | --- | | OpenCode config | `~/.config/opencode/opencode.json` | | OpenCode TUI config | `~/.config/opencode/tui.json` | -| OpenCode auth tokens | `~/.opencode/auth/openai.json` | +| OpenCode auth tokens | `~/.opencode/auth/openai.json` (convention reference in docs; no plugin code reads this path) | +| OpenCode host auth store | `~/.local/share/opencode/auth.json`, read and backfilled from the account pool by `backfillHostOpenAIAuthFromPool` | | Plugin config | `~/.opencode/openai-codex-auth-config.json` | | Global account pool | `~/.opencode/oc-codex-multi-auth-accounts.json` | | Project account pool | `~/.opencode/projects//oc-codex-multi-auth-accounts.json` | -| Flagged accounts | `~/.opencode/oc-codex-multi-auth-flagged-accounts.json` | -| TUI quota cache | OpenCode state path plus `~/.opencode/oc-codex-multi-auth-tui-quota.json` fallback | +| Flagged accounts | `~/.opencode/projects//oc-codex-multi-auth-flagged-accounts.json` when `perProjectAccounts` is on (default), else `~/.opencode/oc-codex-multi-auth-flagged-accounts.json` | +| TUI quota cache | OpenCode state path (`api.state.path.state`), then `$OPENCODE_STATE_DIR`, then `~/.local/state/opencode/oc-codex-multi-auth-tui-quota.json` | | Logs | `~/.opencode/logs/codex-plugin/` | --- @@ -152,7 +155,7 @@ This guarantee is intentionally local-filesystem/same-host. A process that exits - The canonical package/plugin name is `oc-codex-multi-auth` (legacy npm name `oc-chatgpt-multi-auth` is migration-only). - Node engines: `>=18`. - OAuth callback port remains `1455`; callback path is `/auth/callback`. -- ChatGPT-backed Codex requests require `store: false`. +- ChatGPT-backed Codex requests require `store: false`, `stream: true`, and `reasoning.encrypted_content`. Legacy transformation mode (`transformRequestBody`) enforces all three unconditionally. Native mode carries `store: false` and `reasoning.encrypted_content` through the shipped config templates and `stream` through the host payload. - Multi-turn continuity depends on `reasoning.encrypted_content` and the host-supplied conversation history. - Account pool limits: max **20** accounts; auth-failure cooldown **30s**; auto-removal after **3** consecutive auth failures. - Account bootstrap can hydrate from Codex CLI storage under `~/.codex` unless `CODEX_AUTH_SYNC_CODEX_CLI=0`. @@ -160,7 +163,8 @@ This guarantee is intentionally local-filesystem/same-host. A process that exits - Credentials and account metadata stay local unless the user exports or migrates them. - Diagnostic commands redact sensitive account/token details by default. - The optional keychain backend must fall back without deleting JSON credentials silently. -- Session recovery rewrites OpenCode message/part files under the host storage root (`$XDG_DATA_HOME/opencode/storage` or platform equivalent) when `sessionRecovery` is enabled. +- When `sessionRecovery` is enabled, the request path detects recoverable errors and shows a recovery toast in the current plugin runtime. The full message/part rewriting and auto-resume engine in `lib/recovery/hook.ts` exists but is not hooked into host event streams or request handlers. +- The shutdown handler registers cleanup on SIGINT, SIGTERM, and beforeExit. Running inside the OpenCode host it drains cleanup without terminating the process. The standalone CLI opts in and exits 130 on SIGINT and 143 on SIGTERM. --- diff --git a/docs/configuration.md b/docs/configuration.md index 5178f787..f438246c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -47,13 +47,15 @@ controls how much thinking the model does. | `gpt-5.5-fast` | none, low, medium, high, xhigh | | `gpt-5.4` | none, low, medium, high, xhigh (retired from Codex 2026-08-31; auto-upgrades to `gpt-5.6-terra`) | | `gpt-5.4-mini` | none, low, medium, high, xhigh (retired from Codex 2026-08-31; auto-upgrades to `gpt-5.6-luna`) | +| `gpt-5.4-nano` | none, low, medium, high, xhigh | | `gpt-5.4-pro` | low, medium, high, xhigh (optional/manual model) | | `gpt-5-codex` | low, medium, high (default: high) | -| `gpt-5.3-codex` | low, medium, high, xhigh (legacy alias to `gpt-5-codex`) | -| `gpt-5.3-codex-spark` | low, medium, high, xhigh (entitlement-gated legacy alias; add manually) | -| `gpt-5.2-codex` | low, medium, high, xhigh (legacy alias to `gpt-5-codex`) | -| `gpt-5.1-codex-max` | low, medium, high, xhigh | -| `gpt-5.1-codex` | low, medium, high | +| `gpt-5.3-codex` | low, medium, high, xhigh (distinct legacy model id; falls back to `gpt-5-codex`) | +| `gpt-5.3-codex-spark` | low, medium, high, xhigh (entitlement-gated distinct model id, not an alias of `gpt-5-codex`; add manually) | +| `gpt-5.2-codex` | low, medium, high, xhigh (distinct legacy model id; falls back to `gpt-5-codex`) | +| `gpt-5.2` | none, low, medium, high, xhigh | +| `gpt-5.1-codex-max` | low, medium, high, xhigh (default: high; `xhigh` only when explicitly requested) | +| `gpt-5.1-codex` | low, medium, high (legacy alias to `gpt-5-codex`) | | `gpt-5.1-codex-mini` | medium, high | | `gpt-5.1` | none, low, medium, high | @@ -81,13 +83,13 @@ GPT-6 Astra notes: - Astra is OpenAI's frontier model, launched 2026-09-03. Efforts are low through `ultra`, matching OpenAI's Codex model list. Its API reference page says only "`reasoning.effort` supports `low`, `medium`, `high`, `xhigh`, and `max`", which is not a contradiction: `ultra` is a Codex client-side tier that is rewritten to `max` before the request leaves the client, so an API reference has no reason to list it. `gpt-5.6-sol` shows the same split. - Astra is opt-in, like the 5.6 tiers: neither the `gpt-5` alias nor the plugin default resolves to it. It rolled out to a limited set of organizations first and to Plus/Pro/Business/Enterprise over the following days, so an account outside the rollout auto-degrades `gpt-6-astra → gpt-5.6-sol → gpt-5.6-terra → gpt-5.6-luna → gpt-5.5`. Disable with `CODEX_AUTH_DISABLE_GPT6_AUTO_FALLBACK=1`. - Bare `gpt-6` is a **plugin-side** alias for `gpt-6-astra`. OpenAI publishes no bare `gpt-6` id. -- "GPT-6 Astra Pro" appears in launch-day press but is very likely not a model id at all: `/api/docs/models/gpt-6-astra-pro` returns 404 while the real `gpt-5.5-pro` and `gpt-5.4-pro` pages both return 200, and it is absent from both the `ChatModel` and `ResponsesOnlyModel` enums of the OpenAPI spec added by the SDK PR that introduced Astra (openai/openai-python#3791) — an enum that does list `gpt-5.5-pro`. The plugin maps `gpt-6-astra-pro*` onto `gpt-6-astra` anyway, so a user who typed it after reading the press gets a working request instead of an unknown slug on the wire. +- "GPT-6 Astra Pro" appears in launch-day press but is very likely not a model id at all: `/api/docs/models/gpt-6-astra-pro` returns 404 while the real `gpt-5.5-pro` and `gpt-5.4-pro` pages both return 200, and it is absent from both the `ChatModel` and `ResponsesOnlyModel` enums of the OpenAPI spec added by the SDK PR that introduced Astra (openai/openai-python#3791), an enum that does list `gpt-5.5-pro`. The plugin maps `gpt-6-astra-pro*` onto `gpt-6-astra` anyway, so a user who typed it after reading the press gets a working request instead of an unknown slug on the wire. - Astra is sent 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. - Astra's instructions come from the `gpt_5_2_prompt.md` fallback. Its catalog entry exists but ships an empty `base_instructions` where every sibling carries 11k to 21k characters, and the loader treats empty as absent. Astra stays registered as a catalog slug, so it switches over on its own if OpenAI fills the field in. - The catalog marks Astra `visibility: "hide"` with `priority: 1` and `minimal_client_version: 0.153.0`, which is the staged rollout rather than a program gate. Unlike the Daybreak tiers it still ships in the config templates, because it has an auto-fallback chain: an unentitled account costs one round trip and lands on a working model, where a Daybreak request would hard-fail. Cyber tier notes (Daybreak-gated): -- `gpt-daybreak-blue-latest` (defensive security) and `gpt-daybreak-red-latest` (cyber-permissive, for authorized security research) are catalog-verified cyber-specialty models — `model_specialty: "cyber"`, `use_responses_lite: true`, `tool_mode: "code_mode_only"`, efforts low through ultra. `gpt-5.6-cyber` is OpenAI's published alias fronting them; it belongs to the 5.6 generation, not GPT-6. +- `gpt-daybreak-blue-latest` (defensive security) and `gpt-daybreak-red-latest` (cyber-permissive, for authorized security research) are catalog-verified cyber-specialty models with `model_specialty: "cyber"`, `use_responses_lite: true`, `tool_mode: "code_mode_only"`, efforts low through ultra. `gpt-5.6-cyber` is OpenAI's published alias fronting them; it belongs to the 5.6 generation, not GPT-6. - All three require Daybreak program approval, and Blue/Red are `visibility: "hide"` in the catalog. They are therefore **not** in the shipped config templates, for the same reason `gpt-5.3-codex-spark` is not: shipping an entitlement-gated id to every user causes avoidable startup failures. The plugin routes them fully, so an entitled user adds the id by hand and it works. - None of the three has a fallback chain, deliberately. Degrading a cyber-specialty request onto a general model would answer a security-research prompt with a model that was never asked for, so an unentitled account gets a hard failure instead of a silent substitution. - `gpt-daybreak-blue` and `gpt-daybreak-red` are accepted as short forms of the `-latest` ids. @@ -97,10 +99,10 @@ GPT-5.6 notes: - 5.6 models are served over the **responses-lite** path. Their catalog entry sets `use_responses_lite: true` and `tool_mode: "code_mode_only"`, so the plugin reshapes the request the way Codex does: tool definitions move into `input` as a leading `additional_tools` developer item, the Codex instructions follow as a developer message, top-level `instructions` is emptied, `tools` is omitted, `parallel_tool_calls` is forced off, image `detail` fields are stripped, and an `x-openai-internal-codex-responses-lite: true` header is sent. Pre-5.6 models keep the classic shape. - No 5.6 tier accepts `none` or `minimal`; both are raised to `low`. - `max` and `ultra` are new in 5.6. Requesting them on an older family steps down to `xhigh` (then `high` where xhigh is unsupported). -- `ultra` is a client-side tier. Codex rewrites it to `max` before the request leaves the client, and the subagent orchestration that distinguishes ultra lives in the Codex client rather than the request body. This plugin is a proxy, so `-ultra` is accepted as an alias and sent on the wire as `max` — it does **not** spawn subagents. -- 5.6 is opt-in: the legacy `gpt-5` alias and the plugin default still resolve to `gpt-5.5` / `gpt-5.4`. Because 5.6 shipped as a limited preview, an account without access falls back down the 5.6 tiers and then to `gpt-5.5` automatically — this works under the default `strict` policy, like the `gpt-5.5`/`gpt-5-codex` auto-fallbacks, and can be disabled with `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1`. The lite shape is applied per request attempt, so a request that falls back from `gpt-5.6-sol` to `gpt-5.5` is re-serialized into the classic shape and keeps its tools. +- `ultra` is a client-side tier. Codex rewrites it to `max` before the request leaves the client, and the subagent orchestration that distinguishes ultra lives in the Codex client rather than the request body. This plugin is a proxy, so `-ultra` is accepted as an alias and sent on the wire as `max`, and it does **not** spawn subagents. +- 5.6 is opt-in: the legacy `gpt-5` alias and the plugin default still resolve to `gpt-5.5` / `gpt-5.4`. Because 5.6 shipped as a limited preview, an account without access falls back down the 5.6 tiers and then to `gpt-5.5` automatically. This works under the default `strict` policy, like the `gpt-5.5`/`gpt-5-codex` auto-fallbacks, and can be disabled with `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1`. The lite shape is applied per request attempt, so a request that falls back from `gpt-5.6-sol` to `gpt-5.5` is re-serialized into the classic shape and keeps its tools. - Client identity defaults to `originator: opencode` for every responses-lite model (the 5.6 tiers, GPT-6 Astra and both Daybreak tiers) and `codex_cli_rs` for other models. Override with `CODEX_AUTH_CLIENT_IDENTITY=codex|opencode`. -- Instructions for the 5.6 tiers come from the Codex model catalog — see "System instructions" below. +- Instructions for the 5.6 tiers come from the Codex model catalog. See "System instructions" below. ### System instructions @@ -108,7 +110,7 @@ Modern Codex carries a full `base_instructions` string **per model** in its cata | Model | Instruction source | |-------|--------------------| -| `gpt-6-astra` | `gpt_5_2_prompt.md` for now — registered as a catalog slug, so it switches to catalog text the moment openai/codex publishes an entry | +| `gpt-6-astra` | `gpt_5_2_prompt.md` for now. Astra has a catalog entry, but it ships an empty `base_instructions` where every sibling carries 11k to 21k characters, and the loader treats empty as absent, so the prompt file stays in charge until OpenAI fills the field | | `gpt-daybreak-blue-latest`, `gpt-daybreak-red-latest` | catalog | | `gpt-5.6-cyber` | `gpt_5_2_prompt.md` (the catalog has no entry under this slug, only the Daybreak ids it fronts) | | `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` | catalog (each tier has distinct text) | @@ -117,7 +119,7 @@ Modern Codex carries a full `base_instructions` string **per model** in its cata | `gpt-5.2` | catalog | | `gpt-5-codex`, `gpt-5.1*`, `gpt-5.2-codex`, `gpt-5.4-nano`, `gpt-5.4-pro` | `*_prompt.md` file (absent from the catalog) | -Catalog-sourced instructions cache per model id (`catalog--instructions.md`); file-sourced instructions keep the historical per-family cache. This matters because `gpt-5.5` and `gpt-5.4` share the `gpt-5.4` family but have different catalog text — a family-keyed cache would let one serve the other's prompt. `models.json` is fetched once per release tag and shared across models. If the pinned Codex release has no catalog entry for a model, the plugin falls back to that family's prompt file. +Catalog-sourced instructions cache per model id (`catalog--instructions.md`); file-sourced instructions keep the historical per-family cache. This matters because `gpt-5.5` and `gpt-5.4` share the `gpt-5.4` family but have different catalog text, and a family-keyed cache would let one serve the other's prompt. `models.json` is fetched once per release tag and shared across models. If the pinned Codex release has no catalog entry for a model, the plugin falls back to that family's prompt file. For context sizing, shipped templates use: - `gpt-6-astra`: `context=1050000`, `output=128000` @@ -148,7 +150,7 @@ what they mean: - `low` - light reasoning, fastest - `medium` - balanced (default) - `high` - deep reasoning -- `xhigh` - max depth for complex tasks (default for legacy `gpt-5.3-codex` / `gpt-5.2-codex` aliases and `gpt-5.1-codex-max`; available for `gpt-5.5`, `gpt-5.5-fast`, `gpt-5.4`, and optional `gpt-5.4-pro`) +- `xhigh` - max depth for complex tasks (default for `gpt-5.3-codex` / `gpt-5.2-codex`; available for `gpt-5.1-codex-max`, `gpt-5.5`, `gpt-5.5-fast`, `gpt-5.4`, `gpt-5.4-nano`, `gpt-5.2`, and optional `gpt-5.4-pro`. `gpt-5.1-codex-max` defaults to `high` and only sends `xhigh` when explicitly requested) ### Reasoning Summary @@ -262,7 +264,7 @@ The sample above intentionally sets `"retryAllAccountsMaxRetries": 3` as a bound | option | default | what it does | |--------|---------|--------------| -| `requestTransformMode` | `native` | request shaping mode: `native` keeps OpenCode payloads unchanged; `legacy` enables Codex compatibility rewrites | +| `requestTransformMode` | `native` | request shaping mode: `native` normalizes model names, sets instruction identity lines, and upserts `## Backend Model Identity`; `legacy` enables full Codex compatibility rewrites | | `codexMode` | `true` | legacy-only bridge prompt behavior (applies when `requestTransformMode=legacy`) | | `codexTuiV2` | `true` | enables codex-style terminal ui output (set `false` to keep legacy output) | | `codexTuiColorProfile` | `truecolor` | terminal color profile for codex ui (`truecolor`, `ansi256`, `ansi16`) | @@ -289,11 +291,11 @@ The sample above intentionally sets `"retryAllAccountsMaxRetries": 3` as a bound | `fallbackOnUnsupportedCodexModel` | `false` | legacy fallback toggle mapped to `unsupportedCodexPolicy` (prefer using `unsupportedCodexPolicy`) | | `fallbackToGpt52OnUnsupportedGpt53` | `true` | legacy compatibility toggle for the `gpt-5.3-codex -> gpt-5.2-codex` edge when generic fallback is enabled | | `unsupportedCodexFallbackChain` | `{}` | optional per-model fallback-chain override (map of `model -> [fallback1, fallback2, ...]`; default includes `gpt-6-astra` and the 5.6 tiers down to `gpt-5.5`, and `gpt-5.5`/`gpt-5-codex` down to `gpt-5.2`). These entry IDs auto-fallback by default, even when selected directly, both for common entitlement gates and when every enabled account has an active upstream rate/quota block for the requested model; set `CODEX_AUTH_DISABLE_GPT6_AUTO_FALLBACK=1`, `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1`, `CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1`, or `CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1` to opt out. Directly selected non-entry IDs stay strict under this auto gate. GPT-5.5 Pro and GPT-6 Astra Pro are not mapped: neither is a Codex-routable id. The Daybreak cyber tiers are deliberately chainless, so an unentitled account fails loudly rather than being answered by a general model. | -| `sessionRecovery` | `true` | auto-recover from common api errors | -| `autoResume` | `true` | auto-resume after thinking block recovery | +| `sessionRecovery` | `true` | classify recoverable API errors and show recovery toasts in the TUI | +| `autoResume` | `true` | auto-resume flag (supported by underlying recovery engine in `lib/recovery/hook.ts`) | | `tokenRefreshSkewMs` | `60000` | refresh tokens this many ms before expiry | | `rateLimitToastDebounceMs` | `60000` | debounce rate limit toasts | -| `parallelProbing` | `false` | enable concurrent account health probes | +| `parallelProbing` | `false` | enable concurrent account health probes. Probe infrastructure exists in `lib/parallel-probe.ts` with test coverage, but runtime probe scheduling in the main fetch loop uses direct sequential rotation checks, so this toggle has no runtime consumer today | | `parallelProbingMaxConcurrency` | `2` | max concurrent probes when parallel probing is enabled (1–5) | | `emptyResponseMaxRetries` | `2` | retries after an empty SSE/response body | | `emptyResponseRetryDelayMs` | `1000` | delay in ms between empty-response retries | @@ -302,6 +304,18 @@ The sample above intentionally sets `"retryAllAccountsMaxRetries": 3` as a bound | `streamStallTimeoutMs` | `45000` | max time to wait for next SSE chunk before aborting | | `quotaNotifications` | disabled | optional macOS Notification Center alerts for aggregate 5-hour and weekly pool quotas. `autoProtectCredits` defaults to `true` and polls the same endpoint to exclude fully spent subscription quotas from rotation; `intervalMs` defaults to 30 minutes with a 30-second minimum, `notifyEveryCheck` defaults to `false`, and `thresholds` defaults to `[25, 10, 0]` | +### Retry Budgets by Profile + +`retryProfile` picks the per-class retry budgets, and `retryBudgetOverrides` replaces any single class. The classes, in the order the tables above list them, are `authRefresh`, `network`, `server`, `rateLimitShort`, `rateLimitGlobal`, and `emptyResponse`. Budgets come from `lib/request/retry-budget.ts`. + +| profile | authRefresh | network | server | rateLimitShort | rateLimitGlobal | emptyResponse | +|---------|-------------|---------|--------|----------------|-----------------|---------------| +| `conservative` | 2 | 2 | 2 | 2 | 1 | 1 | +| `balanced` (default) | 4 | 4 | 4 | 4 | 3 | 2 | +| `aggressive` | 8 | 8 | 8 | 8 | 10 | 4 | + +The two rate-limit classes split on a 5000 ms threshold (`RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS` in `lib/request/rate-limit-backoff.ts`). When a 429 arrives, the backoff helper computes a wait from the server's `retry-after` header, 1000 ms by default, doubling per consecutive 429 and capped at 60 seconds. A wait of at most 5000 ms on a non-exhausted quota window consumes `rateLimitShort` and retries the same account after the wait. Anything else, a longer wait, an exhausted window, or a spent `rateLimitShort` budget, marks the account rate-limited and rotates; when every account is limited, the wait-and-retry loop consumes `rateLimitGlobal`. + For upstream rate/quota blocks, automatic model fallback runs **before** configured waiting (`retryAllAccountsRateLimited` and its wait/retry limits). It only moves to a model with an eligible account under that target's pool policy: unavailable @@ -378,10 +392,10 @@ by default the plugin is strict (`unsupportedCodexPolicy: "strict"`) except for set `unsupportedCodexPolicy: "fallback"` to enable model fallback after account/workspace attempts are exhausted. defaults when fallback policy is enabled and `unsupportedCodexFallbackChain` is empty (plus the always-on public-selector auto-fallbacks for common entitlement gates): -- `gpt-6-astra -> gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5` (then the `gpt-5.5` chain) -- `gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5` (then the `gpt-5.5` chain) -- `gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5` -- `gpt-5.6-luna -> gpt-5.5` +- `gpt-6-astra -> gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5 -> gpt-5.2` +- `gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5 -> gpt-5.2` +- `gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5 -> gpt-5.2` +- `gpt-5.6-luna -> gpt-5.5 -> gpt-5.2` - `gpt-5.5 -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.2` - `gpt-5-codex -> gpt-5.6-terra -> gpt-5.5 -> gpt-5.2` - `gpt-5.4 -> gpt-5.6-terra -> gpt-5.5 -> gpt-5.2` (the successor its catalog entry names) @@ -446,7 +460,7 @@ override any config with env vars (boolean values are truthy only for `"1"`): | `CODEX_AUTH_QUOTA_NOTIFICATIONS=1` | enable macOS aggregate quota notifications | | `CODEX_AUTH_AUTO_PROTECT_CREDITS=0` | disable periodic quota checks that protect paid Credits (enabled by default) | | `CODEX_AUTH_QUOTA_NOTIFICATIONS_INTERVAL_MS=1800000` | override the quota check interval (minimum 30000 ms) | -| `CODEX_AUTH_SESSION_RECOVERY=0` | disable automatic session recovery hooks | +| `CODEX_AUTH_SESSION_RECOVERY=0` | disable recoverable error classification and warning toasts | | `CODEX_AUTH_AUTO_RESUME=0` | disable auto-resume after thinking-block recovery | | `CODEX_AUTH_FAST_SESSION=1` | enable fast-session defaults | | `CODEX_AUTH_FAST_SESSION_STRATEGY=always` | force fast mode on every prompt | @@ -471,8 +485,8 @@ override any config with env vars (boolean values are truthy only for `"1"`): | `CODEX_AUTH_FALLBACK_GPT53_TO_GPT52=0` | disable only the legacy `gpt-5.3-codex -> gpt-5.2-codex` edge | | `CODEX_AUTH_DISABLE_GPT6_AUTO_FALLBACK=1` | disable automatic `gpt-6-astra -> gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5` rollout fallback | | `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1` | disable automatic `gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5` preview fallback | -| `CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1` | disable automatic `gpt-5.5 -> gpt-5.4` fallback during rollout | -| `CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1` | disable automatic canonical Codex/GPT-5.4-family fallback | +| `CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1` | disable automatic `gpt-5.5 -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.2` fallback | +| `CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1` | disable automatic `gpt-5-codex -> gpt-5.6-terra -> gpt-5.5 -> gpt-5.2` fallback | | `CODEX_AUTH_ACCOUNT_ID=acc_xxx` | force specific workspace id | | `CODEX_AUTH_CLIENT_IDENTITY=codex` | force one client identity for all models: `codex` (`originator: codex_cli_rs`) or `opencode` (alias `host`; `originator: opencode`). Default: `opencode` for every responses-lite model (5.6 tiers, GPT-6 Astra, Daybreak), `codex` for everything else | | `CODEX_AUTH_DISABLE_CODEX_USER_AGENT=1` | keep the host runtime's `User-Agent` instead of the identity's `User-Agent` | @@ -485,7 +499,7 @@ override any config with env vars (boolean values are truthy only for `"1"`): | `CODEX_AUTH_SYNC_CODEX_CLI=0` | disable hydrating accounts from Codex CLI `~/.codex` storage (on by default) | | `CODEX_CONSOLE_LOG=1` | also mirror plugin logs to the console | | `CODEX_COLLABORATION_MODE=plan` | collaboration mode hint for request shaping (`OPENCODE_COLLABORATION_MODE` is accepted as an alias) | -| `OPENCODE_STATE_DIR=/path` | override OpenCode state dir used for the TUI quota cache file | +| `OPENCODE_STATE_DIR=/path` | override OpenCode state dir used for the TUI quota cache file (default `~/.local/state/opencode`) | ### Advanced / power-user environment variables @@ -601,10 +615,10 @@ opencode run "task" --model=openai/gpt-5.6-sol-high | `~/.opencode/auth/openai.json` | OpenCode OAuth tokens | | `~/.opencode/oc-codex-multi-auth-accounts.json` | global V3 account pool | | `~/.opencode/projects//oc-codex-multi-auth-accounts.json` | per-project account pool | -| `~/.opencode/oc-codex-multi-auth-flagged-accounts.json` | flagged/deactivated account metadata | +| `~/.opencode/projects//oc-codex-multi-auth-flagged-accounts.json` | flagged/deactivated account metadata, written beside the active accounts file. With the default `perProjectAccounts` this is the per-project path; with project storage off it is `~/.opencode/oc-codex-multi-auth-flagged-accounts.json` | | `~/.opencode/logs/codex-plugin/` | request/debug logs when enabled | | `~/.opencode/cache/` | instruction/catalog and auto-update caches | -| `$OPENCODE_STATE_DIR` or OpenCode state dir + `oc-codex-multi-auth-tui-quota.json` | TUI quota cache | +| `~/.local/state/opencode/oc-codex-multi-auth-tui-quota.json` | TUI quota snapshot cache shared by the provider and TUI plugins; `$OPENCODE_STATE_DIR` overrides the directory when set | | `$XDG_DATA_HOME/opencode/storage/…` (Windows: `%APPDATA%/opencode/storage`) | OpenCode session message/part store (session recovery) | | `openai-codex-accounts.json` / `openai-codex-flagged-accounts.json` / `openai-codex-blocked-accounts.json` | legacy migration sources only | diff --git a/docs/development/ARCHITECTURE.md b/docs/development/ARCHITECTURE.md index 7cbf31de..579331bf 100644 --- a/docs/development/ARCHITECTURE.md +++ b/docs/development/ARCHITECTURE.md @@ -13,7 +13,7 @@ Runtime architecture for the `oc-codex-multi-auth` OpenCode plugin, installer, C 3. Preserve Codex backend invariants: `stream: true`, `store: false`, and `reasoning.encrypted_content`. 4. Make multi-account state visible through account switching, health checks, diagnostics, quota status, and recovery commands. 5. Keep account storage local by default, with explicit export/import and optional OS keychain migration. -6. Keep the broad OpenCode tool surface modular: every registered `codex-*` tool is its own file under `lib/tools/`. +6. Keep the broad OpenCode tool surface modular, where every registered `codex-*` tool is its own file under `lib/tools/`. 7. Keep public docs search-friendly without overstating support, affiliation, or production/commercial use. --- @@ -46,7 +46,7 @@ index.ts |- auth loader: default-browser callback, open-URL-manually callback, device code, manual URL paste |- account manager + V3 storage + optional keychain |- custom provider fetch pipeline - |- runtime metrics, retry budgets, circuit breaker, recovery hooks + |- runtime metrics, retry budgets, circuit breaker, recoverable-error toasts |- ToolContext construction v lib/tools/index.ts @@ -61,21 +61,22 @@ lib/request/fetch-helpers.ts + lib/request/request-transformer.ts |- rewrite URL to Codex/ChatGPT backend |- native mode: preserve host payload shape |- legacy mode: apply compatibility rewrites - |- force store:false and include reasoning.encrypted_content + |- legacy mode: force store:false, stream:true, and reasoning.encrypted_content |- GPT-6 Astra / Daybreak / GPT-5.6: responses-lite reshape + opencode client identity |- other models: codex_cli_rs client identity (default) |- resolve modelAccountPools preferred accounts |- select/refresh account (hybrid health scoring) |- attach OAuth headers + |- rate-limit and quota header extraction v ChatGPT-backed Codex endpoint | v lib/request/response-handler.ts |- SSE parsing - |- error mapping - |- quota/rate-limit/header extraction - |- empty-response retries + |- streaming pass-through + |- stream stall guards + |- empty-response detection (the retry loop lives in index.ts) OpenCode TUI runtime | @@ -110,7 +111,7 @@ tui.ts ## Documentation Layout -The current docs tree mirrors the codebase boundaries above: user docs cover setup and operations, and maintainer docs cover internal architecture and validation. +The current docs tree mirrors the codebase boundaries above. User docs cover setup and operations, and maintainer docs cover internal architecture and validation. ```text docs/ @@ -144,22 +145,24 @@ High-level provider fetch flow: 1. Parse OpenCode request URL and body. 2. Resolve plugin config from defaults, `~/.opencode/openai-codex-auth-config.json`, and environment overrides (boolean env truthy only for `"1"`). 3. Choose request transform mode: - - `native` keeps OpenCode payloads unchanged except required Codex invariants. + - `native` keeps the host payload shape. It normalizes the model name, sets the backend instruction identity line, and upserts one `## Backend Model Identity` developer message naming the outgoing model, refreshed again when fallback changes the model. - `legacy` fetches Codex/OpenCode prompts and applies compatibility rewrites. 4. Enforce ChatGPT-backed Codex invariants: - `stream: true` - `store: false` - `include: ["reasoning.encrypted_content"]` or equivalent inclusion + + Legacy transformation mode (`transformRequestBody`) sets all three unconditionally. Native mode leaves them to the shipped config templates (`store: false`, `reasoning.encrypted_content`) and the host payload (`stream`). 5. Normalize model aliases and fallback candidates (including GPT-6 Astra, the Daybreak cyber tiers, and the GPT-5.6 Sol/Terra/Luna tiers). 6. For responses-lite models (GPT-6 Astra, Daybreak, GPT-5.6), apply the responses-lite reshape (`lib/request/helpers/responses-lite.ts`): tools move into `input` as `additional_tools`, instructions become a developer message, top-level `tools`/`instructions` are cleared for lite shape, image `detail` is stripped, and `x-openai-internal-codex-responses-lite: true` is set. -7. Resolve client identity (`lib/request/helpers/client-identity.ts`): responses-lite models default to `originator: opencode`; other models default to `codex_cli_rs`. Override with `CODEX_AUTH_CLIENT_IDENTITY`. +7. Resolve client identity with `lib/request/helpers/client-identity.ts`. Responses-lite models default to `originator: opencode`, other models to `codex_cli_rs`. Override with `CODEX_AUTH_CLIENT_IDENTITY`. 8. Resolve accounts and `preferred`/`strict` policy from `modelAccountPools` and `modelAccountPoolModes`; only preferred pools fall back to the general pool when unavailable. 9. Resolve account/workspace selection with the configured `rotationStrategy` (default `hybrid` health scoring), cooldown, token bucket, and explicit `CODEX_AUTH_ACCOUNT_ID` constraints. 10. Refresh tokens through the queued refresh path when needed. 11. Attach OAuth/Codex headers and forward the request. -12. Parse SSE responses, quota headers, retryable errors, empty responses, and unsupported-model details. -13. Update runtime metrics, account health, circuit breaker state, TUI quota cache, and persisted storage. -14. On recoverable failures, apply session recovery / auto-resume hooks when enabled. +12. Parse the response. `lib/request/response-handler.ts` owns SSE parsing, stream stall guards, and empty-response detection. `lib/request/fetch-helpers.ts` owns rate-limit and quota header extraction, error mapping, and fallback. +13. Update runtime metrics, account health, circuit breaker state, TUI quota cache, and persisted storage. Retries draw from the per-request budget tracker in `lib/request/retry-budget.ts`. +14. On recoverable failures, classify the error and show a recovery toast in the current plugin runtime. The message/part rewriting and auto-resume engine in `lib/recovery/hook.ts` is not invoked by host event streams or request handlers. --- @@ -175,7 +178,11 @@ Context is preserved through: Legacy mode exists for compatibility with older OpenCode/AI SDK payload behavior. It removes unsupported `item_reference` items and message IDs that cannot be looked up when `store: false` is active. Native mode is the default and preserves the host payload shape as much as possible. -Responses-lite is a separate body shape layered on top of the same stateless contract: tool definitions live in the input prefix rather than the top-level `tools` field. +The two modes source the invariants differently. Legacy transformation sets `store: false`, `stream: true`, and `reasoning.encrypted_content` inclusion unconditionally inside `transformRequestBody`. Native mode does not rewrite the body for them. It relies on the installer templates, which ship `store: false` and `reasoning.encrypted_content` on every model entry, and on the host payload, which already carries `stream`. + +Native mode still marks the backend model. It sets the instruction identity line and upserts one `## Backend Model Identity` developer message naming the outgoing model, so a selector label never hides the real model ID from the backend. + +Responses-lite is a separate body shape layered on top of the same stateless contract. Tool definitions live in the input prefix rather than the top-level `tools` field. --- @@ -212,17 +219,19 @@ Canonical OpenCode plugin state lives under `~/.opencode`, while OpenCode config | --- | --- | | `~/.config/opencode/opencode.json` | OpenCode provider/plugin config managed by installer | | `~/.config/opencode/tui.json` | OpenCode TUI plugin config managed by installer | -| `~/.opencode/auth/openai.json` | OpenCode auth token file | +| `~/.opencode/auth/openai.json` | OpenCode auth token file (convention reference in docs; no plugin code reads this path) | +| `~/.local/share/opencode/auth.json` | OpenCode host auth store, read and backfilled from the account pool by `backfillHostOpenAIAuthFromPool` | | `~/.opencode/openai-codex-auth-config.json` | plugin runtime config | | `~/.opencode/oc-codex-multi-auth-accounts.json` | global V3 account pool | | `~/.opencode/projects//oc-codex-multi-auth-accounts.json` | project-scoped V3 account pool | -| `~/.opencode/oc-codex-multi-auth-flagged-accounts.json` | flagged/deactivated account metadata | +| `~/.opencode/projects//oc-codex-multi-auth-flagged-accounts.json` | flagged/deactivated account metadata, project-scoped when `perProjectAccounts` is on (default) | +| `~/.opencode/oc-codex-multi-auth-flagged-accounts.json` | flagged/deactivated account metadata, global when `perProjectAccounts` is off | | `~/.opencode/backups/` | account backup/export target | | `~/.opencode/logs/codex-plugin/` | request/debug logs when enabled | Storage invariants: -1. V1/V2 account files migrate into V3 on load/save paths (V2 surfaces a typed recovery error rather than silent discard). +1. V1 account files migrate into V3 on load/save paths. V2 is rejected with the typed `UNKNOWN_V2_FORMAT` recovery error instead of a silent discard. Versions above 3 are rejected with `UNSUPPORTED_SCHEMA_VERSION`. 2. Per-project storage is enabled by default and keyed by detected project identity. 3. JSON files are written atomically where supported. 4. Optional keychain storage is opt-in via `CODEX_KEYCHAIN=1`. @@ -230,9 +239,17 @@ Storage invariants: --- +## Shutdown and Keychain Details + +`lib/shutdown.ts` registers one cleanup pass per process on SIGINT, SIGTERM, and beforeExit. As a host plugin the process is not the package's to terminate, so the handlers drain cleanup and return, leaving exit ownership with OpenCode. The standalone CLI entrypoints call `setShutdownOwnsProcess(true)` and exit 130 on SIGINT and 143 on SIGTERM (`128 + signal number`). + +Keychain entries live under the OS keychain service name `oc-codex-multi-auth`. The global pool uses the account key `accounts:global`, and a project pool uses `accounts:`. Migrating a JSON pool into the keychain renames the original file to `.migrated-to-keychain.` and keeps it at mode 0600 as the rollback artifact. That file is the user's recovery path if keychain lookups fail or `CODEX_KEYCHAIN` is later unset. + +--- + ## Session Recovery Storage -When `sessionRecovery` is true (default), `lib/recovery/` may rewrite OpenCode host storage: +When `sessionRecovery` is true (default), the request path classifies errors with `detectErrorType` and `isRecoverableError` and shows a recovery toast. The full repair engine in `lib/recovery/hook.ts` (`handleSessionRecovery`, message/part rewriting through `lib/recovery/storage.ts`, and optional auto-resume) is not wired into host event streams or request handlers, so it does not run in the current plugin runtime. The storage paths below describe what that engine reads and writes when wired. | Path | Purpose | |------|---------| @@ -254,6 +271,8 @@ Recovered classes: `tool_result_missing`, `thinking_block_order`, `thinking_disa The request path also writes quota snapshots from response headers, so the TUI can reflect the account/workspace used by the latest request. +The shared cache file resolves in this order. `tui.ts` passes the OpenCode state path (`api.state.path.state`) to `getTuiQuotaCachePath`. That function falls back to `$OPENCODE_STATE_DIR`, then to `~/.local/state/opencode/oc-codex-multi-auth-tui-quota.json`. There is no `~/.opencode/` fallback. + --- ## Model Catalog and Fallback Notes @@ -273,17 +292,33 @@ The default installer preserves `provider.openai`. `--modern` writes the modern `--full` adds 59 explicit selector IDs for scripts. `--legacy` writes the explicit-only template (59 entries) for older OpenCode versions. -Unsupported-model behavior is strict by default. Default auto-fallbacks still cover common entitlement gates for `gpt-6-astra` → the GPT-5.6 tiers → `gpt-5.5`, and for `gpt-5.5` / `gpt-5-codex` through `gpt-5.6-terra` / `gpt-5.6-luna` / `gpt-5.2`. GPT-5.4 and GPT-5.4 Mini were retired from Codex on 2026-08-31 and are no longer fallback targets. Full generic fallback can be enabled through config or environment variables. +Unsupported-model behavior is strict by default. Default auto-fallbacks still cover common entitlement gates for `gpt-6-astra` → the GPT-5.6 tiers → `gpt-5.5` → `gpt-5.2`, and for `gpt-5.5` / `gpt-5-codex` through `gpt-5.6-terra` / `gpt-5.6-luna` / `gpt-5.2`. The same terminal `gpt-5.2` ends each GPT-5.6 tier's own chain, and `gpt-5.2` repeats on every tier row on purpose, because the resolver reads the chain of whichever model the request is currently on. GPT-5.4 and GPT-5.4 Mini were retired from Codex on 2026-08-31 and are no longer fallback targets. Full generic fallback can be enabled through config or environment variables. --- ## Rotation and Reliability -- `rotationStrategy` default `hybrid`: stick while healthy, otherwise score-select (health + tokens + freshness). Alternatives: `sticky`, `round-robin`. +- `rotationStrategy` defaults to `hybrid`. `lib/accounts/rotation.ts` keeps the current account for the family while it is selectable, then `selectHybridAccount` in `lib/rotation.ts` scores candidates as `health*2 + tokens*5 + hoursSinceUsed*2.0` and takes the best score. When every candidate is blocked, selection falls back to the least-recently-used account, and the request loop discards that fallback if it is still ineligible. Alternatives: `sticky`, `round-robin`. - `lib/rotation.ts` owns hybrid health scoring; `lib/accounts/rotation.ts` wires it into account manager state. -- Circuit breaker isolates repeated failures per account/path. -- Empty-response retries use `emptyResponseMaxRetries` / `emptyResponseRetryDelayMs`. -- Optional `parallelProbing` can probe account health concurrently (default off). +- Rotation health uses `HealthScoreTracker` in `lib/rotation.ts`: +1 per success, -10 on rate limit, -20 on other failure, +2 per hour of passive recovery, clamped to 0-100. +- The standalone CLI defines health differently. `health` and `status` count an account healthy when `enabled && hasRefreshToken`. That check reads credentials, not rotation scores. +- Circuit breaker isolates repeated failures. It opens after 3 failures inside a 60s window, resets after 30s, and allows 1 half-open probe attempt. The key is `${accountId}:${workspaceIdentityHash}:${modelFamily}`, where the workspace hash is a truncated SHA-256 of the account's workspace identity key, or `index-` when no workspace identity exists. It is not keyed per URL path. One degraded endpoint cannot poison other families on the same account. +- Retry budgets: `lib/request/retry-budget.ts` tracks six per-request classes (`authRefresh`, `network`, `server`, `rateLimitShort`, `rateLimitGlobal`, `emptyResponse`). Profiles set the limits: `conservative` 2/2/2/2/1/1, `balanced` 4/4/4/4/3/2, `aggressive` 8/8/8/8/10/4, in class order. Config selects the profile with per-class overrides, and `beginnerSafeMode` forces `conservative`. An exhausted budget fails the request instead of retrying without bound. +- Empty-response retries use `emptyResponseMaxRetries` / `emptyResponseRetryDelayMs` and consume the `emptyResponse` budget class. +- Optional `parallelProbing` can probe account health concurrently (default off; note that `lib/parallel-probe.ts` races requests first-success-wins rather than running read-only health checks, and is currently uncalled by runtime entrypoints). + +### Error Classification and Rotation Matrix + +| Status / Condition | Consumed Budget | Action Taken | Health Impact | Storage Side-Effect | +| :--- | :--- | :--- | :--- | :--- | +| **429 (delay <= 5000ms)** | `rateLimitShort` | Jittered sleep `addJitter(max(100, delayMs), 0.2)` and retry on same account | None | None (no cooldown window written) | +| **429 (delay > 5000ms)** | None (immediate rotate); `rateLimitGlobal` when all accounts blocked | Rotate to next candidate account; when all accounts are blocked, wait and retry | -10 | Records `rateLimitResetTimes` per model family | +| **401 Invalidated** | None (`authRefresh` applies during token refresh) | Increment `authFailures`; if >= 3, remove account; else 30s group cooldown | None | Persists updated failure count or account removal | +| **5xx / Server Error** | `server` | Trip circuit breaker, rotate to next account | -20 | None (unless server payload carries rate-limit reset) | +| **Network Error** | `network` | Trip circuit breaker, rotate to next account | -20 | None | +| **Workspace Deactivated** | None | Flag account and remove from active pool | -20 | Writes active pool and flagged storage files | +| **Stream Interrupted** | `server` | Rotate to next account if within budget | -20 | None | +| **Token Bucket Depleted** | None | Rotate immediately (`rate-limit-local`) | None | None (local throttle only, no upstream penalty) | --- diff --git a/docs/development/CONFIG_FIELDS.md b/docs/development/CONFIG_FIELDS.md index 75023ce0..5bbe403b 100644 --- a/docs/development/CONFIG_FIELDS.md +++ b/docs/development/CONFIG_FIELDS.md @@ -251,7 +251,7 @@ Defaults come from `lib/config.ts` / `lib/schemas.ts`. Environment overrides win | `sessionRecovery` | `true` | `CODEX_AUTH_SESSION_RECOVERY` | Auto-recover common API errors | | `autoResume` | `true` | `CODEX_AUTH_AUTO_RESUME` | Auto-resume after thinking-block recovery | | `autoUpdate` | `true` | `CODEX_AUTH_AUTO_UPDATE` | Daily npm update check + cache refresh | -| `parallelProbing` | `false` | `CODEX_AUTH_PARALLEL_PROBING` | Concurrent account health probes | +| `parallelProbing` | `false` | `CODEX_AUTH_PARALLEL_PROBING` | Concurrent account health probes; probe infrastructure exists in `lib/parallel-probe.ts` with test coverage, but the main fetch loop probes sequentially, so this flag has no runtime consumer today | | `parallelProbingMaxConcurrency` | `2` | `CODEX_AUTH_PARALLEL_PROBING_MAX_CONCURRENCY` | Max concurrent probes (1–5) | | `emptyResponseMaxRetries` | `2` | `CODEX_AUTH_EMPTY_RESPONSE_MAX_RETRIES` | Retries after empty SSE bodies | | `emptyResponseRetryDelayMs` | `1000` | `CODEX_AUTH_EMPTY_RESPONSE_RETRY_DELAY_MS` | Delay between empty-response retries | @@ -264,6 +264,22 @@ Defaults come from `lib/config.ts` / `lib/schemas.ts`. Environment overrides win | `quotaNotifications.notifyEveryCheck` | `false` | (file only) | Alert after every poll, not only on threshold crossings | | `quotaNotifications.thresholds` | `[25, 10, 0]` | (file only) | Remaining-percent alert thresholds; `[]` disables threshold alerts | +### Numeric bounds + +`lib/schemas.ts` validates the config file with Zod. An out-of-bounds file value fails validation for that key, the loader logs a validation warning, and the key is dropped, so the default applies. It is never clamped to the nearest bound. Environment numeric overrides take a different path through `resolveNumberSetting` in `lib/config.ts`, which applies only a lower floor and no upper bound. + +| field | config-file bounds (Zod) | env bounds (resolver) | +|-------|--------------------------|-----------------------| +| `fastSessionMaxInputItems` | 8 to 200 | 8, no ceiling | +| `parallelProbingMaxConcurrency` | 1 to 5 | 1 to 5 (clamped) | +| `toastDurationMs` | at least 1000 | 1000 | +| `fetchTimeoutMs` | at least 1000 | 1000 | +| `streamStallTimeoutMs` | at least 1000 | 1000 | +| `quotaNotifications.intervalMs` | at least 30000 | clamped up to 30000 | +| `retryBudgetOverrides.*` | integer, at least 0 | (file only) | + +So `parallelProbingMaxConcurrency: 9` in the file falls back to the default `2`, while `CODEX_AUTH_PARALLEL_PROBING_MAX_CONCURRENCY=9` is accepted with no ceiling. + ### `modelAccountPools` The plugin runtime config can map effective model IDs to preferred stable account or @@ -341,7 +357,25 @@ Not part of `PluginConfigSchema`, but used by runtime modules: | `OPENCODE_SKIP_EMAIL_HYDRATE=1` | Skip email hydrate during account bootstrap | | `FORCE_INTERACTIVE_MODE=1` | Force interactive menu paths for tests/special shells | | `CODEX_AUTH_SYNC_CODEX_CLI=0` | Disable `~/.codex` account hydrate (on unless `"0"`) | +| `CODEX_AUTH_ACCOUNT_ID` | Force a specific workspace/account id during `opencode auth login` | +| `CODEX_KEYCHAIN=1` | Opt in to OS-native keychain account storage | +| `DEBUG_CODEX_PLUGIN=1` | Enable debug logging (request logging implies it) | +| `ENABLE_PLUGIN_REQUEST_LOGGING=1` | Log request metadata (no raw bodies) | +| `CODEX_PLUGIN_LOG_BODIES=1` | Include raw request/response bodies in request logs (sensitive) | +| `CODEX_PLUGIN_LOG_LEVEL=debug` | Set request-log level (`debug` / `info` / `warn` / `error`) | | `CODEX_CONSOLE_LOG=1` | Mirror plugin logs to console | +| `CODEX_AUTH_PREWARM=0` | Disable startup prewarm when legacy transform is enabled (native mode does not prewarm) | +| `OPENAI_BASE_URL=https://gateway.example/v1` | OpenAI-compatible OAuth inference gateway; requires `CODEX_AUTH_ALLOW_OPENAI_BASE_URL=1` | +| `CODEX_AUTH_ALLOW_OPENAI_BASE_URL=1` | Explicitly allow the trusted gateway to receive the ChatGPT OAuth access token (HTTPS required, HTTP only on loopback) | +| `CODEX_AUTH_DISABLE_GPT6_AUTO_FALLBACK=1` | Disable the automatic `gpt-6-astra -> gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5` rollout fallback chain | +| `CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1` | Disable the automatic `gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5` preview fallback chain | +| `CODEX_AUTH_DISABLE_GPT55_AUTO_FALLBACK=1` | Disable the automatic `gpt-5.5 -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.2` fallback | +| `CODEX_AUTH_DISABLE_CODEX_AUTO_FALLBACK=1` | Disable the automatic `gpt-5-codex -> gpt-5.6-terra -> gpt-5.5 -> gpt-5.2` fallback | +| `CODEX_AUTH_CLIENT_IDENTITY=codex` | Force one client identity for all models: `codex` or `opencode` (alias `host`) | +| `CODEX_AUTH_CLIENT_VERSION=0.150.0` | Override the Codex CLI version advertised in the `codex_cli_rs` User-Agent | +| `CODEX_AUTH_HOST_VERSION=1.18.0` | Override the opencode version advertised in the `opencode` User-Agent | +| `CODEX_AUTH_DISABLE_CODEX_USER_AGENT=1` | Keep the host runtime's `User-Agent` instead of the identity's | +| `CODEX_AUTH_SEND_ORGANIZATION_HEADER=1` | Restore legacy `openai-organization` request pinning (off by default) | | `CODEX_COLLABORATION_MODE` / `OPENCODE_COLLABORATION_MODE` | Collaboration mode hint for request shaping | | `OPENCODE_STATE_DIR` | Override state directory for TUI quota cache | diff --git a/docs/development/CONFIG_FLOW.md b/docs/development/CONFIG_FLOW.md index 3d8112fe..c98f372a 100644 --- a/docs/development/CONFIG_FLOW.md +++ b/docs/development/CONFIG_FLOW.md @@ -249,6 +249,8 @@ Important runtime behavior: | `~/.opencode/auth/openai.json` | OAuth token storage | | `~/.opencode/oc-codex-multi-auth-accounts.json` | global account storage | | `~/.opencode/projects//oc-codex-multi-auth-accounts.json` | per-project account storage | +| `~/.opencode/projects//oc-codex-multi-auth-flagged-accounts.json` | flagged/deactivated account metadata for the project scope. The flagged file is always written beside the active accounts file, so with the default `perProjectAccounts` it is per-project, and with project storage off it is `~/.opencode/oc-codex-multi-auth-flagged-accounts.json` | +| `~/.local/state/opencode/oc-codex-multi-auth-tui-quota.json` | TUI quota snapshot cache shared by the provider and TUI plugins. `$OPENCODE_STATE_DIR` overrides the directory when set | | `~/.opencode/logs/codex-plugin/` | plugin request/debug logs | ## See Also diff --git a/docs/development/GITHUB_DISCOVERABILITY.md b/docs/development/GITHUB_DISCOVERABILITY.md index aeb429b0..09f09ec5 100644 --- a/docs/development/GITHUB_DISCOVERABILITY.md +++ b/docs/development/GITHUB_DISCOVERABILITY.md @@ -143,14 +143,14 @@ The image should immediately communicate: When the product summary or positioning changes, update all of these together so the repo does not present three different descriptions of itself: -- `README.md` — H1, intro paragraph, feature bullets -- `package.json` — `description` and `keywords` -- `.codex-plugin/plugin.json` — `description` +- `README.md`: H1, intro paragraph, feature bullets +- `package.json`: `description` and `keywords` +- `.codex-plugin/plugin.json`: `description` - GitHub repository **About** text and topics (set in the GitHub UI) -- `AGENTS.md` and `lib/AGENTS.md` — overview paragraphs +- `AGENTS.md` and `lib/AGENTS.md`: overview paragraphs - `docs/README.md`, `docs/index.md`, `docs/DOCUMENTATION.md` - `docs/architecture.md` and `docs/development/ARCHITECTURE.md` -- `docs/_config.yml` — docs site title/description +- `docs/_config.yml`: docs site title/description `test/doc-parity.test.ts` pins the package description and keyword set against `package.json`, so a drift there fails the suite. diff --git a/docs/development/TUI_PARITY_CHECKLIST.md b/docs/development/TUI_PARITY_CHECKLIST.md index edc42234..2d2490df 100644 --- a/docs/development/TUI_PARITY_CHECKLIST.md +++ b/docs/development/TUI_PARITY_CHECKLIST.md @@ -18,7 +18,7 @@ Use this checklist to keep `oc-codex-multi-auth` aligned with the Antigravity-st - Core actions visible: - `Add account` - `Check quotas` - - `Deep probe accounts` + - `Deep check accounts` - `Verify flagged accounts` - `Start fresh` - `Delete all accounts` @@ -49,7 +49,7 @@ Use this checklist to keep `oc-codex-multi-auth` aligned with the Antigravity-st ## Health/Quota Check Parity - `Check quotas` scans all active accounts and prints per-account results. -- `Deep probe` performs stricter validation and surfaces richer diagnostic output. +- `Deep check accounts` performs stricter validation and surfaces richer diagnostic output. - Output includes index progress (`[i/N]`) and per-account status (`OK`, `ERROR`, `DISABLED`). - Summary line always shown at end (`ok/error/disabled` counts). @@ -78,7 +78,7 @@ Use this checklist to keep `oc-codex-multi-auth` aligned with the Antigravity-st - Visual controls: - `codexTuiColorProfile`: `truecolor` / `ansi256` / `ansi16` - `codexTuiGlyphMode`: `ascii` / `unicode` / `auto` -- Privacy controls: `maskEmail: true` or `CODEX_TUI_MASK_EMAIL=1` masks the account email across **every** human-facing surface — interactive auth menu, `codex-list` / `codex-status` / `codex-limits` / `codex-health` / `codex-dashboard` output, runtime and log messages, the standalone CLI login menu, and TUI prompt quota status. A user-defined account label is preferred over the email wherever one exists. `maskEmailInQuotaDetails: true` or `CODEX_TUI_MASK_EMAIL_DETAILS=1` additionally masks the email in the quota **details** view. The shared helpers live in `lib/account-display.ts`; new display surfaces must route through them rather than formatting the email directly. +- Privacy controls: `maskEmail: true` or `CODEX_TUI_MASK_EMAIL=1` masks the account email across **every** human-facing surface, including the interactive auth menu, `codex-list` / `codex-status` / `codex-limits` / `codex-health` / `codex-dashboard` output, runtime and log messages, the standalone CLI login menu, and TUI prompt quota status. A user-defined account label is preferred over the email wherever one exists. `maskEmailInQuotaDetails: true` or `CODEX_TUI_MASK_EMAIL_DETAILS=1` additionally masks the email in the quota **details** view. The shared helpers live in `lib/account-display.ts`; new display surfaces must route through them rather than formatting the email directly. ## Tooling Parity diff --git a/docs/faq.md b/docs/faq.md index 00ec9757..44404bc5 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,7 +2,7 @@ ## What is this project? -`oc-codex-multi-auth` is an OpenCode plugin that lets you sign in with ChatGPT Plus/Pro through OAuth and use GPT-5/Codex model presets from OpenCode, including multi-account rotation, health checks, and recovery tools. +`oc-codex-multi-auth` is an OpenCode plugin that lets you sign in with ChatGPT Plus/Pro through OAuth and use the GPT-5, GPT-6, and Codex model presets from OpenCode, including multi-account rotation, health checks, and recovery tools. ## Who is it for? @@ -69,7 +69,7 @@ Tokens, account state, plugin config, quota cache, and logs are stored locally o ## Is there an API-key login? -No. The plugin registers four OAuth methods (default browser, open URL manually, device code, manual URL/code paste). A dummy SDK key string is used internally for the OpenAI client; ChatGPT OAuth tokens do the real auth. +No. The plugin registers four OAuth methods (default browser, open URL manually, device code, and manual URL paste). A dummy SDK key string is used internally for the OpenAI client, and ChatGPT OAuth tokens do the real auth. ## What should I do if authentication fails? diff --git a/docs/getting-started.md b/docs/getting-started.md index f653c389..c9e0da5d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -35,7 +35,7 @@ The **default** install takes no flag and does not write a model catalog at all: npx -y oc-codex-multi-auth@latest ``` -It normalizes the plugin entry in `~/.config/opencode/opencode.json`, enables the TUI status plugin, and clears the cached plugin copy so OpenCode reinstalls the latest package. It preserves `provider.openai` and leaves model definitions to OpenCode. Use it when OpenCode already supplies the OAuth model entries you need; note that `--variant` presets and `gpt-5.5-fast` come only from this plugin's catalogs. +It normalizes the plugin entry in `~/.config/opencode/opencode.json`, enables the TUI status plugin, and clears the cached plugin copy so OpenCode reinstalls the latest package. It preserves `provider.openai` and leaves model definitions to OpenCode. Use it when OpenCode already supplies the OAuth model entries you need; `--variant` presets and `gpt-5.5-fast` come only from this plugin's catalogs. If you want direct explicit selector IDs such as `openai/gpt-5.5-medium` (modern bases **plus** explicit entries): @@ -96,18 +96,18 @@ Then choose: 1. `OpenAI` 2. One of the **four** plugin OAuth methods: - - `Codex OAuth (ChatGPT Plus/Pro)` — opens the default browser; completes through a localhost callback - - `Codex OAuth (Open URL Manually)` - prints the authorization URL after port 1455 is listening; open it in any browser; the callback completes automatically through localhost - - `Codex OAuth (Device Code)` — headless / SSH - - `Codex OAuth (Manual URL Paste)` - paste the full callback URL, including its `state` parameter. The state is what ties the pasted value to this login attempt, so a bare code and a mismatched state are both rejected before token exchange + - `Codex OAuth (ChatGPT Plus/Pro)` opens the default browser and completes through a localhost callback + - `Codex OAuth (Open URL Manually)` prints the authorization URL after port 1455 is listening. Open it in any browser, and the callback completes automatically through localhost + - `Codex OAuth (Device Code)` for headless or SSH sessions + - `Codex OAuth (Manual URL Paste)` pastes the full callback URL, including its `state` parameter. The state is what ties the pasted value to this login attempt, so a bare code and a mismatched state are both rejected before token exchange -There is **no** registered “Manual API Key” login path for this plugin. The provider still presents a dummy SDK key (`chatgpt-oauth`) internally; real auth is always OAuth. +There is **no** registered "Manual API Key" login path for this plugin. The provider still presents a dummy SDK key (`chatgpt-oauth`) internally, and real auth is always OAuth. If the default browser cannot be launched (no `xdg-open` on PATH, for example), the login is not cancelled: the authorization URL is printed and the listener keeps waiting, so opening that URL in any browser still completes the login. Both browser-based OAuth methods use the same local callback port as Codex CLI. The authorize redirect is `http://localhost:1455/auth/callback`, while the local callback server binds `http://127.0.0.1:1455/auth/callback` and `[::1]:1455` for dual-stack localhost redirects. Authorization and token exchange go to `auth.openai.com`. -Account records persist the granted OAuth scope. The required scopes are `openid`, `profile`, `email`, and `offline_access`; an account whose recorded scope is explicitly missing one of them is marked for re-auth instead of being silently reused. An account whose scope is simply unrecorded is left enabled — absent metadata is not treated as a failed grant — and an account previously marked for re-auth is restored automatically once a complete scope is known. +Account records persist the granted OAuth scope. The required scopes are `openid`, `profile`, `email`, and `offline_access`. An account whose recorded scope is explicitly missing one of them is marked for re-auth instead of being silently reused. An account whose scope is simply unrecorded is left enabled, because absent metadata is not treated as a failed grant, and an account previously marked for re-auth is restored automatically once a complete scope is known. A blank scope in the token response falls back to the scope the login requested, so an empty string never overwrites known-good scope metadata. ### Remote or Headless Login @@ -115,11 +115,11 @@ If you are on SSH, WSL, or another environment where the browser callback flow i - **If localhost port 1455 is reachable** (including via `ssh -L 1455:localhost:1455 user@remote`): 1. rerun `opencode auth login` - 2. choose `Codex OAuth (Open URL Manually)` - it prints the URL after the listener is ready; open it in any browser; login completes automatically through localhost + 2. choose `Codex OAuth (Open URL Manually)`, which prints the URL after the listener is ready. Open it in any browser and login completes automatically through localhost - **If localhost is not reachable** (containers, restricted networks): 1. rerun `opencode auth login` - 2. choose `Codex OAuth (Device Code)` - follow the verification link and one-time code - 3. if device code is unavailable, fall back to `Codex OAuth (Manual URL Paste)` - paste the full callback URL, including its `state` parameter + 2. choose `Codex OAuth (Device Code)` and follow the verification link and one-time code + 3. if device code is unavailable, fall back to `Codex OAuth (Manual URL Paste)` and paste the full callback URL, including its `state` parameter ## Add the Plugin to OpenCode @@ -193,7 +193,7 @@ ENABLE_PLUGIN_REQUEST_LOGGING=1 opencode run "test" --model=openai/gpt-5.5 --var The first request should create logs under `~/.opencode/logs/codex-plugin/`. -Use `opencode debug config` when you want to verify custom or template-defined models. Default install preserves the existing model catalog; `--modern` installs compact entries such as `gpt-5.5` and `gpt-5.6-sol`, while `--full` additionally exposes explicit entries such as `gpt-5.5-medium` / `gpt-5.5-fast-medium` / `gpt-5.5-high`. +Use `opencode debug config` when you want to verify custom or template-defined models. Default install preserves the existing model catalog; `--modern` installs compact entries such as `gpt-5.5` and `gpt-5.6-sol`, while `--full` also exposes explicit entries such as `gpt-5.5-medium` / `gpt-5.5-fast-medium` / `gpt-5.5-high`. ## Multi-Account Setup diff --git a/docs/index.md b/docs/index.md index 7853350f..42ab9e93 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # oc-codex-multi-auth Docs -Use your ChatGPT Plus/Pro subscription inside OpenCode with OAuth login, Codex/GPT-5 model routing (including GPT-5.6), multi-account rotation, account switching, health checks, quota status, diagnostics, and recovery tools. +Use your ChatGPT Plus/Pro subscription inside OpenCode with OAuth login, Codex/GPT-5/GPT-6 model routing (including GPT-6 Astra and GPT-5.6), multi-account rotation, account switching, health checks, quota status, diagnostics, and recovery tools. ## User Guides diff --git a/docs/privacy.md b/docs/privacy.md index b98c64cd..4fac554c 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -2,7 +2,7 @@ This page explains how `oc-codex-multi-auth` handles local data, upstream requests, and debugging artifacts. -**Last updated:** 2026-07-18 +**Last updated:** 2026-09-14 ## Overview @@ -80,7 +80,7 @@ When `CODEX_KEYCHAIN=1` is set, account pools can be stored in the OS credential | Item | Default path | |------|----------------| -| TUI quota cache | `$OPENCODE_STATE_DIR` when set, otherwise OpenCode's state directory (typically `~/.local/state/opencode/`), file `oc-codex-multi-auth-tui-quota.json`, with a `~/.opencode/` fallback in some builds | +| TUI quota cache | `~/.local/state/opencode/oc-codex-multi-auth-tui-quota.json`; the caller's state directory or `OPENCODE_STATE_DIR` overrides the directory | Caches recent quota/usage snapshots for prompt status display. @@ -202,7 +202,7 @@ Also remove any project-scoped account files and keychain entries if you migrate rm ~/.opencode/openai-codex-auth-config.json rm -rf ~/.opencode/cache/ rm -rf ~/.opencode/logs/codex-plugin/ -rm -f ~/.opencode/oc-codex-multi-auth-tui-quota.json +rm -f ~/.local/state/opencode/oc-codex-multi-auth-tui-quota.json ``` ### Revoke OAuth Access diff --git a/docs/tools-and-cli.md b/docs/tools-and-cli.md index 668a5d1c..06c59d5c 100644 --- a/docs/tools-and-cli.md +++ b/docs/tools-and-cli.md @@ -26,9 +26,9 @@ Registered from **24 per-file factories** under `lib/tools/` via `createToolRegi | `codex-switch` | Switch the active account (interactive picker when index omitted) | | `codex-warm` | Open every enabled account's usage window (one minimal request each) | | `codex-status` | Active account, model family, routing / pool mode | -| `codex-limits` | Visible rate-limit / quota state | +| `codex-limits` | Live 5-hour and weekly Codex usage per account (fetched via `fetchCodexUsage`) | | `codex-reset` | Inspect or redeem banked rate-limit reset credit | -| `codex-dashboard` | Interactive multi-account management surface | +| `codex-dashboard` | Read-only snapshot report of account eligibility, retry budgets, and refresh queue health | ### Account metadata and routing @@ -39,13 +39,13 @@ Registered from **24 per-file factories** under `lib/tools/` via `createToolRegi | `codex-note` | Attach a private note to an account | | `codex-pool` | Manage model account pools and `preferred`/`strict` routing modes | | `codex-remove` | Remove a saved account (confirm required) | -| `codex-refresh` | Refresh tokens / re-auth guidance for an account | +| `codex-refresh` | Manually refresh OAuth tokens for all accounts | ### Diagnostics and resilience | Tool | Purpose | |------|---------| -| `codex-health` | Health summary across accounts | +| `codex-health` | Live health verification across accounts by refreshing each refresh token (makes network calls) | | `codex-metrics` | Runtime counters and request metrics | | `codex-doctor` | Beginner-friendly diagnostics with fix hints | | `codex-diag` | Redacted diagnostic snapshot export | @@ -80,7 +80,7 @@ codex-note index=2 note="weekend only" codex-doctor codex-health codex-export -codex-import dryRun=true +codex-import path="~/backup.json" dryRun=true codex-keychain ``` @@ -92,12 +92,12 @@ Account indices are **1-based**. Destructive tools require an explicit confirm f | Tool | Args | |------|------| -| `codex-setup` | `wizard?` (bool) — menu-driven setup when terminal supports it | -| `codex-help` | `topic?` — `setup`, `switch`, `pools`, `health`, `backup`, `dashboard` | -| `codex-next` | `format?` — `text` \| `json` | +| `codex-setup` | `wizard?` (bool), menu-driven setup when terminal supports it | +| `codex-help` | `topic?` (`setup`, `switch`, `pools`, `health`, `backup`, `dashboard`) | +| `codex-next` | `format?` (`text` \| `json`) | | `codex-list` | `tag?`, `format?`, `includeSensitive?` | -| `codex-switch` | `index?` — omit for interactive picker when supported | -| `codex-warm` | optional `format="text"` (default) or `format="json"` | +| `codex-switch` | `index?`, omit for interactive picker when supported | +| `codex-warm` | `format?` (`text` \| `json`) | | `codex-status` | `format?`, `includeSensitive?` | | `codex-limits` | `format?`, `includeSensitive?` | | `codex-reset` | `action?` (`status` \| `consume`), `creditId?`, `confirm?` (required true to redeem), `dryRun?`, `account?` (1-based), `format?`, `includeSensitive?` | @@ -106,12 +106,12 @@ Account indices are **1-based**. Destructive tools require an explicit confirm f | `codex-tag` | `index?`, `tags` (CSV; empty clears) | | `codex-note` | `index?`, `note` (empty clears) | | `codex-pool` | `action?` (`status` \| `set` \| `add` \| `remove` \| `clear` \| `set-mode`), `model?`, `accounts?` (1-based number array), `poolMode?` (`preferred` \| `strict`), `dryRun?`, `format?`, `includeSensitive?` | -| `codex-remove` | `index?`, `confirm` (must be `true` to delete) | +| `codex-remove` | `index?`, `confirm?` (must be `true` to delete, omitted or false is a no-op that prints guidance) | | `codex-refresh` | _(none)_ | | `codex-health` | `format?`, `includeSensitive?` | | `codex-metrics` | `format?` | | `codex-doctor` | `deep?`, `fix?` (safe automated fixes), `format?` | -| `codex-diag` | _(none)_ — redacted snapshot only | +| `codex-diag` | _(none)_, redacted snapshot only | | `codex-diff` | `left`, `right` (paths), `section?` (`accounts` \| `config` \| `both`) | | `codex-export` | `path?`, `force?`, `timestamped?` (default true when path omitted) | | `codex-import` | `path`, `dryRun?` | @@ -119,10 +119,12 @@ Account indices are **1-based**. Destructive tools require an explicit confirm f ### Operational notes -- **`codex-warm` / CLI `warm`:** one lightweight request per enabled account to open usage windows. CLI exits non-zero if any account fails; disabled accounts are skipped. -- **`codex-reset`:** banked WHAM/rate-limit reset credits. `action="consume"` is irreversible and requires `confirm=true` (use `dryRun=true` to preview). -- **`codex-pool`:** accepts 1-based numbers but persists **stable account IDs** in `~/.opencode/openai-codex-auth-config.json`. Restart OpenCode after mutations. -- **Standalone default storage:** CLI commands read the **global** accounts file unless `--config-path` points at a project pool. In-session tools use the active per-project path when `perProjectAccounts` is true. +- **`codex-warm` / CLI `warm`.** One lightweight request per enabled account to open usage windows. CLI exits non-zero if any account fails; disabled accounts are skipped. +- **`codex-reset`.** Banked WHAM/rate-limit reset credits. `action="consume"` is irreversible and requires `confirm=true` (use `dryRun=true` to preview). +- **`codex-pool`.** Accepts 1-based numbers but persists **stable account IDs** in `~/.opencode/openai-codex-auth-config.json`. Restart OpenCode after mutations. +- **Tool `codex-health` vs CLI `health`.** The tool refreshes every account's token against the auth server, so it makes real network calls and reports the live result. The CLI `health` command scans the local JSON storage and counts accounts where `enabled && hasRefreshToken`, with no network calls. +- **Standalone default storage.** CLI commands read the **global** accounts file unless `--config-path` points at a project pool. In-session tools use the active per-project path when `perProjectAccounts` is true. +- **Keychain routing.** `status`, `list`, `health`, and `dashboard` parse the JSON accounts file directly. `warm` and `limits` load the plugin storage runtime, so they honor `CODEX_KEYCHAIN=1`. `doctor` reads the JSON file directly unless `--fix` is passed, and `--fix` repairs through the storage runtime (an explicit `--config-path` forces keychain off for the repair). --- @@ -138,7 +140,7 @@ Bin: `oc-codex-multi-auth` (also via `npx -y oc-codex-multi-auth@latest …`). | `doctor` | Local account/config diagnostics | | `status` | Account/config status | | `list` | List configured accounts | -| `limits` | Stored rate-limit state | +| `limits` | Live 5-hour and weekly quota usage from the usage endpoint | | `dashboard` | Prints guidance (does not start a full dashboard server) | | `health` | Local token/account health summary | | `diag` | Alias for `doctor --deep` | @@ -158,7 +160,7 @@ oc-codex-multi-auth diag oc-codex-multi-auth warm ``` -`warm` exits non-zero if any account failed. Disabled accounts are skipped. +`warm` exits non-zero if any account failed. Disabled accounts are skipped. `limits` exits 1 when it cannot load storage or any account's usage fetch fails. A successful warm request can clear unchanged cooldown state and the responding model's own rate-limit marker, not other model or family markers. If the account @@ -178,7 +180,7 @@ the warm result, and newer concurrent block writes are preserved. | `--dry-run` | Show changed config paths without values or writes | | `--no-cache-clear` | Skip clearing OpenCode plugin cache | -Choose only one of `--plugin-only`, `--modern`, `--full`, or `--legacy`. Use `update [--dry-run]` when refreshing the package; it clears the managed OpenCode cache without reading or writing `opencode.json` or `tui.json`. +Choose only one of `--plugin-only`, `--modern`, `--full`, or `--legacy`. Use `update [--dry-run]` when refreshing the package. It clears the managed OpenCode cache without reading or writing `opencode.json` or `tui.json`. ### Standalone options @@ -209,11 +211,11 @@ For `doctor --fix`, an explicit `--config-path` repairs only the selected JSON p ## Related runtime concepts -- **Rotation:** `rotationStrategy` = `hybrid` (default) | `sticky` | `round-robin` in `~/.opencode/openai-codex-auth-config.json` or `CODEX_AUTH_ROTATION_STRATEGY`. -- **Model pools:** `modelAccountPools` + `codex-pool` route effective model IDs through specific accounts. `preferred` mode falls back to the general pool; `strict` mode never leaves its configured pool. -- **Per-project accounts:** default `true` under `~/.opencode/projects//`. +- **Rotation.** `rotationStrategy` is `hybrid` (default), `sticky`, or `round-robin`, set in `~/.opencode/openai-codex-auth-config.json` or `CODEX_AUTH_ROTATION_STRATEGY`. +- **Model pools.** `modelAccountPools` + `codex-pool` route effective model IDs through specific accounts. `preferred` mode falls back to the general pool. `strict` mode never leaves its configured pool. +- **Per-project accounts.** Default `true` under `~/.opencode/projects//`. - **Stateless Codex contract:** `store: false` and `reasoning.encrypted_content`. -- **GPT-5.6:** responses-lite path; client identity defaults to host/opencode for 5.6. +- **GPT-5.6.** Responses-lite path; client identity defaults to the host identity (`opencode`) for 5.6. See also: diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 66ea1ba9..c60220ce 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -2,11 +2,11 @@ Common setup, authentication, model, and request-debugging issues for `oc-codex-multi-auth` (current package line, including the 24-tool surface and GPT-5.6 catalog). -For install modes, the full tool list (with args), and standalone CLI commands (`doctor`, `status`, `list`, `limits`, `health`, `diag`, `warm`), see [Tools and CLI](tools-and-cli.md) and [Getting Started](getting-started.md). For advanced env vars (`CODEX_THREAD_ID`, `OPENCODE_CODEX_PROMPT_URL`, etc.) see [Configuration](configuration.md#advanced--power-user-environment-variables). +For install modes, the full tool list (with args), and standalone CLI commands (`doctor`, `status`, `list`, `limits`, `dashboard`, `health`, `diag`, `warm`), see [Tools and CLI](tools-and-cli.md) and [Getting Started](getting-started.md). For advanced env vars (`CODEX_THREAD_ID`, `OPENCODE_CODEX_PROMPT_URL`, etc.) see [Configuration](configuration.md#advanced--power-user-environment-variables). --- -> **Quick Reset**: Most issues can be resolved by deleting `~/.opencode/auth/openai.json` and running `opencode auth login` again. +> **Quick Reset**: Most issues can be resolved by clearing the `openai` entry in OpenCode's host auth store (`~/.local/share/opencode/auth.json`, the same path on every platform including Windows; older host layouts used `~/.opencode/auth/openai.json`) and running `opencode auth login` again. Clearing the host auth entry only resets the host OAuth fallback. To fully clear pooled accounts, also remove `~/.opencode/oc-codex-multi-auth-accounts.json`, any project-specific account files under `~/.opencode/projects//`, and flagged account files (`*-flagged-accounts.json`). When `CODEX_KEYCHAIN=1` is active, stored accounts live in the OS keychain under service `oc-codex-multi-auth` rather than JSON, requiring `codex-keychain rollback` or OS keychain tools to clear. See [Privacy](privacy.md) for full cleanup procedures. If you prefer guided recovery before manual debugging, run: @@ -45,7 +45,7 @@ codex-doctor deep=true format="json"
✅ RESOLVED: OpenCode plugin blocking (v4.9.0+) -**Status:** Fixed in v4.9.0 by renaming the package. +**Status.** Fixed in v4.9.0 by renaming the package. **What was happening:** @@ -100,7 +100,7 @@ Update your `~/.config/opencode/opencode.json`: ```bash npm view oc-codex-multi-auth version ``` -5. **If the plugin is present but still won’t load**, rerun `npx -y oc-codex-multi-auth@latest` so the installer refreshes the config and clears OpenCode's cached plugin copy. +5. **If the plugin is present but still won't load**, rerun `npx -y oc-codex-multi-auth@latest` so the installer refreshes the config and clears OpenCode's cached plugin copy.
@@ -111,7 +111,7 @@ Update your `~/.config/opencode/opencode.json`:
Slow first response / startup latency -**What’s normal:** +**What's normal:** - The first request may fetch **Codex instructions** and/or the **OpenCode codex prompt** from GitHub (catalog + prompt caches under `~/.opencode/cache/`). - Default `requestTransformMode` is **`native`**. Startup prewarm of prompt caches only runs when legacy transform is enabled (`CODEX_AUTH_REQUEST_TRANSFORM_MODE=legacy` or config `requestTransformMode: "legacy"`) and is not disabled with `CODEX_AUTH_PREWARM=0`. @@ -149,9 +149,9 @@ Update your `~/.config/opencode/opencode.json`: - A pool mutation reports `config_locked` with `retryable: true` in JSON output. - Text output says the plugin configuration is locked by another process and no change was made. -**Cause:** Another OpenCode process is updating `~/.opencode/openai-codex-auth-config.json`. Pool dry-runs do not acquire this lock. Every non-dry mutation, including a possible no-op, waits for the bounded retry window and is revalidated under the lock so its result cannot rely on a stale preview. +**Cause.** Another OpenCode process is updating `~/.opencode/openai-codex-auth-config.json`. Pool dry-runs do not acquire this lock. Every non-dry mutation, including a possible no-op, waits for the bounded retry window and is revalidated under the lock so its result cannot rely on a stale preview. -**Solution:** No partial change was applied. Retry the same `codex-pool` action shortly. If contention persists, finish or stop other processes that are actively changing plugin configuration, then retry. +**Solution.** No partial change was applied. Retry the same `codex-pool` action shortly. If contention persists, finish or stop other processes that are actively changing plugin configuration, then retry.
@@ -222,10 +222,10 @@ Failed to access Codex API 1. **Alternate login:** - Re-run `opencode auth login` - **If localhost port 1455 is reachable** (including via `ssh -L 1455:localhost:1455 user@remote`): - choose **`Codex OAuth (Open URL Manually)`** - it prints the URL after the listener is ready; open it in any browser; login completes automatically through localhost + choose **`Codex OAuth (Open URL Manually)`**, which prints the URL after the listener is ready. Open it in any browser and login completes automatically through localhost - **If localhost is not reachable** (containers, restricted networks): - choose **`Codex OAuth (Device Code)`** - follow the verification link and one-time code; - if device code is unavailable, fall back to **`Codex OAuth (Manual URL Paste)`** - paste the full callback URL, including its `state` parameter + choose **`Codex OAuth (Device Code)`** and follow the verification link and one-time code. + If device code is unavailable, fall back to **`Codex OAuth (Manual URL Paste)`** and paste the full callback URL, including its `state` parameter 2. **Check port 1455 availability:** ```bash @@ -236,7 +236,7 @@ Failed to access Codex API netstat -ano | findstr :1455 ``` -3. **Stop Codex CLI if running** — both use port 1455 +3. **Stop Codex CLI if running.** Both use port 1455 @@ -251,19 +251,34 @@ Failed to access Codex API - Open the URL directly in browser (don't use a stale link) - For SSH/WSL/remote: if localhost port 1455 is reachable (including via SSH port forwarding), choose **Open URL Manually**; if localhost is not reachable, choose **Device Code**; use **Manual URL Paste** only as a last resort +The callback window is five minutes long and starts when the login listener starts, not when you open the URL. A login left waiting past five minutes releases port 1455, so start a fresh login and open the new URL promptly. + + + +
+Device Code Login Fails or Times Out + +**Mechanics:** +- The one-time code expires in about 15 minutes, and the plugin stops polling after 15 minutes with a timeout message. +- The plugin polls the device authorization endpoint every 5 seconds, or the interval the server returns with the session. +- A 403 or 404 while polling means authorization is not finished yet, so the plugin keeps polling until the deadline. Complete the sign-in in the browser and the next poll succeeds. + +**If the device login never starts:** +- A 404 from the start request means the auth server does not have device code login enabled. Retry with browser login or `Codex OAuth (Manual URL Paste)`. +
403 Forbidden Error -**Cause:** ChatGPT subscription issue +**Cause.** ChatGPT subscription issue **Check:** 1. Active ChatGPT Plus or Pro subscription 2. Subscription not expired 3. Billing is current -**Solution:** Visit [ChatGPT](https://chatgpt.com) and verify subscription status +**Solution.** Visit [ChatGPT](https://chatgpt.com) and verify subscription status
@@ -274,11 +289,11 @@ Failed to access Codex API - Requests fail with: `Usage not included in your plan` - Often reported on Business/Team workspaces -**Cause:** The plugin is using the wrong workspace/account id (personal vs business). +**Cause.** The plugin is using the wrong workspace/account id (personal vs business). **Solutions:** 1. Upgrade to the current release of `oc-codex-multi-auth` (workspace routing logic was hardened for Business + Personal dual accounts in the 5.x line and renamed in 6.0.0). -2. Re-run `opencode auth login` and select the correct workspace when prompted. +2. Re-run `opencode auth login` and choose the intended workspace in the browser session. There is no CLI or web prompt for workspace selection, and each login binds the account to the token's `chatgpt_account_id` claim. 3. If running non-interactively, set `CODEX_AUTH_ACCOUNT_ID` to the workspace account id and re-login. 4. Verify the workspace has Codex access in the ChatGPT UI. @@ -294,11 +309,11 @@ Failed to access Codex API consuming the quota of whichever member logged in last. - `codex-health` reports a Business member credential conflict. -**Cause:** A Business workspace id identifies the subscription, not an +**Cause.** A Business workspace id identifies the subscription, not an individual seat. Older builds could match the host OAuth fallback by that shared id and replace every matching entry with the last member's token. -**Solution:** Upgrade to a build with member-aware account identity, then remove +**Solution.** Upgrade to a build with member-aware account identity, then remove the affected entries and run `opencode auth login` once for each member. Make sure the browser is signed in as the intended member for each login. The plugin stores the token's `chatgpt_account_user_id`, so each entry keeps its own bearer @@ -313,17 +328,17 @@ credentials cannot be reconstructed and require re-login. **Symptoms:** - One ChatGPT login (one email / Apple ID) holding **two workspace - subscriptions** - for example Team and Plus. + subscriptions**, for example Team and Plus. - `codex-limits` reports the same plan and the same percentage for every entry. - `codex-switch` to the other account keeps draining the same pool. - Logging in again under the other workspace appears to overwrite every entry. -**Cause:** The OAuth flow requests `id_token_add_organizations=true`, so the +**Cause.** The OAuth flow requests `id_token_add_organizations=true`, so the id_token lists every organization the login belongs to. Releases before this fix persisted one account entry per organization, but all of those entries shared the login's single OAuth token. The Codex backend meters quota by the `chatgpt-account-id` header and ignores organization ids, so an entry whose id -was an organization id silently fell back to the token's default subscription - +was an organization id silently fell back to the token's default subscription. N entries, one pool. Each workspace subscription is a distinct ChatGPT account with its own @@ -332,10 +347,12 @@ Each workspace subscription is a distinct ChatGPT account with its own **Solutions:** 1. Upgrade to a release containing this fix. One `opencode auth login` now - persists exactly one account, bound to the token's ChatGPT account id and - labelled with the workspace you selected. -2. Log in once per workspace: run `opencode auth login`, pick the first - workspace, then run it again and pick the second. Each login appends a + persists exactly one account, bound to the token's `chatgpt_account_id` + claim. The workspace is chosen in the browser session during login, and + there is no workspace prompt in the CLI or the web flow. +2. Log in once per workspace. Run `opencode auth login` and choose the first + workspace in the browser session, then run it again and choose the second. + Each login appends a separate account carrying its own token, so `codex-limits` reports the two subscriptions independently. 3. **Existing entries are not rewritten.** Accounts persisted by an older @@ -389,6 +406,20 @@ mode, then add the second workspace. +
+Token Refresh Failures + +When a stored access token expires, the plugin refreshes it with the stored refresh token. A failed refresh reports one of four reasons. + +- `http_error`: the auth server returned an HTTP error, and the status code is carried along. A 4xx usually means the refresh token was revoked or expired, so re-run `opencode auth login`. A 5xx is transient, so retry later. +- `invalid_response`: the server answered but the body failed schema validation. Transient, so retry, and check upstream status if it repeats. +- `missing_refresh`: the account has no refresh token to exchange. Re-run `opencode auth login`. +- `network_error`: the request never completed (DNS, TLS, or connection failure). Check connectivity and any proxy. + +A successful refresh keeps the prior refresh token when the response omits one, so a refresh never drops the stored credential on its own. `network_error`, `invalid_response`, and 5xx `http_error` are treated as transient and do not count toward permanent account removal. A 4xx `http_error` or `missing_refresh` counts as genuine auth invalidation and drives the account toward flagged storage. + +
+ --- ## Model Issues @@ -396,7 +427,7 @@ mode, then add the second workspace.
Model Not Found -**Error:** `Model 'openai/gpt-5-codex-low' not found` +**Error.** `Model 'openai/gpt-5-codex-low' not found` **Cause 1: Config key mismatch** @@ -420,9 +451,9 @@ opencode run "test" --model=openai/gpt-5-codex-low # Must match config key |-------|---------| | `--model=gpt-5-codex-low` | `--model=openai/gpt-5-codex-low` | -**Note:** `opencode models openai` currently shows only OpenCode's built-in provider catalog. If you add template-defined or custom models, use `opencode debug config` to confirm they were merged into the effective config. +**Note.** `opencode models openai` currently shows only OpenCode's built-in provider catalog. If you add template-defined or custom models, use `opencode debug config` to confirm they were merged into the effective config. -**Selector note:** a compact modern (`--modern`) install exposes base OAuth families with the `--variant` presets. The default install writes no catalog, so if a selector below is missing, reinstall with `--modern`. Prefer: +**Selector note.** A compact modern (`--modern`) install exposes base OAuth families with the `--variant` presets. The default install writes no catalog, so if a selector below is missing, reinstall with `--modern`. Prefer: ```bash opencode run "test" --model=openai/gpt-5.5 --variant=medium @@ -435,7 +466,7 @@ Use explicit IDs such as `openai/gpt-5.5-medium` only after installing with `--f
Per-Model Options Not Applied -**Symptom:** All models behave the same despite different `reasoningEffort` +**Symptom.** All models behave the same despite different `reasoningEffort` **Debug:** ```bash @@ -462,7 +493,7 @@ resolvedConfig: { reasoningEffort: 'low', ... } ← Should show your options - Request fails with an entitlement-style 400/403 mentioning model support for ChatGPT Codex OAuth - Common after switching workspaces or selecting a model your workspace is not currently entitled to -**Cause:** The selected model is currently not entitled for the active ChatGPT account/workspace. +**Cause.** The selected model is currently not entitled for the active ChatGPT account/workspace. **Solutions:** 1. Re-auth/login to refresh workspace selection: @@ -480,14 +511,14 @@ resolvedConfig: { reasoningEffort: 'low', ... } ← Should show your options CODEX_AUTH_SEND_ORGANIZATION_HEADER=1 opencode # restore legacy openai-organization pinning ``` If the model still fails only through the plugin, run `codex-health` and compare the failing pooled account ids against the account the Codex CLI uses (`~/.codex/auth.json`). -4. Default public selectors that are commonly entitlement-gated can auto-fallback: `gpt-6-astra` degrades into the GPT-5.6 tiers, the GPT-5.6 preview tiers (`gpt-5.6-sol`/`gpt-5.6-terra`/`gpt-5.6-luna`) degrade down the tier chain to `gpt-5.5`, and `gpt-5.5`/canonical `gpt-5-codex` degrade through `gpt-5.6-terra`, `gpt-5.6-luna`, then `gpt-5.2`. 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. The Daybreak-gated cyber tiers (`gpt-daybreak-blue-latest`, `gpt-daybreak-red-latest`, `gpt-5.6-cyber`) have no chain by design: an unentitled account gets a hard failure rather than a silent substitution by a general model. -5. Enable fallback policy if you also want automatic downgrades for manual/legacy selectors: +4. Default public selectors that are commonly entitlement-gated can auto-fallback: `gpt-6-astra` degrades into the GPT-5.6 tiers, the GPT-5.6 preview tiers (`gpt-5.6-sol`/`gpt-5.6-terra`/`gpt-5.6-luna`) degrade down the tier chain to `gpt-5.5`, and `gpt-5.5` degrades through `gpt-5.6-terra` and `gpt-5.6-luna` to `gpt-5.2`, while canonical `gpt-5-codex` degrades through `gpt-5.6-terra` and `gpt-5.5` to `gpt-5.2`. 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. The Daybreak-gated cyber tiers (`gpt-daybreak-blue-latest`, `gpt-daybreak-red-latest`, `gpt-5.6-cyber`) have no chain by design: an unentitled account gets a hard failure rather than a silent substitution by a general model. +5. Enable fallback policy if you also want automatic downgrades for manual/legacy selectors (live targets `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5`, and `gpt-5.2` succeed retired GPT-5.4 IDs): ```bash CODEX_AUTH_UNSUPPORTED_MODEL_POLICY=fallback opencode ``` 6. Default fallback chain (auto-fallback for `gpt-6-astra`, the 5.6 tiers and `gpt-5.5`/`gpt-5-codex`; full chain when policy is `fallback` and not overridden): - - `gpt-6-astra -> gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5` (then the `gpt-5.5` chain below) - - `gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5` (then the `gpt-5.5` chain below) + - `gpt-6-astra -> gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5 -> gpt-5.2` + - `gpt-5.6-sol -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.5 -> gpt-5.2` - `gpt-5.5 -> gpt-5.6-terra -> gpt-5.6-luna -> gpt-5.2` - `gpt-5-codex -> gpt-5.6-terra -> gpt-5.5 -> gpt-5.2` - `gpt-5.4 -> gpt-5.6-terra -> gpt-5.5 -> gpt-5.2` (the successor its catalog entry names) @@ -504,12 +535,11 @@ resolvedConfig: { reasoningEffort: 'low', ... } ← Should show your options "unsupportedCodexPolicy": "fallback", "fallbackOnUnsupportedCodexModel": true, "unsupportedCodexFallbackChain": { - "gpt-5.5": ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"], - "gpt-5.4": ["gpt-5.4-mini", "gpt-5.4-nano"], - "gpt-5.4-pro": ["gpt-5.4"], - "gpt-5-codex": ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"], + "gpt-5.5": ["gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.2"], + "gpt-5.6-sol": ["gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.2"], + "gpt-5-codex": ["gpt-5.6-terra", "gpt-5.5", "gpt-5.2"], "gpt-5.3-codex": ["gpt-5-codex", "gpt-5.2-codex"], - "gpt-5.3-codex-spark": ["gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"] + "gpt-5.3-codex-spark": ["gpt-5-codex", "gpt-5.3-codex", "gpt-5.2-codex"] } } ``` @@ -553,7 +583,7 @@ AI_APICallError: Item with id 'msg_abc123' not found. Items are not persisted when `store` is set to false. ``` -**Cause:** Old plugin version (fixed in v2.1.2+) +**Cause.** Old plugin version (fixed in v2.1.2+) **Solution:** ```bash @@ -576,7 +606,7 @@ Should see: `Successfully removed all X message IDs`
Context Not Preserved -**Symptom:** Model doesn't remember previous turns +**Symptom.** Model doesn't remember previous turns **Check logs:** ```bash @@ -667,7 +697,7 @@ Your input exceeds the context window - `codex-switch`, `codex-label`, or `codex-remove` returns a missing index message - You expected an interactive picker -**Cause:** Interactive pickers require an interactive TTY session. In non-interactive sessions, you must pass `index`. +**Cause.** Interactive pickers require an interactive TTY session. In non-interactive sessions, you must pass `index`. **Solutions:** 1. Pass explicit index arguments: @@ -709,7 +739,7 @@ Your input exceeds the context window - "fail to authorize" after successful login - Safari shows "Safari can't open the page" -**Cause:** Safari's "HTTPS-Only Mode" blocks `http://localhost` callback. +**Cause.** Safari's "HTTPS-Only Mode" blocks `http://localhost` callback. **Solutions:** @@ -726,6 +756,14 @@ Your input exceeds the context window
Port Conflict (Address Already in Use) +Login fails fast when the callback port cannot be bound. The error message is: + +``` +OAuth callback server failed to start on localhost loopback port 1455. Retry with "Codex OAuth (Device Code)" or "Codex OAuth (Manual URL Paste)". +``` + +No browser is opened and no URL is printed in that case. Free port 1455, then retry `opencode auth login`. Device Code and Manual URL Paste avoid the loopback listener entirely. + **macOS / Linux:** ```bash lsof -i :1455 @@ -764,6 +802,31 @@ ssh -L 1455:localhost:1455 user@remote --- +## Rotation, Recovery, and Notifications + +
+Circuit-open account rotations + +The plugin keeps a circuit breaker per account and model family in the runtime request pipeline (`lib/circuit-breaker.ts` keyed via `index.ts:2713-2724`). Three failures inside a 60-second window open the circuit for a 30-second cooldown. While it is open, requests short-circuit to the next account instead of retrying the degraded one, and the log line reads `[circuit-breaker] Circuit open ... Rotating account.` After the cooldown the circuit admits a single probe request. A successful probe closes the circuit, and a failed probe reopens it. No action is required. If one account rotates constantly, run `codex-health` to verify its refresh token and `codex-diag` for the breaker aggregates. (Note that standalone `lib/health.ts` defines an isolated breaker instance for diagnostic summaries, whereas active request routing wires its breaker directly in `index.ts`). + +
+ +
+Recovery toasts in the OpenCode TUI + +Recoverable request errors (a missing tool result, an out-of-order thinking block, or a thinking-mode violation) surface a warning toast in the OpenCode TUI. Warning and error toasts are always shown. The informational "Using <account> (N/N)" account-selection toast is the only one a setting controls, via `accountToasts` in plugin config or `CODEX_AUTH_ACCOUNT_TOASTS=0`. Toast duration follows `toastDurationMs` in plugin config or `CODEX_AUTH_TOAST_DURATION_MS` (default 5000 ms, minimum 1000 ms). While `lib/recovery/hook.ts` contains an underlying session repair and auto-resume engine, runtime requests in `index.ts` currently surface the warning toast without executing host session mutation hooks. + +
+ +
+macOS quota notifications + +On macOS the plugin can post Notification Center alerts when account usage crosses a configured percentage threshold. Control it with the `quotaNotifications` plugin config block: `enabled` turns delivery on, `thresholds` lists percentages (0-100), `intervalMs` sets the poll interval (minimum 30000), and `autoProtectCredits` (default on) also records a durable quota block when usage is exhausted. Delivery runs through `osascript`, so Linux and Windows stay silent. Cross-process state lives in `oc-codex-multi-auth-quota-notifications.json` beside the active accounts file. + +
+ +--- + ## Debug Techniques
@@ -843,7 +906,7 @@ Include: | Issue | Solution | |-------|----------| | Auth problems | Verify subscription at [ChatGPT Settings](https://chatgpt.com/settings) | -| Free tier | Not supported — requires Plus or Pro | +| Free tier | Not supported, requires Plus or Pro | | Usage limits | Check subscription limits | | Account flagged | Contact OpenAI support | @@ -854,4 +917,4 @@ Include: --- -**Next**: [Configuration Guide](configuration.md) | [Architecture](development/ARCHITECTURE.md) | [Back to Home](index.md) +**Next.** [Configuration Guide](configuration.md) | [Architecture](development/ARCHITECTURE.md) | [Back to Home](index.md) diff --git a/index.ts b/index.ts index fc25f657..fc27c95c 100644 --- a/index.ts +++ b/index.ts @@ -1306,10 +1306,12 @@ export const OpenAIOAuthPlugin: Plugin = async ({ client }: PluginInput) => { }; const supportsInteractiveMenus = (): boolean => { + if (process.env.FORCE_INTERACTIVE_MODE === "1") return true; if (!process.stdin.isTTY || !process.stdout.isTTY) return false; if (process.env.OPENCODE_TUI === "1") return false; if (process.env.OPENCODE_DESKTOP === "1") return false; if (process.env.TERM_PROGRAM === "opencode") return false; + if (process.env.ELECTRON_RUN_AS_NODE === "1") return false; return true; }; diff --git a/lib/AGENTS.md b/lib/AGENTS.md index 03e047e3..1729e4b6 100644 --- a/lib/AGENTS.md +++ b/lib/AGENTS.md @@ -28,7 +28,7 @@ lib/ ├── logger.ts # debug/request logging ├── oauth-constants.ts # OAuth port/path constants ├── oauth-success.ts # OAuth success HTML source copied during build -├── parallel-probe.ts # parallel health checks +├── parallel-probe.ts # parallel account probes, first success wins ├── proactive-refresh.ts # token refresh before expiry ├── prompts/ # Codex/OpenCode prompts and ETag caches ├── quota-notification-state.ts # cross-process threshold/delivery state file @@ -41,7 +41,7 @@ lib/ ├── runtime.ts # pure runtime helpers and metrics/explainability types ├── schemas.ts # Zod schemas ├── shutdown.ts # graceful shutdown -├── storage.ts # V3 JSON storage facade +├── storage.ts # barrel re-exporting lib/storage/ (V3 JSON account storage) ├── storage/ # atomic writes, paths, migrations, keychain, backup/import/export ├── table-formatter.ts # CLI table formatting ├── tools/ # 24 codex-* tool factories + registry @@ -73,10 +73,10 @@ lib/ | Account selection | `accounts/rotation.ts`, `rotation.ts` | hybrid health + token bucket | | Account rate limits | `accounts/rate-limits.ts` | per-account tracking | | Account persistence | `accounts/persistence.ts`, `accounts/state.ts` | account manager state and save/load coordination | -| Storage format | `storage.ts`, `storage/load-save.ts` | V3 with migration from V1/V2 | +| Storage format | `storage.ts`, `storage/load-save.ts` | V3, with V1 migrated to V3 on load | | Storage paths | `storage/paths.ts` | project root detection | | Storage keychain | `storage/keychain.ts` | optional native keychain backend | -| Storage migrations | `storage/migrations.ts` | V1/V2 → V3 upgrade | +| Storage migrations | `storage/migrations.ts` | V1 → V3 upgrade; V2 files throw a StorageError with code UNKNOWN_V2_FORMAT | | Backups/import/export | `storage/backup.ts`, `storage/export-import.ts` | timestamped backups and dry-run import preview | | Tool registry | `tools/index.ts` | `ToolContext`, `createToolRegistry` | | TUI quota status | `tui-status.ts`, `tui-quota-cache.ts`, `codex-usage.ts` | prompt quota display and usage cache | @@ -84,7 +84,7 @@ lib/ | Health monitoring | `health.ts` | account health status | | Account display / masking | `account-display.ts` | label-preferred rendering, `maskEmail` behavior | | Reset credits | `codex-reset.ts`, `codex-usage.ts` | banked rate-limit reset credit list/redeem; `codex-usage.ts` reads the same counts off the usage endpoint for `codex-limits`, sharing `normalizeResetCreditCount` so both agree | -| Parallel probes | `parallel-probe.ts` | concurrent health checks | +| Parallel probes | `parallel-probe.ts` | races probe requests across candidate accounts, first success wins, losing probes aborted | | Runtime helpers | `runtime.ts` | routing visibility, metrics, pure helper types | | Graceful shutdown | `shutdown.ts` | cleanup on exit | | Table formatting | `table-formatter.ts` | CLI output tables | diff --git a/lib/config.ts b/lib/config.ts index f06868c6..11cc1bb7 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -588,16 +588,20 @@ function resolveNumberSetting( envName: string, configValue: number | undefined, defaultValue: number, - options?: { min?: number }, + options?: { min?: number; max?: number }, ): number { const envValue = parseNumberEnv(process.env[envName]); const candidate = envValue ?? configValue ?? defaultValue; const min = options?.min; + const max = options?.max; + let result = candidate; if (min !== undefined) { - return Math.max(min, candidate); + result = Math.max(min, result); } - // istanbul ignore next -- dead code: all callers pass { min: ... } - return candidate; + if (max !== undefined) { + result = Math.min(max, result); + } + return result; } function resolveStringSetting( @@ -1022,7 +1026,7 @@ export function getParallelProbingMaxConcurrency(pluginConfig: PluginConfig): nu "CODEX_AUTH_PARALLEL_PROBING_MAX_CONCURRENCY", pluginConfig.parallelProbingMaxConcurrency, 2, - { min: 1 }, + { min: 1, max: 5 }, ); } diff --git a/lib/request/response-handler.ts b/lib/request/response-handler.ts index 65bd4fcf..7cf8414c 100644 --- a/lib/request/response-handler.ts +++ b/lib/request/response-handler.ts @@ -182,6 +182,7 @@ export async function convertSseToJson( }); } } + fullText += decoder.decode(); if (LOGGING_ENABLED) { logRequest("stream-full", { fullContent: fullText }); diff --git a/lib/rotation.ts b/lib/rotation.ts index 10c66ce9..1a9e6c53 100644 --- a/lib/rotation.ts +++ b/lib/rotation.ts @@ -359,7 +359,7 @@ export interface HybridSelectionConfig { healthWeight: number; /** Weight for token count (default: 5) */ tokenWeight: number; - /** Weight for freshness/last used (default: 0.1) */ + /** Weight for freshness/last used (default: 2.0) */ freshnessWeight: number; } diff --git a/lib/tools/AGENTS.md b/lib/tools/AGENTS.md index c308ebd8..d4d792a1 100644 --- a/lib/tools/AGENTS.md +++ b/lib/tools/AGENTS.md @@ -4,9 +4,9 @@ Per-tool modules for the 24 `codex-*` tools registered by the plugin. ## Status -All current tools live here. `index.ts` builds a `ToolContext` and passes it -to `createToolRegistry(ctx)` from `./index.ts`, which wires every `codex-*` -tool into the OpenCode plugin surface. +All current tools live here. The plugin entry `index.ts` builds a `ToolContext` +and passes it to `createToolRegistry(ctx)` from `./index.ts`, which wires every +`codex-*` tool into the OpenCode plugin surface. ## Layout @@ -14,6 +14,8 @@ tool into the OpenCode plugin surface. lib/tools/ AGENTS.md index.ts # ToolContext type + createToolRegistry(ctx) barrel + doctor-repair.ts # shared doctor repair pass (refresh + stale-state clear); used by codex-doctor and CLI --fix + refresh-account.ts # shared single-use refresh-token persistence; used by account-management tools codex-list.ts # one file per tool codex-switch.ts codex-warm.ts diff --git a/test/AGENTS.md b/test/AGENTS.md index 115c44cf..4307fd73 100644 --- a/test/AGENTS.md +++ b/test/AGENTS.md @@ -7,7 +7,7 @@ model catalog, rotation logic, storage, tools/CLI, TUI, and recovery. The suite is large and evolves frequently. Use the current test tree and local commands as the source of truth instead of hard-coded totals or committed file -lists. As of this writing the suite is 113 test files: 95 top-level plus +lists. As of this writing the suite is 136 test files: 118 top-level plus `chaos/` (9), `property/` (6), and `contracts/` (3). ```bash @@ -75,7 +75,7 @@ coverage. - Do not rely on `dist/` in tests; import from source. - Do not skip tests without justification. - Do not assert on wall-clock timing; use fake timers or injected clocks. -- Do not commit an exhaustive test-file list into docs — it goes stale. Point at +- Do not commit an exhaustive test-file list into docs. It goes stale. Point at `find test -name '*.test.ts'` instead. - When changing a documented contract (tool count, config key, storage path, catalog size), update `doc-parity.test.ts` and the affected docs in the same diff --git a/test/README.md b/test/README.md index 7e1cef44..4663c8b5 100644 --- a/test/README.md +++ b/test/README.md @@ -6,7 +6,7 @@ The tree evolves frequently. Use `rg --files test` (or `find test -name '*.test. as the source of truth rather than any list committed here; this file describes the **shape** of the suite, not an exhaustive inventory. -Current size: 113 test files — 95 at the top level plus `chaos/` (9), +Current size: 136 test files, 118 at the top level plus `chaos/` (9), `property/` (6), and `contracts/` (3). ## Layout diff --git a/test/doc-parity.test.ts b/test/doc-parity.test.ts index e7b161fc..ca504ecf 100644 --- a/test/doc-parity.test.ts +++ b/test/doc-parity.test.ts @@ -194,6 +194,8 @@ describe("runtime documentation parity", () => { [ "1455", "`reasoning.encrypted_content`", + "auth.json", + "`~/.local/share/opencode/auth.json`", ], ], [ diff --git a/test/plugin-config.test.ts b/test/plugin-config.test.ts index 2e3ededc..a74b02e5 100644 --- a/test/plugin-config.test.ts +++ b/test/plugin-config.test.ts @@ -26,6 +26,7 @@ import { getAutoUpdate, getAccountToastsEnabled, getQuotaNotifications, + getParallelProbingMaxConcurrency, } from '../lib/config.js'; import type { PluginConfig } from '../lib/types.js'; import * as fs from 'node:fs'; @@ -931,4 +932,25 @@ describe('Plugin Configuration', () => { expect(getQuotaNotifications({}).intervalMs).toBe(600_000); }); }); + + describe('getParallelProbingMaxConcurrency', () => { + it('returns default 2 when unconfigured', () => { + expect(getParallelProbingMaxConcurrency({})).toBe(2); + }); + + it('honors file config within bounds', () => { + expect(getParallelProbingMaxConcurrency({ parallelProbingMaxConcurrency: 4 })).toBe(4); + }); + + it('clamps environment overrides to min 1 and max 5', () => { + process.env.CODEX_AUTH_PARALLEL_PROBING_MAX_CONCURRENCY = '9'; + expect(getParallelProbingMaxConcurrency({})).toBe(5); + + process.env.CODEX_AUTH_PARALLEL_PROBING_MAX_CONCURRENCY = '0'; + expect(getParallelProbingMaxConcurrency({})).toBe(1); + + process.env.CODEX_AUTH_PARALLEL_PROBING_MAX_CONCURRENCY = '3'; + expect(getParallelProbingMaxConcurrency({})).toBe(3); + }); + }); }); diff --git a/test/retry-budget.test.ts b/test/retry-budget.test.ts index 79573e4f..337fc1d7 100644 --- a/test/retry-budget.test.ts +++ b/test/retry-budget.test.ts @@ -43,6 +43,35 @@ describe("retry-budget", () => { expect(tracker.getUsage().network).toBe(1); }); + it("verifies budget consumption semantics across error stages", () => { + const limits: RetryBudgetLimits = { + authRefresh: 2, + network: 2, + server: 2, + rateLimitShort: 2, + rateLimitGlobal: 1, + emptyResponse: 1, + }; + const tracker = new RetryBudgetTracker(limits); + + // An initial long-delay rate limit rotates without consuming rateLimitGlobal + expect(tracker.getUsage().rateLimitGlobal).toBe(0); + expect(tracker.getRemaining("rateLimitGlobal")).toBe(1); + + // An invalidated 401 response triggers account rotation/cooldown without consuming authRefresh directly + expect(tracker.getUsage().authRefresh).toBe(0); + expect(tracker.getRemaining("authRefresh")).toBe(2); + + // Global all-accounts blocked wait consumes rateLimitGlobal + expect(tracker.consume("rateLimitGlobal")).toBe(true); + expect(tracker.getRemaining("rateLimitGlobal")).toBe(0); + expect(tracker.consume("rateLimitGlobal")).toBe(false); + + // Token refresh attempts consume authRefresh + expect(tracker.consume("authRefresh")).toBe(true); + expect(tracker.getUsage().authRefresh).toBe(1); + }); + it("clones constructor limits to avoid external mutation", () => { const limits: RetryBudgetLimits = { authRefresh: 1,