Handle blank Codex transport errors
This commit is contained in:
@@ -84,9 +84,21 @@ class OpenAICodexProvider(LLMProvider):
|
||||
)
|
||||
return LLMResponse(content=content, tool_calls=tool_calls, finish_reason=finish_reason)
|
||||
except Exception as e:
|
||||
msg = f"Error calling Codex: {e}"
|
||||
retry_after = getattr(e, "retry_after", None) or self._extract_retry_after(msg)
|
||||
return LLMResponse(content=msg, finish_reason="error", retry_after=retry_after)
|
||||
response = _codex_error_response(e)
|
||||
exc_type = "CodexHTTPError" if isinstance(e, _CodexHTTPError) else type(e).__name__
|
||||
logger.warning(
|
||||
"Codex API request failed: type={} kind={} retryable={} status={} "
|
||||
"error_type={} error_code={} retry_after={} summary={}",
|
||||
exc_type,
|
||||
response.error_kind,
|
||||
response.error_should_retry,
|
||||
response.error_status_code,
|
||||
response.error_type,
|
||||
response.error_code,
|
||||
response.retry_after,
|
||||
_codex_log_summary(exc_type, response),
|
||||
)
|
||||
return response
|
||||
|
||||
async def chat(
|
||||
self, messages: list[dict[str, Any]], tools: list[dict[str, Any]] | None = None,
|
||||
@@ -139,9 +151,22 @@ def _build_headers(account_id: str, token: str) -> dict[str, str]:
|
||||
|
||||
|
||||
class _CodexHTTPError(RuntimeError):
|
||||
def __init__(self, message: str, retry_after: float | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
*,
|
||||
status_code: int | None = None,
|
||||
retry_after: float | None = None,
|
||||
error_type: str | None = None,
|
||||
error_code: str | None = None,
|
||||
should_retry: bool | None = None,
|
||||
):
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
self.retry_after = retry_after
|
||||
self.error_type = error_type
|
||||
self.error_code = error_code
|
||||
self.should_retry = should_retry
|
||||
|
||||
|
||||
async def _request_codex(
|
||||
@@ -156,10 +181,16 @@ async def _request_codex(
|
||||
async with client.stream("POST", url, headers=headers, json=body) as response:
|
||||
if response.status_code != 200:
|
||||
text = await response.aread()
|
||||
raw = text.decode("utf-8", "ignore")
|
||||
retry_after = LLMProvider._extract_retry_after_from_headers(response.headers)
|
||||
error_type, error_code = LLMProvider._extract_error_type_code(raw)
|
||||
raise _CodexHTTPError(
|
||||
_friendly_error(response.status_code, text.decode("utf-8", "ignore")),
|
||||
_friendly_error(response.status_code, raw),
|
||||
status_code=response.status_code,
|
||||
retry_after=retry_after,
|
||||
error_type=error_type,
|
||||
error_code=error_code,
|
||||
should_retry=_should_retry_status(response.status_code, error_type, error_code, raw),
|
||||
)
|
||||
return await consume_sse(response, on_content_delta, on_tool_call_delta)
|
||||
|
||||
@@ -170,6 +201,94 @@ def _prompt_cache_key(messages: list[dict[str, Any]]) -> str:
|
||||
|
||||
|
||||
def _friendly_error(status_code: int, raw: str) -> str:
|
||||
_ = raw
|
||||
if status_code == 429:
|
||||
return "ChatGPT usage quota exceeded or rate limit triggered. Please try again later."
|
||||
return f"HTTP {status_code}: {raw}"
|
||||
return f"HTTP {status_code}: Codex API request failed"
|
||||
|
||||
|
||||
def _codex_error_response(exc: Exception) -> LLMResponse:
|
||||
"""Convert Codex transport/API failures into actionable, retryable metadata."""
|
||||
exc_type = "CodexHTTPError" if isinstance(exc, _CodexHTTPError) else type(exc).__name__
|
||||
detail = str(exc).strip()
|
||||
|
||||
status_code = getattr(exc, "status_code", None)
|
||||
error_kind: str | None = None
|
||||
default_detail: str | None = None
|
||||
should_retry: bool | None = getattr(exc, "should_retry", None)
|
||||
|
||||
if isinstance(exc, (httpx.TimeoutException, asyncio.TimeoutError)):
|
||||
error_kind = "timeout"
|
||||
default_detail = "timed out waiting for response"
|
||||
should_retry = True if should_retry is None else should_retry
|
||||
elif isinstance(exc, httpx.RemoteProtocolError):
|
||||
error_kind = "connection"
|
||||
default_detail = "network protocol error while reading response"
|
||||
should_retry = True if should_retry is None else should_retry
|
||||
elif isinstance(exc, (httpx.NetworkError, httpx.TransportError)):
|
||||
error_kind = "connection"
|
||||
default_detail = "network connection failed"
|
||||
should_retry = True if should_retry is None else should_retry
|
||||
elif isinstance(exc, _CodexHTTPError):
|
||||
error_kind = "http"
|
||||
default_detail = "HTTP request failed"
|
||||
|
||||
if status_code is not None and should_retry is None:
|
||||
retry_content = None if int(status_code) == 429 and isinstance(exc, _CodexHTTPError) else detail
|
||||
should_retry = _should_retry_status(
|
||||
int(status_code),
|
||||
getattr(exc, "error_type", None),
|
||||
getattr(exc, "error_code", None),
|
||||
retry_content,
|
||||
)
|
||||
|
||||
detail = detail or default_detail or "unexpected error"
|
||||
message = f"Error calling Codex ({exc_type}): {detail}"
|
||||
retry_after = getattr(exc, "retry_after", None) or LLMProvider._extract_retry_after(message)
|
||||
return LLMResponse(
|
||||
content=message,
|
||||
finish_reason="error",
|
||||
retry_after=retry_after,
|
||||
error_status_code=int(status_code) if status_code is not None else None,
|
||||
error_kind=error_kind,
|
||||
error_type=getattr(exc, "error_type", None),
|
||||
error_code=getattr(exc, "error_code", None),
|
||||
error_retry_after_s=retry_after,
|
||||
error_should_retry=should_retry,
|
||||
)
|
||||
|
||||
|
||||
def _codex_log_summary(exc_type: str, response: LLMResponse) -> str:
|
||||
"""Return a bounded diagnostic summary without request body or raw upstream payload."""
|
||||
if response.error_status_code is not None:
|
||||
parts = [f"HTTP {response.error_status_code}"]
|
||||
if response.error_type:
|
||||
parts.append(f"type={response.error_type}")
|
||||
if response.error_code:
|
||||
parts.append(f"code={response.error_code}")
|
||||
return " ".join(parts)
|
||||
|
||||
kind = (response.error_kind or "").strip()
|
||||
if kind:
|
||||
return f"{exc_type} {kind}"
|
||||
|
||||
return exc_type
|
||||
|
||||
|
||||
def _should_retry_status(
|
||||
status_code: int,
|
||||
error_type: str | None,
|
||||
error_code: str | None,
|
||||
content: str | None,
|
||||
) -> bool:
|
||||
if status_code == 429:
|
||||
return LLMProvider._is_retryable_429_response(
|
||||
LLMResponse(
|
||||
content=content or "",
|
||||
finish_reason="error",
|
||||
error_status_code=status_code,
|
||||
error_type=error_type,
|
||||
error_code=error_code,
|
||||
)
|
||||
)
|
||||
return status_code in LLMProvider._RETRYABLE_STATUS_CODES or status_code >= 500
|
||||
|
||||
Reference in New Issue
Block a user