fix(vision): convert SVG/unsupported image formats to PNG before embedding into history - #52688
Conversation
…dding vision_analyze embedded SVG (and BMP/TIFF) tool-results into conversation history with media_type image/svg+xml. Anthropic only accepts jpeg/png/ gif/webp, so the request fails with a non-retryable 400. Because the image is baked into immutable history and re-sent every turn, the session is permanently wedged on resume — retries re-send the same bad bytes. Add _normalize_to_supported_image(): SVG is rasterized to PNG (best-effort via cairosvg/svglib/rsvg-convert/inkscape), other non-supported raster formats are re-encoded to PNG via Pillow, and if conversion is impossible the tool returns an actionable error instead of a session-wedging payload. Wired into both the native-vision fast path and the auxiliary-API path so the whole bug class is covered, not just the one call site. All 99 existing vision tests pass.
Related (competing approaches, not a duplicate) to the native-image-format cluster: #47299 (Codex data-URL downgrade to text placeholder), #47523 (reject corrupt/unsupported native inputs — same |
… rasterizer; AUTHOR_MAP for #52688 - _normalize_to_supported_image: ensure cache/vision exists before writing the converted PNG (fresh HERMES_HOME had no dir -> FileNotFoundError). - resolver: SVG passes through as image/svg+xml instead of erroring; the call sites rasterize to PNG via the salvaged normalize step (cairosvg / svglib / rsvg-convert / inkscape, best-effort with actionable error). - normalization offloaded via asyncio.to_thread at both call sites. - tests: resolver pass-through + rasterization + no-converter error paths. - AUTHOR_MAP: jonathan@mintrx.com -> JAlmanzarMint (PR #52688 salvage).
The salvaged #52688 rasterizer shell-out predates the TUI subprocess stdin= guard; a rasterizer that prompts on stdin could hang the tool under prompt_toolkit. DEVNULL it.
… rasterizer; AUTHOR_MAP for #52688 - _normalize_to_supported_image: ensure cache/vision exists before writing the converted PNG (fresh HERMES_HOME had no dir -> FileNotFoundError). - resolver: SVG passes through as image/svg+xml instead of erroring; the call sites rasterize to PNG via the salvaged normalize step (cairosvg / svglib / rsvg-convert / inkscape, best-effort with actionable error). - normalization offloaded via asyncio.to_thread at both call sites. - tests: resolver pass-through + rasterization + no-converter error paths. - AUTHOR_MAP: jonathan@mintrx.com -> JAlmanzarMint (PR #52688 salvage).
The salvaged #52688 rasterizer shell-out predates the TUI subprocess stdin= guard; a rasterizer that prompts on stdin could hang the tool under prompt_toolkit. DEVNULL it.
… rasterizer; AUTHOR_MAP for #52688 - _normalize_to_supported_image: ensure cache/vision exists before writing the converted PNG (fresh HERMES_HOME had no dir -> FileNotFoundError). - resolver: SVG passes through as image/svg+xml instead of erroring; the call sites rasterize to PNG via the salvaged normalize step (cairosvg / svglib / rsvg-convert / inkscape, best-effort with actionable error). - normalization offloaded via asyncio.to_thread at both call sites. - tests: resolver pass-through + rasterization + no-converter error paths. - AUTHOR_MAP: jonathan@mintrx.com -> JAlmanzarMint (PR #52688 salvage).
The salvaged #52688 rasterizer shell-out predates the TUI subprocess stdin= guard; a rasterizer that prompts on stdin could hang the tool under prompt_toolkit. DEVNULL it.
|
Merged via PR #57890 — your commit was cherry-picked onto current main with your authorship preserved in git history (ac94f2c). Your SVG rasterization ladder (cairosvg → svglib → rsvg-convert → inkscape) and the format-normalization step now run behind the new unified image-source resolver, so SVG/BMP/TIFF sources are auto-converted to PNG before embedding exactly as you designed. Small follow-ups added on top: mkdir for the converted-PNG output dir on fresh installs, stdin=DEVNULL on the rasterizer shell-out (a subprocess guard that landed after your PR was written), and thread-offloading the conversion. Thanks! |
… rasterizer; AUTHOR_MAP for NousResearch#52688 - _normalize_to_supported_image: ensure cache/vision exists before writing the converted PNG (fresh HERMES_HOME had no dir -> FileNotFoundError). - resolver: SVG passes through as image/svg+xml instead of erroring; the call sites rasterize to PNG via the salvaged normalize step (cairosvg / svglib / rsvg-convert / inkscape, best-effort with actionable error). - normalization offloaded via asyncio.to_thread at both call sites. - tests: resolver pass-through + rasterization + no-converter error paths. - AUTHOR_MAP: jonathan@mintrx.com -> JAlmanzarMint (PR NousResearch#52688 salvage).
The salvaged NousResearch#52688 rasterizer shell-out predates the TUI subprocess stdin= guard; a rasterizer that prompts on stdin could hang the tool under prompt_toolkit. DEVNULL it.
… rasterizer; AUTHOR_MAP for NousResearch#52688 - _normalize_to_supported_image: ensure cache/vision exists before writing the converted PNG (fresh HERMES_HOME had no dir -> FileNotFoundError). - resolver: SVG passes through as image/svg+xml instead of erroring; the call sites rasterize to PNG via the salvaged normalize step (cairosvg / svglib / rsvg-convert / inkscape, best-effort with actionable error). - normalization offloaded via asyncio.to_thread at both call sites. - tests: resolver pass-through + rasterization + no-converter error paths. - AUTHOR_MAP: jonathan@mintrx.com -> JAlmanzarMint (PR NousResearch#52688 salvage).
The salvaged NousResearch#52688 rasterizer shell-out predates the TUI subprocess stdin= guard; a rasterizer that prompts on stdin could hang the tool under prompt_toolkit. DEVNULL it.
… rasterizer; AUTHOR_MAP for NousResearch#52688 - _normalize_to_supported_image: ensure cache/vision exists before writing the converted PNG (fresh HERMES_HOME had no dir -> FileNotFoundError). - resolver: SVG passes through as image/svg+xml instead of erroring; the call sites rasterize to PNG via the salvaged normalize step (cairosvg / svglib / rsvg-convert / inkscape, best-effort with actionable error). - normalization offloaded via asyncio.to_thread at both call sites. - tests: resolver pass-through + rasterization + no-converter error paths. - AUTHOR_MAP: jonathan@mintrx.com -> JAlmanzarMint (PR NousResearch#52688 salvage).
The salvaged NousResearch#52688 rasterizer shell-out predates the TUI subprocess stdin= guard; a rasterizer that prompts on stdin could hang the tool under prompt_toolkit. DEVNULL it.
… rasterizer; AUTHOR_MAP for NousResearch#52688 - _normalize_to_supported_image: ensure cache/vision exists before writing the converted PNG (fresh HERMES_HOME had no dir -> FileNotFoundError). - resolver: SVG passes through as image/svg+xml instead of erroring; the call sites rasterize to PNG via the salvaged normalize step (cairosvg / svglib / rsvg-convert / inkscape, best-effort with actionable error). - normalization offloaded via asyncio.to_thread at both call sites. - tests: resolver pass-through + rasterization + no-converter error paths. - AUTHOR_MAP: jonathan@mintrx.com -> JAlmanzarMint (PR NousResearch#52688 salvage).
The salvaged NousResearch#52688 rasterizer shell-out predates the TUI subprocess stdin= guard; a rasterizer that prompts on stdin could hang the tool under prompt_toolkit. DEVNULL it.
Summary
vision_analyzecould embed an SVG (or BMP/TIFF) image into conversation history withmedia_type: image/svg+xml. Anthropic's vision API only acceptsimage/jpeg,image/png,image/gif, orimage/webp, so the request fails with a non-retryable 400:Because a vision tool-result is baked into immutable conversation history and re-sent on every subsequent turn, this permanently wedges the session —
hermes --resume <id>re-sends the same poison payload and fails identically every time. Retries cannot clear bytes already in the request.Root cause
_detect_image_mime_type()correctly returnsimage/svg+xml(andimage/bmp) for those file types, but both consumption sites — the native-vision fast path (_native_vision_fast_path) and the auxiliary-API path (vision_analyze_tool) — then base64-embedded the image with that unsupported media_type and shipped it to the provider.Fix
Add
_normalize_to_supported_image(), called before any image is base64-embedded:cairosvg→svglib+reportlab→rsvg-convert→inkscape(all soft deps).Wired into both call sites so the whole bug class is fixed, not just the one site the symptom surfaced at. Temp PNGs are cleaned up on the same path as downloaded images.
Test Plan
tests/tools/test_vision_native_fast_path.py,tests/tools/test_vision_tools.py).image/svg+xml→ normalized toimage/png, embeds cleanly.media_type400 on every resume); confirmed the fix prevents an unsupported media_type from ever entering history.