Codex desktop: switching an existing thread to a custom API provider can break continuity and remote compact still hits api.openai.com
Summary
When using the Codex desktop app, switching authentication/model routing from ChatGPT-plan auth to a custom third-party Responses-compatible API provider is currently very fragile for long-running threads.
The existing thread can remain visible, but when the thread needs compaction or certain follow-up work, Codex may still send the request to https://api.openai.com/v1/responses instead of the configured custom provider base_url. Because the active key belongs to the third-party provider, the request fails with 401 invalid_api_key.
This creates a serious continuity/UX problem: users can have a long-running task half completed on disk, switch to a third-party provider because their ChatGPT/Codex plan quota is exhausted, and then discover that the same thread cannot continue reliably. The practical workaround becomes opening a new thread, manually recovering Git/worktree state, creating handoff files, and later re-synchronizing the old thread when ChatGPT quota returns. That is error-prone and can cause duplicate work or conflicting edits.
Expected behavior
One of these behaviors would be much safer:
- Provider routing should be thread-consistent: once a custom provider is active, all model-dependent subrequests for that thread (including remote/local compaction) should honor the configured provider/base URL; or
- If an existing thread cannot safely switch providers, Codex should explicitly block the switch and offer a first-class
Continue with another providerflow that carries forward the thread summary, working directory, branch/worktree, diff state, approvals, and pending tasks; or - Codex should automatically generate a durable handoff/checkpoint before switching auth/provider, then let the new provider resume from that checkpoint without requiring users to manually inspect Git/worktrees.
Reproduction
- Start a long-running Codex/Work task using ChatGPT-plan auth.
- Let the task modify a local repository/worktree and continue until the thread is long enough to require compaction.
- Exhaust or otherwise stop using ChatGPT/Codex plan quota.
- Configure a custom third-party Responses-compatible provider with its own API key and
base_url. - Return to the existing thread and attempt to continue.
- The thread may remain visible, but compaction/follow-up can fail with a request to
https://api.openai.com/v1/responsesand401 invalid_api_key, instead of using the custom provider endpoint.
Why this matters
- Long-running tasks can be stranded halfway through even though the repository state is still present locally.
- Users are forced to manually manage
config.toml, auth, Git branches/worktrees, local history, and handoff files just to change billing/provider. - Returning later to ChatGPT-plan auth risks stale-thread context causing repeated work or edits that conflict with changes made by the third-party-provider thread.
- A third-party API key being routed to the wrong host is also an undesirable credential-routing failure mode, even if the request is rejected.
Requested product behavior
Please make provider switching a first-class, resumable operation at the thread/task level, with automatic preservation of:
- thread/task summary and pending checklist
- current working directory
- branch/worktree identity
- uncommitted diff state
- approvals/permissions state
- model/provider routing for all subrequests, including compaction
The user should not need to understand Git worktrees or manually create a handoff document simply because they switched from ChatGPT-plan usage to a custom API provider and later back again.
2 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
The "existing thread keeps hitting
api.openai.comafter a provider switch" half of this has a specific, verifiable mechanism in the shared app-server code (main@ 1f41cc5d92): thread resume pins the provider recorded when the thread was created, and a config-level provider switch never retargets it.1. Resume pins the persisted provider. When a thread is loaded/resumed, its persisted metadata is merged into the effective config overrides:
https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/app-server/src/request_processors/thread_processor.rs#L166-L184
typesafe_overrides.model_provider = Some(persisted_metadata.model_provider.clone())— unconditionally, unless the resume request itself carries an explicit model/provider override (has_model_resume_override, #L266-L275). So a thread created under ChatGPT-plan auth stays routed to the built-inopenaiprovider no matter whatconfig.tomlnow says. New threads read the new config and work — exactly your observed split. It also explains the inconsistency: whether a given follow-up honors the new provider depends on whether the code path that resumed the thread passed an explicit model override (which suppresses the pinning) or not.2. Why it's compaction that fails loudest. Remote vs. local compaction is chosen from the thread's provider capabilities:
https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/core/src/session/turn.rs#L1183-L1236
With the pinned
openaiprovider,remote_compactionisV1/V2, so compaction posts to the OpenAI/responsescompact endpoint with whatever credentials are now active — your third-party key — producing401 invalid_api_key. Had the thread actually been routed to the custom provider, the capability check would have selected local compaction and this failure wouldn't exist. So the compaction 401 is downstream of the pinned provider, not a separate compaction bug.3. The credential-routing smell you flagged is real and worth prioritizing. The current shape sends the active (third-party) API key to
api.openai.combecause provider identity and credentials are resolved independently: the provider comes from thread metadata, the key from current auth state. A cheap invariant — before issuing any request, assert the resolved credentials belong to the resolved provider, otherwise fail with "thread is pinned to provideropenaibut active credentials are for<provider>" — would convert both the silent mis-routing and the confusing 401 into an actionable message.Fix outline matching your requested behaviors:
thread/settings/updateas a first-class switch that rewrites the persisted thread metadata, so subsequent resumes pin the new provider (your option 1). The compaction path then follows automatically via the capability check in (2).A regression test shape: create a thread under provider A with remote-compaction support, persist, switch config to provider B (no remote compaction), resume without explicit model override, trigger auto-compact — assert the request targets provider B's base URL (or the explicit conflict error), never provider A's.