Skip to content

fix(core): cap images per request and compact on image-count errors - #59303

Open
rekram1-node wants to merge 2 commits into
devfrom
image-limit
Open

rekram1-node wants to merge 2 commits into
devfrom
image-limit

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Sessions that read a lot of screenshots die once history passes the provider's image cap. Every later request replays the same images and gets a 400 like request contains 52 images, exceeding the maximum of 50 allowed per request, even plain text prompts. Nothing recovers: the error isn't classified as overflow, so compaction never runs, and boundImages only limits bytes, not count.

Fixes #47721 (and the same report in #47487).

Change

Two parts:

 boundImages(messages)
   drop oldest images when bytes > 25 MiB, down to 15 MiB
+  drop oldest images when count > 30, down to 20
   replace dropped images with a placeholder
+    placeholder names the file ("/tmp/shot-3.png") so the model can read it again

The count cut works like the byte cut: it moves in steps of 10 images, so the replaced prefix (and the prompt cache) stays the same between cuts. 30 matches the lowest cap I found (some OpenCode Go routes reject Too many images in request: 31 > 30; Console/Zen and Azure allow 50). It's a single constant for now, so a per-model limit can replace it later.

 provider-error.ts overflow patterns
+  /request contains \d+ images, exceeding the maximum/   Console/Zen
+  /too many images in request/                            OpenCode Go 30-image routes
+  /exceeded maximum number of images/                     Azure OpenAI
+  /too many images and documents/                         Anthropic, Bedrock

If a provider rejects on count anyway (lower cap, or images arriving in one turn), it now counts as context overflow, so the runner compacts and the compaction request falls back to text.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" stroke="currentColor" stroke-width="1.5">
<style>
.old{animation:fade 4s ease-in-out infinite}
.txt{opacity:0;animation:show 4s ease-in-out infinite}
.new{opacity:.85;animation:drop 4s ease-in-out infinite}
@keyframes fade{0%,35%{opacity:.85}55%,100%{opacity:0}}
@keyframes show{0%,40%{opacity:0}60%,100%{opacity:.6}}
@keyframes drop{0%,10%{opacity:0;transform:translateY(-10px)}25%,100%{opacity:.85;transform:none}}
@media (prefers-reduced-motion:reduce){.old,.txt,.new{animation:none}.old{opacity:0}.txt{opacity:.6}}
</style>
<g class="old"><rect x="12" y="26" width="20" height="20" rx="3"/><rect x="38" y="26" width="20" height="20" rx="3"/></g>
<g class="txt" stroke-linecap="round"><path d="M14 32h16M14 40h10M40 32h16M40 40h10"/></g>
<g opacity=".85"><rect x="64" y="26" width="20" height="20" rx="3"/><rect x="90" y="26" width="20" height="20" rx="3"/><rect x="116" y="26" width="20" height="20" rx="3"/><rect x="142" y="26" width="20" height="20" rx="3"/><rect x="168" y="26" width="20" height="20" rx="3"/><rect x="194" y="26" width="20" height="20" rx="3"/></g>
<g class="new"><rect x="220" y="26" width="14" height="20" rx="3" opacity=".5"/></g>
<path d="M64 56v4h150v-4" opacity=".4"/>
<path d="M12 16v-4h46v4" opacity=".4"/>
</svg>

Out of scope

  • Per-model image limits in the catalog.
  • Already-stuck sessions recover on the next prompt (the cap applies at request time), nothing to migrate.

Verification

  • bun test test/session-model-request.test.ts (core): 12 pass
  • bun test test/provider-error.test.ts (ai): 42 pass
  • bun test test/session-compaction*.test.ts test/session-runner*.test.ts (core): 323 pass
  • bun typecheck in core and ai, bun run check from the root: clean

Added by the opencode bot (AI-generated).

Related: #48715, #51073, #48095

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the 50-image case I tested. With 52 images in history, the base sends all 52 and the provider's count error is not classed as overflow. With this PR, 30 images are sent and the error is classed as context overflow, so compaction runs. Tests pass (session-model-request, provider-error, and the compaction/runner suites). The packages' typecheck script fails here with Script not found "check" (a local bun setup problem, not this PR), so I ran typecheck:tsc in core and ai instead: both clean.

Public API is unchanged: this only touches internal request shaping and error classification.

One gap worth closing before merge: Console Go also enforces a 30-image cap on some routes (#48715, #51073). It rejects with Too many images in request: 31 > 30. None of the new patterns match that message, and the count cap of 40 is above 30, so those sessions stay stuck on the same 400 with no compaction (details inline). Also, the read tool only sets the file's basename as its name, so the placeholder won't contain the full path the description shows (inline).

Related: #48715, #51073, #48095

/token limit exceeded/i,
/request_too_large/i,
// Image-count caps: compaction drops the replayed images, so these recover the same way.
// OpenCode Console/Zen gateway

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console Go also rejects with [invalid_request_error] Too many images in request: 31 > 30 on some routes (#48715, #51073). None of these patterns match it: I checked, and isContextOverflow returns false. Since the count cap of 40 is above 30, those sessions still get stuck with no compaction. Could you add a pattern for it, e.g. /too many images in request/i?

// A per-model limit would replace this constant; the target stays a fixed fraction of it.
const IMAGE_COUNT_TRIGGER = 40
const IMAGE_COUNT_TARGET = 30
const imageRemoved = (name: string | undefined) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The read tool sets name: path.basename(input) in packages/core/src/tool/read-filesystem.ts, so for screenshots the placeholder will say read "shot-3.png" again, not the full /tmp/shot-3.png that the description and the new test use. Relative to the project directory, that bare name can point the model at the wrong file. It may be worth keeping the full path for tool-result files, or making the test use the name read actually produces.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit handles the Console Go 30-image case: Too many images in request: 31 > 30 is now classed as context overflow, and with the 30/20 cap a 52-image history sends 30 images. Tests pass (session-model-request, provider-error, and the compaction/runner suites), and typecheck:tsc is clean in core and ai. Public API is unchanged. The earlier point that the placeholder gives only the file's basename is still open.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core: request accumulates 52 images, exceeds provider max of 50

1 participant