Repository navigation
Description
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
ollamaprovider 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.jsonmodel 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
- Configure a custom
openai-compatibleprovider pointing at a local Ollama server, with an explicitlimit.contextwell above expected usage (e.g. 32768). - Start a task that requires 2+ sequential tool calls (e.g. "create a new project folder, init git, create a README").
- Observe that after the first tool call's
step-finish (tool-calls), acompactionpart is logged immediately with"overflow": false, followed by a full summarization turn and a synthetic continuation prompt. - 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
argszero commentedon Sep 19, 2026
I'd like to work on this.
I traced the trigger on
dev(the1.18.31line 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:495is the check that agrees with your log:needsCompactionis set only whenisOverflow({ tokens: usage.tokens, ... })is true. Since your log says"overflow": false, that is not the path firing.processor.ts:629: aContextOverflowErrorsetsctx.needsCompaction = trueunconditionally, and the loop then returns"compact"(processor.ts:693) — an auto-compaction regardless of the client-side token math.packages/opencode/src/session/message-v2.ts:685fromProviderError.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 theexceeds the available context sizepattern 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) andlimit.contextin youropencode.json(32768). Every step the server rejects → opencode compacts → retries → rejected again, i.e. one compaction per tool call, each logged withoverflow: false. That also explains why Copilot/Claude is unaffected: there the declared limit and the server window agree.Plan:
openai-compatibleprovider whose declaredlimit.contextis larger than the server's actual window, asserting that a provider-side overflow error produces at most one compaction instead of one perstep-finishwithreason: "tool-calls";ContextOverflowErrorbranch, do not start another auto-compaction whenisOverflow()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.contextvs. 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.ydixken commentedon Sep 20, 2026
Can confirm as well.
argszero commentedon Sep 20, 2026
Correction to my earlier analysis, plus the actual arithmetic behind this report.
My first comment blamed the provider-error branch (
processor.ts~621-631, aContextOverflowErrorfrom the SDK settingneedsCompaction). That was wrong, and your own log is the proof:prompt.tscomputes the compaction part'soverflowfield asoverflow: !handle.message.finish, so a stream that dies from a provider error leavesfinishunset and you would seeoverflow: true. Your log showsoverflow: false, which rules that path out. The trigger is the client-side token check atprocessor.ts:491-496.Verified numbers (dev @
d870e22, callingisOverflow/usablefrompackages/opencode/src/session/overflow.tsdirectly with your model config):context: 32768, output: 32768(yours)context: 32768, output: 8192context: 32768, input: 32768, output: 32768So with
limit.contextequal tolimit.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-496setsneedsCompaction-> the loop returns"compact"->prompt.ts:1320-1328callscompaction.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 withoverflow: false, exactly as you observed. Copilot/Claude are unaffected because their declaredlimit.outputleaves a real input window.Two things worth noting in the code while I am here:
usable()disagree about how much output to reserve: thelimit.inputbranch reservesmin(20_000, maxOutputTokens)while the fallback branch subtracts the whole output allowance, so a model that declaresoutput >= contextends up with an input budget of almost nothing.test/session/compaction.test.tsalready documents the mirror image of this asymmetry as a bug.compaction.ts:448-459already has the right shape for the terminal case: when compaction itself cannot proceed it records aContextOverflowErrorand 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):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 aContextOverflowErrornaming the model's declaredlimit.context/limit.output, mirroring the existing terminal case incompaction.ts;openai-compatiblefixture whose declaredlimit.contextfar 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.
GangstaVik commentedon Sep 20, 2026
@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
argszero commentedon Sep 20, 2026
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
ContextOverflowErrornaming the declared limits instead of compacting again. The first compaction still runs exactly as before, so theusable()arithmetic that makes a model withlimit.context == limit.outputmeasure as an overflow is left untouched.thdxr commentedon Oct 11, 2026
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 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊