fix(gateway,image-routing): stop cross-routing docs as images + transcode uncommon formats - #25936
Conversation
…code uncommon formats Two related fixes for the same user-visible failure on Discord: HTTP 400 'Could not process image' from Anthropic. 1. gateway/run.py: per-attachment MIME is now authoritative when routing media to image / audio buckets. Previously the loop OR'd per-attachment MIME with the message-level MessageType.PHOTO, so when a user uploaded images alongside a non-image (e.g. an .md transcript), the document got swept into image_paths, base64-encoded as a vision content part, and rejected by the provider. Now we only fall back to message-level type when the per-attachment MIME slot is empty, preserving backward compat with adapters that don't populate media_types. 2. agent/image_routing.py: anything outside the universally-accepted set (PNG / JPEG / GIF / WEBP) is now transcoded to PNG with Pillow before declaring media_type, instead of getting rejected by Anthropic / OpenAI / Gemini with the same generic 400. Covers AVIF (Chromium screenshots), HEIC (iPhone), BMP, TIFF, ICO, SVG. Pillow stays a soft dep; pillow-heif and pillow-avif-plugin are lazy-registered. If decoding fails, the image is reported in 'skipped' rather than crashing the turn. Tests: - tests/gateway/test_mixed_attachment_routing.py (new): 5 tests covering image+document, 3 images+document (the exact reproduction of the bug), legacy unknown-MIME backward compatibility, document-only no-route, audio+image no-cross-route. - tests/agent/test_image_routing.py: 10 new tests for AVIF/TIFF/ICO/SVG sniffing, BMP/TIFF -> PNG transcode end-to-end, PNG/JPEG pass-through byte-preservation, corrupted-format graceful skip. All 48 tests in the affected files pass (118/118 across all image tests). Closes NousResearch#25935
…stalled Pillow is a soft dependency. The two transcode-end-to-end tests used 'from PIL import Image' at the top, which made them fail with ModuleNotFoundError on CI runners that don't have Pillow installed. Switch to pytest.importorskip so the tests skip cleanly on bare envs and still run as proper end-to-end coverage where Pillow is present. Verified locally: - Pillow installed: 41/41 tests pass. - Pillow uninstalled: 39 pass, 2 skip (the two transcode tests).
|
Pushed 8e0139d -- the two failures attributable to this PR ( The other 19 failures in that CI run ( |
|
Latest run after the Pillow fix: 19 failed, 22873 passed, 161 skipped (the 2 extra skips are the Pillow-soft-dep transcode tests skipping correctly when Pillow isn't on the runner). For sanity I also pulled the most recent Diff vs |
|
Merged via PR #53923 (commit 505bc27d) — #53923 Salvaged onto current main with both bugs fixed (mixed-attachment per-attachment routing + uncommon-format transcode), plus the document-context-note path widened so a doc mixed into a photo message reaches the agent as a readable file. Your work was combined with two other independent fixes for this issue and all three contributors are credited in the PR; @shashwatgokhe's authorship is preserved on the merge commit. Thanks for the fix. |
What does this PR do?
Hey, second time contributing here so be gentle.
I was hitting
HTTP 400: Could not process imagefrom Anthropic on Discord with no obvious cause. The first time it happened my own previous PR (#25745) seemed to cover it -- but then it kept happening, so I dug deeper and it turned out there were actually two different things happening that both surface as the same generic provider error.I tried fixing both since they share the same user-visible symptom (and the same error message). Happy to split into two PRs if you'd rather review them separately.
The actual bug that hit me
In
gateway/run.py, the loop that routes per-attachment media looked like:The Discord adapter sets the message-level type to
PHOTOwhenever any one attachment is an image. So if I upload3 PNGs + 1 .md transcript, the loop sweeps the.mdintoimage_pathstoo, the gateway base64s it as a vision content part, and Anthropic 400s the whole turn.I confirmed this in the gateway log:
Image routing: native (model supports vision). 4 image(s) will be attached inline.-- but only 3 PNGs were ever cached. The 4th was the document.Fix: trust the per-attachment MIME when it's known, only fall back to the message-level type when the per-attachment slot is empty (legacy adapters that don't populate
media_typesstill work).The smaller one (format compatibility)
Anthropic / OpenAI / Gemini only natively accept PNG / JPEG / GIF / WEBP. But Discord lets people upload AVIF (Chromium screenshots), HEIC (iPhone photos), BMP, TIFF, ICO, SVG. Those also produce the same generic 400. Pillow is already used elsewhere in the codebase as a soft dep, so this transcodes anything outside the universal set to PNG before declaring
media_type. Falls through cleanly if Pillow / pillow-heif / pillow-avif-plugin isn't installed (image is skipped with a logged warning rather than crashing the turn).Related Issue
Fixes #25935
Type of Change
Changes Made
gateway/run.py-- per-attachment MIME is now authoritative in_prepare_inbound_message_text. Message-levelPHOTO/VOICE/AUDIOis only consulted when the per-attachment MIME slot is empty.agent/image_routing.py:_sniff_mime_from_bytesextended to recognise AVIF, TIFF (both endians), ICO, SVG (was previously PNG / JPEG / GIF / WEBP / BMP / HEIC)_UNIVERSALLY_SUPPORTED_MIMESset listing the formats accepted by every major provider_transcode_to_pnghelper using Pillow + optionalpillow-heifandpillow-avif-plugin_file_to_data_urlnow transcodes anything outside the safe set to PNG before encodingskippedrather than sent brokentests/gateway/test_mixed_attachment_routing.py-- new file, 5 tests covering: image + document, multi-image + document (the exact failure I hit), unknown-MIME backward compatibility, document-only no-route, image + audio no-cross-routetests/agent/test_image_routing.py-- 10 new tests for sniffing AVIF/TIFF/ICO/SVG, BMP→PNG and TIFF→PNG transcode end-to-end, PNG/JPEG pass-through (bytes preserved), corrupted-input handled gracefullyTesting
Locally:
tests/agent/test_image_routing.py,tests/gateway/test_mixed_attachment_routing.py,tests/gateway/test_native_image_buffer_isolation.py)What I sent through manually after restarting the gateway:
.mdtranscript in one Discord message -> Anthropic accepts, turn completes (previously 400).jpg(Discord MIME-lying scenario) -> transcoded to PNG, accepted.webpthat's actually PNG bytes (the original feat(kanban): add --sort option to 'hermes kanban list' #25745 scenario) -> still worksNotes for reviewers
pillow-heifandpillow-avif-pluginare even softer (lazy-registered, swallowed on ImportError). If none are installed, behaviour matches today's: the unsupported image gets reported inskippedand the rest of the turn proceeds, rather than the turn dying with a 400._file_to_data_urlwhich is the single funnel for native image attachment, so it should also cover non-Discord platforms if they hit similar formats.Happy to take feedback, trim things, or split this into two PRs if it'd be easier to review one at a time.