fix(errors): log when the empty-stub guard and malformed-body classification fire

Add distinct, greppable log lines at both fix sites so the condition is
observable in the field instead of only inferable from the absence of the old
'Cannot compress further' spiral:

- chat_completion_helpers: warn when an empty partial-stream stub is replaced
  with the placeholder (0 chars recovered, no tool call).
- error_classifier: warn when a malformed-body 400 is classified as
  format_error rather than context overflow, with num_messages/approx_tokens.

Extend the litellm-shape classifier test to assert the warning is emitted.
This commit is contained in:
Aaron Weiker 2026-07-25 09:07:20 -07:00 committed by Teknium
parent 207a6c969d
commit 4587d77e0e
3 changed files with 28 additions and 5 deletions

View file

@ -3959,6 +3959,14 @@ def interruptible_streaming_api_call(agent, api_kwargs: dict, *, on_first_delta=
# and the continuation prompt still reads as an interrupted turn.
if not _partial_text:
_partial_text = "[response interrupted]"
logger.warning(
"Empty partial-stream stub (0 chars recovered, no tool "
"call) — substituting placeholder content so the assistant "
"message is not persisted empty (would otherwise 400 every "
"later request with 'messages must have non-empty content' "
"/ INVALID_REQUEST_BODY). error=%s",
result["error"],
)
_stub_msg = SimpleNamespace(
role="assistant", content=_partial_text, tool_calls=None,
reasoning_content=None,

View file

@ -1326,6 +1326,14 @@ def _classify_400(
any(p in error_msg for p in _INVALID_MESSAGE_BODY_PATTERNS)
or error_code_lower == "invalid_request_body"
):
logger.warning(
"Malformed message array 400 (invalid request body) classified as "
"format_error, NOT context overflow — failing fast + falling back "
"instead of entering the compression loop. This usually means an "
"empty-content assistant stub is in the transcript; num_messages=%s "
"approx_tokens=%s. error=%.200s",
num_messages, approx_tokens, error_msg,
)
return result_fn(
FailoverReason.format_error,
retryable=False,

View file

@ -1326,7 +1326,7 @@ class TestClassifyApiError:
assert result.retryable is False
assert result.should_compress is not True
def test_400_litellm_invalid_request_body_shape(self):
def test_400_litellm_invalid_request_body_shape(self, caplog):
"""litellm/Bedrock proxy shape (errorMessage/errorCode) → format_error.
The proxy in front of Anthropic surfaces the empty-content rejection
@ -1335,8 +1335,10 @@ class TestClassifyApiError:
are not the standard error.message / message, so err_body_msg used to
come back empty is_generic=True mis-routed into compression on a
large session. Both the message pattern and the errorCode must be
recognized.
recognized, and a distinct warning must be logged so the condition is
observable in the field.
"""
import logging
proxy_msg = ("The provided request body is invalid: claude "
"messages.208: all messages must have non-empty content "
"except for the optional final assistant message")
@ -1350,12 +1352,17 @@ class TestClassifyApiError:
"errorArgs": {"reason": "claude messages.208: ..."},
},
)
result = classify_api_error(
e, approx_tokens=66000, context_length=200000, num_messages=219,
)
with caplog.at_level(logging.WARNING, logger="agent.error_classifier"):
result = classify_api_error(
e, approx_tokens=66000, context_length=200000, num_messages=219,
)
assert result.reason == FailoverReason.format_error
assert result.retryable is False
assert result.should_compress is not True
assert any(
"Malformed message array 400" in r.getMessage()
for r in caplog.records
), "Expected a distinct warning identifying the malformed-body 400"
def test_400_real_context_overflow_still_compresses(self):
"""Guard: the new empty-content guard must NOT swallow real overflows.