diff --git a/strix/core/execution.py b/strix/core/execution.py index 412cf998..6fafd7fa 100644 --- a/strix/core/execution.py +++ b/strix/core/execution.py @@ -57,9 +57,10 @@ logger = logging.getLogger(__name__) StreamEventSink = Callable[[str, Any], None] -_INPUT_REJECTION_CODES = frozenset({400, 404, 422}) -# Replies meaning "this model takes no images", not image errors in general: a -# context overflow that counts "image/vision expansion" must not match. +_INPUT_REJECTION_CODES = frozenset({400, 404, 415, 422}) +# Replies meaning "this model takes no images" or "this image is not a decodable +# image"; stripping images and retrying fixes both. Not image errors in general: +# a context overflow that counts "image/vision expansion" must not match. _IMAGE_REJECTION = re.compile( r"no endpoints found that support image input" # OpenRouter r"|image_url is only supported by certain models" # OpenAI @@ -68,8 +69,15 @@ _IMAGE_REJECTION = re.compile( r"|doesn't support the image field" # Bedrock Converse r"|unknown variant `image_url`" # DeepSeek, e.g. via Vercel AI Gateway r"|'[^']*image[^']*' functionality not supported" # Vercel AI Gateway (AI SDK), unverified - r"|does not contain a valid JPG, PNG, WebP, or ICO image" # xAI: URL fetched, not an image - r"|unsupported content-type encountered when downloading image", # xAI + r"|\binvalid_image\b" # xAI error code + r"|does not contain a valid JPG, PNG, WebP, or ICO image" # xAI + r"|unsupported content-type encountered when downloading image" # xAI + r"|does not represent a valid image" # OpenAI + r"|could not process image" # Anthropic, Bedrock + r"|provided image is not valid" # Gemini + r"|could not be loaded as a valid image" # Mistral + r"|url did not return an image" # OpenRouter + r"|not an allowed image type|cannot identify image file", # vLLM/PIL-based hosts re.IGNORECASE, ) _MAX_COMPACTIONS_PER_CYCLE = 2 diff --git a/tests/test_execution_transient_retry.py b/tests/test_execution_transient_retry.py index 93cd4d16..759938c6 100644 --- a/tests/test_execution_transient_retry.py +++ b/tests/test_execution_transient_retry.py @@ -203,6 +203,33 @@ async def test_run_cycle_does_not_retry_permanent_error( "supported content types are [image/jpeg, image/png, image/webp].", True, ), + ( + 400, + "The image data you provided does not represent a valid image. Please check " + "your input and try again with one of the supported image formats.", + True, + ), + (400, '{"type":"invalid_request_error","message":"Could not process image"}', True), + ( + 400, + '{"code":400,"message":"Provided image is not valid.","status":"INVALID_ARGUMENT"}', + True, + ), + ( + 400, + "Image, 'https://example.com/', could not be loaded as a valid image. " + "Allowed formats are JPEG, PNG, WEBP, GIF", + True, + ), + ( + 415, + '{"error":{"message":"URL did not return an image (received text/error content): ' + 'https://example.com/","code":415}}', + True, + ), + (400, "image_url content type is not an allowed image type: text/html", True), + (400, "cannot identify image file <_io.BytesIO object>", True), + (400, '{"error":{"code":"invalid_image_detail"}}', False), ( 400, "OpenRouterException: Message: This model's maximum context length is 1048576 "