Skip to content

fix(xai): GIF attachments reach Grok as PNG instead of being silently stripped (port opencode#53787) - #135372

Open
teknium1 wants to merge 1 commit into
mainfrom
opencode-port/xai-gif-transcode
Open

teknium1 wants to merge 1 commit into
mainfrom
opencode-port/xai-gif-transcode

Conversation

@teknium1

@teknium1 teknium1 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

GIF images attached to an xAI (Grok) turn now reach the model as PNG. Before this, they were silently dropped and the model answered as if nothing had been attached.

What changes

  • agent/codex_responses_adapter.py: on the xai_responses issuer, image/gif joins the inline subtypes the converter rejects, and a rejected inline raster is re-encoded to PNG with the Pillow transcoder native attachments already use (agent.image_routing._transcode_to_png). SVG keeps its rasterizer. If an image can't be re-encoded, it gets the existing text placeholder.
  • The rejected set is threaded through _chat_content_to_responses_parts / _tool_output_items, so user turns, replayed history, vision_analyze tool results and auxiliary Responses calls all get the same handling.
  • tools/vision_tools_image_prep.unsupported_inline_image_media_type takes an optional also_rejected set for per-backend refusals. The default behavior is unchanged.
  • Other backends (Codex, OpenAI, Anthropic, custom Responses relays) still receive the original GIF.

Root cause

xAI decodes only JPG/PNG/WebP/ICO, and a GIF input_image gets HTTP 400 invalid_image. Hermes classifies that 400 as image_corrupt (_IMAGE_CORRUPT_PATTERNS, #69078) and strips the image from the retry. The retried turn succeeds, so the user sees an answer with no error, but the model never saw the image.

Validation

Check origin/main this PR
Direct API, grok-4.7 /v1/responses (stream + non-stream) and grok-4.20 /v1/chat/completions, GIF part 400 Downloaded response does not contain a valid JPG, PNG, WebP, or ICO image. n/a (vendor behavior)
Live hermes chat -q … --image solid-green.gif (provider xai, model grok-4.7), neutral filename, "do not use tools" NOIMAGE ×3 green ×3
Wire capture (HERMES_DUMP_REQUESTS=1) of the user turn attempt 1 input_image data:image/gif, then attempt 2 a text-only string (image stripped) a single attempt carrying input_image data:image/png
Same harness, real PNG control green ×2 green ×2
test_xai_gif_is_reencoded_to_png_and_other_backends_keep_it red (sources swapped to git show origin/main: copies) green
tests/agent/test_codex_responses_adapter.py, test_anthropic_adapter.py, test_image_routing.py, tests/tools/test_vision_tools.py — green

tests/agent/transports/ shows the same 70 failures on a pristine origin/main archive as on this branch in this host's environment (provider-plugin snapshot/guard environment), so they are unrelated to this diff and CI is the judge.

Live probe cost: ~12 xAI calls on grok-4.7 / grok-4.20 at ~16k input tokens each, well under $0.50.

Source

Ported from anomalyco/opencode#53787 ("only send png, jpeg and webp images to xAI"). OpenCode replaces the GIF with an ERROR: Cannot read … text part at its model-request layer. Hermes re-encodes it to PNG at the Responses wire seam instead, so the model still sees the image. Hermes also already owns the transcoder and the per-backend placeholder seam (#29711), so the port only extends that seam to accept a per-issuer rejected set.

Infographic

xai-gif-to-png

… dropped

Port from anomalyco/opencode#53787: only send png, jpeg and webp images to xAI.

xAI decodes only JPG/PNG/WebP/ICO. A GIF input_image answers HTTP 400
invalid_image on both Responses and Chat Completions; the corrupt-image
recovery then strips the image from the retry, so the turn "succeeds" and
the model answers as if nothing was attached (live: grok-4.7 replied
NOIMAGE 3/3 for a solid-green GIF).

The Responses converter is the seam every xAI image crosses (user turns,
history replay, tool results, auxiliary vision calls), and it already knows
the issuer. On the xai_responses issuer GIF joins the rejected set and is
re-encoded to PNG through the same Pillow transcoder native attachments use;
SVG keeps its rasterizer. Other backends still receive the original GIF.
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on a2f5a68 — fix(xai): GIF attachments reach Grok as PNG instead of being

debug info

CI timings

CI timings · View report · View job

Wall time 6m49s vs 45m13s (-84.9%). 8 job(s) slower, 7 faster, 1 unchanged.

  • OS-specific tests / macOS-only tests (1/2): -88.0s
  • Python tests / Run tests (2/2): -64.0s
  • OS-specific tests / Windows-only tests (arm64): -60.0s
  • OS-specific tests / Windows-only tests: -54.0s
  • Check no committed infographics / check-no-committed-infographics: +34.0s

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/vision Vision analysis and image generation provider/xai xAI (Grok) area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Oct 9, 2026
@greptile-apps

greptile-apps Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with no blocking behavior or structural issues found.

Summary

xAI Responses requests now send rejected inline GIFs as PNG instead of sending a format Grok rejects.

  • Grok turns receive attached GIFs as PNG images.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[User image or tool-result image] --> B[Responses image conversion]
  B --> C{Backend rejects inline format?}
  C -->|No| D[Send original image]
  C -->|Yes| E[Convert to PNG]
  E --> F{Conversion succeeds?}
  F -->|Yes| G[Send PNG]
  F -->|No| H[Send text placeholder]
Loading

Reviews (1) · Last reviewed commit: "fix(xai): GIF attachments reach Grok as ..." · Reviewed by Greptile

@teknium1 teknium1 added the ci-reviewed applied to manually approve dangerous changes label Oct 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history ci-reviewed applied to manually approve dangerous changes comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have provider/xai xAI (Grok) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/vision Vision analysis and image generation type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants