Vision images re-attach as base64 into every LLM request - image-heavy chats hit provider 413 and die silently
Vision images re-attach as base64 into every LLM request — image-heavy chats hit provider 413 and die silently
Summary
Images loaded via vision_load remain in chat history as image_url parts forever. Every subsequent LLM call re-attaches all of them as base64 into the request body. On image-heavy long-running chats the body reaches tens of MB while the context indicator shows only 12–24% token usage. Providers behind Cloudflare (e.g. z.ai) reject the body with 413 Request Entity Too Large; LiteLLM retries 3× with the identical oversized body, all fail, and the turn dies with no visible error — the UI hangs at "Reasoning.../waiting for input" until the user restarts. Affects all presets/endpoints; text compaction does not fix it (the text shrinks, the images stay).
Reproduction
- In a long-running chat,
vision_loadseveral multi-MB images (e.g. 6 PNGs of ~3 MB each) across several turns - Keep chatting — every turn now sends the accumulated base64 payloads of all historical images in the request body
- Once accumulated payload crosses the provider's body limit (observed threshold on a Cloudflare-fronted endpoint at ~25 MB total PNGs → base64-inflated body), every turn fails:
litellm.exceptions.APIError: APIError: OpenAIException - <html>... 413 Request Entity Too Large ...
LiteLLM Retried: 3 timesNo error is surfaced in the WebUI; the chat appears silently hung. Real-world correlation from our instance: vision_load at 12:25 → 413 at 12:29; 6 beat frames (~18 MB) loaded 13:58 → turn dead; same chat 413'd again at 14:00 and 15:13 — while token counts showed 12–24%.
Root cause
helpers/history.py:794treatsimage_urlparts as permanent history items; vision-loaded images (stored as files underchats/<id>/images/vision-load/, referenced by path) are never pruned.helpers/litellm_transport.py:937-945(content_part_from_chat) converts every historicalimage_urlpart into aninput_imagepart for each request. Local paths are read and base64-encoded by the transport, so N historical images ≈ N × (file size × 1.37) bytes in every single request body.- The context indicator counts tokens, not body bytes — a chat can show 12% context while its request body exceeds the provider's upload limit.
- On
413,exception_mapping_utils.py:633raises a genericAPIError; LiteLLM retries 3× with the same body; the failure surfaces only as a generic "Critical error occurred, retrying..." and then a silent dead turn.
Proposed fix
Trim historical image parts from the outgoing request (they serve no purpose after the model has seen them once). We run this as a user-space extension on call_chat_model_turn/start (verified working — a turn against the exact history that 413'd four times completed without error):
- Keep only the K most recent image parts in the outgoing message list (K=2 works well); replace older ones with a text placeholder
"[earlier image removed to keep request size down]" - Operate on a copy — never mutate stored history
- Optionally: convert old images to low-detail thumbnails instead of placeholders, if visual continuity matters
A more conservative alternative: cap the cumulative base64 payload per request (e.g. 10 MB) and drop/replace oldest images until under the cap, logging a warning.
Before/after
| Scenario | Before | After |
|---|---|---|
| Chat with many historical vision images | Every request re-sends all images; eventual deterministic 413; silent hang | Only K recent images sent; body stays bounded |
| Token indicator | Shows 12–24% while body is 25+ MB (misleading) | Unchanged (indicator is token-based; a body-size warning would help further) |
| 413 from provider | 3 identical retries, silent dead turn | Avoided in the first place |
| Compaction of image-heavy chat | Does not help (text shrinks, images stay) | Unaffected — images trimmed at request time |
| Normal image use (recent images) | Works | Unchanged (kept) |
Environment
- Agent Zero v2.10, Docker (Kali container), Chat Completions mode
- Provider: z.ai coding endpoint behind Cloudflare (body-size limit observed at roughly 20–30 MB)
- Reproduced across 4 separate incidents on 2026-08-28 in the same chat
Related: #1855 (Chat Completions mode plain-text logging gap — same "turn dies silently" UX class).
Source: agent0ai/agent-zero