fix(agent): strip images on Kimi/Moonshot 'failed to decode image' 400
Truncated or corrupt image bytes baked into immutable conversation history get re-sent on every retry. Kimi/Moonshot reject them with HTTP 400 'prepare image failed ... failed to decode image: invalid or unsupported image format', which was missing from _IMAGE_REJECTION_PHRASES, so the turn exhausted retries and wedged the session instead of stripping the images and recovering. Adds the phrase to the recovery list plus a regression test mirroring the exact Kimi error body. Complements PR #76896 (proactive full-decode validation in vision_tools) with reactive recovery for already-poisoned sessions. Fixes #76884.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user