anomalyco / anomalyco/opencode
tool/read: full base64 data URI persisted inside tool-result message record (context bloat)
@kitlangton is already working on this.
Since Sep 16, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Summary
The v2 read tool persists the entire base64 data URI of an image inside the tool-result record that gets stored in the session's message table — both as the type: "file" content part's uri field and again in the tool's output. Because message records are serialized back into the conversation (and clients render file parts as [file: data:image/jpeg;base64,...]), a single screenshot can inject ~100KB+ of base64 text into context — roughly the image size again — so image-heavy sessions blow past the context limit at ~2x the cost. v1 kept the binary in a separate attachments side-channel and only put a tiny "Image read successfully" string in the output.
Environment
- opencode version: v2.0.3 (verified against v2.0.4 source at
v2.0.4tag) - OS: Darwin 25.6.0 (arm64)
- Terminal: Apple_Terminal, xterm-256color, truecolor
- Shell: /bin/zsh
- Install/channel: latest
- Active plugins:
opencode-dynamic-context-pruning(local plugin),@dietrichgebert/ponytail,opencode-models-discovery@latest
Reproduction
- In a v2 session, run the
readtool on an image file (e.g., a ~300KB JPEG screenshot). - Inspect the persisted session message for the tool result: SQLite table
session_message(columndata, look atcontent[].state.content). - Observe
{ "type": "file", "uri": "data:image/jpeg;base64,<~400k chars>", "mime": ..., "name": ... }— the complete data URI is stored verbatim, and the tool'soutputfield wraps the same fullFileContentagain.
Expected Behavior
Binary payloads should not be persisted inside the tool-result text record. The model should still receive the image as real media (image_url parts), but message records should keep a small reference — as v1 did with attachments — so persisted history stays small and clients can render a preview/thumbnail instead of a giant data URI.
Actual Behavior
The full base64 data URI is embedded in the stored message record and rendered verbatim back into agent context. Context/usage accounting then counts it again (model-request.ts boundImages() adds Buffer.byteLength(item.uri) for tool-result file image parts). Net effect: each image effectively costs ~2x in context/usage than the pixels sent to the model.
Additional Context
- DB evidence from a real session: one assistant message was 127KB;
content[1].state.content[1].urialone was 126,067 chars — the entiredata:image/jpeg;base64,...string stored verbatim. - v1 (
v1.4.14,packages/opencode/src/tool/read.ts) returns{ output: "Image read successfully", attachments: [{ type: "file", mime, url }] }— binary in a side-channel, never in the output text. - v2 (
packages/core/src/tool/plugin/read.ts) buildstoModelContentwith afilecontent part whoseuriis the full data URI, and also returns that wholeFileContentas the tool output. - Provider adapters (
packages/ai/src/protocols/openai-chat.tslowerToolMessages/lowerMedia, also openai-responses/anthropic) do correctly lower file parts toimage_urlmedia parts — so the model request is fine; this is storage + transcript-rendering bloat, not a provider issue. - Suggested fix areas: (1) keep binary payloads out of persisted tool-result records (reuse the v1 attachments pattern / store a file reference); (2) clients should render file parts without inlining data URIs into the transcript text; (3) revisit counting raw URI bytes in
boundImages(). - Frequency: every image read; consistently reproducible.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.