Skip to content

compaction.reserved silently ignored for models without limit.input (e.g. GLM-5.2) — revival of #13980 #38835

Description

@winstern1998-commits

Summary

compaction.reserved is silently ignored for any model whose limit lacks an input field. This was reported in #13980, which was closed by the stale bot without being fixed — the current code still ignores reserved on this code path. Filing this to revive it with a fuller analysis and to tie together several related reports.

Environment

  • opencode: 1.17.14 (source pinned to commit 203885d24; overflow.ts identical on current HEAD)
  • Model: zhipuai/glm-5.2 (limit = { context: 1000000, output: 131072 }, no input)

Reproduction

Config:

"compaction": { "auto": true, "reserved": 648928 }

Use a built-in model that has no limit.input (e.g. zhipuai/glm-5.2). Observe that compaction does not trigger at input - reserved; it triggers at context - maxOutputTokens = 1000000 - 32000 = 968000. The reserved value has zero effect.

Root cause

packages/opencode/src/session/overflow.tsusable() has two mutually exclusive branches, and reserved only participates in one:

const reserved =
  input.cfg.compaction?.reserved ??
  Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax))
return input.model.limit.input
  ? Math.max(0, input.model.limit.input - reserved)          // branch A: reserved applies
  : Math.max(0, context - ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax))  // branch B: reserved ignored

Models without limit.input (which is common — most models.dev entries only declare context + output) fall into branch B, where reserved is computed but never used. maxOutputTokens = min(limit.output, OUTPUT_TOKEN_MAX=32000) (from provider/transform.ts), so for glm-5.2 the threshold is 1000000 - 32000 = 968000 regardless of reserved.

The config schema (packages/core/src/v1/config/provider.ts) does support limit.input on custom providers, but there is no way to override limit.input for a built-in provider's model — a custom provider with the same name as a built-in does not replace the built-in entry. So users of built-in providers cannot work around this in config.

Expected vs actual

Value
Expected threshold limit.input - reserved (reserved takes effect)
Actual threshold context - maxOutputTokens = 968000 (reserved silently ignored)

Impact

Silent failure: the user sets reserved, gets no error, but the setting never takes effect. This is a UX trap — there's no signal that the config is being ignored for their model.

Proposed fix

Make reserved apply in branch B as well:

: Math.max(0, context - ProviderTransform.maxOutputTokens(input.model, input.outputTokenMax) - reserved)

This makes reserved a consistent "trigger N tokens earlier" knob regardless of whether limit.input is declared. Default reserved is min(COMPACTION_BUFFER=20000, maxOutputTokens), so the default threshold for branch-B models shifts earlier by ~20K — safer (triggers before the hard limit) and consistent with branch A's behavior.

Open semantic question for maintainers: should reserved be subtracted from limit.input (branch A) or from context - maxOutputTokens (branch B)? These differ when limit.input != context - maxOutputTokens. The two branches should agree on the reference point.

Related issues

Complementary upstream

The models.dev registry also omits limit.input for these models (systematic across the zhipuai provider). I'll file that separately against models.dev. But the opencode-side fix is needed regardless, since reserved should not be silently dropped for any model.

Metadata

Metadata

Assignees

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