oc-codex-multi-auth is an OpenCode plugin: ChatGPT Plus/Pro OAuth, multi-account
rotation with health scoring and cooldowns, quota-aware Codex/GPT routing
(GPT-5.6/GPT-6/Daybreak responses-lite included), diagnostics and recovery tools.
The npm bin is an installer for OpenCode config plus a thin standalone CLI
(doctor, status, list, limits, dashboard, health, diag, warm,
update) — not a daemon.
- OpenCode V1 loads
index.ts(provider plugin) andtui.ts(prompt quota status) from the builtdist/exports. - OpenCode V2 loads the same package through the
setuphook on each default export instead — see OPENCODE V2 below. - Runtime state stays local under
~/.opencode; per-project account pools are on by default. Project-root detection walks up for markers (.git,package.json,.opencode, …) and stops at the home directory —$HOMEitself is never a project; outside a project the global pool is used.
./
├── index.ts # V1 plugin: auth loader, fetch pipeline, rotation, ToolContext; default export { id, server, setup }
├── tui.ts # V1 TUI plugin: prompt quota status + details; default export also carries V2 `setup`
├── lib/ # core runtime — module map and subsystem tables in lib/AGENTS.md
├── lib/tools/ # 24 `codex-*` tool factories + registry — see lib/tools/AGENTS.md
├── test/ # vitest suites — see test/AGENTS.md
├── scripts/ # npm bin (installer + standalone CLI), build and audit helpers
├── config/ # shipped opencode.json templates (minimal/modern/legacy)
├── docs/ # user docs; docs/development/ holds maintainer guides
├── skills/ # repo-local setup skill
├── assets/ # static assets
├── .codex-plugin/ # plugin metadata (version must match package.json)
└── dist/ # build output — generated, never edit
| Task | Location |
|---|---|
| V1 plugin orchestration | index.ts — OAuth loader, request pipeline, metrics, recovery, ToolContext assembly |
| TUI quota status | tui.ts, lib/tui-status.ts, lib/tui-quota-cache.ts, lib/codex-usage.ts |
| OpenCode V2 adapter | lib/opencode-v2.ts + lib/opencode-v2-provider.ts, lib/opencode-v2-rpc.ts, lib/opencode-v2-status.ts, lib/opencode-v2-tui.ts |
codex-* tools |
lib/tools/index.ts registry + one factory file per tool |
| OAuth (PKCE, callback server, device/manual login) | lib/auth/ |
| Account selection, health, cooldowns | lib/accounts.ts, lib/accounts/, lib/rotation.ts, lib/health.ts, lib/parallel-probe.ts |
| Account storage (V3 JSON, keychain, locks, refresh coordination) | lib/storage.ts, lib/storage/ |
| Request transform, SSE, retries | lib/request/ and lib/request/helpers/ |
| Model families + prompt templates | lib/prompts/ |
| Plugin config + env overrides | lib/config.ts, lib/schemas.ts |
| Session recovery | lib/recovery.ts, lib/recovery/ |
| Installer + standalone CLI | scripts/install-oc-codex-multi-auth.js, scripts/install-oc-codex-multi-auth-core.js |
| Maintainer architecture | docs/development/ARCHITECTURE.md; user-facing: docs/architecture.md |
V2 loads the same package via setup, not the V1 server hook. index.ts's
default export is { id, server, setup }; setup dynamically imports
lib/opencode-v2.ts and passes createPluginRuntime (the V1 factory), so
rotation, the auth loader, the tool registry, and the disposal hook are shared —
only the host contract differs. Per-location createStorageScope
(lib/storage/state.ts) keeps each V2 location's account-storage state separate.
- The adapter reroutes the
openaiprovider/models through a distinctaisdk:package identity (lib/opencode-v2-provider.ts), then installsaisdkhooks that re-add the stateless contract (store: false,reasoning.encrypted_content,opencode/<version>UA) and strip server-side references (previousResponseId,conversation) that the Codex backend rejects. - Quota/accounts status reaches TUIs over
CodexStatusRpc.status— a remote TUI never touches credential files. - The adapter stays inert until a pooled account or
openaiOAuth connection exists. Install mode--v2writes apluginsentry only; it refuses to migrate V1pluginentries.
- ESM only (
"type": "module"), Node>=22.19.0. Internal imports use.jsspecifiers sodist/resolves under Node ESM. - ESLint flat config:
no-explicit-anyis an error; unused args take a_prefix; floating/misused promises are errors. Test files are relaxed (seeeslint.config.js). - No lib-wide barrel: import the focused module directly.
lib/accounts.ts,lib/storage.ts, andlib/recovery.tsare domain facades that keep their subdirectory's pre-split import surface stable. - Stateless Codex contract on every request:
store: falseplus thereasoning.encrypted_contentinclude. - Responses-lite models (GPT-6 Astra/Sol/Luna, Daybreak, GPT-5.6 Sol/Terra/Luna) get lite body shaping and default client identity
opencode; other models default tocodex_cli_rs. - Boolean env overrides are truthy only for the literal
"1"— never"true"/"yes". - Credential writes are atomic: 0600 temp file (POSIX mode bits only — Windows uses profile ACLs), fsync, rename, parent-dir fsync. A refresh committed upstream but not yet saved to the pool is journaled in a
<accounts-file>.refresh.pending.<hash>file (one per consumed token; legacy unsuffixed.refresh.pendingstill replayed) beside the accounts file and applied on the next load. - OS keychain backend is opt-in (
CODEX_KEYCHAIN=1) and holds the same V3 JSON blob; any keychain failure falls back to the JSON path — never delete the JSON copy on a keychain error. Onwin32, Credential Manager's blob-size cap means oversized pools are size-checked and stay on the JSON path.
- Do not edit
dist/ortmp*directories. - No
as any,@ts-ignore, or@ts-expect-error. - No public security issues — see
SECURITY.md. - No hardcoded ports. The only permitted one is the registered OAuth callback port
1455, vialib/oauth-constants.ts. - Do not remove
store: falseorreasoning.encrypted_contentfrom the request path or shipped config templates. - Do not expose account emails, access/refresh tokens, or raw prompt/response bodies in diagnostics, tool output, or logs.
- Do not run the installer to repair a developer machine's config — it writes that machine's real
opencode.json/tui.json.updaterefreshes the package cache without touching config. - Do not identify a plugin entry by the spelling of its last path segment — resolve what it points at. A path outside package-manager output (
node_modules, the versioned OpenCode package cache) belongs to whoever wrote it and is never rewritten or removed. oc-chatgpt-multi-authis the retired name — reference it only in migration/cleanup logic.
npm run build # clean dist + tsc + emit oauth-success.html
npm run typecheck # tsc --noEmit
npm run lint # eslint .ts + scripts/*.js
npm test # vitest run
npm run test:coverage # vitest run --coverage (per-file floors)
npm run audit:ci # prod audit + dev allowlist
npx vitest run test/<name>.test.ts # one suiteThe installer writes the real ~/.config/opencode/opencode.json and tui.json of
whoever runs it:
npx -y oc-codex-multi-auth@latest # register plugin entries only
npx -y oc-codex-multi-auth@latest --full # also install the explicit model catalog
npx -y oc-codex-multi-auth@latest update # refresh package cache; never reads/writes configOther modes: --modern (compact config), --legacy (explicit-only), --v2,
--plugin-only, --dry-run, --no-cache-clear. An existing plugin entry is
kept as-is, including one pointing at a working checkout.
- OAuth redirect
http://localhost:1455/auth/callback; the callback server binds both127.0.0.1and[::1]on port 1455 (IPv6 bind failure is tolerated when IPv4 works). - State files: plugin config
~/.opencode/openai-codex-auth-config.json; per-project accounts~/.opencode/projects/<project-key>/oc-codex-multi-auth-accounts.json; global accounts~/.opencode/oc-codex-multi-auth-accounts.json; flagged accounts, credential snapshots (backups/codex-credential-snapshot-*), pending-rotation journals (<accounts-file>.refresh.pending.<hash>), and quota-notification state sit beside the active accounts file; request logs~/.opencode/logs/codex-plugin/when enabled. - Model catalog: 10 modern bases / 53 variants; legacy template ships 53 explicit entries. Routed but unshipped (add by hand):
gpt-daybreak-blue-latest,gpt-daybreak-red-latest,gpt-5.6-cyber. Retired but still resolved/falling back:gpt-5.4-mini,gpt-5-codex,gpt-5.1-codex,gpt-5.1-codex-max,gpt-5.1-codex-mini. - Prompt templates sync from Codex CLI GitHub releases with ETag caching; catalog instructions come from
model_messages.instructions_templatewhenbase_instructionsis empty;BUNDLED_CODEX_INSTRUCTIONS(lib/prompts/codex-instructions.ts) is the offline last resort. - 5xx server errors rotate accounts with the same health penalty as network errors; RFC 8594 deprecation headers are logged as warnings.