Skip to content

[Bug] non-OpenAI providers unusable without a ChatGPT login after v2.23.0 #2132

Closed

Description

@marmeladema

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

  1. Run opencodex bound to a non-loopback hostname (mine is in Docker, so 0.0.0.0)
    with a data-plane credential configured — either OPENCODEX_API_AUTH_TOKEN or an
    entry in config.apiKeys. isApiAuthRequired is then true.

  2. Configure a non-OpenAI provider (e.g: Cloudflare AI Gateway).

  3. Ensure ~/.codex/auth.json has no token — no ChatGPT login, no OpenAI API key.

  4. 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 than env_key deliberately: should_refresh_models()
    in codex-rs is uses_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
    becomes CodexAuth::from_api_key(...) (login/src/auth/external_bearer.rs:51)
    and is sent as Authorization: Bearer, exactly like env_key.

  5. 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" from resolveResponsesApiAuth; the
    main-credential error is only reachable after admission has already succeeded
    (core.ts:1114 runs only once resolveDataPlaneAdmissionSecret has 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:1088

    const 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.codexAccountMode is only ever
    set for the provider named openai (src/providers/registry.ts:2675, enforced
    again at src/server/management/provider-routes.ts:582). Every other provider
    falls through to the placeholder authCtx = { kind: "main", accountId: null }.

  • src/codex/auth-context.ts:490 — materializeCodexUpstreamAuth sees
    kind === "main" plus the substitute flag, requires a stored main token, finds
    none, and throws CodexMainSubstitutionUnavailableError → 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/responses

Screenshots 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

  1. coderabbitai commented on Aug 19, 2026

    @coderabbitai
    Contributor
    🔗 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 plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. changed the title [-]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[/+] on Aug 19, 2026
  3. lidge-jun commented on Aug 19, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 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 → 401 No 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이 작성했습니다

  4. lidge-jun commented on Aug 20, 2026

    @lidge-jun
    Owner

    Fixed on dev by #2137, merged as be12328.

    Verified present on the branch rather than assumed: git log origin/dev shows the merge commit above. These PRs target dev rather than main, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions