fix(auth): retry Codex device-code login on 429 with clear rate-limit message (#47860)

The OpenAI device-code login (POST auth.openai.com/.../deviceauth/usercode)
had no retry or 429 handling — a transient throttle from OpenAI surfaced as
a bare "Device code request returned status 429" with no guidance, reading
as a hard login failure.

- Retry the device-code request with capped exponential backoff (honoring
  Retry-After), up to 4 attempts.
- On persistent 429, raise a clear AuthError tagged CODEX_RATE_LIMITED_CODE
  (classified transient, not a credential problem) with a wait hint.
- Apply the same 429 classification to the token-exchange step (same bug
  class).

Unrelated to PR #47399 (Responses-API cache headers); this is the OAuth
device-code path in hermes_cli/auth.py.
This commit is contained in:
Teknium 2026-06-17 05:48:35 -07:00 committed by GitHub
parent 06d907dc4e
commit cbfa018aef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 150 additions and 12 deletions

View file

@ -7231,23 +7231,61 @@ def _codex_device_code_login() -> Dict[str, Any]:
issuer = "https://auth.openai.com"
client_id = CODEX_OAUTH_CLIENT_ID
# Step 1: Request device code
try:
with httpx.Client(timeout=httpx.Timeout(15.0)) as client:
resp = client.post(
f"{issuer}/api/accounts/deviceauth/usercode",
json={"client_id": client_id},
headers={"Content-Type": "application/json"},
# Step 1: Request device code. OpenAI's auth endpoint rate-limits this
# request (HTTP 429) when login is attempted too often from the same
# IP/account — retry with capped backoff (honoring ``Retry-After``)
# before surfacing a clear, actionable message instead of a bare status.
resp = None
max_attempts = 4
for attempt in range(1, max_attempts + 1):
try:
with httpx.Client(timeout=httpx.Timeout(15.0)) as client:
resp = client.post(
f"{issuer}/api/accounts/deviceauth/usercode",
json={"client_id": client_id},
headers={"Content-Type": "application/json"},
)
except Exception as exc:
raise AuthError(
f"Failed to request device code: {exc}",
provider="openai-codex", code="device_code_request_failed",
)
except Exception as exc:
if resp.status_code != 429:
break
if attempt < max_attempts:
retry_after = _parse_retry_after_seconds(
getattr(resp, "headers", None)
)
# Exponential backoff (2s, 4s, 8s) capped, preferring the
# server-provided Retry-After when present.
delay = retry_after if retry_after is not None else 2 ** attempt
delay = max(1, min(int(delay), 60))
print(
"OpenAI is rate-limiting login requests "
f"(429); retrying in {delay}s..."
)
_time.sleep(delay)
if resp is not None and resp.status_code == 429:
retry_after = _parse_retry_after_seconds(getattr(resp, "headers", None))
wait_hint = (
f" Try again in about {retry_after}s."
if retry_after is not None
else " Wait a minute and run the login again."
)
raise AuthError(
f"Failed to request device code: {exc}",
provider="openai-codex", code="device_code_request_failed",
"OpenAI is rate-limiting Codex login requests (HTTP 429). "
"This is a temporary throttle on OpenAI's side, not a credential "
f"problem.{wait_hint}",
provider="openai-codex", code=CODEX_RATE_LIMITED_CODE,
)
if resp.status_code != 200:
if resp is None or resp.status_code != 200:
status = resp.status_code if resp is not None else "unknown"
raise AuthError(
f"Device code request returned status {resp.status_code}.",
f"Device code request returned status {status}.",
provider="openai-codex", code="device_code_request_error",
)
@ -7335,6 +7373,22 @@ def _codex_device_code_login() -> Dict[str, Any]:
provider="openai-codex", code="token_exchange_failed",
)
if token_resp.status_code == 429:
retry_after = _parse_retry_after_seconds(
getattr(token_resp, "headers", None)
)
wait_hint = (
f" Try again in about {retry_after}s."
if retry_after is not None
else " Wait a minute and run the login again."
)
raise AuthError(
"OpenAI is rate-limiting Codex login requests (HTTP 429) during "
"token exchange. This is a temporary throttle on OpenAI's side, "
f"not a credential problem.{wait_hint}",
provider="openai-codex", code=CODEX_RATE_LIMITED_CODE,
)
if token_resp.status_code != 200:
raise AuthError(
f"Token exchange returned status {token_resp.status_code}.",