Skip to content

feat: ChatGPT multi-account parity (fallback, quota, cachekeep, modals, CLI)#7

Merged
ualtinok merged 1 commit into
cortexkit:mainfrom
iceteaSA:feat/parity
Jun 20, 2026
Merged

feat: ChatGPT multi-account parity (fallback, quota, cachekeep, modals, CLI)#7
ualtinok merged 1 commit into
cortexkit:mainfrom
iceteaSA:feat/parity

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Brings the OpenAI Codex OAuth plugin to feature parity with the Anthropic sibling, on a shared design + log structure agreed across both plugins.

Account management

  • Multiple ChatGPT accounts: main (opencode auth store) + fallbacks (plugin store), reactive fallback on 401/403/429 with a body-replay guard, identity dedup by stable ChatGPT account id.
  • Routing (main-first / fallback-first) with non-destructive activeId switching; per-account quota killswitch with 5h/weekly thresholds.
  • /openai-account add via OAuth (browser + headless device-code); openai-auth CLI for headless fallback-account management.

Quota

  • Per-turn passive push on both transports (x-codex-* HTTP headers + WS codex.rate_limits frame), normalized 5h + weekly windows.
  • Sidebar readout (used %, reset, pacing); /openai-quota polls wham/usage for main + every fallback.

Cache keep-warm (/openai-cachekeep)

  • Persistent idle prompt-cache warmer; replays the latest real request as a store:false shadow request just before cache expiry. Optional subagent mode (30-min idle cap). Proven live: ~99% cache hit on idle warms, negligible quota impact.

Token refresh

  • Cross-process file-lock + lease on the main token refresh so concurrent opencode instances don't double-refresh; dual-write to the opencode auth slot.

Cost

  • OAuth model cost-zeroing, opt-out via costZeroing.enabled: false (default on).

Observability

  • Leveled, secret-redacting (compound-key aware), rotating log file. Canonical channel taxonomy; setting changes at info, routine at debug, per-request at trace, best-effort failures at warn (a clean session is silent at the default level).

TUI / RPC

  • Interactive DialogSelect control surfaces for all /openai-* commands (account L1→L2 submenus, OSC-52 auth-URL copy). Multi-session RPC fix: disambiguate concurrent opencode instances by process.pid so command modals reach their own server.

Config coexistence

  • One openai-auth.json holds transport settings + the account store, content-discriminated so neither clobbers the other.

Verification

  • 330 tests pass, tsc clean, biome 0 warnings; both bundles build. Full-suite run repeated 20× with 0 flakes.
  • Dogfooded live: OAuth round-trip, multi-account add + non-destructive switch + route-through-fallback, correct per-account quota attribution, cachekeep (incl. subagent mode), persisted toggles, and the multi-session modal RPC fix.
  • Design + diff reviewed by independent cross-family model councils (0 must-fix at the final gate).

Notes for reviewers

  • The 7 /openai-* command nouns, modal UX, log structure, and config keys are intentionally aligned with the Anthropic sibling plugin so both behave identically at a given log level.
  • Provider-specific (not shared): the web_search prompt-cache stabilizer, WS / raw-WS transports, and Codex request rewriting.
  • A paid OpenAI-API fallback tier was intentionally out of scope (billing implications); fallback is ChatGPT-OAuth accounts only.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds multi‑account ChatGPT OAuth with automatic fallback, live quota, cache keep‑warm, a TUI sidebar with modals, and an openai-auth CLI. Preserves the configurable Codex endpoint and retryable WebSocket behavior while reaching parity with the Anthropic plugin.

  • New Features

    • Multi‑account OAuth with reactive fallback (401/403/429), routing modes, safe activeId switching, per‑account killswitch, and ChatGPT identity dedup.
    • Quota push on HTTP (x-codex-*) and WS (codex.rate_limits), normalized 5‑hour + weekly windows; sidebar readout with pacing and persisted 429 backoff; /openai-quota refresh for main and fallbacks.
    • Cache keep‑warm: replays the last real request as a store:false shadow request before expiry; optional subagent mode; dedup with error backoff.
    • OAuth + refresh: browser and headless device‑code login; cross‑process refresh file‑lock with a renewable lease; dual‑write to the main auth slot; leveled, secret‑redacting, rotating logs.
    • TUI + RPC + CLI: 7 interactive /openai-* modals over a project‑scoped, per‑process loopback RPC (port‑file + bearer token); preferences control account order; openai-auth CLI (login, list, remove).
    • Codex request rewriting, OAuth‑eligible model cost zeroing, and docs (ARCHITECTURE.md, STRUCTURE.md).
  • Migration

    • Add fallbacks via /openai-account add or openai-auth login; manage with /openai-account or openai-auth list/remove.
    • Toggle keep‑warm with /openai-cachekeep (optional subagent).
    • No breaking changes; openai-auth.json migrates in place. Optional envs: OPENCODE_OPENAI_AUTH_LOG_LEVEL, OPENCODE_OPENAI_AUTH_LOG_FILE, OPENCODE_OPENAI_AUTH_STATE_FILE, OPENCODE_TUI_PREFERENCES_FILE, OPENCODE_OPENAI_AUTH_RPC_DIR.

Written for commit 2e0e7b2. Summary will update on new commits.

Review in cubic

@socket-security

socket-security Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​opentui/​core@​0.4.1871009298100
Addedjsonc-parser@​3.3.110010010092100
Addedsolid-js@​1.9.1310010010094100
Added@​opentui/​solid@​0.4.19810010098100

View full report

@socket-security

socket-security Bot commented Jun 18, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @opentui/core is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: packages/opencode/package.jsonnpm/@opentui/core@0.4.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@opentui/core@0.4.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm entities is 91.0% likely obfuscated

Confidence: 0.91

Location: Package overview

From: ?npm/@opentui/solid@0.4.1npm/entities@6.0.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/entities@6.0.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm seroval is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/solid-js@1.9.13npm/seroval@1.5.4

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/seroval@1.5.4. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 issues found and verified against the latest diff

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/core/refresh-file-lock.ts Outdated
Comment thread packages/opencode/src/core/oauth.ts Outdated
Comment thread packages/opencode/src/core/quota-manager.ts Outdated
Comment thread packages/opencode/src/index.ts Outdated
Comment thread packages/opencode/src/core/accounts.ts
Comment thread packages/opencode/src/rpc/rpc-server.ts
Comment thread packages/opencode/src/commands.ts Outdated
Comment thread packages/opencode/src/rpc/rpc-dir.ts Outdated
Comment thread packages/opencode/src/quota-normalize.ts
Comment thread packages/opencode/src/ws.ts
@iceteaSA

Copy link
Copy Markdown
Contributor Author

All 13 cubic findings addressed (commit 59dd080)

Thanks @cubic-dev-ai — all 13 were code-verified valid and fixed. Each fix was independently re-reviewed by a cross-family model council (the 4 concurrency-critical ones went through 3 review rounds). Summary:

P1

  1. Lock-steal race (refresh-file-lock.ts) — replaced the non-atomic rm+wx reclaim with a wx-guarded eviction marker: only one contender can exclusively claim ${lockPath}.evicting, it re-checks the lock is still stale before removing it (never deletes a fresh lock), and the final wx (O_EXCL) create elects a single owner. Stress-tested 8-concurrent × 20 rounds → exactly 1 winner each. Main-refresh additionally re-checks the lease token as defense-in-depth.
  2. OAuth binds all interfaces (oauth.ts) — now listen(OAUTH_PORT, '127.0.0.1'). The /cancel endpoint is no longer network-reachable.
  3. refreshMain not token-keyed (quota-manager.ts) — added inflightMainFp; a different-token call starts a fresh fetch, and the finally only clears the slot if it still owns it (symmetric with the fallback path).
  4. WS quota attribution race (index.ts/ws-pool.ts) — replaced the shared-mutable lastMainAccess/lastPrimaryAccountId with per-connection capture: each request captures its own token+account-id into a per-request onQuota closure. The pool key now includes an account discriminator so a socket is never reused across accounts.

P2

  1. saveAccountState unlocked RMW — now serialized under the state-path lock; saveAccounts also holds the state-lock across its state read→write (lock order config→state, no cycle).
  2. readJsonIfPresent swallows non-ENOENT — now only ENOENT returns not-present; parse/permission errors rethrow, with an actionable corrupt-config message at boot (no silent clobber).
  3. atomic-write temp orphanrename wrapped; temp unlinked on failure then rethrows.
  4. Date.now() vs now() — fixed in the lock-staleness fallback.
  5. 500 to destroyed socket (rpc-server.ts) — response write guarded on headersSent/writableEnded/destroyed.
  6. refreshSidebar unhandled rejection.catch(() => {}) on all fire-and-forget call sites.
  7. rpc-dir env override not absolute — now resolve()d.
  8. NaN used_percent bypasses quota gateNumber.isFinite guard in the WS/wham/header normalizers and the additional_rate_limits loops.
  9. codex.rate_limits skips idle reset (ws.ts) — resetIdleTimeout now called in that branch.

Gates: 364 tests pass, tsc clean, biome 0, both bundles build. New regression tests cover #1 (single-winner election), #3 (token dedup), #5 (state-lock serialization), #6 (rethrow), #10, #12.

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 18, 2026

Copy link
Copy Markdown

All 13 cubic findings addressed (commit 59dd080)

Thanks @cubic-dev-ai — all 13 were code-verified valid and fixed. Each fix was independently re-reviewed by a cross-family model council (the 4 concurrency-critical ones went through 3 review rounds). Summary:

P1

...

@iceteaSA Thanks for the thorough follow-up — I've gone through the code to verify each fix, and they all check out.

The ones I can confirm from the diffs here:

The rest (lock staleness fallback, destroyed-socket guard, absolute-path resolve, NaN guards, idle reset) match the descriptions but live in files I don't have the full diff for — the patterns are consistent with what I see elsewhere in the codebase.

364 tests, clean tsc, 0 biome warnings, no flakes across 20 runs — that's a solid gate. The 8-concurrent × 20-round stress test on the eviction marker is especially convincing.

The codebase is in good shape. No remaining concerns from my side.

Tip: get faster answers by chatting with cubic’s review copilot in the review UI

@iceteaSA iceteaSA force-pushed the feat/parity branch 7 times, most recently from 081aaf4 to 9611249 Compare June 19, 2026 06:57

@ualtinok ualtinok left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the very thorough PR — design, tests, and the cubic follow-up all look strong. I reviewed it locally against current main (now at v0.1.2). Requesting one required change plus a couple of items to confirm before merge.

Blocker (must fix)

Full local test suite fails on this branch:

src/tests/oauth.test.ts
(fail) OAuth server concurrency (C1/C2) > startOAuthServer clears failed in-flight starts so a later retry can succeed

This first failure cascades into the other OAuth concurrency tests (unknown-state 400, missing-state 400, idle-server close, timeout cleanup). Each of those passes in isolation — they only fail because the blocker test above leaves a listener bound and never reaches its blocker.close().

Root cause looks like a test bug, not an implementation bug. The implementation binds explicitly to loopback:

// core/oauth.ts
server.listen(OAUTH_PORT, '127.0.0.1', ...)

but the test's blocker binds to the unspecified address:

// tests/oauth.test.ts ~419
await new Promise<void>((resolve) => blocker.listen(OAUTH_PORT, resolve))
await expect(startOAuthServer()).rejects.toThrow()

On macOS (and any host where listen(port) does not pre-empt a separate explicit 127.0.0.1 bind), startOAuthServer() then succeeds, so the rejects.toThrow() assertion fails and blocker is never closed. I verified directly: a plain listen(1455) does not block a subsequent listen(1455, '127.0.0.1') on this platform.

Suggested fix — bind the blocker to the same address the server uses, and always close it:

await new Promise<void>((resolve) =>
  blocker.listen(OAUTH_PORT, '127.0.0.1', resolve),
)
try {
  await expect(startOAuthServer()).rejects.toThrow()
} finally {
  await new Promise<void>((resolve) => blocker.close(() => resolve()))
}

Please confirm bun run test passes the full suite locally (and in CI) after the fix.

Please confirm before merge

  1. CI visibility — the PR currently only shows Socket checks; I don't see the repo CI workflow (typecheck/build/test/format/lint) reporting on this PR. Please make sure the full CI run is green and visible here. (typecheck, build, format:check, lint all pass locally for me; only the test suite is red because of the above.)

  2. New TUI dependency alerts — Socket flagged High "obfuscated code" warnings on the new deps (@opentui/core@0.4.1, transitive entities, seroval). These are new runtime deps for the sidebar. Please confirm these are acceptable/triaged, since they widen the plugin's supply-chain surface.

  3. ./tui export ships sourcepackage.json exposes "./tui" as ./src/tui.tsx (not a built artifact). Confirm that's intentional and that OpenCode's TUI plugin loader consumes the .tsx source directly at runtime.

Looked good (no action)

  • Merges cleanly onto current main; the v0.1.2 retryable-websocket / header-timeout handling and codexApiEndpoint config are preserved in the merge.
  • Account/state writes go through writeJsonAtomic(..., mode: 0o600), and refresh tokens are SHA-256 hashed before being persisted in operation-error records — no plaintext-token-at-rest issue found.
  • typecheck / format:check / lint / build all pass; package builds and packs with the new cli.js, tui-preferences.js, and rpc/* entry points.

Once the OAuth test is green in CI and the three confirmations above are settled, I'm happy to merge.

…s, CLI)

Adds multi-account OAuth with reactive fallback, push-based Codex quota
tracking (x-codex-* headers + codex.rate_limits WS frame), idle prompt-cache
keep-warm (main + optional subagents), a TUI sidebar, 7 /openai-* command
modals, an openai-auth CLI, leveled redacting logging, and a loopback RPC
between loader and TUI. Preserves upstream's configurable codexApiEndpoint and
retryable-websocket handling.
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Thanks for the careful local review — the blocker diagnosis was exactly right, and I've rebased the branch onto current main (v0.1.2, 4f7beb7) so this is now reviewed against the same tree you tested. Single squashed commit, mergeable_state: clean.

Blocker — fixed

Your root-cause was precise: the test bug, not the implementation. The impl correctly binds server.listen(OAUTH_PORT, '127.0.0.1', …); the test's blocker bound to the unspecified address, so on macOS startOAuthServer() wasn't pre-empted, the rejects.toThrow() assertion failed, and the unreachable blocker.close() leaked a listener that cascaded into the other OAuth concurrency tests.

Applied your suggested fix verbatim — bind the blocker to the same 127.0.0.1 so the conflict is deterministic on every platform, and always close it via try/finally so a failed assertion can't leak the listener:

await new Promise<void>((resolve) =>
  blocker.listen(OAUTH_PORT, '127.0.0.1', resolve),
)
try {
  await expect(startOAuthServer()).rejects.toThrow()
} finally {
  await new Promise<void>((resolve) => blocker.close(() => resolve()))
}

Transparency note: I'm on Linux, where listen(port) binds 0.0.0.0 and does collide with the later explicit 127.0.0.1 bind (EADDRINUSE → the assertion already passed), so I couldn't reproduce the macOS-specific failure here — but binding the blocker explicitly to 127.0.0.1 makes the conflict deterministic on both platforms, and closing in finally removes the cascade regardless. Full suite green locally after the fix and the v0.1.2 rebase:

bun run build   → clean
bun run types   → clean
bun test        → 392 pass / 0 fail
bunx biome check src → 0 warnings

(392, up from the 390 you'd have seen — the rebase picked up your two new ws-pool retryable-websocket tests, which pass alongside the parity changes.)

Confirmations

1. CI visibility. This is the expected behavior for a fork PR (iceteaSA:feat/paritycortexkit:main): your .github/workflows/ci.yml is on: pull_request, but GitHub gates workflow runs on fork PRs behind maintainer approval, so only the Socket app checks (which run without that gate) are reporting. There's nothing I can toggle from the branch — the "Approve and run workflows" button is on your side of this PR. The force-push I just did re-fired the synchronize event, so the prompt should be current. Once you approve, the full types/build/test/format:check/lint job will report here.

2. New TUI dependency alerts. Triaged — these aren't novel supply-chain surface:

  • @opentui/core@0.4.1 (MIT, anomalyco/opentui) — this is the TUI runtime OpenCode itself uses; @opencode-ai/plugin declares @opentui/core/keymap/solid as peer dependencies (see the lockfile entry for @opencode-ai/plugin). The sidebar renders on OpenCode's existing TUI stack rather than adding a new one. The "obfuscated code" flag is the heuristic firing on its prebuilt native-FFI + bundled/minified dist.
  • entities@7.0.1 (BSD-2-Clause, fb55/entities) — the canonical HTML entity codec used across the JS ecosystem; pulled transitively by @opentui/solid.
  • seroval@1.5.4 (MIT, lxsmnsyc/seroval) — Solid.js's official serializer; pulled transitively by solid-js.

All three are OSI-licensed and reputable, and this is the same @opentui + solid-js stack the Anthropic sibling plugin ships for its sidebar. If you'd prefer to tighten the surface I'm happy to pin exact versions (@opentui/core is currently >=0.4.0) — just say the word.

3. ./tui export ships .tsx. Intentional, and yes — OpenCode's TUI plugin loader consumes the .tsx source directly at runtime (the package declares oc-plugin: ["server", "tui"]; the loader bundles the ./tui entry in-process). This is byte-identical to the sibling plugin's export map, and it's been dogfooded live throughout development — the sidebar renders, the /openai-* modals open, and OSC-52 copy works against the shipped .tsx. The types point at the built ./dist/tui.d.ts.

Let me know once you've kicked the CI run — happy to address anything the full workflow surfaces.

@iceteaSA iceteaSA requested a review from ualtinok June 20, 2026 05:55
@ualtinok ualtinok merged commit 7142ad0 into cortexkit:main Jun 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants