diff --git a/agent/error_classifier.py b/agent/error_classifier.py index b6358161bb..7bcfbaf0cb 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -284,6 +284,16 @@ _IMAGE_TOO_LARGE_PATTERNS = [ "image dimensions exceed", # Anthropic: "image dimensions exceed max allowed size: 8000 pixels" "dimensions exceed max allowed size", # Anthropic dimension-cap (wording variant) "max allowed size: 8000", # Anthropic dimension-cap (explicit pixel ceiling) + # Vendors that reject the same oversized image without using the word + # "image". MiniMax's Anthropic-compatible endpoint returns + # "media exceeds size limit: max 10485760 bytes (2013)" for a native + # image part above its 10 MB ceiling (#76039). Matched on the "media" + # fragment to mirror "image exceeds" above and catch reworded variants. + # A non-image media rejection (audio/video) that lands here is safe: the + # shrink pass finds no image parts, returns False, and the caller + # surfaces the original error unchanged. + "media exceeds", + "media too large", # "request_too_large" on a request known to contain an image → image is # the likely culprit; we still try the shrink path before giving up. ] diff --git a/tests/run_agent/test_image_shrink_recovery.py b/tests/run_agent/test_image_shrink_recovery.py index 2ff4519273..0c2984b4e2 100644 --- a/tests/run_agent/test_image_shrink_recovery.py +++ b/tests/run_agent/test_image_shrink_recovery.py @@ -53,8 +53,39 @@ class TestImageTooLargeClassification: assert result.reason == FailoverReason.image_too_large assert result.retryable is True + def test_minimax_400_media_exceeds_message(self): + """A vendor that rejects an oversized image without the word "image". + MiniMax's Anthropic-compatible endpoint returns this for a native + image part over its 10 MB ceiling. It used to fall through to + _REQUEST_VALIDATION_PATTERNS and classify as format_error / + non-retryable, which skipped shrink recovery and left the oversized + part baked into replayed history — every later turn re-sent the same + bytes and failed identically (#76039). + """ + err = _FakeApiError( + status_code=400, + message="media exceeds size limit: max 10485760 bytes (2013)", + body={ + "type": "error", + "error": { + "type": "invalid_request_error", + "message": "media exceeds size limit: max 10485760 bytes (2013)", + }, + }, + ) + result = classify_api_error(err, provider="minimax", model="MiniMax-M3") + assert result.reason == FailoverReason.image_too_large + assert result.retryable is True + def test_unrelated_400_still_not_image_too_large(self): + """The new "media" patterns must not widen into ordinary 400s.""" + err = _FakeApiError( + status_code=400, + message="messages: unexpected role \"tool\" at index 3", + ) + result = classify_api_error(err, provider="minimax", model="MiniMax-M3") + assert result.reason != FailoverReason.image_too_large