diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 7520a1fcc4..99da6974ca 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -4686,6 +4686,17 @@ def run_conversation( # every subsequent message queued behind the stuck turn — # the P1 in issue #21160. The 404 passes the 4xx gate below. "no endpoints found that support image input", + # Kimi / Moonshot / other OpenAI-compatible Chinese + # providers reject truncated or corrupt image bytes with + # HTTP 400 "Invalid request: prepare image failed ... + # failed to decode image: invalid or unsupported image + # format". Like the Codex case above, the bad bytes are + # baked into immutable conversation history and re-sent on + # every retry, wedging the session. Strip the images so the + # turn recovers instead of exhausting retries. (issue + # #76884; complements the proactive full-decode validation + # in tools/vision_tools._normalize_to_supported_image) + "failed to decode image", ) _err_lower = _err_body.lower() _looks_like_image_rejection = any( diff --git a/tests/run_agent/test_image_rejection_fallback.py b/tests/run_agent/test_image_rejection_fallback.py index cd254ee6aa..d4a643e7dd 100644 --- a/tests/run_agent/test_image_rejection_fallback.py +++ b/tests/run_agent/test_image_rejection_fallback.py @@ -139,12 +139,21 @@ class TestImageRejectionPhraseIsolation: "model does not support image", "image_url'. expected", "no endpoints found that support image input", + "failed to decode image", ) def _matches(self, body: str) -> bool: low = body.lower() return any(p in low for p in self._REJECTION_PHRASES) + def test_kimi_truncated_image_trips_recovery(self): + # Kimi/Moonshot reject truncated image bytes with this 400; the + # bad bytes are in immutable history so stripping must fire. + body = ("HTTP 400: Invalid request: prepare image failed error, " + "status code: 400, message: failed to decode image: invalid " + "or unsupported image format") + assert self._matches(body) is True + def test_anthropic_image_too_large_does_not_trip(self): # From agent/error_classifier.py _IMAGE_TOO_LARGE_PATTERNS — # these must route to image_too_large / _try_shrink_image_parts_in_messages,