Skip to content

core: compaction ignores agents.compaction.model since "shared model request" refactor (v2 beta) #44094

Closed

Description

@shangweijun

core: compaction ignores agents.compaction.model since "shared model request" refactor (v2 beta)

Summary

Since the Aug 19–21 refactor that routes title and compaction through a shared model-request flow, manual compaction in v2 beta always uses the session's current model and silently ignores agents.compaction.model. The config key is accepted (it shows up in opencode2 debug agents) but never consulted for model selection — a silent regression for v1 configs that set a dedicated compaction model.

Environment

  • opencode version: 0.0.0-beta-17898
  • OS: Windows 11 (Windows_NT 10.0.26200.9168, x64)
  • Terminal: Git Bash / mintty (TERM=xterm-256color)
  • Shell: bash (Git Bash, /usr/bin/bash)
  • Install/channel: beta (npm @opencode-ai/cli 0.0.0-beta-17898, auto-updated)
  • Active plugins: none found in config

Reproduction

  1. Configure a dedicated compaction model, e.g. in opencode.json:
    {
      "agents": {
        "build": { "model": "opencode-go/deepseek-v4-flash#max" },
        "compaction": { "model": "opencode-go/mimo-v2.5" }
      }
    }
  2. Restart the service (opencode2 service restart) and verify the merged registry:
    opencode2 debug agents → compaction agent does show "model": { "id": "mimo-v2.5", "providerID": "opencode-go" }.
  3. Trigger manual compaction (keybind session.compact, or POST /api/session/{sessionID}/compact).
  4. Observe the actual model used: the request follows the session's model (e.g. deepseek-v4-flash#max), not the configured compaction model (observable via cost/token accounting deltas matching the session model's pricing, and via the summary request behavior).

Expected Behavior

When agents.compaction.model is set, compaction should use that model (as v1 did, and as the dev branch code still does). At minimum, an accepted-but-ignored config key should produce a warning instead of silent no-op.

Actual Behavior

Compaction model resolution is hard-wired to the session model:

  • packages/core/src/session/compaction.ts (beta branch): models.resolve(input.session) → modelRequests.prepare({ scope: { session, agentID: Agent.ID.make("compaction"), model: plan.resolved }, ... })
  • packages/core/src/session/runner/model.ts: resolve(session) returns the session's session.model (falling back to a default only when none is set).
  • packages/core/src/session/model-request.ts: prepare uses input.scope.model directly (line ~284 const resolved = input.scope.model); scope.agentID is used only as a request label (agent: input.scope.agentID), never to look up the agent's own model.

So agents.compaction.model never participates in model selection in beta builds.

Additional Context

  • Docs (opencode.ai/v2/docs/compaction) state compaction uses the session's current model with no separate model setting — that matches beta behavior, but the config key is still accepted without warning, which silently breaks migrated v1 configs (including the v1 built-in compaction agent model field).
  • The dev branch still has the old behavior: packages/opencode/src/session/compaction.ts (dev, last change Aug 12) resolves:
    const agent = yield* agents.get("compaction")
    const model = agent.model
      ? yield* provider.getModel(agent.model.providerID, agent.model.modelID)
      : yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
  • Likely related commits on the beta branch:
    • ace822308f (2026-08-19) "refactor(core): move compaction config into state"
    • ebc2504ef3 (2026-08-20) "refactor(core): route title and compaction through shared model request"
    • e5da5bfab2 / 3a1fb5ae65 (2026-08-21) "simplify effect workflows" / "simplify projection state"

Feature request: in the shared model-request flow, honor agents.compaction.model (the agent model, like the dev branch) when set — e.g. resolve the compaction scope's model from the compaction agent first, falling back to the session model. Otherwise the beta channel can never use a cheaper/larger model for compaction while keeping the session model for chat.

Activity

  1. github-actions commented on Aug 22, 2026

    @github-actions
    Contributor

    This issue might be related to an existing issue. Please check:

    • [Bug] agent.compaction.variant config is ignored during compaction #41578: Similar report where agents.compaction config fields are ignored during compaction — that issue covers the variant field being silently ignored; this issue covers model being ignored on the beta branch after the shared model-request refactor. They share the same root area and may benefit from a combined fix.
  2. zce commented on Aug 30, 2026

    @zce

    Hit this after migrating too. Some context that might help the design discussion:

    The usual instinct for keeping the session model on compaction is cache preservation, but the compaction request can't hit the session cache anyway: it's a serialized transcript in one user message with an empty system prompt, sharing no token prefix with the conversation. Post-compaction requests are also rebuilt from the checkpoint, so the session model's cache is invalidated regardless of which model wrote the summary. A dedicated cheaper model loses nothing, while the session model pays full input price on a prompt that can approach the entire context window.

    If this gets fixed, two adjacent pieces probably need to land with it, or a smaller-window compaction model won't be usable in practice:

    Usage recording would follow for free, since it already bills against plan.resolved.

    For what it's worth, the workaround people run today is rewriting body.model in a session.http.request hook. It works, but can't fix usage accounting or the trigger math, and fails silently back to the session model when the request shape changes.

  3. rcdailey commented on Sep 27, 2026

    @rcdailey

    Two other reports of the same problem:

    This is a regression from v1, where a dedicated compaction model worked fine. I rely heavily on cheaper models from other providers for compaction, so right now every compaction runs on my most expensive model.

    From what I can tell, it started with #43555 (route title and compaction through shared model requests). SessionCompaction.prepare in packages/core/src/session/compaction.ts passes context.model and context.agent.id, so the compaction agent's model and variant are never read.

    I realize this repo gets a huge volume of issues and PRs, and that makes regressions like this easy to miss. I'm hoping this gets fixed soon.

  4. mcoffin commented on Sep 28, 2026

    @mcoffin

    I'm following this issue because I hit the exact same... agents.title.model is set to a lightweight model for title generation but silently falls back to the primary model every time. I've already traced the root cause to the Model.Service.read() caching bug where cached?.data === data returns true because the internal Map is mutated in place rather than replaced, so the stale byProvider lookup never rebuilds.

  5. eriknovikov commented on Oct 1, 2026

    @eriknovikov

    I also need this in V2 (currently on v2.0.16).
    It would be really helpful if session.compact (/compact or Ctrl+x+c) honored agents.compaction.model instead of always using the currently selected session model.

    The idea is to always use a faster/cheaper model for compaction. It should not change the model used for subsequent turns.
    Thanks in advance!

  6. sergiopadure commented on Oct 2, 2026

    @sergiopadure
    Contributor

    Still reproducible on the current v2 branch (d113a40, 2026-10-02) and on 2.0.22. In case it helps whoever picks this up, the gap seems to be:

    • plugin/agent.ts registers the compaction agent (name, primary, hidden), so agents.compaction.model is merged and shown by debug agents.
    • session/context.ts has selectTitle, which honours the title agent's model (if (agent.model) return yield* model.get(...)). There is no equivalent for compaction, and nothing in core looks up Agent.ID.make("compaction").
    • All three compaction paths pass the session's own context to SessionCompaction.compact: manual (session/runner/llm.ts, context.select(sessionID) plus context.resolveModel(selected.session)), auto and overflow (the step's loaded context). session/compaction.ts then uses context.model for the ceiling and the request (agent: context.agent.id, model: context.model).

    So a selectCompaction alongside selectTitle, used to build the compaction context's model, would probably cover it. The ceiling should arguably stay on the session model's window, while the request runs on the compaction model.

    Measured impact here: 0 of 255 compactions ran on the configured compaction model. They all ran on the session model, which for Claude sessions means the full older history is billed at that model's cache-write price (see #52761 for the cost figures). A cheaper compaction model is the main lever we have against that.

  7. rekram1-node commented on Oct 3, 2026

    @rekram1-node
    Collaborator

    We can honor it, the reason it wasn't was because the cache hit is normally cheaper (since it uses same model).

  8. rcdailey commented on Oct 3, 2026

    @rcdailey

    @rekram1-node I agree if the cache is warm in most of the time compaction is used. The problem is, you don't even offer that choice to people very easily today. The current design assumes warm cache is the majority case. For Anthropic, which as I believe 5 minute TTL on cache, it means most of my compaction is a cache miss which for Opus, is more expensive than Deepseek Flash for compaction. And for me personally, most of the time I do compaction it's usually when returning to my desk from work the day prior, which for most providers is a cache miss since hours have passed.

    Also note that not every provider reduces cost based on caching such as the NanoGPT subscription; input tokens count toward rate limits no matter if it's cached or not. Probably not a common case; but still a reason the flexibility is appreciated.

    What I'll typically do is use a very very cheap model for compaction so that cache doesn't matter; and keep my main model selection at Opus 5.5 for everything between. It's less tedious to remember to swap my model before I run /compact which was the case for v1.

    I appreciate you taking the time to respond here!

  9. sergiopadure commented on Oct 3, 2026

    @sergiopadure
    Contributor

    Thanks for looking at this. One data point on the cache argument: on V2 today, compaction mostly does not get the warm cache, even when the cache is warm.

    In #52761 (same setup, OpenCode 2.0.12 to 2.0.22), 132 compactions ran within 5 minutes of a model call that had read at least 80% of its input from the cache. Only 1 of those 132 compactions read 50% or more of its input from the cache. The median read share was 7.4% on the OpenAI route and 4.2% on Anthropic Messages routes, where about 96% of the compaction input was billed as a new cache write. So at the moment, compacting on the session model pays close to the full input price.

    Honouring agents.compaction.model only when it is set would keep the same-model default for people who benefit from a warm cache, and would give the rest of us the cheaper option. Once #52761 is fixed, the trade-off may well favour the default again.

  10. sergiopadure commented on Oct 3, 2026

    @sergiopadure
    Contributor

    @rekram1-node, in case it helps: #52875 implements this. It adds selectCompaction next to selectTitle and moves only the summary request to the compaction model. The decision when to compact stays on the session model, and behaviour is unchanged when no compaction model is configured.

    The PR description contains Fixes #44094, but the PR targets v2, which is not the default branch, so GitHub does not register the closing link. That is why the issue-first check labelled it needs:issue.

    Would you like any changes to it?

  11. rekram1-node commented on Oct 4, 2026

    @rekram1-node
    Collaborator

    @sergiopadure there is a known issue for the anthropic case but for other providers it should get a cache hit normally but that's unrelated to this issue

  12. sergiopadure commented on Oct 4, 2026

    @sergiopadure
    Contributor

    Thanks, agreed that it's separate from this issue. One note so it doesn't get lost: the data in #52761 is not only the Anthropic case.

    On the built-in openai provider (gpt-6-luna, gpt-6-sol, gpt-6-astra, OpenCode 2.0.16–2.0.21), 113 automatic compactions read a median of 7.4% of their input from the cache. In each case, the previous call in the session ran at most 5 minutes earlier and read at least 80% of its input from the cache.

    Happy to continue in #52761. If there is an issue for the known Anthropic case, I can add the numbers there too.

  13. rekram1-node commented on Oct 5, 2026

    @rekram1-node
    Collaborator

    True, ik why that happens itll get resolved soon.

  14. rekram1-node commented on Oct 5, 2026

    @rekram1-node
    Collaborator

    @sergiopadure merged a slightly simpler version of ur pr. closing issue. Will release later.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions