fix(image-routing): unblock message queue on OpenRouter 'no endpoints' image 404 (#53901)

The agent's image-rejection fallback strips images and retries text-only when
a provider rejects image content, which is what lets the gateway drain its
queued messages. The fallback only fires on a hardcoded phrase list, and the
OpenRouter wording — HTTP 404 'No endpoints found that support image input' —
was missing. For OpenRouter-routed non-vision models the fallback never fired,
the retry loop re-sent the same rejected request until exhaustion, and every
subsequent message (including plain text) stayed queued behind the stuck turn.

Add the phrase to _IMAGE_REJECTION_PHRASES (the 404 already passes the 4xx
gate). Add a positive test and a guard test so the sibling OpenRouter
'no endpoints ... data policy / guardrail' 404s do NOT get their images
stripped.

Fixes #21160. Reported by @liu14goal14-ux; PR #21198 by @ygd58.
This commit is contained in:
Teknium 2026-06-27 19:07:02 -07:00 committed by GitHub
parent a94f657a50
commit 1a570dae00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 28 additions and 0 deletions

View file

@ -2259,6 +2259,15 @@ def run_conversation(
# "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(

View file

@ -195,6 +195,7 @@ class TestImageRejectionPhraseIsolation:
"does not support vision",
"model does not support image",
"image_url'. expected",
"no endpoints found that support image input",
)
def _matches(self, body: str) -> bool:
@ -244,10 +245,28 @@ class TestImageRejectionPhraseIsolation:
# match the agent cascaded into compression / context-too-large
# recovery instead of just stripping the images.
"Invalid 'input[56].content[1].image_url'. Expected a valid URL, but got a value with an invalid format.",
# OpenRouter 404 when no upstream endpoint for the model accepts
# image input — issue #21160. The exact wording from the report.
"HTTP 404: No endpoints found that support image input",
]
for body in bodies:
assert self._matches(body) is True, f"false negative on: {body}"
def test_openrouter_data_policy_no_endpoints_does_not_trip(self):
"""OpenRouter has several 'no endpoints ...' 404 bodies. Only the
image-input one is an image rejection the guardrail / data-policy
variants (agent/error_classifier.py) are about routing restrictions,
not vision, and must route to their own handler, not get their images
stripped.
"""
bodies = [
"No endpoints available matching your guardrail restrictions",
"No endpoints available matching your data policy",
"No endpoints found matching your data policy",
]
for body in bodies:
assert self._matches(body) is False, f"false positive on: {body}"
def test_codex_data_url_rejection_does_not_false_match_other_url_errors(self):
"""The narrow 'image_url'. expected' phrase (keyed on the
field-path apostrophe used in the Codex Responses error format)