Skip to content

Image cap ignores modelCapabilities.limits.vision.max_prompt_images, and each eviction past the 50-image cap rewrites the prompt-cache prefix #5111

Open

Description

@IslamTayeb

Describe the bug

The runtime caps images per model request at 50 and, past the cap, replaces the oldest retained tool-result image in place with the "You've reached the maximum number of images you can view (50)…" notice. Two problems:

  1. modelCapabilities.limits.vision.max_prompt_images is accepted and ignored. The SDK serializes it into session.open / setModel (confirmed by SDK triage in Image request cap ignores modelCapabilities.limits.vision.max_prompt_images, and its newest-first eviction invalidates the prompt cache on every request copilot-sdk#2823), the runtime's own api.schema.json declares it, and setting it to 1,000,000 changes nothing: the cap still applies at 50 and the runtime still logs Removed {n} images from the model request to stay within the 50-image request limit…. Same class as modelCapabilities.limits.maxContextWindowTokens is ignored by the runtime; only the undocumented provider.maxContextWindowTokens works copilot-sdk#2756 (maxContextWindowTokens).

  2. Every image admitted past the cap rewrites the prompt-cache prefix. The replacement lands in the oldest tool batch, i.e. early in the message array, so the provider's cacheable prefix ends there and the rest of the conversation is re-written as cache-creation tokens. In an image-heavy agent session this happens on every request that follows a new tool image: one production session had 51 such rewrites, 16.0M cache-write tokens and cost $120.63 against $23.70 for the same job with the evicted images kept.

This is the unresolved half of #4831 ("some way for an old image to drop out instead of holding the slot"). v1.0.88 raised the cap from 1 to 50 but kept the retention policy. SDK triage on github/copilot-sdk#2823 found both behaviours live in the runtime and asked for it to be routed here.

Affected version

Runtime bundled in @github/copilot-sdk-linux-x64 1.0.15 (used through @github/copilot-sdk 1.0.15, Node v25.2.1), claude-opus-5, BYOK Anthropic. Linux x86_64.

Steps to reproduce the behavior

Script, all ten outbound request bodies and the session events: https://gist.github.com/IslamTayeb/46cfa44887d318ecec4efe1d3fabb44a

  1. BYOK Anthropic session with provider.baseUrl pointing at a loopback proxy that records every /v1/messages body, and modelCapabilities: { supports: { vision: true }, limits: { vision: { max_prompt_images: 1000000 } } }.
  2. Workspace with 61 tiny 8×8 PNGs. Prompt: view tile_01 … tile_60, 10 per response. Then Reply with exactly: ok. Then view tile_61.
  3. Observe the bodies (image base64 replaced by sha256 + length in the gist; messages otherwise verbatim, cache_control markers included) and the assistant.usage event per request:
request images in body tool_results replaced by the notice earlier message rewritten (index) cacheReadTokens cacheWriteTokens
1 0 0 – 0 13,912
2 10 0 none 13,912 1,692
3 20 0 none 15,604 1,703
4 30 0 none 17,307 1,695
5 40 0 none 19,002 1,695
6 50 0 none 20,697 1,695
7 50 10 2 13,912 10,400
8 50 10 none 24,312 48
9 50 10 none 24,360 58
10 50 11 4 13,715 10,886

Request 7 (60 images in history, override at 1,000,000) carries exactly 50 and the runtime logged Removed 10 images from the model request to stay within the 50-image request limit. User-provided images were prioritized over tool-generated images, with newer messages kept first. The same tool_result in message index 2, request 6 → request 7:

{"type":"tool_result","tool_use_id":"toolu_011jisLSffC4K4nSk18SbGs5","content":[{"type":"text","text":"Viewed image file successfully."},{"type":"image","source":{"type":"base64","media_type":"image/png","data":"<sha256:06e76d363b3c678f base64_chars:100>"}}]}
{"type":"tool_result","tool_use_id":"toolu_011jisLSffC4K4nSk18SbGs5","content":[{"type":"text","text":"Viewed image file successfully."},{"type":"text","text":"You've reached the maximum number of images you can view (50) so I can't provide the image for you to see."}]}

cacheReadTokens falls from 20,697 to 13,912 (system prompt + tools + first user message) and everything after is re-written. Requests 8 and 9 add no image and nothing is rewritten. Request 10 admits one more image, replaces one more old tool_result (message 4), and rewrites again.

At production scale, two runs of the same job on the same harness, same SDK, no override; the only difference is whether a proxy put the evicted images back before the request reached the API:

requests tool images requests after an image past the 50th full prefix rewrites cache-write tokens cache-read tokens cost
runtime eviction reaches the API 200 108 51 52 (request 1 + the 51) 16,035,273 32,205,310 $120.63
evicted images restored by the proxy 174 76 26 1 (request 1) 381,220 34,377,849 $23.70

In the first run every one of the 51 requests that followed an image arrival past the cap shows cacheReadTokens ≤ 16,144 (system + tools) and a 189K–381K write; no other request does. Those 51 rewrites are 15.68M of the 16.04M write tokens.

Expected behavior

  • max_prompt_images is honoured, or documented as ignored and rejected at the API boundary rather than silently accepted.
  • If a cap must be enforced, it should not mutate messages the provider has already cached. The runtime's eviction is already oldest-first and sticky (the dropped set goes 10 → 10 → 10 → 11 above and never reorders), so the cost is not instability of the retained set; it is that each admission past the cap replaces one already-sent tool_result. The prefix-stable options are no eviction (the Anthropic API accepted 120 images in one request in our probe; 50 is a Copilot policy, Preserve image replay compatibility across CLI image limits copilot-sdk#2670), or refusing the new image at the tail and keeping the old ones, which starves the model of what it just rendered. Honouring the override is the behaviour the SDK already advertises.

Additional context

Activity

  1. added
    area:context-memoryContext window, memory, compaction, checkpoints, and instruction loading
    area:modelsModel selection, availability, switching, rate limits, and model-specific behavior
    and removed on Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:context-memoryContext window, memory, compaction, checkpoints, and instruction loadingarea:modelsModel selection, availability, switching, rate limits, and model-specific behavior

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions