Skip to content

Releases: ndycode/oc-codex-multi-auth

v6.9.1

Choose a tag to compare

@ndycode ndycode released this 17 Jul 21:47

Published to npm as oc-codex-multi-auth@6.9.1.

npm install -g oc-codex-multi-auth@6.9.1

Fixed

gpt-5.6-sol rejected through the plugin while working everywhere else (#196, #201)

Follow-up to the 6.8.2 identity fix, which did not resolve the report: sol still returned model_not_supported_with_chatgpt_account on every pooled account while terra/luna passed, and the same accounts ran sol fine in the Codex TUI and in plain opencode.

The remaining difference was who the plugin claims to be, not the request version or payload:

  • The upstream model catalog gates sol, terra, and luna identically (minimal_client_version: 0.144.0, use_responses_lite: true) — no version- or shape-level cause can produce a sol-only failure.
  • Plain opencode's native ChatGPT-Codex path does not imitate the Codex CLI: it sends originator: opencode with an opencode/<version> (<platform> <release>; <arch>) User-Agent to the same endpoint. The backend evaluates sol entitlement per originator, and for some account cohorts the codex_cli_rs claim from a non-Codex client fails that check while the host identity passes.

The GPT-5.6 (responses-lite) tiers now present the host (opencode) identity by default — the identity affected accounts are proven to pass sol with. Every other model keeps the Codex CLI identity from 6.8.2. Verified live on a sol-entitled account: both identities return 200 there, so setups where codex_cli_rs already works are unaffected.

Also in this release:

  • The advertised opencode version self-syncs with the real host build (reused from the host's own injected User-Agent when present).
  • CODEX_AUTH_CLIENT_VERSION / CODEX_AUTH_HOST_VERSION values are sanitized to safe product-token characters so a badly quoted env value can't corrupt the User-Agent.

New escape hatches:

CODEX_AUTH_CLIENT_IDENTITY=codex      # force the Codex CLI identity for all models
CODEX_AUTH_CLIENT_IDENTITY=opencode   # force the host identity for all models (alias: host)
CODEX_AUTH_HOST_VERSION=1.18.0        # override the advertised opencode version

v6.9.0

Choose a tag to compare

@ndycode ndycode released this 17 Jul 13:36

Published to npm as oc-codex-multi-auth@6.9.0.

npm install -g oc-codex-multi-auth@6.9.0

Added

Model-specific account pools (#200)

A new modelAccountPools config field maps an effective model ID to a preferred set of accounts, so a model can be routed through the accounts entitled to it — e.g. pin gpt-5.6-sol to the accounts inside the Sol preview — instead of burning rotation attempts on accounts that will reject it.

{
  "modelAccountPools": {
    "gpt-5.6-sol": ["org-example-account-id"]
  }
}
  • All three rotation strategies (sticky, round-robin, hybrid) restrict selection to healthy, selectable accounts in the preferred pool while one is available; existing quota, cooldown, and token-health rules still apply within the pool.
  • If every preferred account is unavailable — disabled, unknown in this project, cooling down, or rate-limited — selection falls back transparently to the healthy general pool rather than failing the request.
  • Model keys match case-insensitively against the effective model after request-model normalization; unmapped models and empty lists use the general pool directly.
  • Pool references are stable account IDs, not indexes, so adding, removing, or reordering accounts never silently changes a model's routing.

codex-pool tool (#200)

The 24th codex-* tool manages those mappings with ordinary 1-based account numbers while resolving and persisting only stable IDs:

codex-pool
codex-pool action="set" model="gpt-5.6-sol" accounts=[7,8]
codex-pool action="add" model="gpt-5.6-sol" accounts=[9]
codex-pool action="remove" model="gpt-5.6-sol" accounts=[7]
codex-pool action="clear" model="gpt-5.6-sol"
  • dryRun=true previews any mutation; format="json" gives structured output with stable IDs redacted unless includeSensitive=true.
  • Config writes are atomic and serialized (in-process queue + cross-process file lock via the new proper-lockfile dependency), preserve every unrelated raw config key, and refuse to replace malformed JSON or an invalid existing pool.
  • The plugin config is global while account storage is per-project by default, so references that don't resolve in the current project are reported but never automatically pruned.
  • Restart OpenCode after an applied mutation.

Pool-mode routing diagnostics (#200)

codex-status, codex-dashboard, and codex-metrics (text and TUI views) now report accountPoolModegeneral, preferred, or general-fallback — and configuredAccountPoolSize, so a fallback out of a configured pool is visible instead of silent.


Contributed by @lubshad.

v6.8.2

Choose a tag to compare

@ndycode ndycode released this 16 Jul 11:40

Published to npm as oc-codex-multi-auth@6.8.2.

npm install -g oc-codex-multi-auth@6.8.2

Fixed

gpt-5.6-sol rejected through the plugin while working in the Codex CLI/TUI for the same account (#196, #199)

Follow-up to the 6.8.1 auto-fallback fix: that made Sol degrade gracefully, this addresses the reported rejection itself. Diffing the plugin's request against upstream openai/codex found two client-identity mismatches versus what the Codex CLI sends:

  • Missing Codex User-Agent. The plugin declared originator: codex_cli_rs but sent the host runtime's UA. The backend reads the client version from the UA product token, and the model catalog gates the 5.6 tiers on minimal_client_version: 0.144.0. Requests now carry codex_cli_rs/<version> (<os> <osver>; <arch>). Opt out with CODEX_AUTH_DISABLE_CODEX_USER_AGENT=1; override the advertised version with CODEX_AUTH_CLIENT_VERSION.
  • Non-upstream openai-organization pinning. The plugin pinned the token's organizations[0] on every request for accounts carrying an organization claim — a header upstream Codex never sends on ChatGPT-Codex requests (workspace routing is carried entirely by chatgpt-account-id), and one that can shift the backend's entitlement evaluation to a workspace outside the narrow Sol preview while the broader Terra/Luna preview still passes. The header is no longer sent by default; restore the legacy behavior with CODEX_AUTH_SEND_ORGANIZATION_HEADER=1. The org-variant account deduplication is untouched.

Verified against the live backend: gpt-5.5, gpt-5.6-sol, and gpt-5.6-terra all stream successfully with the new identity (probed from the published package). The failing condition itself is workspace-account-specific, so if Sol still fails for you on 6.8.2, please comment on #196 with codex-health output.

v6.8.1

Choose a tag to compare

@ndycode ndycode released this 15 Jul 17:31

Published to npm as oc-codex-multi-auth@6.8.1.

npm install -g oc-codex-multi-auth@6.8.1

Fixed

gpt-5.6-sol no longer hard-fails for accounts outside the GPT-5.6 preview (#196, #197)

6.7.0 documented that an account without 5.6 access degrades gpt-5.6-solgpt-5.6-terragpt-5.6-lunagpt-5.5, but the chain was only traversed under unsupportedCodexPolicy: "fallback" — the default-selector auto-fallback allowlist listed only gpt-5.5 and gpt-5-codex, so under the default strict policy a Sol request burned through every pooled account and returned an entitlement error. The three 5.6 tiers now auto-degrade out of the box, exactly as documented. Opt out with CODEX_AUTH_DISABLE_GPT56_AUTO_FALLBACK=1. Bare gpt-5.6 is also canonicalized to gpt-5.6-sol inside the fallback resolver, so custom chains keyed as gpt-5.6 resolve correctly.

The carried-over reasoning effort is also re-clamped per fallback hop: gpt-5.6-sol-max degrading to gpt-5.5 previously sent max — an effort only 5.6 accepts — to the fallback target, turning the graceful degrade into a hard 400. (#198)

Multi-process refresh-token clobber (#198)

A process persisting its in-memory account pool blind-overwrote the accounts file, including a refresh token another process had rotated after this process loaded its snapshot. Refresh tokens are single-use, so the clobbered token was dead on arrival — the next refresh with it failed and the auth-failure path eventually removed a still-valid workspace. Accounts now carry a persisted tokenRotatedAt stamp (written on rotation, propagated to token-sharing siblings, and stamped by codex-refresh too), and every save runs as a read-modify-write transaction under the storage lock that adopts newer on-disk credentials into both the persisted payload and live memory. Files from older builds have no stamp and keep the previous behavior. Two sibling defects of the same class are fixed alongside: the refresh queue re-consumed a just-rotated single-use token for callers arriving right after the rotation settled (now served the settled result for 30s), and the mutating tools (codex-switch/codex-remove/codex-label/codex-refresh) saved stale snapshots that silently overwrote concurrent rotation state — they now mutate inside a single storage transaction. codex-keychain migrate/rollback also actually run under the storage lock their docstring claimed, with hardened rollback error paths.

Truncated SSE streams are no longer reported as successes (#198)

A non-streaming response whose SSE stream ended without any terminal event was returned as raw SSE text at the original 2xx status — the rotation loop credited the account with a success for a failed turn and the client got an unparseable body. Such streams now surface as a 502 incomplete_stream error; bodies with no SSE framing still pass through as plain JSON.

Uncapped retry-after headers (#198)

The body retry_after_ms/retry_after fields were capped at 5 minutes but the equivalent headers were not, so a bogus header (e.g. retry-after: 86400) benched a healthy account for hours, persisted across processes. Header values now get the same cap; quota reset-at headers remain uncapped since those windows legitimately reset hours out.

TUI status line no longer trusts idle-stale quota snapshots (#198)

The 5-minute refresh returned any fingerprint-matching shared snapshot as current with no age check, so once the cache file existed the /wham/usage fetch never ran again — an hours-old percentage (with a reset time already in the past) rendered as fresh. Snapshots older than one refresh interval now trigger a live re-fetch and render as stale only as a fallback.

codex-reset redeem idempotency actually works now (#198)

The documented double-spend protection was inert: each attempt sent a fresh random redeem_request_id, so the backend could never recognize a retry. The key is now derived deterministically from the credit id, and a failed consume POST reports the redemption outcome as unknown (redeemed: null) instead of false — the request may have reached the backend, and claiming failure could push the user to spend a second credit.

Smaller fixes (#198)

  • Proactive token refresh no longer skips accounts that have a refresh token but neither access token nor expiry.
  • codex-switch/codex-remove/codex-label reject fractional account indices instead of silently flooring them, and a failed persist is reported as a failed operation rather than a success with a caveat.

Full details in the CHANGELOG.

v6.8.0

Choose a tag to compare

@ndycode ndycode released this 14 Jul 09:57

Published to npm as oc-codex-multi-auth@6.8.0.

npm install -g oc-codex-multi-auth@6.8.0

Added

codex-reset — view and redeem banked Codex rate-limit reset credits (#193)

OpenAI grants eligible plans a small number of rate-limit reset credits, but exposes redemption only in the Codex desktop app, the IDE extensions, and the Codex CLI /usage screen. Users of this plugin — Linux users in particular — had no way to spend a credit they already own without switching tools.

codex-reset                                     # banked credits + current usage
codex-reset action="consume"                    # preview only — does NOT redeem
codex-reset action="consume" confirm=true       # redeem
codex-reset action="consume" confirm=true dryRun=true
codex-reset format="json"

Redeeming is irreversible and spends a finite credit, so it is never implicit. action="consume" only issues the POST when confirm=true; without it you get the same preview dryRun gives you. Each redemption carries a fresh redeem_request_id so a retry cannot spend two credits, an unavailable credit id is refused rather than posted, and once the credit is spent a failure of the follow-up usage read is reported as usageError alongside redeemed: true — never as an unredeemed credit.

Fixed

Disabled rate-limit windows no longer render as a full quota (#194)

OpenAI encodes a switched-off window as window-minutes: 0 / limit_window_seconds: 0 with used-percent: 0 rather than omitting it. Both quota paths kept that window because used-percent was numeric, producing a phantom segment next to the real weekly window:

before:  7d 77% · quota 100%
after:   7d 77%

A window is now rejected on its explicit zero length; a window whose length header is absent is merely unknown and still shows under the generic quota label. Two sibling defects of the same cause are fixed alongside the reported one: the /wham/usage path rounded a zero-second window up to one minute, surfacing a disabled window as a real 1m limit, and codex-limits printed both windows unconditionally. Caches already poisoned by an older build heal on read, so you do not need to delete oc-codex-multi-auth-tui-quota.json.

Reported by @aic0d3r, correlated with OpenAI temporarily disabling the 5-hour Codex limit for some paid plans.

Notes

The codex-reset listing path is verified against the live backend. The redeem (POST) path is covered by tests against a mocked fetch but has not yet been confirmed against a live redemption — confirming it costs a real, non-refundable credit. Treat the first real redemption as the contract test.

Full changelog: v6.7.1...v6.8.0

v6.7.1

Choose a tag to compare

@ndycode ndycode released this 10 Jul 08:34

Patch for a total GPT-5.6 outage in 6.7.0. If you use gpt-5.6-sol, gpt-5.6-terra, or gpt-5.6-luna, upgrade — every request on 6.7.0 fails.

Fixed

GPT-5.6 requests no longer fail with HTTP 400 (#191, #192).

The backend rejects any request carrying the x-openai-internal-codex-responses-lite header that does not also set reasoning.context = "all_turns":

{
  "error": {
    "message": "X-OpenAI-Internal-Codex-Responses-Lite requires `reasoning.context` to be `all_turns`.",
    "type": "invalid_request_error",
    "param": "reasoning.context",
    "code": "unsupported_value"
  }
}

6.7.0 sent the header but never the field, so every gpt-5.6-* turn returned HTTP 400. Because that error is not model_not_supported_with_chatgpt_account, the sol → terra → luna → gpt-5.5 degradation never triggered — the request hard-failed instead of falling back.

This matches upstream codex-rs/core/src/client.rs (build_reasoning):

context: model_info.use_responses_lite.then_some(ReasoningContext::AllTurns),

ReasoningContext is serde(rename_all = "snake_case") and context is skip_serializing_if = Option::is_none, so responses-lite models send "all_turns" and every other model omits the field entirely.

The field is written inside the responses-lite reshape, which shapeBodyForModel applies to a structuredClone for lite models only. The canonical body stays free of context, so a gpt-5.6-sol request that falls back to gpt-5.5 is re-serialized in the classic shape without it — pinned by a mutation-isolation test and an end-to-end fallback test, both of which fail if the clone boundary is removed.

Credits

Reported, diagnosed, and fixed by @UnknOownU, who has GPT-5.6 access and verified the fix against the live Codex backend (identical POST /codex/responses 400s without the field, 200s with it). This is the first live-backend confirmation of the responses-lite path shipped in 6.7.0.

Full changelog: v6.7.0...v6.7.1

v6.7.0

Choose a tag to compare

@ndycode ndycode released this 09 Jul 23:36

Added

GPT-5.6 support — gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna (#189).

Bare gpt-5.6 aliases the Sol flagship tier. Reasoning-effort support follows the Codex model catalog (codex-rs/models-manager/models.json), not the launch coverage — which claims max/ultra are Sol-exclusive:

Model Efforts
gpt-5.6-sol low, medium, high, xhigh, max, ultra
gpt-5.6-terra low, medium, high, xhigh, max, ultra
gpt-5.6-luna low, medium, high, xhigh, max

No tier accepts none or minimal; both floor to low. Requesting max/ultra on a pre-5.6 model steps down to xhigh, then high.

ultra never goes on the wire. Codex treats it as a client-side tier and rewrites it to max before sending (client.rs, reasoning_effort_for_request); the subagent orchestration that distinguishes it lives in the Codex client, not the request body. It is accepted as an alias, sent as max, and excluded from ReasoningConfig["effort"] so the invariant is enforced at compile time. It does not spawn subagents here.

5.6 is opt-in: the legacy gpt-5 alias and the plugin default still resolve to gpt-5.5 / gpt-5.4. Since GPT-5.6 shipped as a limited preview, an account without access degrades sol → terra → luna → gpt-5.5 rather than failing every request.

Fixed

GPT-5.6 is served over the responses-lite request path (#189).

Its catalog entry sets use_responses_lite: true and tool_mode: "code_mode_only". Codex sends those models a different body: tools move into input as a leading additional_tools developer item, base instructions follow as a developer message, top-level instructions becomes "" and tools is omitted, parallel_tool_calls is forced off, image detail is stripped, and an x-openai-internal-codex-responses-lite: true header is sent. Sending the classic shape to a code_mode_only model hands it tools in a field it does not read.

The lite shape is applied at serialization, per attempt, against the model actually being sent — never to the canonical body — so a gpt-5.6-sol request that falls back to gpt-5.5 is re-serialized classic and keeps its tools instead of stranding them in an additional_tools item.

System instructions now come from the Codex model catalog (#190).

Modern Codex carries a full base_instructions string per model and sends that, not the legacy *_prompt.md files. The plugin was sending gpt_5_2_prompt.md — which opens "You are GPT-5.2 running in the Codex CLI" — to gpt-5.2, gpt-5.4, gpt-5.4-mini, and gpt-5.5, so those models got the wrong system prompt and a false identity.

⚠️ Behavior change. This alters the system prompt for existing gpt-5.2, gpt-5.4, gpt-5.4-mini, and gpt-5.5 users. The new text is what Codex itself sends. Models absent from the catalog (gpt-5-codex, gpt-5.1*, gpt-5.2-codex, gpt-5.4-nano, gpt-5.4-pro) keep their prompt file, and a catalog miss falls back to it.

Catalog instructions cache per model id, not per family: gpt-5.5 and gpt-5.4 share the gpt-5.4 family but carry different text. Keys are namespaced (catalog: / family:) because slug-space and family-space overlap — gpt-5.4-nano has no catalog entry but belongs to the gpt-5.4 family, itself a catalog slug. models.json is fetched once per release tag; fetchCatalogText memoizes and shares the in-flight promise, so the concurrent fan-out in prewarmCodexInstructions collapses to one download.

minimal effort no longer reaches the backend for GPT-5.6 (#189). It was clamped only in the Codex branch, which keys off the model name containing codex — 5.6 slugs do not. It now floors to low.

Effort-suffix parsing consolidated (#189). gpt-5.1-codex-max is a model id ending in -max, not a max-effort variant of gpt-5.1-codex. The new max suffix is guarded by a negative lookbehind scoped to that branch alone, so gpt-5-codex-low and gpt-5.1-codex-max-xhigh still parse.

Notes

GPT-5.6 support is verified against the upstream Codex source and the live model catalog, and by 2690 tests — but no request in this release has been exercised against the live Codex backend. GPT-5.6 is preview-gated; accounts without access exercise the fallback chain.

Full changelog: v6.6.0...v6.7.0

v6.6.0

Choose a tag to compare

@ndycode ndycode released this 09 Jul 12:27

Fixed

The plugin no longer takes over process termination inside the opencode host (#187).

lib/shutdown.ts installed a process-wide SIGINT/SIGTERM handler that called process.exit(0) after running cleanup. Because this package loads as a plugin inside the opencode process, that exit won the race against opencode's own asynchronous shutdown — so pressing Ctrl+C exited without opencode printing the session id.

Process termination is now owned explicitly rather than inferred from the signal. It defaults to off, so the plugin runs its cleanup and leaves termination to the host. Only an entrypoint that is the process opts in: the standalone warm CLI (the one bin path that installs the handler, because refreshing a token persists credentials under the storage lock) calls setShutdownOwnsProcess(true) and now exits 130/143 (128 + signo) on a signal instead of reporting 0, which masked an interrupt as success.

The debounced-save flush is still awaited on shutdown in both modes, so the no-lost-rotations guarantee from #110 holds.

runCleanup() no longer drops work when a drain overlaps. It emptied the cleanup queue before awaiting it, so a beforeExit firing during an in-flight signal drain returned immediately against an already-empty queue rather than awaiting the real cleanup. Concurrent callers now share the in-flight promise, cleared once settled so sequential calls still re-drain.

Added

  • setShutdownOwnsProcess(boolean), exported from lib/shutdown.ts and the lib/index.ts barrel, lets a standalone entrypoint claim ownership of process termination. The flag is read at signal time, not captured when the handlers are installed, so an entrypoint may opt in after the first registerCleanup().

Upgrading

No action required for plugin users — the fix is the default. If you embed this package and rely on it terminating the process on a signal, call setShutdownOwnsProcess(true) at startup.


npm: https://www.npmjs.com/package/oc-codex-multi-auth/v/6.6.0
Full diff: v6.5.0...v6.6.0

v6.5.0

Choose a tag to compare

@ndycode ndycode released this 30 Jun 10:44
755aa46

Added

  • oc-codex-multi-auth warm standalone CLI command runs the account warm-up directly — in plain Node via the package bin, with no agent/model in the loop and therefore no token cost. It opens every enabled account's rolling usage window (one minimal POST /codex/responses each), skips disabled accounts, classifies a quota/usage_limit 429 as a distinct failure rather than "warmed", supports --json, and exits non-zero when any account failed. Run it (or npx -y oc-codex-multi-auth@latest warm) at the start of a session to stagger the rolling quota cooldowns. This addresses the request to run the warm-up as a direct command instead of an agent-invoked tool; the in-conversation codex-warm tool remains for users who want it mid-session. (#182)

Fixed

  • A standalone command's non-zero exit code now propagates to the process exit code instead of being dropped, so oc-codex-multi-auth warm correctly exits 1 when an account fails (useful in scripts and CI) while read-only commands still exit 0.
  • warm no longer hard-fails when the Codex system-prompt file cannot be resolved (offline, cache miss, or a standalone run without the bundled prompt): the warm ping falls back to a minimal instruction, since it only needs a valid request to open the window, not the full prompt.

Notes

  • This is a minor release that adds a new entry point; nothing in the existing request path, rotationStrategy config, or the in-conversation codex-warm tool changes behavior. The standalone warm is the first standalone CLI command that performs network I/O (token refresh + one request per account) — the other standalone commands (status, health, doctor, limits, list) remain read-only and network-free.

v6.4.1

Choose a tag to compare

@ndycode ndycode released this 30 Jun 07:07

Fixed

  • Local token-bucket depletion no longer leaks into persisted, cross-process state. 6.4.0 made a depleted account rotate by writing a synthetic window into rateLimitResetTimes — but that field is saved to the shared accounts file and reloaded by every process, so one process exhausting its own in-memory proactive limiter could spuriously mark a server-healthy account as rate-limited in other concurrent processes (the multi-process / PID-offset deployment this tool targets). Account selection (sticky, hybrid, round-robin) and getMinWaitTimeForFamily are now token-bucket-aware directly: a locally-depleted account is skipped in-memory with no persisted state, and an all-depleted pool waits for token refill instead of returning a spurious 503. (#183)
  • A locally-depleted account no longer accrues a server-429-style health penalty. The depletion path previously called recordRateLimit, mis-attributing a purely-local proactive throttle as an upstream rejection and deprioritizing a perfectly healthy account in hybrid scoring for hours. (#183)
  • codex-warm no longer reports a quota-exhausted account as "warmed". A 429 is now classified by reason: a quota/usage_limit 429 (the window is already spent) is surfaced as a distinct failure, while a transient tokens/concurrent 429 (window active) still counts as warmed. (#182)

Notes

  • This is a patch release that corrects a regression introduced in 6.4.0; the rotationStrategy config (#183) and codex-warm tool (#182) added in 6.4.0 are unchanged in behavior except for the fixes above. Single-process usage was unaffected by the regression — the leak only manifested across concurrent processes sharing one accounts file.
  • The local token bucket is intentionally in-memory and per-process; only real server 429s belong in the persisted rateLimitResetTimes. Selectors now consult the bucket in-memory rather than encoding local back-pressure into shared state.