Skip to content

Output-token reservation is never checked against the context window — a large system prompt causes a hard provider 400 before any model work #1194

Description

@anandgupta42

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

  1. packages/opencode/src/provider/transform.ts:1274ProviderTransform.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.
  2. 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.
  3. 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:

  1. Clamp the reservation so input + reservation <= window, rather than sending a request that cannot succeed.
  2. 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.
  3. 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.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions