From 351915781b373c86c68a3e59c23893f103f61ae9 Mon Sep 17 00:00:00 2001 From: Axl Ibiza Date: Wed, 12 Aug 2026 00:28:21 -0500 Subject: [PATCH 1/2] refactor(agent): extract image rejection phrases Signed-off-by: Axl Ibiza --- agent/conversation_loop.py | 46 +--------- agent/conversation_loop_image_recovery.py | 45 +++++++++ ...t_conversation_loop_image_recovery_seam.py | 91 +++++++++++++++++++ 3 files changed, 137 insertions(+), 45 deletions(-) create mode 100644 agent/conversation_loop_image_recovery.py create mode 100644 tests/agent/test_conversation_loop_image_recovery_seam.py diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 1a236e6eacc55..74a6fa84d3444 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -35,6 +35,7 @@ compression_skipped_due_to_lock, conversation_history_after_compression, ) +from agent.conversation_loop_image_recovery import _IMAGE_REJECTION_PHRASES from agent.context_engine import automatic_compaction_status_message from agent.display import KawaiiSpinner from agent.error_classifier import FailoverReason, classify_api_error @@ -4045,51 +4046,6 @@ def _perform_api_call(next_api_kwargs): except Exception: pass _err_status = getattr(api_error, "status_code", None) - _IMAGE_REJECTION_PHRASES = ( - "only 'text' content type is supported", - "only text content type is supported", - "image_url is not supported", - "image content is not supported", - "multimodal is not supported", - "multimodal content is not supported", - "multimodal input is not supported", - "vision is not supported", - "vision input is not supported", - "does not support images", - "does not support image input", - "does not support multimodal", - "does not support vision", - "model does not support image", - # ChatGPT-account Codex backend - # (https://chatgpt.com/backend-api/codex) rejects - # data:image/...base64 URLs in input_image fields - # with HTTP 400 "Invalid 'input[N].content[K].image_url'. - # Expected a valid URL, but got a value with an - # invalid format." The OpenAI Responses API on the - # public endpoint accepts data URLs, but the - # ChatGPT-account variant does not. Without this - # phrase the agent cascaded into compression / - # context-too-large recovery instead of just - # stripping the images. Match is narrow on - # purpose — keyed on the field-path apostrophe so - # we don't false-trip on other URL validation - # errors. (issue #23570) - "image_url'. expected", - # DeepSeek's OpenAI-compatible API reports text-only - # request-body variants as: - # "unknown variant `image_url`, expected `text`". - "unknown variant `image_url`, expected `text`", - "unknown variant image_url, expected text", - # OpenRouter routes a request to upstream endpoints and, - # when none of the candidate endpoints for the model accept - # image input, returns HTTP 404 "No endpoints found that - # support image input". Without this phrase the agent never - # strips the images, the retry loop re-sends the same - # rejected request until exhaustion, and the gateway leaves - # 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", - ) _err_lower = _err_body.lower() _looks_like_image_rejection = any( p in _err_lower for p in _IMAGE_REJECTION_PHRASES diff --git a/agent/conversation_loop_image_recovery.py b/agent/conversation_loop_image_recovery.py new file mode 100644 index 0000000000000..35306abc60655 --- /dev/null +++ b/agent/conversation_loop_image_recovery.py @@ -0,0 +1,45 @@ +_IMAGE_REJECTION_PHRASES = ( + "only 'text' content type is supported", + "only text content type is supported", + "image_url is not supported", + "image content is not supported", + "multimodal is not supported", + "multimodal content is not supported", + "multimodal input is not supported", + "vision is not supported", + "vision input is not supported", + "does not support images", + "does not support image input", + "does not support multimodal", + "does not support vision", + "model does not support image", + # ChatGPT-account Codex backend + # (https://chatgpt.com/backend-api/codex) rejects + # data:image/...base64 URLs in input_image fields + # with HTTP 400 "Invalid 'input[N].content[K].image_url'. + # Expected a valid URL, but got a value with an + # invalid format." The OpenAI Responses API on the + # public endpoint accepts data URLs, but the + # ChatGPT-account variant does not. Without this + # phrase the agent cascaded into compression / + # context-too-large recovery instead of just + # stripping the images. Match is narrow on + # purpose — keyed on the field-path apostrophe so + # we don't false-trip on other URL validation + # errors. (issue #23570) + "image_url'. expected", + # DeepSeek's OpenAI-compatible API reports text-only + # request-body variants as: + # "unknown variant `image_url`, expected `text`". + "unknown variant `image_url`, expected `text`", + "unknown variant image_url, expected text", + # OpenRouter routes a request to upstream endpoints and, + # when none of the candidate endpoints for the model accept + # image input, returns HTTP 404 "No endpoints found that + # support image input". Without this phrase the agent never + # strips the images, the retry loop re-sends the same + # rejected request until exhaustion, and the gateway leaves + # 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", +) diff --git a/tests/agent/test_conversation_loop_image_recovery_seam.py b/tests/agent/test_conversation_loop_image_recovery_seam.py new file mode 100644 index 0000000000000..d790b96e24a27 --- /dev/null +++ b/tests/agent/test_conversation_loop_image_recovery_seam.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import ast +import inspect +from pathlib import Path + +from agent import conversation_loop +from agent.conversation_loop_image_recovery import _IMAGE_REJECTION_PHRASES + + +_EXPECTED_IMAGE_REJECTION_PHRASES = ( + "only 'text' content type is supported", + "only text content type is supported", + "image_url is not supported", + "image content is not supported", + "multimodal is not supported", + "multimodal content is not supported", + "multimodal input is not supported", + "vision is not supported", + "vision input is not supported", + "does not support images", + "does not support image input", + "does not support multimodal", + "does not support vision", + "model does not support image", + "image_url'. expected", + "unknown variant `image_url`, expected `text`", + "unknown variant image_url, expected text", + "no endpoints found that support image input", +) + + +_MODULE_PATH = Path(__file__).parents[2] / "agent" / "conversation_loop_image_recovery.py" +_LOOP_PATH = Path(conversation_loop.__file__) + + +def test_image_rejection_phrase_tuple_preserves_value_order_and_identity(): + assert _IMAGE_REJECTION_PHRASES == _EXPECTED_IMAGE_REJECTION_PHRASES + assert conversation_loop._IMAGE_REJECTION_PHRASES is _IMAGE_REJECTION_PHRASES + + +def test_original_module_resolves_private_compatibility_reference(): + source = inspect.getsource(conversation_loop.run_conversation) + assert "_IMAGE_REJECTION_PHRASES" in source + assert conversation_loop._IMAGE_REJECTION_PHRASES is _IMAGE_REJECTION_PHRASES + + +def test_original_module_has_no_duplicate_local_tuple_definition(): + tree = ast.parse(_LOOP_PATH.read_text(encoding="utf-8")) + local_assignments = [ + node + for node in ast.walk(tree) + if isinstance(node, (ast.Assign, ast.AnnAssign, ast.NamedExpr)) + and any( + isinstance(target, ast.Name) + and target.id == "_IMAGE_REJECTION_PHRASES" + for target in ( + list(node.targets) + if isinstance(node, ast.Assign) + else [node.target] + ) + ) + ] + assert local_assignments == [] + assert "from agent.conversation_loop_image_recovery import _IMAGE_REJECTION_PHRASES" in ( + _LOOP_PATH.read_text(encoding="utf-8") + ) + + +def test_image_recovery_module_has_no_import_time_side_effect_nodes(): + tree = ast.parse(_MODULE_PATH.read_text(encoding="utf-8")) + assert not any( + isinstance(node, (ast.Import, ast.ImportFrom)) + and not ( + isinstance(node, ast.ImportFrom) + and node.module == "__future__" + ) + for node in tree.body + ) + assert not any( + isinstance(node, ast.Expr) + and isinstance(node.value, ast.Call) + for node in tree.body + ) + assert [ + target.id + for node in tree.body + if isinstance(node, ast.Assign) + for target in node.targets + if isinstance(target, ast.Name) + ] == ["_IMAGE_REJECTION_PHRASES"] From 1d40006e04808bd8b3b6503005c7599c7b145e38 Mon Sep 17 00:00:00 2001 From: "Axl Ibiza, MBA" Date: Wed, 12 Aug 2026 01:03:41 -0500 Subject: [PATCH 2/2] ci(attribution): add andrexibiza email mapping for campaign PRs Contributor email mapping required by check-attribution CI for the god-file kill campaign PRs (Axl Ibiza, MBA ). Signed-off-by: Axl Ibiza, MBA --- contributors/emails/andrexibiza@gmail.com | 1 + 1 file changed, 1 insertion(+) create mode 100644 contributors/emails/andrexibiza@gmail.com diff --git a/contributors/emails/andrexibiza@gmail.com b/contributors/emails/andrexibiza@gmail.com new file mode 100644 index 0000000000000..01c4cfb74a35b --- /dev/null +++ b/contributors/emails/andrexibiza@gmail.com @@ -0,0 +1 @@ +andrexibiza