fix(agent): log Codex transport failure details

This commit is contained in:
Gille
2026-08-11 21:20:28 -06:00
committed by kshitij
parent bb597e1c02
commit da1170670b
2 changed files with 133 additions and 0 deletions
+81
View File
@@ -27,6 +27,62 @@ from agent.stream_single_writer import claim_stream_writer, stream_writer_is_cur
logger = logging.getLogger(__name__)
def _codex_request_failure_details(error: BaseException) -> tuple[int | None, str]:
"""Return the serialized request size and exception class chain.
OpenAI connection exceptions retain the final ``httpx.Request``. Reading
its already-buffered content gives us the exact byte count handed to the
transport without logging any request content. The class-only chain keeps
the underlying transport failure visible without exposing URLs or payloads
from exception messages.
"""
request_body_bytes: int | None = None
exception_classes: list[str] = []
current: BaseException | None = error
seen: set[int] = set()
while current is not None and id(current) not in seen and len(seen) < 8:
seen.add(id(current))
exception_classes.append(type(current).__name__)
if request_body_bytes is None:
request = getattr(current, "request", None)
if request is not None:
try:
content = request.content
except Exception:
content = None
if isinstance(content, str):
request_body_bytes = len(content.encode("utf-8"))
elif isinstance(content, (bytes, bytearray, memoryview)):
request_body_bytes = len(content)
cause = current.__cause__
if cause is None and not current.__suppress_context__:
cause = current.__context__
current = cause
return request_body_bytes, " <- ".join(exception_classes)
def _log_codex_request_failure(
agent: Any,
error: BaseException,
*,
stream_opened: bool,
) -> None:
request_body_bytes, exception_chain = _codex_request_failure_details(error)
logger.warning(
"Codex Responses request failed: "
"serialized_request_body_bytes=%s stream_opened=%s "
"exception_chain=%s model=%s",
request_body_bytes if request_body_bytes is not None else "unknown",
str(stream_opened).lower(),
exception_chain,
getattr(agent, "model", "unknown"),
)
def _coerce_usage_int(value: Any) -> int:
if isinstance(value, bool):
return 0
@@ -1253,6 +1309,7 @@ def run_codex_stream(agent, api_kwargs: dict, client: Any = None, on_first_delta
the terminal event's ``output`` field.
"""
import httpx as _httpx
from openai import APIConnectionError as _APIConnectionError
from agent import relay_llm
@@ -1356,6 +1413,18 @@ def run_codex_stream(agent, api_kwargs: dict, client: Any = None, on_first_delta
exc,
)
continue
_log_codex_request_failure(
agent,
exc,
stream_opened=writer_token["value"] is not None,
)
raise
except _APIConnectionError as exc:
_log_codex_request_failure(
agent,
exc,
stream_opened=writer_token["value"] is not None,
)
raise
def _interrupt_or_superseded() -> bool:
@@ -1389,11 +1458,23 @@ def run_codex_stream(agent, api_kwargs: dict, client: Any = None, on_first_delta
agent._client_log_context(), exc,
)
continue
_log_codex_request_failure(
agent,
exc,
stream_opened=writer_token["value"] is not None,
)
raise
except RuntimeError:
if event_stream.final_response is not None:
return event_stream.final_response
raise
except _APIConnectionError as exc:
_log_codex_request_failure(
agent,
exc,
stream_opened=writer_token["value"] is not None,
)
raise
# A terminal response has already been assembled at this point
# (``final`` is built), so a transport error while draining the
@@ -0,0 +1,52 @@
"""Diagnostics for Codex Responses transport failures."""
from __future__ import annotations
import logging
from types import SimpleNamespace
import httpx
import pytest
from openai import APIConnectionError
from agent.codex_runtime import run_codex_stream
def test_transport_failure_logs_exact_request_bytes_and_class_chain(caplog):
request_content = b'{"input":"payload"}'
request = httpx.Request(
"POST",
"https://example.invalid/responses",
content=request_content,
)
transport_error = httpx.RemoteProtocolError(
"server disconnected without sending a response",
request=request,
)
connection_error = APIConnectionError(request=request)
connection_error.__cause__ = transport_error
class FailingResponses:
def create(self, **_kwargs):
raise connection_error
client = SimpleNamespace(responses=FailingResponses())
agent = SimpleNamespace(
_interrupt_requested=False,
_current_api_request_id="request-id",
_fallback_index=0,
is_subagent=False,
model="gpt-5.6-sol",
provider="openai-codex",
session_id="",
)
with caplog.at_level(logging.WARNING, logger="agent.codex_runtime"):
with pytest.raises(APIConnectionError):
run_codex_stream(agent, {"model": "gpt-5.6-sol"}, client=client)
message = caplog.messages[-1]
assert f"serialized_request_body_bytes={len(request_content)}" in message
assert "stream_opened=false" in message
assert "exception_chain=APIConnectionError <- RemoteProtocolError" in message
assert "payload" not in message