What happens
With a large system prompt on a model that declares a 65,536-token context window, the request fails with a hard provider HTTP 400 before any model work happens — zero tool calls, no tokens generated.
The provider's own accounting in the error:
litellm.BadRequestError: OpenAIException - Requested token count exceeds the
model's maximum context length of 65536 tokens. You requested a total of 68564
tokens: 52180 tokens from the input messages and 16384 tokens for the
completion.
52,180 input + 16,384 reserved for the completion = 68,564 against a 65,536 window.
Why it is a client-side defect, not a prompt problem
Measured in a controlled A/B (N=4 per arm plus a deterministic single-turn check), same task, same machine, idle:
| System prompt |
Output reservation |
Outcome |
| small |
16,384 |
starts normally, completes the turn |
| small |
4,096 |
starts normally, completes the turn |
| large (~52K) |
16,384 |
HTTP 400 before any model work. Zero tool calls. |
| large (~52K) |
4,096 |
starts normally, completes the turn |
The failure is config-dependent, not content-dependent. The identical large prompt runs fine once the reservation drops. Nothing is wrong with the prompt content; the client sends a request that arithmetically cannot succeed.
Verified mechanism
packages/opencode/src/provider/transform.ts:1274 — ProviderTransform.maxOutputTokens(model, override) returns Math.min(model.limit.output, ceiling) where ceiling is OUTPUT_TOKEN_MAX (32_000, line 21) or the OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX override. It takes no input-size argument and never reads model.limit.context. For a model declaring { context: 65536, output: 16384 } it returns 16,384 unconditionally.
packages/opencode/src/session/llm.ts:144-147 — the live agent path passes that value straight into the chat.params hook and on to streamText at line 253. Nothing compares it against the assembled system + messages.
packages/opencode/src/session/llm/request.ts:129 — the Effect-path twin has the same defect.
There is no check anywhere between config and the wire that input + reservation <= context.
Compaction does not save us
Compaction.isOverflow (packages/opencode/src/session/compaction.ts:80-95) and session/overflow.ts:16-19 do reserve headroom, but they cannot prevent this:
- They are driven by the previous assistant message's reported token counts. On the first request of a session there is nothing to check, and the reported failure happens on the first request.
- Compaction only shortens conversation messages. In this case all 52,180 tokens are the system prompt, which compaction never touches. Even a perfectly-timed compaction cannot make this request fit.
So compaction is structurally unable to fix a prompt-side overflow, which is exactly what makes an unclamped reservation fatal rather than merely wasteful.
What the user sees today
Worse than "an opaque 400" — the error is not even classified as an overflow:
- The message does not match any pattern in
OVERFLOW_PATTERNS (packages/opencode/src/provider/error.ts:30-47). The closest is /maximum context length is \d+ tokens/i; the provider writes "maximum context length of 65536 tokens".
- Status is
400, not 413, and the body carries no context_length_exceeded code, so neither fallback in parseAPICallError (error.ts:325-337) fires.
The result is a generic MessageV2.APIError, and SessionRetry.retryable (session/retry.ts:71-79) returns undefined for a non-retryable 400. The session dies showing the raw provider text with no indication that one config value would fix it.
Expected
The client knows the input size, the requested reservation, and the window before it sends anything. It should:
- Clamp the reservation so
input + reservation <= window, rather than sending a request that cannot succeed.
- Keep a floor — if even the floor does not fit, fail early and loudly client-side with the actual numbers (input tokens, requested reservation, window) and what to change.
- Not silently shrink the budget to something unusable.
Notes
- Models that declare a separate
limit.input budget must be left alone; input and completion budgets are not shared there.
- Configs that already fit must be completely unaffected.
What happens
With a large system prompt on a model that declares a 65,536-token context window, the request fails with a hard provider
HTTP 400before any model work happens — zero tool calls, no tokens generated.The provider's own accounting in the error:
52,180 input + 16,384 reserved for the completion = 68,564 against a 65,536 window.
Why it is a client-side defect, not a prompt problem
Measured in a controlled A/B (N=4 per arm plus a deterministic single-turn check), same task, same machine, idle:
The failure is config-dependent, not content-dependent. The identical large prompt runs fine once the reservation drops. Nothing is wrong with the prompt content; the client sends a request that arithmetically cannot succeed.
Verified mechanism
packages/opencode/src/provider/transform.ts:1274—ProviderTransform.maxOutputTokens(model, override)returnsMath.min(model.limit.output, ceiling)whereceilingisOUTPUT_TOKEN_MAX(32_000, line 21) or theOPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAXoverride. It takes no input-size argument and never readsmodel.limit.context. For a model declaring{ context: 65536, output: 16384 }it returns 16,384 unconditionally.packages/opencode/src/session/llm.ts:144-147— the live agent path passes that value straight into thechat.paramshook and on tostreamTextat line 253. Nothing compares it against the assembledsystem+messages.packages/opencode/src/session/llm/request.ts:129— the Effect-path twin has the same defect.There is no check anywhere between config and the wire that
input + reservation <= context.Compaction does not save us
Compaction.isOverflow(packages/opencode/src/session/compaction.ts:80-95) andsession/overflow.ts:16-19do reserve headroom, but they cannot prevent this:So compaction is structurally unable to fix a prompt-side overflow, which is exactly what makes an unclamped reservation fatal rather than merely wasteful.
What the user sees today
Worse than "an opaque 400" — the error is not even classified as an overflow:
OVERFLOW_PATTERNS(packages/opencode/src/provider/error.ts:30-47). The closest is/maximum context length is \d+ tokens/i; the provider writes "maximum context length of 65536 tokens".400, not413, and the body carries nocontext_length_exceededcode, so neither fallback inparseAPICallError(error.ts:325-337) fires.The result is a generic
MessageV2.APIError, andSessionRetry.retryable(session/retry.ts:71-79) returnsundefinedfor a non-retryable 400. The session dies showing the raw provider text with no indication that one config value would fix it.Expected
The client knows the input size, the requested reservation, and the window before it sends anything. It should:
input + reservation <= window, rather than sending a request that cannot succeed.Notes
limit.inputbudget must be left alone; input and completion budgets are not shared there.