Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 1 addition & 45 deletions agent/conversation_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions agent/conversation_loop_image_recovery.py
Original file line number Diff line number Diff line change
@@ -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",
)
1 change: 1 addition & 0 deletions contributors/emails/andrexibiza@gmail.com
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
andrexibiza
91 changes: 91 additions & 0 deletions tests/agent/test_conversation_loop_image_recovery_seam.py
Original file line number Diff line number Diff line change
@@ -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"]
Loading