fix(agent): log Codex transport failure details
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user