From 0c31854e33d9242aa05a22cdf2f97d91b3852b8b Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Wed, 30 Sep 2026 11:34:02 +0800 Subject: [PATCH 1/2] fix(summary): log LLM error response body A bare "400 Bad Request" hid why the provider rejected summary calls (ApodexHarness #646/#648). Log the (truncated) response body on HTTP status errors before the retry decision. Co-Authored-By: Claude Opus 5.5 (1M context) --- agent_core/providers/summary.py | 26 ++++++++++++++++++++++++++ tests/test_summary_engine.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/agent_core/providers/summary.py b/agent_core/providers/summary.py index 858597a..7831fba 100644 --- a/agent_core/providers/summary.py +++ b/agent_core/providers/summary.py @@ -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) @@ -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", diff --git a/tests/test_summary_engine.py b/tests/test_summary_engine.py index 66d3fcf..37e8855 100644 --- a/tests/test_summary_engine.py +++ b/tests/test_summary_engine.py @@ -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)" From ac5f9d55a7c450108f12dbf81ed384eb95bca2ab Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Wed, 30 Sep 2026 11:42:48 +0800 Subject: [PATCH 2/2] docs(changes): note summary HTTP error logging --- changes/summary-log-error-body.fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/summary-log-error-body.fix.md diff --git a/changes/summary-log-error-body.fix.md b/changes/summary-log-error-body.fix.md new file mode 100644 index 0000000..5bc4ab9 --- /dev/null +++ b/changes/summary-log-error-body.fix.md @@ -0,0 +1 @@ +Summary LLM failures now log the provider HTTP response body, making rejected requests easier to diagnose.