fix(agent): shrink oversized inline images before compressing on 413 - #37412
fix(agent): shrink oversized inline images before compressing on 413#37412mcull wants to merge 1 commit into
Conversation
A 413 'payload too large' on a turn carrying a large base64 image (e.g. a 15–25 MB iPhone photo posted in Slack) used to skip the image-shrink recovery path because that path is only wired into Anthropic's 400 'image exceeds 5 MB maximum' error. Anthropic returns 413 (not 400) when the *whole* request is oversized, so the existing handler went straight to _compress_context — which drops *old* messages but leaves the giant inline image on the current turn intact. Result: 413 → compress (no shrink) → 413 → … → 'Cannot compress further' → session auto-reset. Try _try_shrink_image_parts_in_messages once before counting a compression attempt. If it changes anything, retry immediately; otherwise fall through to compression as before. Symmetric with the existing 400/image_too_large path. Gated by the same image_shrink_retry_attempted flag so a second 413 still falls through to compression instead of looping. Triggered by Marc's items-for-sale workflow on Slack.
|
Confirming this on a second provider – I hit the identical failure on GitHub Copilot, not just Anthropic. A single ~4 MB phone JPEG (base64 ≈ 5.2 MB on the wire) trips Copilot's request-body cap as a bare One optional suggestion to strengthen the regression coverage. The two tests here mock Happy to drop it in as a suggested addition if useful – either you cherry-pick it or I can push it to your branch / open it as a follow-up, whatever you prefer. Either way, thanks for the fix – this bug's been biting real photo workflows. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for identifying a real 413 recovery gap. Current main still sends FailoverReason.payload_too_large directly into compression at agent/conversation_loop.py:3337-3401, so shrinking inline images first remains useful.
Problems
- The proposed call at
agent/conversation_loop.py:2868changes onlyapi_messages. On currentmain, that list is derived from canonicalmessagesatagent/conversation_loop.py:792-835and prompt caching deep-copies it atagent/prompt_caching.py:84-99. A recovered retry can therefore succeed while the persisted history still contains the oversized image, causing a later turn to rebuild and resend it. - The new test mocks the shrinker (
tests/run_agent/test_413_compression.py:429-431), so it does not cover canonical persistence, the actual reduced wire payload, or the one-shot guard after a second 413.
Suggested changes
- Salvage this into the current retry loop with
_retry.image_shrink_retry_attempted, and mirror successful image replacements into canonicalmessagesbefore_persist_session()and retry. PR #62005 contains the current-main replacement/persistence pattern. - Add a detached canonical/API regression that verifies request → repaired persistence → repaired retry ordering and verifies a second 413 falls through to compression.
Automated hermes-sweeper review.
| # above (FailoverReason.image_too_large). | ||
| if ( | ||
| not image_shrink_retry_attempted | ||
| and agent._try_shrink_image_parts_in_messages(api_messages) |
There was a problem hiding this comment.
On current main, api_messages can be a deep-copied cache request while canonical messages remains unchanged (agent/conversation_loop.py:792-899, agent/prompt_caching.py:84-99). Please record and mirror successful URL replacements into canonical history and persist before retrying, or a later turn can resend the original oversized image.
Problem
When a user posts a large photo (e.g. 15–25 MB iPhone HEIC/JPEG, common via Slack/Telegram), the gateway downloads it and the agent inlines it as a base64
data:URL on the user turn. With Anthropic, this triggers a 413 "payload too large" because the whole request body blows past the cap.The existing image-shrink recovery (
_try_shrink_image_parts_in_messages) only fires onFailoverReason.image_too_large, which is matched against Anthropic's 400 "image exceeds 5 MB maximum" wording. A whole-request 413 routes topayload_too_large→ compression-only path.Compression drops old messages but leaves the huge inline image on the current turn intact, so:
This is the exact failure mode Marc hit on the items-for-sale household workflow — drop a photo in the grove Slack channel, get a session reset before the agent can analyze it.
Fix
In the 413 branch of the recovery handler, try
_try_shrink_image_parts_in_messagesonce before counting a compression attempt. If it changes anything, retry immediately. Otherwise fall through to existing compression behavior.image_too_largepath (same helper, same flag)image_shrink_retry_attemptedso a second 413 still falls through to compression rather than looping on shrinkTests
Added two tests to
tests/run_agent/test_413_compression.py:test_413_with_oversized_image_shrinks_before_compression— 413 + shrinkable image → shrink fires, compression does not runtest_413_falls_back_to_compression_when_no_image_to_shrink— 413 + no image → existing compression path still worksAll 36 tests in
test_413_compression.py+test_image_shrink_recovery.pypass.