Summary
/v1/responses rejects requests authenticated with Authorization: Bearer <token> even though that is exactly what the Codex CLI sends when a provider is injected with env_key (the runtime contract since codex-cli 0.146). resolveResponsesApiAuth only reads x-opencodex-api-key ("Dedicated header ONLY"), so any user who injects the proxy with env_key — the modern runtime's preferred form — gets 401 authentication_error: opencodex API key required on every model, including native OpenAI passthrough.
This is inconsistent with the rest of the proxy: resolveApiAuth (the non-responses path) already accepts authorization as a fallback, and the grok injector already emits env_key.
This issue also owns the directly coupled failure previously tracked in #1687: after an env_key admission bearer is accepted, codexAccountMode: "direct" must replace the proxy admission credential with the stored main ChatGPT/OpenAI credential before forwarding upstream. The two failures are one end-to-end auth contract and must be fixed and tested together.
Reproduction
# 1. Inject the proxy with env_key (codex-cli 0.146+ contract)
# [model_providers.opencodex]
# base_url = "http://127.0.0.1:10100/v1"
# env_key = "OPENCODEX_API_AUTH_TOKEN"
# 2. Start the proxy with OPENCODEX_API_AUTH_TOKEN exported, then request:
curl -X POST http://127.0.0.1:10100/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENCODEX_API_AUTH_TOKEN" \
-d '{"model":"gpt-5.6-luna","input":[{"role":"user","content":[{"type":"input_text","text":"hi"}]}],"stream":true,"store":false}'
Logs and screenshots
Admission failure:
{"error":{"message":"opencodex API key required","type":"authentication_error","code":"invalid_api_key"}}
The same request with x-opencodex-api-key: $OPENCODEX_API_AUTH_TOKEN succeeds (200), proving the token is valid — only the header is rejected.
After bearer admission is accepted, direct-mode forwarding can instead fail with the second half of this contract:
{"error":{"message":"OpenCodex admission credentials cannot be forwarded upstream","type":"invalid_request_error","code":"invalid_request_error"}}
Area
Proxy runtime / Codex integration / direct OpenAI forwarding
Version
2.14.2 (also present on the affected main/dev lineage when reported)
OS
Ubuntu 24.04 (VPS, root)
Config shape
{
"providers": {
"openai": {
"adapter": "openai-responses",
"baseUrl": "https://chatgpt.com/backend-api/codex",
"codexAccountMode": "direct",
"authMode": "forward"
}
}
}
Root cause
There are two sequential failures in the same env_key flow.
1. Responses admission rejects the bearer form
src/server/auth-cors.ts resolveResponsesApiAuth only accepts the dedicated OpenCodex header:
const actual = req.headers.get("x-opencodex-api-key")?.trim();
if (!actual) return null;
Meanwhile resolveApiAuth already accepts Authorization: Bearer as a fallback, and modern Codex env_key injection populates that standard bearer header.
2. Direct-mode forwarding must substitute the admission bearer
Once the caller is admitted, the same bearer is a proxy admission credential and must never be forwarded to ChatGPT/OpenAI. In codexAccountMode: "direct", when a stored main Codex account exists, the forwarding path must replace it with the real account access token and chatgpt-account-id.
The regression tracked in former #1687 was that validateForwardAdmissionCredential rejected the admission bearer unconditionally while the ctx.kind === "main" substitution path had also been removed. That makes a correct env_key request fail even after admission is fixed.
Required fix
Treat this as one end-to-end contract:
resolveResponsesApiAuth accepts Authorization: Bearer <OPENCODEX_API_AUTH_TOKEN> as an admission fallback while preserving the dedicated header path.
- Codex injection uses the supported
env_key contract, with compatibility handling for older runtimes where required.
- For direct-mode OpenAI forwarding, an admitted proxy bearer is never sent upstream.
- If a stored main ChatGPT/OpenAI account is available, replace the admission bearer with the stored access token and the matching
chatgpt-account-id.
- If substitution is impossible, remain fail-closed and reject the request rather than leaking the admission credential.
- Pool mode and non-bearer dedicated-header flows remain unchanged.
Security invariant
Accepting the bearer for proxy admission does not mean it may be forwarded upstream. Admission validation and upstream authentication are separate credential domains.
The proxy must therefore preserve both properties:
- accept the modern Codex
env_key bearer when its value matches the configured OpenCodex admission token;
- never forward that admission token to ChatGPT/OpenAI, substituting a real stored account credential only when one is available.
Acceptance matrix
x-opencodex-api-key with the valid admission secret -> admitted, unchanged.
Authorization: Bearer <valid admission secret> -> admitted.
- no valid admission key where auth is required -> 401.
- direct mode + admission bearer + main account present -> upstream receives the stored ChatGPT/OpenAI credential, not the admission token.
- direct mode + admission bearer + no usable main account -> fail closed with no credential leak.
- pool mode -> existing pool credential override remains unchanged.
- dedicated-header admission -> existing forwarding behavior remains unchanged.
- regression coverage proves the OpenCodex admission secret cannot appear in upstream request headers.
Alternatives considered
- Keep
env_http_headers and do nothing: breaks modern Codex env_key users.
- Fix only admission: still leaves direct-mode requests failing at the forward-credential guard.
- Fix only forwarding: requests never reach that stage when
/v1/responses rejects the bearer.
- Forward the admission bearer as-is: invalid and unsafe because the OpenCodex admission secret is not an upstream ChatGPT/OpenAI credential.
Related
Summary
/v1/responsesrejects requests authenticated withAuthorization: Bearer <token>even though that is exactly what the Codex CLI sends when a provider is injected withenv_key(the runtime contract since codex-cli 0.146).resolveResponsesApiAuthonly readsx-opencodex-api-key("Dedicated header ONLY"), so any user who injects the proxy withenv_key— the modern runtime's preferred form — gets401 authentication_error: opencodex API key requiredon every model, including native OpenAI passthrough.This is inconsistent with the rest of the proxy:
resolveApiAuth(the non-responses path) already acceptsauthorizationas a fallback, and thegrokinjector already emitsenv_key.This issue also owns the directly coupled failure previously tracked in #1687: after an
env_keyadmission bearer is accepted,codexAccountMode: "direct"must replace the proxy admission credential with the stored main ChatGPT/OpenAI credential before forwarding upstream. The two failures are one end-to-end auth contract and must be fixed and tested together.Reproduction
Logs and screenshots
Admission failure:
{"error":{"message":"opencodex API key required","type":"authentication_error","code":"invalid_api_key"}}The same request with
x-opencodex-api-key: $OPENCODEX_API_AUTH_TOKENsucceeds (200), proving the token is valid — only the header is rejected.After bearer admission is accepted, direct-mode forwarding can instead fail with the second half of this contract:
{"error":{"message":"OpenCodex admission credentials cannot be forwarded upstream","type":"invalid_request_error","code":"invalid_request_error"}}Area
Proxy runtime / Codex integration / direct OpenAI forwarding
Version
2.14.2 (also present on the affected main/dev lineage when reported)
OS
Ubuntu 24.04 (VPS, root)
Config shape
{ "providers": { "openai": { "adapter": "openai-responses", "baseUrl": "https://chatgpt.com/backend-api/codex", "codexAccountMode": "direct", "authMode": "forward" } } }Root cause
There are two sequential failures in the same
env_keyflow.1. Responses admission rejects the bearer form
src/server/auth-cors.tsresolveResponsesApiAuthonly accepts the dedicated OpenCodex header:Meanwhile
resolveApiAuthalready acceptsAuthorization: Beareras a fallback, and modern Codexenv_keyinjection populates that standard bearer header.2. Direct-mode forwarding must substitute the admission bearer
Once the caller is admitted, the same bearer is a proxy admission credential and must never be forwarded to ChatGPT/OpenAI. In
codexAccountMode: "direct", when a stored main Codex account exists, the forwarding path must replace it with the real account access token andchatgpt-account-id.The regression tracked in former #1687 was that
validateForwardAdmissionCredentialrejected the admission bearer unconditionally while thectx.kind === "main"substitution path had also been removed. That makes a correctenv_keyrequest fail even after admission is fixed.Required fix
Treat this as one end-to-end contract:
resolveResponsesApiAuthacceptsAuthorization: Bearer <OPENCODEX_API_AUTH_TOKEN>as an admission fallback while preserving the dedicated header path.env_keycontract, with compatibility handling for older runtimes where required.chatgpt-account-id.Security invariant
Accepting the bearer for proxy admission does not mean it may be forwarded upstream. Admission validation and upstream authentication are separate credential domains.
The proxy must therefore preserve both properties:
env_keybearer when its value matches the configured OpenCodex admission token;Acceptance matrix
x-opencodex-api-keywith the valid admission secret -> admitted, unchanged.Authorization: Bearer <valid admission secret>-> admitted.Alternatives considered
env_http_headersand do nothing: breaks modern Codexenv_keyusers./v1/responsesrejects the bearer.Related
env_keybearer flow.