OpenCode plugin that compresses oversized image attachments before they hit Anthropic's 5 MB base64 limit.
License: MIT
Anthropic rejects image payloads once the base64 body crosses 5,242,880 bytes. Because base64 adds about 33% overhead, a raw image can fail well before it looks unusually large on disk.
In OpenCode, that failure is especially annoying because the oversized image can stay in the session history. After that, later prompts may keep failing with the same image-too-large error until the bad message is no longer part of the conversation.
This plugin exists to intercept that case before the request reaches the model. It targets the same recurring problem described in OpenCode issues #2104, #20021, and #33152.
Image attached
-> check base64 size
-> if under limit, pass through unchanged
-> if over limit, resize and recompress
-> send the updated image to the LLM
The plugin uses two hooks:
tool.execute.aftercatches oversized images returned by thereadtool as early as possible.experimental.chat.messages.transformacts as the backup path before the request goes to Anthropic.
The plugin is zero-config today. Import it, list it in opencode.json, and it uses the built-in defaults from src/config.ts.
- Install the package in your OpenCode project.
bun add opencode-image-compact- Add it to your
opencode.jsonplugin array.
{
"plugin": ["opencode-image-compact"]
}That JSON block is the whole setup. No extra plugin options are required for the current release.
This version is zero-config at runtime. It does not read custom plugin options yet.
The current built-in behavior is:
| Setting | Default | Meaning |
|---|---|---|
maxWidth |
1568 |
Longest edge target used during resize |
maxHeight |
1568 |
Matching height bound used during resize |
jpegQuality |
80 |
JPEG output quality |
pngCompressionLevel |
9 |
PNG compression level |
webpQuality |
80 |
WebP output quality |
maxRawBytes |
3500000 |
Raw-byte safety threshold before base64 inflation |
formatConversion |
"never" |
Preserve original format by default |
These defaults matter because the plugin is intentionally conservative. It only changes images when it needs to, and it keeps the original format unless the implementation is changed in code.
Typical oversized screenshot flow:
8.4 MB screenshot
-> resized to 1568 px longest edge
-> recompressed as JPEG quality 80
-> 1.2 MB output
Example log output:
[image-compact] Compressed 8.4 MB -> 1.2 MB (jpeg, 1568x882)
Small image that does not need work:
[image-compact] Image passed through: under size limit
- Animated GIFs pass through unchanged.
- Transparent PNGs stay PNG by default because
formatConversionis"never". - Small images under the safety threshold pass through unchanged.
- Images that still cannot be reduced below the limit fall back to the original buffer, so the plugin does not make the request worse.
- Corrupted or malformed image data is handled defensively. The plugin falls back to the original payload and logs an error when compression fails.
This plugin only helps when OpenCode is sending an inline image payload that the hooks can intercept. If the original image still cannot be reduced enough without breaking the fallback rules, the plugin returns the original image and the API error will still surface.
Check that opencode-image-compact is listed in the plugin array in opencode.json. The current release does not require options, so the plugin entry should just be the package name string.
If you use oh-my-opencode, place opencode-image-compact before it in the array. There is a known issue where the OpenCode plugin loader can stop processing subsequent plugins after loading oh-my-opencode:
{
"plugin": [
"opencode-image-compact",
"oh-my-opencode@latest"
]
}That is expected in this release. The built-in default is formatConversion: "never", which preserves the original format.
Pass-through is normal for images that are already under the threshold, for animated GIFs, for malformed data URIs, or for cases where compression would not safely improve the payload.
Local development:
bun install
bun run typecheck
bun run buildUseful checks:
npm pack --dry-runIf you change the compression behavior, keep the docs aligned with the actual defaults in src/config.ts and the real hook behavior in src/hooks/tool-hook.ts and src/hooks/message-hook.ts.
MIT