Summary
Azure OpenAI hard-caps requests at 50 images. In long agentic sessions where the model reads many images (screenshots, render stills), the full-history resend eventually crosses that cap and every subsequent turn fails with a fatal 400. opencode already ships an auto-recovery designed for exactly this situation — compact, strip media from context, retry — but it never triggers here, because the error is not classified as context_overflow.
Environment
- opencode 1.18.5
- model
openai/gpt-5.6-terra via OpenRouter, routed to the Azure upstream
What happens
A session accumulated 47 image read tool results; the next user turn attached 4 more images and failed with:
{"error":{"message":"Provider returned error","code":400,"metadata":{"raw":"{\n \"error\": {\n \"message\": \"Exceeded maximum number of images (50) allowed in the request.\",\n \"type\": \"invalid_request_error\",\n \"param\": \"input\",\n \"code\": null\n }\n}","provider_name":"Azure","is_byok":false}}}
The turn surfaces as a plain non-retryable api_error. Because the oversized history is resent on every turn, the session is permanently poisoned from this point — no retry can succeed, only compaction/media-stripping could.
Root cause
parseAPICallError (packages/opencode/src/provider/error.ts) classifies an error as context_overflow only when one of these holds:
isContextOverflow(message) matches (packages/llm/src/provider-error.ts) — the pattern list is entirely token/length phrases (prompt is too long, context_length_exceeded, too many tokens, …),
- HTTP status is 413,
body.error.code === "context_length_exceeded".
Azure's image-count error matches none of them: it is a 400 (not 413), the inner code is null (and OpenRouter's wrapper reports numeric code: 400), and no pattern mentions images.
When classification does return context_overflow, SessionCompaction runs with overflow: true, which compacts, strips media from context, and auto-continues with "The previous request exceeded the provider's size limit due to large media attachments…" (packages/opencode/src/session/compaction.ts) — exactly the right remedy for this failure.
Suggested fix
Add an image-count pattern to patterns in packages/llm/src/provider-error.ts:
/maximum number of images/i,
message() appends the raw response body, so the pattern matches even through OpenRouter's double-wrapped payload (the Azure message is nested as an escaped string inside metadata.raw). Other providers have similar per-request media-count caps; the Azure 50-image message is the one observed in the wild.
Repro sketch
- Run a session on an Azure-served OpenAI model (directly, or via OpenRouter with
provider.order: ["azure"]).
- Have the agent
read 50+ images across turns (or attach them as file parts).
- The next turn fails with the fatal 400 above instead of compacting with media-strip and continuing.
Summary
Azure OpenAI hard-caps requests at 50 images. In long agentic sessions where the model
reads many images (screenshots, render stills), the full-history resend eventually crosses that cap and every subsequent turn fails with a fatal 400. opencode already ships an auto-recovery designed for exactly this situation — compact, strip media from context, retry — but it never triggers here, because the error is not classified ascontext_overflow.Environment
openai/gpt-5.6-terravia OpenRouter, routed to the Azure upstreamWhat happens
A session accumulated 47 image
readtool results; the next user turn attached 4 more images and failed with:{"error":{"message":"Provider returned error","code":400,"metadata":{"raw":"{\n \"error\": {\n \"message\": \"Exceeded maximum number of images (50) allowed in the request.\",\n \"type\": \"invalid_request_error\",\n \"param\": \"input\",\n \"code\": null\n }\n}","provider_name":"Azure","is_byok":false}}}The turn surfaces as a plain non-retryable
api_error. Because the oversized history is resent on every turn, the session is permanently poisoned from this point — no retry can succeed, only compaction/media-stripping could.Root cause
parseAPICallError(packages/opencode/src/provider/error.ts) classifies an error ascontext_overflowonly when one of these holds:isContextOverflow(message)matches (packages/llm/src/provider-error.ts) — the pattern list is entirely token/length phrases (prompt is too long,context_length_exceeded,too many tokens, …),body.error.code === "context_length_exceeded".Azure's image-count error matches none of them: it is a 400 (not 413), the inner
codeisnull(and OpenRouter's wrapper reports numericcode: 400), and no pattern mentions images.When classification does return
context_overflow, SessionCompaction runs withoverflow: true, which compacts, strips media from context, and auto-continues with "The previous request exceeded the provider's size limit due to large media attachments…" (packages/opencode/src/session/compaction.ts) — exactly the right remedy for this failure.Suggested fix
Add an image-count pattern to
patternsinpackages/llm/src/provider-error.ts:message()appends the raw response body, so the pattern matches even through OpenRouter's double-wrapped payload (the Azure message is nested as an escaped string insidemetadata.raw). Other providers have similar per-request media-count caps; the Azure 50-image message is the one observed in the wild.Repro sketch
provider.order: ["azure"]).read50+ images across turns (or attach them as file parts).