I run opencode, with T3 Code as the UI, on RunInfra’s hosted open models. Long agent sessions kept dying the same way: one error, and then every request after it failed too. opencode resends the whole history on every turn, so whatever breaks a request once breaks it for good, and the session is stuck.
Three errors did it:
Hosted inference accepts at most 8 images per request. The count covers the whole request, earlier turns included, so the ninth screenshot of a session kills it.Payload too large. The maximum request body is 3.5 MB. A byte limit, separate from the context window. A real session hit it at around 623k tokens, on models that advertise a 1M-token context. Clients compact by tokens, not bytes, so nothing kicks in before the wall.messages[N].tool_calls[0].function.arguments must be a JSON object encoded as a string; received string.
To be fair to RunInfra, the first two are documented: the Chat Completions output limits, the errors page, and /v1/models even publishes max_request_bytes. The problem is that nobody handles them for agent clients.
My read, and it is only my read: hard limits like these are very likely the trade-off behind RunInfra’s price, which in my experience is roughly half of what other providers charge. That’s a reasonable trade. But someone has to absorb the limits, and with an agent, that someone is the client.
The third error was probably mine
I didn’t reproduce the tool-call error; I inferred it from logs. Two opencode servers were working on the same session database: T3 Code’s, and opencode’s own background service, which any opencode CLI command starts on its own. The errors began right after that service came up. Part of the fix is opencode service stop. As a safety net, the plugin also turns any tool-call input that isn’t an object into one.
What the plugin does
It’s an opencode 2.x plugin, scoped to the runinfra provider. It only rewrites the outgoing request; the stored session is never touched.
Every image gets a stable number by first appearance. Kept images get a label in front of them, dropped ones become text:
[imagen 9 · /tmp/shot-menu.png · <one-line description>]
[imagen 2 omitida · <path> · <description> · vuelve a leer ese archivo con read si la necesitas]
The model knows what it lost and can re-read exactly the file it needs. Images with no file behind them (pasted screenshots, data URIs) are saved once to a cache dir so they’re re-readable too.
Descriptions are generated once per image (cached by content hash) by the cheapest RunInfra model that accepts images, picked from GET /v1/models by input_modalities and pricing: today glm-5-3-flash, about $0.0001 per image.
For size: when the request passes 3 MB, it trims the oldest tool output down to 2 MB, and if that’s not enough, long strings in old tool-call inputs. System messages, the last user message and the last 10 messages are never touched. I also set each RunInfra model’s limit.context to about 400000, so opencode compacts before the byte limit instead of after it.
The fix that broke the bill
RunInfra’s prompt cache matches the longest byte-identical prefix of your request. Everything after the first change is billed uncached, and cached input costs about a tenth of the price.
My first version trimmed just enough to fit, every turn. It sounds tidy. It means the cut moves every turn, the prefix changes near the start of the history, and nearly the whole session is billed uncached on every request. I fixed one limit and broke the economics.
The fix is boring: trim in batches, with hysteresis. Images go from 8 down to 4 in one cut. History goes from 3 MB down to 2 MB. The plugin persists those decisions per session, so later turns replay the exact same cuts and the prefix stays byte-identical until the next batch.
I measured it on RunInfra’s API. In the 10 requests after crossing the size limit, cached prompt tokens went from 57% to 98%: 443k uncached tokens down to 17.6k, about 25× fewer. On image turns, 75% to 91%. That’s one run per version, and cache retention is best-effort, so treat the numbers as indicative.
This is what I meant by context budgets in The seam is the product. The budget isn’t only tokens. It’s bytes, image counts and cache prefixes, and moving one changes the bill for the others.
What I asked RunInfra for
The plugin is a workaround. These would make most of it unnecessary:
- Count the image cap only over recent turns, or raise it.
- Accept gzip request bodies, or raise the 3.5 MB limit.
- Mention the limits in the OpenCode setup guide, and set a context limit there that fits in 3.5 MB.
- Treat
""tool-call arguments as{}.
Install it
Paste this into your coding agent:
Install this opencode plugin: https://github.com/OmarAlex24/opencode-runinfra-sanitize
It follows the repo’s README and sets it up, showing you the context-limit change before applying it. By hand, it’s one curl:
curl -fsSL --create-dirs -o ~/.config/opencode/plugins/runinfra-sanitize.ts https://raw.githubusercontent.com/OmarAlex24/opencode-runinfra-sanitize/main/plugins/runinfra-sanitize.ts
Boring, but debuggable: every cut is written to a JSON file per session, and the debug log tells you where the prefix changed.