Skip to content

[Bug] Silent context-overflow death on resume when model reports limit.context=0 (custom providers): uncalibrated thresholds → 400 → empty exit-0 loop #347

Description

@ranxianglei

Summary

Sessions replayed against a custom OpenAI-compatible model that reports no context limit (limit.context = 0) can grow past the backend's actual window and then die on every resume with a 400 Bad Request that opencode swallows (exit 0, no output). The failure is deterministic and unrecoverable for that session id.

Reported from production in ranxianglei/billion-context#317 (2 sessions in 24h; full analysis there). This issue tracks the opencode-acp-side fixes.

Repro (observed in production)

  • opencode 1.14.46, plugin opencode-acp@latest (stable 1.14.19), model vllm-qwen/qwen3.8-27b defined in opencode.json without a limit, backend sglang with real window 262,144.
  • opencode's /config/providers reports "limit": {"context": 0, "output": 0} for the model (verified live).
  • Long-lived headless session, per-message resume (opencode run --session <ses>), compaction.auto: false.

Failure sequence (from ~/.local/share/opencode/log/2026-08-28T142830.log, session ses_7fb2e8b92c3eZ6yUJKdhkjVe1Z):

  1. state.modelContextLimit is undefinedlib/state/model-limits.ts record() drops limit <= 0, so the catalog never gets an entry, and hooks.ts:100 (if (state && input.model?.limit?.context)) never sets it. The persisted state file has no modelContextLimit key at all.
  2. All percentage thresholds resolve to undefined (parseLimitValue in lib/messages/inject/utils.ts: percent + undefined window → undefined): minContextLimit "45%", maxContextLimit "55%", emergencyThresholdPercent 98%overMaxLimit/overMinLimit permanently false.
  3. Only the absolute growth nudge (50K from baseline) fires — advisory text only. Observed: nudge at 206,326 tokens; model voluntarily compressed just 37,178 tokens total (3 blocks); live replay grew to ~230K.
  4. Request: input 230,527 + max_tokens 32,000 (opencode's fallback for limit.output = 0; user's options.maxTokens: 16384 is not honored — it leaks into the body as a raw maxTokens key sglang ignores) = 262,527 > 262,144 → sglang: Requested token count exceeds the model's maximum context length of 262144 tokens (400, isRetryable: false).
  5. opencode logs session.processor error=Bad Request, publishes session.error, goes idle, exits 0 with no output. Daemon retries 3× (inputs 230,275 / 230,433 / 230,527 — grow ~150 tokens per retry as the nudge message is appended). Fresh session id works immediately.

Note: with max_tokens = 16,384 the same request would have fit (246,911 < 262,144) — the 32,000 fallback pushed it over by 383 tokens.

Impact

  • Any custom-provider deployment (local vLLM/sglang/LM Studio, etc.) where the model has no catalog entry: the percentage-based protection system is silently disabled. Sessions accumulate until replay + completion reserve exceeds the real window, then the session is permanently stuck (every resume 400s; only a new session id heals it).
  • No error surface: exit 0 + empty output. Operators see "agent stopped replying" with nothing in the logs except a buried AI_APICallError.
  • lastCompaction: 0 in state files is a red herring (it only tracks opencode native compaction, which is disabled via compaction.auto: false) — do not chase it.

Immediate workaround (no code)

  1. Declare the real window in opencode.json (schema supports per-model limit; both keys required):
"qwen3.8-27b": { "limit": { "context": 262144, "output": 16384 }, ... }
  1. Optional: absolute thresholds in acp.jsonc (numbers pass through parseLimitValue untouched, no model window needed):
{ "compress": { "maxContextLimit": 150000, "minContextLimit": 100000 } }

Both are partial: nudges stay advisory — a small local model can still under-compress.

Proposed fixes (opencode-acp)

  1. Loud failure instead of silent blindness (small, do first): when limit.context <= 0 / state.modelContextLimit === undefined persists across N requests, log a prominent one-time WARN per session: "model reports no context window; percentage thresholds (min/max/emergency) are disabled — set limit in opencode.json or absolute compress.maxContextLimit/minContextLimit". Today this state is invisible, which is why the blindness went unnoticed for a long-lived session.
  2. Request-side hard guard (the real fix; the "auto-fallback to a compacted rebuild" asked for in billion-context#317): in messages.transform, estimate the wire size (tokenization already exists for nudges) and if it exceeds safeBudget = knownWindow − completionReserve (knownWindow = state.modelContextLimit or absolute maxContextLimit; completionReserve = configured limit.output or a conservative default), deterministically prune-to-fit: drop/summarize the oldest compressible (non-protected) tool outputs until the estimate fits — independent of model cooperation. This converts a hard 400 into a degraded-but-working turn.
  3. Learn the real window from 400s (design note / blocked): sglang's error text states the exact limit (maximum context length of 262144); parsing it and writing state.modelContextLimit would self-heal calibration. Blocked today because opencode exposes no response-error hook to plugins (only experimental.chat.system.transform, experimental.chat.messages.transform, experimental.text.complete). Track upstream; if a hook lands, implement auto-recalibration.

Related upstream issue (opencode)

Filed separately against anomalyco/opencode: opencode run exits 0 with no output on provider 400 (should exit non-zero + print the error), and options.maxTokens is not honored for custom models (32,000 fallback used instead; the configured value leaks as a raw maxTokens body key).

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

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions