Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions agent_core/providers/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ class SummaryCandidate(TypedDict):
})


_ERROR_BODY_LOG_LIMIT = 2000


def _error_body_excerpt(response: httpx.Response) -> str:
try:
body = response.text
except Exception: # body may be unread or undecodable
return "(unreadable)"
body = body.strip()
if not body:
return "(empty)"
if len(body) > _ERROR_BODY_LOG_LIMIT:
return body[:_ERROR_BODY_LOG_LIMIT] + f"...[truncated {len(body) - _ERROR_BODY_LOG_LIMIT} chars]"
return body


def default_summary_retryable(error: Exception) -> bool:
"""Retry transport faults and 5xx, but not permanent request errors."""
status = getattr(getattr(error, "response", None), "status_code", None)
Expand Down Expand Up @@ -222,6 +238,16 @@ async def _summarize_one(
return ""
except Exception as error:
logger.warning("Summary LLM attempt %d failed: %s", attempt + 1, error)
if isinstance(error, httpx.HTTPStatusError):
# The status line alone ("400 Bad Request") hides why the
# provider rejected the request; log the response body.
logger.warning(
"Summary LLM HTTP %d response body (candidate %d, model %s): %s",
error.response.status_code,
candidate_index + 1,
model,
_error_body_excerpt(error.response),
)
if not self.retryable(error):
logger.warning(
"Summary candidate %d failed permanently; not retrying",
Expand Down
1 change: 1 addition & 0 deletions changes/summary-log-error-body.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Summary LLM failures now log the provider HTTP response body, making rejected requests easier to diagnose.
32 changes: 32 additions & 0 deletions tests/test_summary_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,35 @@ def err(status: int) -> httpx.HTTPStatusError:
assert default_summary_retryable(err(429))
assert default_summary_retryable(err(503))
assert default_summary_retryable(TimeoutError("timeout"))


def test_error_response_body_is_logged(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
"""A bare "400 Bad Request" hides why the provider rejected the call."""
reason = '{"error":{"message":"invalid image_url in content"}}'
_install(monkeypatch, [_Response(400, reason)])

async def sleep(_delay: float) -> None:
return None

engine = SummaryLLMEngine(sleep=sleep, fallback_limit=10)
with caplog.at_level("WARNING", logger=summary_mod.__name__):
asyncio.run(
engine.summarize(
"content",
"focus",
[{"endpoint": "https://a/v1/chat/completions", "model": "m1"}],
),
)
assert any(
"HTTP 400 response body" in r.getMessage() and reason in r.getMessage()
for r in caplog.records
)


def test_error_body_excerpt_truncates() -> None:
long = "x" * (summary_mod._ERROR_BODY_LOG_LIMIT + 50)
out = summary_mod._error_body_excerpt(httpx.Response(400, text=long))
assert out.endswith("[truncated 50 chars]")
assert summary_mod._error_body_excerpt(httpx.Response(400, text="")) == "(empty)"
Loading