Repository navigation
preview_snapshot accumulates unbounded image attachments, breaking providers with per-request image limits #10187
Description
What happened
While debugging an issue via the opencode provider (Kimi K3, routed through an internal AI Gateway), with T3 controlling/observing a browser preview against Storybook, the session died with: Bad Request: {"error":"Upstream error: INVALID_ARGUMENT: parameter \"messages\" supports at most 5 images; received 6"}. It happened twice in a row within the same investigation session.
Diagnosis
Every preview_snapshot tool call attaches a new screenshot (image/png) to the conversation, and nothing prunes older screenshot attachments from history before the next provider request. Confirmed in the provider event log: two separate opencode sub-sessions in the same thread each failed once enough preview_snapshot calls had accumulated (5+ completions per sub-session), each time with the identical "5 images / received 6" 400 from the upstream gateway. The gateway in this case enforces a hard per-request image cap of 5; opencode has no way to know that cap for a custom/generic OpenAI-compatible provider, and T3's preview_snapshot handler (apps/server/src/mcp/toolkits/preview/handlers.ts) does not bound how many prior screenshots stay live in context.
This is the same shape of problem as #7410 (unbounded preview_snapshot metadata exhausting context), fixed in #9505 by bounding output "at the MCP response boundary... so every provider is covered" — the same approach (cap the number of live image attachments carried forward from recent preview_snapshot calls, replacing older ones with a placeholder) would fix this for every provider, not just this one gateway.
Steps to reproduce
- Configure an opencode provider pointed at a custom OpenAI-compatible endpoint that enforces a per-request image limit (e.g. 5).
- Start a thread with the preview MCP toolkit enabled.
- Have the agent call
preview_snapshotrepeatedly (e.g. iterating on a browser/Storybook page) more times than the endpoint's image limit, in the same thread, without the images being pruned from history. - The next turn's request exceeds the provider's image limit and the whole turn fails with an unrecoverable 400.
Version
0.0.39-nightly.20260905.1289 (commit 8d3c56b487810455f7b695d1b849b4370b132d03)
Environment
macOS 15.6 (darwin arm64), Node v24.19.0, T3 Code nightly desktop app; opencode provider (@opencode-ai/sdk ^1.3.15) against a custom OpenAI-compatible AI gateway serving two internally-routed model deployments (a Kimi K3 deployment and a GLM-5.3-flash deployment).
Evidence
"error":{"name":"APIError","data":{"message":"Bad Request: {"error":"Upstream error: INVALID_ARGUMENT: parameter "messages" supports at most 5 images; received 6"}","statusCode":400,"isRetryable":false,"metadata":{"url":"https:///ai-gateway/mlflow/v1/chat/completions"}}}
(Two occurrences, ~11 minutes apart, same thread, different opencode sub-sessions, different underlying model deployment each time.)
Related issues
#7410 / #9505 — same root cause category (preview_snapshot context bloat), but that fix bounds JSON metadata size, not the count of image attachments, so it would not have prevented this. Not a duplicate.
Fix applied or workaround
None applied to the install. User advised to avoid long single threads with heavy preview_snapshot use against providers with strict per-request image caps, and to start fresh threads periodically instead.
Filed by
Claude Sonnet 5 via t3 triage
Activity
juliusmarminge commentedon Sep 5, 2026
Triage
Confirmed. This is a real bug, and it is not a duplicate of #7410.
Every successful
preview_snapshotreturns a PNG as MCP image content. Those images stay in the provider session and are sent again on later turns. Nothing in T3 bounds how many of them remain live. On a gateway with a hard per-request image cap (here: 5), the next completion is a non-retryable 400 (received 6). Two OpenCode sub-sessions in the same thread each failed after 5+ snapshot completions, which matches per-session accumulation rather than a one-off transport glitch.Where this happens
The screenshot is attached in
apps/server/src/mcp/McpHttpServer.ts(registerPreviewSnapshot), not inapps/server/src/mcp/toolkits/preview/handlers.ts. The handler only invokes the broker. On success the MCP result is always:structuredContentmetadata (screenshot bytes stripped to mime/width/height){ type: "image", mimeType: "image/png" }with the full PNGThere is no count cap, no omission after N calls, and no placeholder for older shots. OpenCode is pointed at this MCP server in
apps/server/src/provider/Layers/OpenCodeAdapter.ts(client.mcp.add). After that, OpenCode owns themessagesarray T3 does not assemble.Related work (same family, different bug)
Suggested fix
The useful provider-independent lever is a session-scoped live-image cap in
registerPreviewSnapshot:screenshot omitted to stay under per-request image limits; metadata above is current).A single-call MCP bound cannot replace images already in OpenCode history. Stopping new image parts after N is what prevents the hard 400. Rewriting older tool-result images is provider-owned (especially OpenCode) and should not be the first fix.
Workaround
Start a fresh thread (or a new provider session) before hitting the endpoint’s image cap. Avoid long Storybook / preview loops with repeated
preview_snapshotagainst custom OpenAI-compatible gateways that cap images.No extra reporter info needed; this is implementable from current main.
juliusmarminge commentedon Sep 5, 2026
I reproduced T3's MCP output behavior on current main, f8b4c464. Six calls to the production
preview_snapshotregistration returned six PNG image parts, one per call, through the real in-process MCP server and preview broker. All calls used the same controlled invocation and client. The broker host returned synthetic one-pixel images; metadata contained no image bytes. The independent focused test passed.This verifies the T3 response behavior, not OpenCode's outgoing model request or the private gateway's HTTP 400. The original failure remains open.
There is an important constraint on the proposed cap: stopping new images after N cannot also retain the latest screenshots. It leaves the old images in provider-owned history and hides subsequent pictures. MCP credentials receive a new session identifier, including before resuming native provider history, so a credential-scoped counter can reset while old images remain. Other image inputs also consume the gateway's limit.
The maintainer selected opt-in text-only snapshots with current defaults preserved. #10232 implements
preview_snapshot({ includeImage: false }). It returns fresh metadata and text without an image content part. Omitted ortruestill returns the PNG. The orchestrator independently ran the exact-main before controls and all 14 candidate tests; public CI and review are complete. The PR is ready for human review and remains unmerged.This is a mitigation only. Browser capture still happens, existing provider history is untouched, and the original gateway rejection has not been replayed. No universal image cap or history pruning has been applied. Keeping this report open.
Audit by GPT 6 Astra via Codex in T3 Code.