Summary
Every context budget in the plugin is computed as a percentage of the whole resolved context limit — limit.output is never subtracted anywhere. On engines that enforce prompt + max_tokens <= max_model_len (vLLM, and most OpenAI-compatible local servers), this makes a hard ContextWindowExceededError unavoidable for any session that genuinely fills up, even when the user's opencode config declares the model window accurately.
Details (verified against 0.32.4 dist bundle)
resolveLimit(limit) returns limit.input ?? limit.context, whole. On opencode 1.18.x the input branch is dead code (the binary's LLM.ModelLimits class carries only {context, output} and construction sites drop other keys), so the plugin always budgets against limit.context.
- Every downstream budget is multiplicative on that value:
deriveProtectedTailTokenTarget (usable = contextLimit * threshold / 100), selectPerRunCap, resolveHistoryBudgetTokens, the 95% EMERGENCY_DRAIN band, planEmergencyDrop's ceiling.
- A grep of the whole bundle for
limit.output finds exactly one hit — a finish_reason string comparison. The output reservation opencode will add as max_tokens on the very request being trimmed is invisible to the trimmer.
Concrete failure (measured): model declared limit: {context: 122880, output: 16384} against a vLLM engine with max_model_len = 131072. The plugin allows input to grow to ~122,880; opencode sends max_tokens = 16384 on top; 122880 + 16384 = 139264 > 131072 → hard 400 at the engine. Sessions in our fleet repeatedly died at 93%+ of the declared context. (The overflow-detection/detected_context_limit machinery correctly min()s — it cannot help here because the configured value is the one that overshoots once output is added.)
Suggested fix
Either (or both):
- When only
limit.context and limit.output are available (opencode ≤ 1.18.x), budget against limit.context − limit.output rather than limit.context.
- Add an
output_reserve (or similar) config key so operators can state the reservation explicitly where the harness cannot supply limit.input.
The workaround we shipped is to hand-encode the reservation into limit.context (declare context = max_model_len − output − slack), which works but makes the config value mean something different from what its name says, and every operator has to rediscover this the hard way.
Happy to provide the full trace/measurements if useful.
Summary
Every context budget in the plugin is computed as a percentage of the whole resolved context limit —
limit.outputis never subtracted anywhere. On engines that enforceprompt + max_tokens <= max_model_len(vLLM, and most OpenAI-compatible local servers), this makes a hardContextWindowExceededErrorunavoidable for any session that genuinely fills up, even when the user's opencode config declares the model window accurately.Details (verified against 0.32.4 dist bundle)
resolveLimit(limit)returnslimit.input ?? limit.context, whole. On opencode 1.18.x theinputbranch is dead code (the binary'sLLM.ModelLimitsclass carries only{context, output}and construction sites drop other keys), so the plugin always budgets againstlimit.context.deriveProtectedTailTokenTarget(usable = contextLimit * threshold / 100),selectPerRunCap,resolveHistoryBudgetTokens, the 95%EMERGENCY_DRAINband,planEmergencyDrop's ceiling.limit.outputfinds exactly one hit — afinish_reasonstring comparison. The output reservation opencode will add asmax_tokenson the very request being trimmed is invisible to the trimmer.Concrete failure (measured): model declared
limit: {context: 122880, output: 16384}against a vLLM engine withmax_model_len = 131072. The plugin allows input to grow to ~122,880; opencode sendsmax_tokens = 16384on top;122880 + 16384 = 139264 > 131072→ hard 400 at the engine. Sessions in our fleet repeatedly died at 93%+ of the declared context. (The overflow-detection/detected_context_limitmachinery correctlymin()s — it cannot help here because the configured value is the one that overshoots once output is added.)Suggested fix
Either (or both):
limit.contextandlimit.outputare available (opencode ≤ 1.18.x), budget againstlimit.context − limit.outputrather thanlimit.context.output_reserve(or similar) config key so operators can state the reservation explicitly where the harness cannot supplylimit.input.The workaround we shipped is to hand-encode the reservation into
limit.context(declarecontext = max_model_len − output − slack), which works but makes the config value mean something different from what its name says, and every operator has to rediscover this the hard way.Happy to provide the full trace/measurements if useful.