Skip to content

core: boundImages spends its image-budget cut on remote images that carry no payload #61391

Open

Description

@pjiang000-eng

Description

boundImages sizes the inline image budget with mediaBytes, which returns 0 for url and ref sources — the comment at packages/core/src/session/model-request.ts:153-154 states that "Remote and provider-referenced media carry no local payload and never count toward the inline budget".

The cut is then chosen by walking sizes in order (:208-214) but applied by document position (:217-221), so zero-cost media occupies slots in the removal queue. When the loop reaches a remote image it subtracts 0 from kept and still advances cut, spending a removal that frees nothing. Remote images are therefore replaced with IMAGE_REMOVED even though deleting them cannot help the budget, and the session loses content that was free to keep.

Steps to reproduce

Two remote images followed by two 13 MiB inline images (26 MiB passes the 25 MiB trigger; one inline image returns it under the 15 MiB target):

expect(removed(boundImages([remote("a.png"), remote("b.png"), inline("c.png"), inline("d.png")]))).toBe(1)
// Expected: 1, Received: 3
cd packages/core && bun run script/test.ts test/session-model-request.test.ts -t "spends the image budget"

Three images are replaced — both remote ones plus one inline — where only the inline one costs anything.

Plugins

none

OpenCode version

dev

Screenshot

No response

Activity

  1. opencodejr commented on Nov 3, 2026

    @opencodejr
    Contributor

    Thanks for the clear report. I reproduced this on the latest dev (3ba597f) with a unit test for boundImages, using two remote (Media.url) images followed by two 13 MiB inline images. The first three images were all replaced with the removal notice: both remote ones and c.png. Removing only c.png would have been enough, so the count was 3 instead of 1.

    The cause is in the cut loop: it moves past remote images even though they count 0 bytes toward the budget, so those slots free nothing. The script below reproduces it.

    Related: #53304 (keeping the removed set stable for the prompt cache, same function).

    Reproduction script (repro-61391.test.ts)
    import { expect, test } from "bun:test"
    import { Message, Media } from "@opencode/ai/effect"
    import { boundImages } from "@opencode/core/session/model-request"
    
    // #61391: remote (url) images carry no inline payload, yet boundImages spends cut slots on them.
    const remote = (name: string) =>
      Message.user({ type: "media", media: Media.url(`https://example.com/${name}`, { mediaType: "image/png" }), filename: name })
    const inline = (name: string) =>
      Message.user({ type: "media", media: Media.base64("a".repeat(13 * 1024 * 1024), "image/png"), filename: name })
    const removed = (messages: ReturnType<typeof boundImages>) =>
      messages.filter((message) => message.content[0]?.type === "text").length
    
    test("spends the image budget only on images with a payload", () => {
      const result = boundImages([remote("a.png"), remote("b.png"), inline("c.png"), inline("d.png")])
      console.log("replaced:", result.map((m, i) => `${["a", "b", "c", "d"][i]}=${m.content[0]?.type}`).join(" "))
      expect(removed(result)).toBe(1)
    })

    Run it: bun repro-61391.test.ts

  2. jimmyallinai commented on Nov 3, 2026

    @jimmyallinai
    Contributor

    I'll take this one. The bot's reproduction matches the report: boundImages spends cut slots on remote/ref images that carry 0 bytes, so two free images plus one inline image get replaced where only the inline one should be.

    Plan: reproduce first with the boundImages unit case (2 remote + 2 x 13 MiB inline, expect 1 removed), then fix the cut loop to skip zero-byte media when choosing what to remove, and run the neighbouring session-model-request tests plus the package typecheck before opening a PR.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions