Skip to content

Auto-compaction fires after every tool-call step for Ollama (openai-compatible) provider, even when nowhere near context limit #49965

Closed as not planned

Description

@jensklose

Bug: Auto-compaction fires after every tool-call step for the Ollama (openai-compatible) provider, even when nowhere near the context limit

Summary

When using a local model through a custom openai-compatible provider (Ollama), opencode's auto-compaction runs unconditionally after every single step-finish with reason: "tool-calls" — even when token usage is a small fraction of the configured context window (limit.context). The compaction event itself is logged with "overflow": false, confirming opencode's own overflow check does not think compaction is needed, yet it runs anyway.

This does not happen with other providers (verified with a GitHub Copilot / Claude Sonnet session using the same agent and same task pattern — zero compaction-agent invocations in the entire session).

The extra compaction round-trip injects a synthetic "Continue if you have next steps, or stop" message after summarizing. Smaller/local models frequently misinterpret this as a cue to stop and ask for clarification instead of continuing the tool-call loop, effectively breaking multi-step agentic tasks after almost every tool call.

Environment

  • opencode version: 1.18.31
  • Provider: custom ollama provider using @ai-sdk/openai-compatible, pointed at a local Ollama server (v0.32.15)
  • Models tested: glm-4.7-agent (custom Modelfile, num_ctx: 32768), qwen3:32b — same behavior on both
  • opencode.json model entry for the affected model:
    "glm-4.7-agent": {
      "name": "GLM 4.7 Agent (local)",
      "tool_call": true,
      "reasoning": true,
      "cost": { "input": 0, "output": 0 },
      "limit": { "context": 32768, "output": 32768 },
      "options": { "num_ctx": 32768, "temperature": 0.1 }
    }

Evidence (from local opencode.db)

Sequence of parts for a single assistant turn using the Ollama provider:

{"reason":"tool-calls","type":"step-finish","tokens":{"total":6478,"input":6266,"output":212,"reasoning":0,"cache":{"write":0,"read":0}},"cost":0}
{"type":"compaction","auto":true,"overflow":false}

Token usage here is 6478 out of a configured 32768 context limit (~20%) — nowhere near overflow — yet the compaction agent still runs immediately afterward. This repeats after every subsequent tool call in the session, each time logging "overflow": false.

For comparison, an equivalent session using the same agent/task pattern but with github-copilot/claude-sonnet-5 shows zero compaction-agent messages across the whole session (SELECT count(*) FROM message WHERE agent='compaction' → 0), confirming the behavior is provider-specific rather than a general agent-loop issue.

Impact

  • Every tool call costs an extra full model round-trip (summarization + continuation prompt), which is especially costly on local/slower models.
  • Local/smaller models often respond to the synthetic "Continue if you have next steps, or stop" message by stopping and asking for clarification rather than continuing autonomously, which breaks multi-step tool-calling tasks (e.g. "create a project, init git, add files" stops after the first mkdir/tool call).

Steps to reproduce

  1. Configure a custom openai-compatible provider pointing at a local Ollama server, with an explicit limit.context well above expected usage (e.g. 32768).
  2. Start a task that requires 2+ sequential tool calls (e.g. "create a new project folder, init git, create a README").
  3. Observe that after the first tool call's step-finish (tool-calls), a compaction part is logged immediately with "overflow": false, followed by a full summarization turn and a synthetic continuation prompt.
  4. Compare against the same task/agent using a different (non-Ollama) provider — no compaction event is logged.

Workaround

Setting "compaction": { "auto": false } in opencode.json stops the unwanted compaction entirely, at the cost of losing real overflow protection for genuinely long sessions.

Suggested fix direction

The auto-compaction trigger condition appears to evaluate incorrectly (or not at all — defaulting to "always compact") for the openai-compatible provider path specifically, independent of the actual overflow computation used for the log field. It would help to align the trigger check with the same overflow value that's already being logged, rather than firing unconditionally for this provider type.

Activity

  1. argszero commented on Sep 19, 2026

    @argszero
    Contributor

    I'd like to work on this.

    I traced the trigger on dev (the 1.18.31 line you're running) and the compaction you're seeing does not come from the token-based overflow check you'd expect:

    • packages/opencode/src/session/processor.ts:495 is the check that agrees with your log: needsCompaction is set only when isOverflow({ tokens: usage.tokens, ... }) is true. Since your log says "overflow": false, that is not the path firing.
    • The path that is firing is the provider-error branch at processor.ts:629: a ContextOverflowError sets ctx.needsCompaction = true unconditionally, and the loop then returns "compact" (processor.ts:693) — an auto-compaction regardless of the client-side token math.
    • That error is built in packages/opencode/src/session/message-v2.ts:685 from ProviderError.parseAPICallError, whose context-overflow test is a message match (packages/opencode/src/provider/error.ts, isContextOverflow(m) → packages/llm/src/provider-error.ts). Ollama's text lands on the exceeds the available context size pattern in that list, so your local model's server-side error is classified as a context overflow.

    Two numbers disagree: the server's real window (Ollama num_ctx / the Modelfile) and limit.context in your opencode.json (32768). Every step the server rejects → opencode compacts → retries → rejected again, i.e. one compaction per tool call, each logged with overflow: false. That also explains why Copilot/Claude is unaffected: there the declared limit and the server window agree.

    Plan:

    1. reproduce with a custom openai-compatible provider whose declared limit.context is larger than the server's actual window, asserting that a provider-side overflow error produces at most one compaction instead of one per step-finish with reason: "tool-calls";
    2. break the loop: on the ContextOverflowError branch, do not start another auto-compaction when isOverflow() is false for the same model and token counts — record the provider error on the message and stop.

    One question before I write it: should the client-side check win like that (stop compacting when it disagrees with the provider), or should the provider error be surfaced verbatim with both numbers (declared limit.context vs. the server's real window) so the misconfiguration is visible? My default is the first. Happy to have the issue assigned if that direction looks right.

  2. ydixken commented on Sep 20, 2026

    @ydixken

    Can confirm as well.

  3. argszero commented on Sep 20, 2026

    @argszero
    Contributor

    Correction to my earlier analysis, plus the actual arithmetic behind this report.

    My first comment blamed the provider-error branch (processor.ts ~621-631, a ContextOverflowError from the SDK setting needsCompaction). That was wrong, and your own log is the proof: prompt.ts computes the compaction part's overflow field as overflow: !handle.message.finish, so a stream that dies from a provider error leaves finish unset and you would see overflow: true. Your log shows overflow: false, which rules that path out. The trigger is the client-side token check at processor.ts:491-496.

    Verified numbers (dev @ d870e22, calling isOverflow/usable from packages/opencode/src/session/overflow.ts directly with your model config):

    model limit usable() isOverflow at your 6478 total tokens
    context: 32768, output: 32768 (yours) 768 true
    context: 32768, output: 8192 24576 false
    context: 32768, input: 32768, output: 32768 12768 false

    So with limit.context equal to limit.output, usable() collapses to 768 tokens and every step-finish in the session is classified as an overflow. The loop then follows: processor.ts:491-496 sets needsCompaction -> the loop returns "compact" -> prompt.ts:1320-1328 calls compaction.create({ auto: true, overflow: false }) -> the compaction turn runs -> the next step-finish overflows again for the same reason. One compaction per tool call, each logged with overflow: false, exactly as you observed. Copilot/Claude are unaffected because their declared limit.output leaves a real input window.

    Two things worth noting in the code while I am here:

    • The two branches of usable() disagree about how much output to reserve: the limit.input branch reserves min(20_000, maxOutputTokens) while the fallback branch subtracts the whole output allowance, so a model that declares output >= context ends up with an input budget of almost nothing. test/session/compaction.test.ts already documents the mirror image of this asymmetry as a bug.
    • compaction.ts:448-459 already has the right shape for the terminal case: when compaction itself cannot proceed it records a ContextOverflowError and stops instead of looping. The repeated-overflow case has no equivalent.

    What I intend to implement (a PR against dev, unless you would rather steer it differently):

    1. break the loop rather than reformat a config mistake inside usable(): after an auto-compaction, if the next overflow check still reports at or above the token count that triggered that compaction, stop auto-compacting and record a ContextOverflowError naming the model's declared limit.context/limit.output, mirroring the existing terminal case in compaction.ts;
    2. test it with a custom openai-compatible fixture whose declared limit.context far exceeds what the fixture server accepts, asserting one compaction per turn instead of one per step-finish.

    The alternative direction is to clamp the output reservation in usable() (for example reserving at most half the window) so a self-contradictory limit cannot starve the input budget. I would rather not change that arithmetic without your call, since the existing tests treat "context minus output" as the intended formula. Either way the loop needs breaking, since the current behaviour also hides a genuine provider overflow behind an infinite compaction cycle.

    I'd like to work on this.

  4. GangstaVik commented on Sep 20, 2026

    @GangstaVik

    @argszero Dude wonderful debugging analysis, please if you have any idea on how to fix that, do so T_T this issue has been bugging me for days now T_T

  5. argszero commented on Sep 20, 2026

    @argszero
    Contributor

    Opened #50233 for this.

    The loop stops by itself now: it remembers the token count that triggered an automatic compaction and, when the next measured step is still at or above that count, it records a ContextOverflowError naming the declared limits instead of compacting again. The first compaction still runs exactly as before, so the usable() arithmetic that makes a model with limit.context == limit.output measure as an overflow is left untouched.

  6. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

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