Skip to content

fix(cli): Codex long-session turn-pin fallback + codex-settings key resolution (#13564 #13563) - #13566

Merged
diegosouzapw merged 4 commits into
diegosouzapw:release/v3.8.51from
opensource-elearning:fix/codex-turn-pin-fallback
Sep 18, 2026
Merged

diegosouzapw merged 4 commits into
diegosouzapw:release/v3.8.51from
opensource-elearning:fix/codex-turn-pin-fallback

Conversation

@opensource-elearning

@opensource-elearning opensource-elearning commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two independent Codex CLI fixes that were blocking long-running Codex sessions and the dashboard configure flow, shipped together because both belong to the same tool surface and both are verified by their own regression tests.

Fix Issue Symptom
Release the native Codex turn pin when the pinned model becomes model-scoped unusable #13564 400 NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE bricks a long session mid-turn
Resolve the codex-settings apiKey through the canonical resolver #13563 400 baseUrl, apiKey and model are required on every dashboard Apply in cloud mode

Issues addressed

What was happening

1. Codex long sessions die to a terminal 400

Native Codex requests carry a turn pin: for the duration of one turn_id the combo locks provider + model. If that model becomes model-scoped unusable mid-turn (per-model quota lockout, connection cooldown, exhausted account), every continuation request for the same turn returned a non-retryable 400 NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE. Claude Code has no such pin, so it naturally falls back to the next healthy combo model on every request; Codex could not.

2. Codex settings always 400 in cloud mode

The codex-settings route diverged from every other CLI tool. baseUrl and model are Zod-gated (min(1)), so the 400 "baseUrl, apiKey and model are required" could only fire on an empty apiKey. The dashboard sends an empty apiKey exactly when cloud mode is on and no management key is selected — while the Apply button stays enabled. The route used an inline if (!apiKey) return 400 plus a hand-rolled getApiKeyById, instead of the shared resolveApiKey(keyId, apiKey) used by cline/forge/openclaw/grok-build/jcode-settings.

Changes

Commit 1 — fix(sse): release native Codex turn pin when pinned model is model-scoped unusable (#13564)

  • open-sse/services/combo.ts — the pinned-turn branch now releases the pin and falls through to full combo routing when all pinned provider+model targets are model-scoped unusable. When the pinned target is not in the combo, it releases too. The pin is preserved when the provider is unhealthy at the provider level (circuit breaker OPEN, provider cooldown).
  • open-sse/services/combo/nativeCodexTurnPin.ts — new releaseNativeCodexTurnPin(body, comboName) helper (idempotent delete of the turn pin).
  • tests/unit/native-codex-turn-pin-model-scoped-fallback.test.ts — rewritten regression suite: pin released + fallback to healthy sibling and re-pinned on success; retries stay pinned without flapping; pin preserved on circuit-breaker OPEN and on provider cooldown; new turns always get free combo routing.
  • config/quality/eslint-suppressions.json — pruned the stale combo.ts no-unused-vars count. This change orphaned the createPinnedModelUnavailableResponse import; the residual unused getBootstrapLatencyMs is pre-existing.

Commit 2 — fix(api): resolve codex-settings apiKey via canonical resolver instead of 400 (#13563)

  • src/app/api/cli-tools/codex-settings/route.ts — replaced the divergent guard + hand-rolled lookup with the canonical resolveApiKey(keyId, validation.data.apiKey) from @/shared/services/apiKeyResolver, matching the sibling CLI tools.
  • tests/unit/codex-settings-api-key-resolution.test.ts — new: empty apiKey + valid keyId resolves the real DB key into auth.json; empty apiKey + no keyId writes the sk_omniroute default instead of 400; explicit apiKey is still written verbatim.

Design notes

  • No change to Claude Code settings. The fallback is entirely inside OmniRoute's combo routing (the turn pin is an OmniRoute concept), so no Claude Code config is touched.
  • Provider-wide outages still preserve the pin. The guards already distinguish model-scoped unavailability (lockout, per-model quota) from provider-wide outages. Releasing on a provider circuit-breaker OPEN would defeat the protection, so those cases keep the pin and the terminal response. Existing tests assert exactly that.
  • Quota-share slot is not leaked. The old early-return paths manually released the quota-share slot; the new fall-through relies on the existing finally release in handleComboChatInner, so there is no counter leak.
  • Empty key no longer means hard failure. The sibling tools have never treated "no key supplied" as fatal: they resolve by keyId, then fall back to an explicit key, then sk_omniroute. Codex now behaves identically, so the dashboard Apply flow works in cloud mode without a pre-selected key.

Verification

All tests below were run from the worktree root. Red-green cycles for both regression suites were observed before the fixes (the bug paths failed first).

Turn-pin suite

node --import tsx/esm --test tests/unit/native-codex-turn-pin-model-scoped-fallback.test.ts

All phases pass (7/7). Wider pin regression sweep (39/39 across 6 other pin files) also green.

Codex-settings key resolution

node --import tsx/esm --test tests/unit/codex-settings-api-key-resolution.test.ts

3/3 pass (was 1/3 green, 2 failing, before the route change).

Adjacent regression

  • codex-settings-wire-api-default.test.ts still passes (wire-api / TOML generation untouched).
  • cli-tools area sweep: 36/36 across keys-route, schema, auth-hardening, apply-container, settings-jsonc, and the two codex-settings files.
  • Changed files are clean for eslint (exit 0) and the pre-commit gate (prettier, eslint with suppressions, any-budget, tracked-artifacts) passed on both commits.

Notes for reviewers

Branch synced to the current release/v3.8.51 tip (base-red #12732 since resolved); mergeable, checks re-running.

…oped unusable (diegosouzapw#13564)

Long-running Codex sessions die with 400 NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE whenever
the model pinned to the current turn becomes model-scoped unusable mid-session (per-model
quota lockout, connection cooldown, exhausted accounts). Claude Code has no equivalent
pin and already falls back to the next healthy combo model; Codex now matches.

Release the turn pin when all pinned provider+model targets are model-scoped unusable
and fall through to full combo routing, re-pinning to whichever model succeeds. Preserve
the pin on provider-wide outages (circuit breaker OPEN, provider cooldown) and when the
pinned target is still healthy.

Also prunes the stale ESLint suppression entry for combo.ts that this change orphaned
(createPinnedModelUnavailableResponse import dropped; pre-existing getBootstrapLatencyMs
remains the sole residual unused var).
…d of 400 (diegosouzapw#13563)

Applying Codex settings from /dashboard/cli-code/codex always failed with
400 "baseUrl, apiKey and model are required" when the dashboard sent an empty
apiKey (cloud mode with no management key selected) — baseUrl and model are
already Zod-gated, so that response could only ever fire on the empty key.

The codex-settings route had diverged from the sibling CLI tools (cline/forge/
openclaw/grok-build/jcode): an inline if(!apiKey) 400 guard plus a hand-rolled
getApiKeyById lookup, instead of the shared resolveApiKey(keyId, apiKey) helper
which resolves by keyId, falls back to the submitted apiKey, then to
sk_omniroute. This change makes codex-settings use the canonical resolver, so:

- empty apiKey + valid keyId -> the real DB key is written to auth.json
- empty apiKey + no keyId   -> sk_omniroute default (config still applies)
- explicit apiKey           -> written verbatim (unchanged)
@diegosouzapw

Copy link
Copy Markdown
Owner

Solid pair of fixes, both well-isolated with real regression coverage. I ran both new test
files at the PR head — 10/10 pass, including the circuit-breaker/cooldown cases that prove the
turn-pin release stays scoped to model-level unavailability. One optional ask: could you add a
changelog.d/fixes/ fragment for #13563/#13564 if that's expected for this repo? Otherwise this
looks merge-ready to me.

@opensource-elearning

Copy link
Copy Markdown
Contributor Author

Changelog fragments added per review feedback — one per fix, as in the #13098 split:

  • changelog.d/fixes/13566-codex-turnpin-fallback.md
  • changelog.d/fixes/13566-codex-settings-apikey.md

I also rebased the branch onto the current release/v3.8.51 tip (it had advanced 111 commits and the PR was CONFLICTING): the only conflict was the clearStaleLKGP import that base moved into ./combo/staleLkgpClear.ts — resolved by keeping base's import (our combo.ts still calls it). The turn-pin changes are untouched.

Verified at the new head (1f24cc5):

  • native-codex-turn-pin-model-scoped-fallback.test.ts + codex-settings-api-key-resolution.test.ts + codex-settings-wire-api-default.test.ts — 14/14 pass
  • npx eslint clean on all four changed files
  • npm run typecheck:core clean
  • npm run check:changelog-integrity — OK (fragments well-formed, no base bullets lost)

PR is now mergeable (MERGEABLE, checks re-running).

Thanks for the review — the circuit-breaker/cooldown scope notes were spot on.

@opensource-elearning

Copy link
Copy Markdown
Contributor Author

@diegosouzapw — fragments added and branch rebased onto the current base tip (now MERGEABLE). Details here: #issuecomment-5694559093. Ready for re-review when checks finish.

@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @opensource-elearning — merging via the release merge-train. Validated in local merge-train (.claude/worktrees/merge-train-20260918-111718-suite.log) on the devbox @ train tip 7bb373fba5e241964c0ffb17bd03a804700839bb, boarded with 55 sibling PRs: typecheck:core, file-size, complexity, cognitive-complexity, changelog-integrity green; 747/747 changed-area node:test cases + 476/476 vitest green (fast parity mode — the full suite ran today on the release tip via the base-red train and runs again on the 3b train). Merged --admin per merge-gates §7.

@diegosouzapw
diegosouzapw merged commit 65263a4 into diegosouzapw:release/v3.8.51 Sep 18, 2026
11 of 16 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…esolution (diegosouzapw#13564 diegosouzapw#13563) (diegosouzapw#13566)

* fix(sse): release native Codex turn pin when pinned model is model-scoped unusable (diegosouzapw#13564)

Long-running Codex sessions die with 400 NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE whenever
the model pinned to the current turn becomes model-scoped unusable mid-session (per-model
quota lockout, connection cooldown, exhausted accounts). Claude Code has no equivalent
pin and already falls back to the next healthy combo model; Codex now matches.

Release the turn pin when all pinned provider+model targets are model-scoped unusable
and fall through to full combo routing, re-pinning to whichever model succeeds. Preserve
the pin on provider-wide outages (circuit breaker OPEN, provider cooldown) and when the
pinned target is still healthy.

Also prunes the stale ESLint suppression entry for combo.ts that this change orphaned
(createPinnedModelUnavailableResponse import dropped; pre-existing getBootstrapLatencyMs
remains the sole residual unused var).

* fix(api): resolve codex-settings apiKey via canonical resolver instead of 400 (diegosouzapw#13563)

Applying Codex settings from /dashboard/cli-code/codex always failed with
400 "baseUrl, apiKey and model are required" when the dashboard sent an empty
apiKey (cloud mode with no management key selected) — baseUrl and model are
already Zod-gated, so that response could only ever fire on the empty key.

The codex-settings route had diverged from the sibling CLI tools (cline/forge/
openclaw/grok-build/jcode): an inline if(!apiKey) 400 guard plus a hand-rolled
getApiKeyById lookup, instead of the shared resolveApiKey(keyId, apiKey) helper
which resolves by keyId, falls back to the submitted apiKey, then to
sk_omniroute. This change makes codex-settings use the canonical resolver, so:

- empty apiKey + valid keyId -> the real DB key is written to auth.json
- empty apiKey + no keyId   -> sk_omniroute default (config still applies)
- explicit apiKey           -> written verbatim (unchanged)

* docs(changelog): add fragments for Codex turn-pin fallback and codex-settings apiKey resolution
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants