Repository navigation
[Bug] non-OpenAI providers unusable without a ChatGPT login after v2.23.0 #2132
Description
Client or integration
Codex CLI
Area
Authentication and account pool
Summary
Since v2.23.0 (#1686 / PR #1861), any request admitted via Authorization: Bearer
fails with a 401 when no ChatGPT credential is stored — even when the request
routes to a non-OpenAI provider that has its own API key and never touches the
Codex backend.
No usable Codex main credential to serve this request
I use opencodex solely to reach Cloudflare AI Gateway models from Codex. I have
deliberately never configured a ChatGPT/OpenAI login, so ~/.codex/auth.json has
no token. This worked through v2.7.x and broke on upgrade.
Expected: a request routed to a key-authenticated provider should not require a
stored Codex main credential, because that credential is irrelevant to the
upstream it is being sent to.
Reproduction
-
Run opencodex bound to a non-loopback hostname (mine is in Docker, so
0.0.0.0)
with a data-plane credential configured — eitherOPENCODEX_API_AUTH_TOKENor an
entry inconfig.apiKeys.isApiAuthRequiredis then true. -
Configure a non-OpenAI provider (e.g: Cloudflare AI Gateway).
-
Ensure
~/.codex/auth.jsonhas no token — no ChatGPT login, no OpenAI API key. -
Point Codex at the proxy so the admission secret travels in
Authorization:[model_providers.opencodex] name = "opencodex" base_url = "http://localhost:10100/v1" wire_api = "responses" [model_providers.opencodex.auth] command = "sh" args = ["-c", "printf %s \"$OPENCODEX_KEY\""]
[.auth]is used rather thanenv_keydeliberately:should_refresh_models()
in codex-rs isuses_codex_backend() || has_command_auth()
(models-manager/src/manager.rs:438), so declaring it is what makes Codex query
/models?client_version=…and get the rich model shape. Note the command's stdout
becomesCodexAuth::from_api_key(...)(login/src/auth/external_bearer.rs:51)
and is sent asAuthorization: Bearer, exactly likeenv_key. -
Send any turn → 401.
Admission itself is fine — this is not a misconfigured key
Worth ruling out up front, because the error mentions a credential:
- The proxy admits the request. An unrecognised secret would return
401 "opencodex API key required"fromresolveResponsesApiAuth; the
main-credential error is only reachable after admission has already succeeded
(core.ts:1114runs only onceresolveDataPlaneAdmissionSecrethas matched). - The bind really is non-loopback.
assertServerAuthConfig
(src/server/auth-cors.ts:287-295) refuses to start a non-loopback bind that has
no data-plane credential, and my instance starts and serves normally.
So both preconditions hold and the credential in play is correct. What is missing is
a Codex main credential, which I have deliberately never configured and which this
request has no reason to need.
Where it goes wrong
-
src/server/responses/core.ts:1088const substituteMainCredential = options.admission?.source === "bearer";
Keyed on how the caller authenticated, never on where the request routes.
-
src/server/responses/core.ts:1093-1102—route.codexAccountModeis only ever
set for the provider namedopenai(src/providers/registry.ts:2675, enforced
again atsrc/server/management/provider-routes.ts:582). Every other provider
falls through to the placeholderauthCtx = { kind: "main", accountId: null }. -
src/codex/auth-context.ts:490—materializeCodexUpstreamAuthsees
kind === "main"plus the substitute flag, requires a stored main token, finds
none, and throwsCodexMainSubstitutionUnavailableError→ the 401 at
core.ts:1152.
The safety rationale in the #1686 comments is sound but scoped too widely. The
concern is forwarding an admission secret upstream, which only applies to routes
that actually forward Authorization — the openai/forward-auth path. A key-auth
provider builds its own headers from scratch (src/adapters/openai-chat.ts:81-83),
so there is nothing to leak and nothing to protect; the ChatGPT credential
requirement is collateral damage.
Regression point
v2.7.13 had one admission path for the whole data plane, and simply served the
request once the secret was recognised:
export function hasValidApiAuth(req, config) {
const actual = req.headers.get("x-opencodex-api-key")?.trim()
|| req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim()
|| req.headers.get("x-api-key")?.trim();
return actual ? isProxyAdmissionSecret(actual, config) : false;
}The Responses transport was later split into resolveResponsesApiAuth
(dedicated header only); #1686 re-admitted bearers but coupled that admission to
mandatory substitution.
Also affected
src/server/responses/compact.ts:325 carries the identical unconditional flag, so
compaction fails the same way.
#2076 (v2.26.0) widens the blast radius for everyone else: it switched the
auto-injected provider block from env_http_headers = { "x-opencodex-api-key" = … }
to env_key = "OPENCODEX_API_AUTH_TOKEN", which moves existing non-loopback
auto-injected installs from the dedicated header onto the failing bearer path.
(This did not affect me — my opencodex runs in a container with no codex binary,
so inject.ts never runs and my config is hand-written.)
Suggested fix
Gate the flag on the route actually targeting the Codex backend, in both files:
const substituteMainCredential = !!route.codexAccountMode
&& options.admission?.source === "bearer";For a non-Codex route the placeholder { kind: "main" } context has no credential
to substitute and needs none, so materializeCodexUpstreamAuth returns the
forwarded headers and the provider adapter supplies its own auth as before.
Workaround (for anyone else hitting this)
Keep the auth block and add the dedicated header, which wins in
resolveResponsesApiAuth so substitution never runs:
[model_providers.opencodex]
name = "opencodex"
base_url = "http://localhost:10100/v1"
wire_api = "responses"
env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_KEY" }
[model_providers.opencodex.auth]
command = "sh"
args = ["-c", "printf %s \"$OPENCODEX_KEY\""]env_http_headers must sit above the [.auth] sub-table header or TOML parses
it as auth.env_http_headers. It is not in codex-rs's conflict list for auth
(only env_key, experimental_bearer_token, requires_openai_auth conflict), so
the combination is accepted and model refresh is preserved.
Version
2.26.0
Operating system
Debian Testing for host, node:22-alpine for the base docker image
Provider and model
cloudflare-workers-ai / @cf/zai-org/glm-5.2
Logs or error output
unexpected status 401 Unauthorized: No usable Codex main credential to serve this request, url: http://localhost:10100/v1/responsesScreenshots and supporting files
No response
Redacted configuration
Codex config:
[model_providers.opencodex]
name = "opencodex"
base_url = "http://localhost:10100/v1"
wire_api = "responses"
[model_providers.opencodex.auth]
command = "sh"
args = ["-c", "printf %s \"$OPENCODEX_KEY\""]Opencodex config:
{
"port": 10100,
"managementUsageMaxReadBytes": 67108864,
"appOwnedMemoryBudgetMb": 256,
"hostname": "0.0.0.0",
"providers": {
"cloudflare-workers-ai": {
"adapter": "openai-chat",
"baseUrl": "https://api.cloudflare.com/client/v4/accounts/XXXX/ai/v1",
"authMode": "key",
"apiKey": "XXXX",
"models": [
"@cf/zai-org/glm-5.2",
"@cf/moonshotai/kimi-k2.7-code"
]
},
"openai": {
"adapter": "openai-responses",
"baseUrl": "https://chatgpt.com/backend-api/codex",
"authMode": "forward",
"codexAccountMode": "pool"
}
},
"defaultProvider": "cloudflare-workers-ai",
"subagentModels": [
"gpt-5.5",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-5.4-mini"
],
"websockets": false,
"codexAutoStart": false,
"openaiProviderTierVersion": 2
}Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
coderabbitai commentedon Aug 19, 2026
🔗 Related PRs
#2109 - fix(providers): allow baseUrl override for Anthropic Claude provider [open]
#2113 - feat(providers): allow trusted encrypted V2 task passthrough [open]
#2110 - fix(providers): allow baseUrl override for Google Antigravity provider [open]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
[-]non-OpenAI providers unusable without a ChatGPT login after v2.23.0[/-][+][Bug] non-OpenAI providers unusable without a ChatGPT login after v2.23.0[/+]lidge-jun commentedon Aug 19, 2026
리뷰 · 우선순위 72 / 80
재현이 코드랑 맞음. 2.23.0 #1686이 bearer 입학을 다시 열면서
substituteMainCredential을 입학 방식에만 걸었음.src/server/responses/core.ts:1088이options.admission?.source === "bearer"한 줄임. 라우트가 Cloudflare든 openai든 같음.codexAccountMode는openai에만 붙음. 나머지는authCtx = { kind: "main", accountId: null }로 떨어짐.materializeCodexUpstreamAuth(src/codex/auth-context.ts:542)가 kind main + substitute면getMainAccountToken()을 찾고 없으면CodexMainSubstitutionUnavailableError→ 401No usable Codex main credential to serve this request. 입학은 이미 통과한 뒤임.x-opencodex-api-key면 이 분기를 안 탐.openai-chat 키 프로바이더는 헤더를 처음부터 만듦.
src/adapters/openai-chat.ts:82-83이Bearer ${provider.apiKey}. 입학 시크릿을 업스트림에 흘릴 구멍이 없음. #1686 가드가 보호하는 건 forward-auth Codex 경로뿐임. 비-OpenAI에 ChatGPT 로그인을 요구하는 건 그 가드의 범위가 아님. compact도 쌍둥이임.src/server/responses/compact.ts:325가 같은 무조건 플래그.#2076이 자동 주입을
env_key = OPENCODEX_API_AUTH_TOKEN으로 바꿔서 bearer 경로로 밀어 넣은 사람은 더 맞음. 리포터는 손 TOML +auth.command라 그 경로로 직접 온 거임. 워크어라운드(전용 헤더를[.auth]위에)는resolveResponsesApiAuth가 전용 헤더를 이기게 해서 플래그를 안 킴. 맞음. 기본 동작은 아님.점수는 72임. 2.26.0에서 ChatGPT 없이 키 프로바이더만 쓰는 설치가 죽음. native-main 펜스(#2130, 방금
dev)랑은 다른 줄. types.ts/config.ts 안 건드려도 고칠 수 있음. 2.27 막판 후보면 #2130 다음임.해결방안: 리포터가 적은 가드가 맞음.
const substituteMainCredential = !!route.codexAccountMode && options.admission?.source === "bearer"를 core랑 compact 둘 다. Codex 라우트는 지금이랑 같고, 키 라우트는 placeholder main이 토큰을 안 찾음. 테스트는 bearer + cloudflare-workers-ai 키 라우트가 401 안 내는 거, openai direct bearer는 메인 토큰 없으면 그대로 401.이 댓글은 grok-bot이 작성했습니다
fix(responses): stop requiring a ChatGPT credential for routed providers
lidge-jun commentedon Aug 20, 2026
Fixed on
devby #2137, merged as be12328.Verified present on the branch rather than assumed:
git log origin/devshows the merge commit above. These PRs targetdevrather thanmain, so GitHub does not auto-close the linked issue — this closure is manual.It ships in the next release cut from
dev. Reopen if you still see it after updating.fix(auth): key admission-bearer substitution on transport, not provid…
fix(responses): stop requiring a ChatGPT credential for routed providers
fix(auth): key admission-bearer substitution on transport, not provid…