Skip to content

Proposal: Make local context-overflow recovery actually reachable #9

Closed

Description

@androidand
Proposal

Why

The 413 auto-recovery for local llama-skein providers has never executed.
Diagnosed live on proxmox / qwopus3.6-27b-v2-mtp-q8-0 (2026-07-27) after the
model repeatedly appeared "stuck" in a Build session while z4 was unaffected.

  1. The handler gates on the wrong error type for this failure mode.
    adjustLocalContextOnOverflow (packages/opencode/src/provider/provider.ts:1379)
    returns early unless error.type === "context_too_large". For a prompt
    that overflows an already-loaded, correctly-configured model, llama-skein
    actually emits type: "exceed_context_size_error", code: "prompt_over_max_safe_ctx" (internal/server/promptguard.go:103-104).
    Correction to an earlier draft of this proposal: context_too_large
    is not dead code and does not "appear nowhere" — it is real and still
    fires, from a different component for a different failure mode:
    proxy/proxymanager.go:1095 sends it when a model fails to load
    because its configured ctx doesn't fit available memory, distinct from a
    prompt being too large for an already-loaded model. Every prior task that
    hardened this function (ctx-display-and-overflow-correctness tasks 1–3,
    all marked complete) refined code that was unreachable for the
    prompt-overflow class specifically
    — the model-misconfigured class it
    already handled correctly and continues to.

  2. The remedy is wrong for this error class. prompt_over_max_safe_ctx
    means the prompt is too large for the host, not the model's ctx-size is
    misconfigured
    . The handler responds by PATCHing the backend to a larger
    ctx_size. On the observed host that is impossible: the model already needs
    32078 MB of a 32624 MB card (98.3%, 546 MB headroom). Growing ctx would
    OOM, not recover.

  3. The ceiling it reads is legitimately absent. The handler takes
    max_fit_ctx and bails on <= 0. For this model llama-skein omits
    max_fit_ctx because its KV budget is negative
    (32624 × 0.92 × 0.85 − 27701 × 1.05 = −3574 MB), so recovery would fail even
    if reached. Meanwhile max_safe_ctx: 74711 is present and is the value
    the backend actually enforces — and fetchLocalModelFit in the same file
    already documents max_safe_ctx as "the authoritative prompt ceiling".
    The two functions disagree about which field is authoritative.

  4. A single failed fit probe wedges the whole session. Discovery sets
    limit.context = fit.max_safe_ctx ?? reported context_length. Fit and
    /v1/models share one 2s AbortController, and discovery runs once at
    startup
    . When the fit probe loses that race, the model silently keeps
    90112 instead of 74711 — 21% over the enforced wall — for the entire
    process lifetime. Compaction then sizes its budget from the wrong number,
    never trims, and every request 413s with recovery dead. This is why the
    symptom is intermittent ("stuck again") and why restarting clears it.

z4 is unaffected only because its gap is proportionally small and rarely
reached (max_safe_ctx 237076 vs reported 262144); proxmox sits at
74711 vs 90112, which a Build session crosses routinely.

What

  • Match the real contract. Recovery MUST trigger on llama-skein's actual
    overflow signal (type: "exceed_context_size_error" / code: "prompt_over_max_safe_ctx", HTTP 413), not on context_too_large. The
    matcher MUST be driven by the published contract, not a hand-copied string.
  • Trim, do not grow. A prompt-overflow 413 MUST be handled by reducing the
    outgoing prompt to the advertised ceiling (triggering compaction), never by
    PATCHing the backend to a larger ctx_size. Backend ctx-patching remains
    valid only for a genuine model-misconfigured overflow.
  • Use the advertised ceiling. The limit MUST be read from the machine-
    readable X-Skein-Max-Safe-Ctx response header (falling back to /api/fit
    max_safe_ctx), never parsed out of the human-readable message, and never
    gated on max_fit_ctx, which is legitimately absent for a VRAM-tight model.
  • Self-heal the stale limit. On a prompt-overflow 413 opencode MUST correct
    the in-memory limit.context for that model from the authoritative value in
    the response, so the session recovers without a restart.
  • Never silently over-estimate. When the /api/fit probe fails, discovery
    MUST NOT fall back to a context_length that exceeds the enforced budget
    without marking the model as unverified; a too-large budget is worse than a
    conservative one. Give fit its own timeout rather than sharing the models
    fetch's 2s budget.

Non-goals

  • No change to llama-skein's fit math or its VRAM safety fractions
    (companion change report-achievable-ctx-for-configured-models).
  • No re-litigation of the display denominator
    (ctx-display-and-overflow-correctness owns that and is complete).
  • Capacity remediation for the proxmox host (lowering --ctx-size to ~74k or
    relocating the model) is an ops action tracked separately, not a code change.

Impact

  • packages/opencode/src/provider/provider.ts: adjustLocalContextOnOverflow
    trigger condition, remedy, and ceiling source; the 413 retry site (~:2126);
    discoverOpenAICompatibleModels fit timeout isolation and fallback policy.
  • packages/opencode/src/provider/error.ts: overflow classification already
    catches 413 — must stay consistent with the new handler.
  • packages/opencode/src/session/compaction.ts: consumes limit.context;
    benefits from the corrected value, no signature change expected.
  • Regression risk is low and well-bounded: the code path being changed is
    currently dead, so behaviour can only improve from "no recovery".

Disposition (2026-09-18)

Shipped. Shipped (provider.ts overflow discriminator + getModelFit). Open 7 (cross-repo drift guard) and 8 (live) are not blocking; reopen as a small change if the contract drifts.

Tasks

  • 1. Add a failing regression test that feeds adjustLocalContextOnOverflow
    a real llama-skein 413 body
    ({"error":{"type":"exceed_context_size_error","code":"prompt_over_max_safe_ctx", "message":"prompt (~106692 tokens) exceeds the safe context ... (max_safe_ctx 74711)"}})
    plus an X-Skein-Max-Safe-Ctx: 74711 header, and asserts recovery fires.
    It MUST fail against main — that failure is the proof the handler is
    currently unreachable.

    • File: packages/opencode/test/provider/provider.test.ts (3 new tests: header path, /api/fit fallback path, and a regression guard that the pre-existing context_too_large class still patches+retries)
    • adjustLocalContextOnOverflow and State exported from provider.ts to make this directly testable, matching the existing mergeDiscoveredModel export convention.
  • 2. Split the overflow classes. Introduce an explicit discriminator:
    prompt_over_max_safe_ctx → trim the prompt;
    a model-ctx-misconfigured overflow → patch backend ctx (existing
    behaviour). Only the latter may call patchConfigModel.

    • File: packages/opencode/src/provider/provider.ts (adjustLocalContextOnOverflow)
    • Note: see proposal.md correction — context_too_large is real (proxy/proxymanager.go:1095, model-failed-to-load class) and its existing patch-and-retry behavior is preserved unchanged, not replaced.
  • 3. Prompt-overflow path: read the ceiling from X-Skein-Max-Safe-Ctx,
    falling back to /api/fit max_safe_ctx. Do NOT consult max_fit_ctx
    and do NOT regex the message text. Drop the maxFit <= 0 → return false
    bail from this path.

    • Implemented via client.getModelFit({model}) (single-model fit lookup, not the fleet-wide getFitReport) as the fallback when the header is absent or invalid.
  • 4. Self-heal: on a prompt-overflow 413, write the authoritative ceiling
    back into the in-memory limit.context for that model so the session
    recovers without a restart, then signal compaction to re-run before the
    retry.

    • Implemented as: mutate s.providers[providerID].models[modelID].limit.context directly (same live-object mutation pattern as the existing setModelContextLimit, but touching only context, not contextMax — the hard n_ctx ceiling hasn't changed, only the safe prompt budget), then return false (no blind inline retry of the same oversized body). "Signal compaction" is the existing reactive needsCompaction = true path in session/processor.ts on any ContextOverflowError (already fires for any 413 — error.ts already classifies by status code, confirmed unchanged) — it was already wired, just never got a correct budget to compact against. No compaction.ts signature change, matching the proposal's stated Impact scope.
  • 5. Give fetchLocalModelFit its own AbortController instead of sharing
    the /v1/models 2s budget, so a slow fit probe cannot silently downgrade
    limit.context to the larger context_length.

    • /v1/models keeps its 2s budget; fit gets its own 3s budget. Updated the existing "discovery completes when /api/fit hangs forever" test's stale comment (it previously asserted the two should share a budget); its assertions (limit.context fallback value, elapsed < 5000) needed no change since 3s < 5s.
  • 6. When fit is unavailable AND the reported context_length exceeds any
    previously-known max_safe_ctx, prefer the conservative known value and
    log at WARN; never silently adopt the larger number.

    • File: discoverOpenAICompatibleModels in provider.ts. Side effect: this also fixed a pre-existing, unrelated-looking failing test (discoverModels:true merges discovered models without overriding manual models, expected 4096 got 8192) — same root cause playing out via a manually-configured model's smaller context being overridden by a larger discovered value, not just a prior-discovery's fit value. Full provider.test.ts suite: 101 pass, 0 fail (was 1 fail before this task).
  • 7. Contract-drift guard: derive the accepted error type/code from the
    llama-skein OpenAPI contract (or a single shared constant) and add a test
    that fails if the two repos diverge again. This class of bug — a
    hand-copied error string — is the actual root cause and MUST NOT recur.

    • Partial: added LLAMA_SKEIN_PROMPT_OVERFLOW_TYPE/_CODE/_MAX_SAFE_CTX_HEADER as single-sourced constants in provider.ts with comments citing the exact llama-skein source lines, so opencode-skein itself no longer has the string duplicated. Did NOT build the cross-repo CI drift guard (would need llama-skein's OpenAPI spec vendored or fetched into this repo's test/CI — real infra work, out of scope for a bug fix). Follow-up.
  • 8. Verify live against proxmox / qwopus3.6-27b-v2-mtp-q8-0: a Build
    session that crosses 74711 tokens compacts and continues instead of
    wedging. Capture before/after in the change notes.

    • Not done — no access to the proxmox host from this environment. Needs manual verification.
  • 9. bun run typecheck green in opencode and tui; bun test green for
    the provider package.

    • bun run typecheck: only the pre-existing src/session/prompt.ts:1152 error remains, caused by unrelated uncommitted WIP (auto-mode) in the working tree — confirmed via bisecting clean checkouts, not caused by this change. tui package not separately checked (no changes there). bun test test/provider/provider.test.ts: 101 pass, 0 fail. bun test test/session/compaction.test.ts: 55 pass, 1 skip, 0 fail.

Unchecked items above: see Disposition in proposal.md (2026-09-18).

Plan changes

7 done

Activity

  1. androidand commented on Sep 17, 2026

    @androidand
    OwnerAuthor

    Archived 2026-09-18 — see the change's Disposition section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions