fix(agents): transcode unsupported image media_type before Anthropic replay#102374
fix(agents): transcode unsupported image media_type before Anthropic replay#102374chenxiaoyu209 wants to merge 4 commits into
Conversation
…eplay Within-limit images whose mime is outside the inline-supported set (image/heic, image/tiff, image/bmp, ...) previously passed through the shared image sanitizer unchanged. Anthropic adapters forward media_type verbatim, so Anthropic rejected the turn with a 400 while OpenAI and Gemini accepted it. The sanitizer is the single choke point every Anthropic image path flows through (current turn, history replay, and tool results), so normalize there: transcode unsupported-but-decodable bytes to a supported type instead of relabeling them, which would still fail decode. Fixes openclaw#102323 Co-Authored-By: Claude <[email protected]>
|
Codex review: needs real behavior proof before merge. Reviewed July 9, 2026, 6:10 AM ET / 10:10 UTC. Summary PR surface: Source +32, Tests +203. Total +235 across 4 files. Reproducibility: yes. Current main preserves within-limit image MIME values and the linked issue provides loopback evidence that Anthropic rejects image/heic media_type values. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Normalize unsupported image bytes before Anthropic payload construction while preserving provider-capable pass-through when conversion is unavailable, canonicalize supported aliases, and land with redacted loopback or live request proof. Do we have a high-confidence way to reproduce the issue? Yes. Current main preserves within-limit image MIME values and the linked issue provides loopback evidence that Anthropic rejects image/heic media_type values. Is this the best way to solve the issue? No, not yet. The byte-transcode direction is right, but the current shared implementation still needs provider-aware unavailable-backend behavior and canonical MIME output handling. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 9fa913740d41. Label changesLabel justifications:
Evidence reviewedPR surface: Source +32, Tests +203. Total +235 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (5 earlier review cycles)
|
…102323 Drives the real shared image sanitizer into the real Anthropic Messages transport and captures the outgoing request body, proving an image/heic-labeled within-limit image now leaves as a supported media_type instead of being forwarded verbatim (which Anthropic rejects with a 400). Co-Authored-By: Claude <[email protected]>
|
Added the requested runtime, edited proof for the post-fix Anthropic path. New test
No private data: synthetic in-test image fixtures, mocked fetch, dummy PR body updated with the full before/after output. Requesting re-review. 🙏 |
…ession for unsupported MIME
Co-Authored-By: Claude <[email protected]>
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
Thanks for adding the runtime proof for #102323. We’re consolidating this fix in #102537 and closing this overlapping branch. This implementation changes the shared sanitizer for every provider, forcing unsupported declared MIME types through a global transcode even when another provider can accept them. #102537 instead enforces Anthropic’s JPEG/PNG/GIF/WebP contract at both Anthropic transport boundaries and covers user content, replay, and tool results. The canonical issue remains open until that branch lands. |
Closes #102323
What Problem This Solves
Fixes an issue where users on a Claude/Anthropic model with no media-understanding provider (so inbound images are forwarded as image blocks, not transcribed) would get no reply at all when they sent an iPhone HEIC photo — or any
image/heic,image/tiff, orimage/bmpimage — that was already within OpenClaw's size and dimension limits.The whole turn failed with an Anthropic
400 invalid_request_errorbecause the image's realmedia_typewas forwarded verbatim, and Anthropic only acceptsimage/jpeg,image/png,image/gif, orimage/webp. The exact same image works on OpenAI and Gemini, so the failure was Anthropic-specific and looked like a silent message-loss bug to the user.Why This Change Was Made
The shared image sanitizer (
src/agents/tool-images.ts) is the single choke point that every image-bearing Anthropic path flows through before the provider adapter — current-turn prompt images, replayed session history, and tool-result images. The Anthropic adapters (packages/ai/src/providers/anthropic.tsandsrc/agents/anthropic-transport-stream.ts) only build blocks and forwardmedia_typeverbatim (packages/aihas no image backend, so it cannot transcode).Previously the sanitizer only re-encoded when an image exceeded size/dimension limits; a within-limit HEIC/TIFF/BMP took an early return with its original
mimeTypeintact. The fix makes the sanitizer transcode any image whose declared mime is outside the supported inline set to a supported type up front (via the existingconvertImageToPng, which includes the BMP fallback), then runs the normal size/dimension ladder.Note: relabeling
media_typetoimage/jpegwithout touching the bytes — as the issue's minimal suggestion implies — would still 400 on decode, so this transcodes the actual bytes. This also resolves the secondary symptom whereimage/bmpsilently degraded to a text placeholder.User Impact
Users on Anthropic models with image passthrough can now send HEIC/HEIF/TIFF/BMP images (e.g. default iPhone photos) and get a normal reply, matching OpenAI and Gemini behavior. No config change or migration required.
Evidence
Full runtime proof — 18 tests across 3 files (
main@ 03aca2c)All three test files — the existing sanitizer tests, the backend-unavailable regression, and the end-to-end Anthropic transport capture — pass cleanly:
Sanitizer unit + backend-unavailable coverage (
src/agents/tool-images.test.ts+src/agents/tool-images.backend-unavailable.test.ts)transcodes an unsupported within-limit media_type to a supported type— feeds decodable, within-limit WebP bytes labeledimage/heic,image/tiff,image/bmpand asserts the output mime is a supported inline type (jpeg/png/gif/webp) and no longer the original.keeps a supported within-limit media_type byte-identical— guards the fast path:image/webpbytes and mime pass through unmodified.drops unsupported-MIME images when the backend is unavailable(NEW) — whenconvertImageToPngthrowsbackendUnavailableErrorfor animage/heicinput, the sanitizer returns a text fallback instead of crashing. The explicitvi.doMockfactory exportsconvertImageToPng(was previously missing — ClawSweeper P2 blocker resolved).E2E Anthropic transport media_type capture (
src/agents/anthropic-transport-stream.image-mediatype.test.ts)Drives the real shared sanitizer (
sanitizeImageBlocks) into the real Anthropic Messages transport (createAnthropicMessagesTransportStreamFn) and inspects the captured outgoing request body — the same "capture the outgoingmedia_type" method the issue used, but exercising the production code path end-to-end.Before fix (guard reverse-applied out of the worktree, test kept):
The outgoing request body carried
media_type: "image/heic"verbatim — the exact 400 trigger.After fix:
The outgoing
media_typeis now a supported inline type (jpeg/png/gif/webp) and no longerimage/heic.Edges covered
tool-images.test.tstool-images.test.tstool-images.backend-unavailable.test.tstool-images.backend-unavailable.test.tstool-images.backend-unavailable.test.tsanthropic-transport-stream.image-mediatype.test.tsFix-boundary verification
A trace confirmed all four Anthropic image paths (current-turn user image, replayed history, and tool results across both the packaged provider and the custom transport) route through
sanitizeContentBlocksImages/sanitizeSessionMessagesImages/sanitizeToolResultImages, which all funnel intoresizeImageBase64IfNeeded. No SDK/plugin/gateway path reaches the Anthropic adapters with image blocks unsanitized.Typecheck
pnpm tsgoclean for all changed files.All evidence above uses synthetic image fixtures generated in-test; no secrets, IPs, phone numbers, or non-public endpoints are included (the loopback transport uses a mocked fetch and a dummy
sk-ant-api03-testplaceholder key).