Skip to content

core: context-only limit leaves compaction using conflicting window sizes #60648

Closed

Description

@wannesdecraene

Description

I use a context override to keep prompts below the higher per-token pricing for long-context OpenAI prompts. I am not trying to set a separate max-input target; the context cost boundary is what matters to me.

On OpenCode Desktop v2.2.3, the Azure GPT-6 Luna model metadata from models.dev in my setup is context=1,050,000 / input=922,000 / output=128,000. My override sets context=256,000 and output=128,000, leaving input unset. With compaction.auto=true and buffer=40,000, the context panel reached about 225,000/256,000; Automatic compaction did not run. Manual /compact then sent max_output_tokens=1,024 and stopped at that cap; depending on reasoning tokens, it left a partial summary or no summary. Removing the model limit override lets manual compaction complete. Normal replies were unaffected.

The v2.2.3 source appears to use limit.input || limit.context for the compaction ceiling, but limit.context for summary output room. With these values, that suggests an 882,000-token ceiling and negative output room against the 256,000 context, clamped to 1,024. See calculateCeiling and outputLimit.

Is this behavior intended? If a context-only override is not meant to bound compaction, please clarify whether both fields must be set. My reason for setting context is to avoid higher long-context prompt pricing; I do not need a separate input-limit policy. If this behaves as designed, the fix is to add the same limit for input as I set for context in my settings. Related: #52954 and #59110.

Plugins

None found in user config; project-level plugins not verified.

OpenCode version

OpenCode Desktop/shared service v2.2.3. CLI v2.2.1 is installed but was not used for this reproduction.

Steps to reproduce

  1. Use the azure/gpt-6-luna model or any other model from models.dev with the catalog limits above. Override only limit.context to 256000 (and leave limit.input unset); set compaction.auto=true and compaction.buffer=40000, or leave it unset as the default 15% will be in place.
  2. Grow a session to about 225k while the context panel shows 256k. After an interrupted run, send another message. Automatic compaction does not start.
  3. Run /compact with the context override still set. The summary request uses max_output_tokens=1024 and stops at the limit. Remove the model limit override and retry; manual compaction completes.

Screenshot

No screenshot attached; can provide one if needed.

Operating System

Windows 11 Pro

Terminal

/ Desktop

Activity

  1. opencodejr commented on Oct 30, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I can confirm the /compact part on v2.2.3, and it still happens on the latest dev (da87c475a).

    What I did, with a simulated model whose limits match your setup (context 256,000 from the override, input 922,000 from the catalog, output 128,000) and compaction: { auto: true, buffer: 40000 }:

    1. Sent a message whose reply reported about 225k prompt tokens, then sent a second message. Automatic compaction did not start, and normal turns went out with max_completion_tokens: 128000.
    2. Ran /compact. The summary request, and its retry, went out with max_completion_tokens: 1024.
    3. With input left out of the limit (context-only 256k), the same summary requests got 40000.

    The script below reproduces it and logs the output limit of each request.

    From the code, the cause looks like this: the compaction threshold and the summary request size come from limit.input (922k − 40k buffer = 882k), but the summary's output room is worked out against limit.context (256k). That gives negative room, which is clamped to the 1,024 minimum.

    On automatic compaction not starting: the code uses the input limit for that by design. The missing docs for this are tracked in #52954. The 1,024 cap on the summary, though, is a real bug.

    Until it's fixed, setting input to the same value as context in your override should make compaction work again. Related: #52954 (which limit sets the threshold), #59110 (reasoning tokens using up the summary budget, which makes a 1,024 cap worse), and #58492 (open PR changing the threshold calculation).

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#60648: a context-only limit override (context 256k < catalog input 922k)
    // makes manual /compact send a tiny max output limit (clamped to the 1,024 minimum).
    // The simulated model's merged limits mirror the report: catalog input=922,000 kept, context overridden to 256,000.
    import { Effect, Stream } from "effect"
    import { appendFileSync, writeFileSync } from "node:fs"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const LOG = "/workspace/repro/requests.log"
    writeFileSync(LOG, "")
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
        config: {
          compaction: { auto: true, buffer: 40000 },
          providers: {
            simulation: {
              models: {
                "gpt-sim-model": { limit: { context: 256_000, ...(process.env.NO_INPUT ? {} : { input: 922_000 }), output: 128_000 } },
              },
            },
          },
        },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          yield* llm.serve((request, index) => {
            const body = request.body as Record<string, unknown>
            const text = JSON.stringify(body)
            const isSummary = /summar/i.test(JSON.stringify(body.messages ?? "").slice(-4000))
            appendFileSync(
              LOG,
              `#${index} max_tokens=${body.max_tokens} max_completion_tokens=${body.max_completion_tokens} bodyChars=${text.length} summaryLike=${isSummary} last=${JSON.stringify((body.messages as any[])?.at(-1)).slice(0,300)}\n`,
            )
            if (index === 0)
              return Stream.make(
                Llm.text("Reply one: the session is now large."),
                Llm.raw({ choices: [], usage: { prompt_tokens: 225_000, completion_tokens: 200, total_tokens: 225_200 } }),
              )
            return Stream.make(
              Llm.text(`Response ${index}: ## Summary of the conversation so far.`),
              Llm.raw({ choices: [], usage: { prompt_tokens: 1000, completion_tokens: 50, total_tokens: 1050 } }),
            )
          })
          yield* ui.submit("Hello, grow the session")
          yield* ui.waitFor("Reply one")
          yield* Effect.sleep(1500)
          yield* ui.screenshot("after-first-reply")
          yield* ui.submit("Another message after the large turn")
          yield* ui.waitFor("Response 1")
          yield* Effect.sleep(1500)
          yield* ui.screenshot("no-auto-compaction")
          yield* ui.submit("/compact")
          yield* Effect.sleep(6000)
          yield* ui.screenshot("after-compact")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. wannesdecraene commented on Oct 30, 2026

    @wannesdecraene
    Author

    I'd like to work on the summary-budget bug: with context=256000 and inherited input=922000, manual compaction incorrectly requests only 1024 output tokens. I'll add a regression test and keep changes to automatic-compaction policy separate. Is this scope sensible given #58492 and #59226? Later I could add the missing docs as the autocompaction not starting is by design.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions