fix(codex): pin correct Cloudflare headers and extend to auxiliary client

The cherry-picked salvage (admin28980's commit) added codex headers only on the
primary chat client path, with two inaccuracies:

  - originator was 'hermes-agent' — Cloudflare whitelists codex_cli_rs,
    codex_vscode, codex_sdk_ts, and Codex* prefixes. 'hermes-agent' isn't on
    the list, so the header had no mitigating effect on the 403 (the
    account-id header alone may have been carrying the fix).
  - account-id header was 'ChatGPT-Account-Id' — upstream codex-rs auth.rs
    uses canonical 'ChatGPT-Account-ID' (PascalCase, trailing -ID).

Also, the auxiliary client (_try_codex + resolve_provider_client raw_codex
branch) constructs OpenAI clients against the same chatgpt.com endpoint with
no default headers at all — so compression, title generation, vision, session
search, and web_extract all still 403 from VPS IPs.

Consolidate the header set into _codex_cloudflare_headers() in
agent/auxiliary_client.py (natural home next to _read_codex_access_token and
the existing JWT decode logic) and call it from all four insertion points:

  - run_agent.py: AIAgent.__init__ (initial construction)
  - run_agent.py: _apply_client_headers_for_base_url (credential rotation)
  - agent/auxiliary_client.py: _try_codex (aux client)
  - agent/auxiliary_client.py: resolve_provider_client raw_codex branch

Net: -36/+55 lines, -25 lines of duplicated inline JWT decode replaced by a
single helper. User-Agent switched to 'codex_cli_rs/0.0.0 (Hermes Agent)' to
match the codex-rs shape while keeping product attribution.

Tests in tests/agent/test_codex_cloudflare_headers.py cover:
  - originator value, User-Agent shape, canonical header casing
  - account-ID extraction from a real JWT fixture
  - graceful handling of malformed / non-string / claim-missing tokens
  - wiring at all four insertion points (primary init, rotation, both aux paths)
  - non-chatgpt base URLs (openrouter) do NOT get codex headers
  - switching away from chatgpt.com drops the headers
This commit is contained in:
Teknium
2026-04-19 11:58:15 -07:00
committed by Teknium
parent 4d0846b640
commit cca3278079
3 changed files with 308 additions and 32 deletions

View File

@@ -1078,22 +1078,8 @@ class AIAgent:
elif "portal.qwen.ai" in effective_base.lower():
client_kwargs["default_headers"] = _qwen_portal_headers()
elif "chatgpt.com" in effective_base.lower():
# Match official Codex CLI headers to avoid Cloudflare challenges.
# The ChatGPT-Account-Id header is critical — without it,
# server-hosted agents get 403 Cloudflare JS challenges.
_codex_headers = {
"User-Agent": "hermes-agent/1.0",
"originator": "hermes-agent",
}
try:
import base64 as _b64
_jwt_payload = json.loads(_b64.b64decode(api_key.split(".")[1] + "=="))
_acct_id = _jwt_payload.get("https://api.openai.com/auth", {}).get("chatgpt_account_id")
if _acct_id:
_codex_headers["ChatGPT-Account-Id"] = _acct_id
except Exception:
pass
client_kwargs["default_headers"] = _codex_headers
from agent.auxiliary_client import _codex_cloudflare_headers
client_kwargs["default_headers"] = _codex_cloudflare_headers(api_key)
else:
# No explicit creds — use the centralized provider router
from agent.auxiliary_client import resolve_provider_client
@@ -5330,20 +5316,10 @@ class AIAgent:
elif "portal.qwen.ai" in normalized:
self._client_kwargs["default_headers"] = _qwen_portal_headers()
elif "chatgpt.com" in normalized:
_codex_headers = {
"User-Agent": "hermes-agent/1.0",
"originator": "hermes-agent",
}
try:
import base64 as _b64
_ak = self._client_kwargs.get("api_key", "")
_jwt_payload = json.loads(_b64.b64decode(_ak.split(".")[1] + "=="))
_acct_id = _jwt_payload.get("https://api.openai.com/auth", {}).get("chatgpt_account_id")
if _acct_id:
_codex_headers["ChatGPT-Account-Id"] = _acct_id
except Exception:
pass
self._client_kwargs["default_headers"] = _codex_headers
from agent.auxiliary_client import _codex_cloudflare_headers
self._client_kwargs["default_headers"] = _codex_cloudflare_headers(
self._client_kwargs.get("api_key", "")
)
else:
self._client_kwargs.pop("default_headers", None)