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:
fkdls112
2026-08-08 05:04:13 +08:00
committed by Teknium
parent 564113572e
commit cd72689e03
2 changed files with 20 additions and 0 deletions
+11
View File
@@ -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,