Skip to content

feat: per-provider/per-model overrides for ALL compress fields via nested compress.providers (issue #344) - #351

Merged
ranxianglei merged 16 commits into
masterfrom
2026-08-29_min-nudge-providers-models
Aug 29, 2026
Merged

feat: per-provider/per-model overrides for ALL compress fields via nested compress.providers (issue #344)#351
ranxianglei merged 16 commits into
masterfrom
2026-08-29_min-nudge-providers-models

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Model: Claude Sonnet 4.6

Summary

Every tunable compress field now supports a three-level cascade override — model > provider > global, field-by-field — via the nested compress.providers map (issue #344 follow-up; scope extended per maintainer request: "应该所有字段都需要三级别 而不是仅仅一个字段").

{
  "compress": {
    "nudgeGrowthTokens": 20000,           // global default
    "providers": {
      "anthropic": {                       // provider-level
        "nudgeGrowthTokens": 5000,
        "models": {
          "claude-sonnet-4-6": {           // model-level (wins per field)
            "minNudgeContextPercent": 30,
            "maxContextLimit": "20%"
          }
        }
      }
    }
  }
}

Overridable fields (23)

maxContextLimit, emergencyThresholdPercent, nudgeFrequency, iterationNudgeThreshold, toolOutputNudgeThreshold, nudgeGrowthTokens, minNudgeGrowthRatio, minNudgeGrowthFloor, minNudgeContextPercent, nudgeForce, protectedTools, showCompression, summaryBuffer, protectTags, protectUserMessages, maxSummaryLengthHard, minCompressRange, maxVisibleSegments, keepEmbedMaxChars, lastSegmentSoftBlock, preserveRecentMessages, preserveRecentTokens, preserveLastUserMessage.

Not overridable: permission (tool registration precedes model info), deprecated minContextLimit/modelMinLimits family (see #352), flat modelMaxLimits (legacy map, still honored), providers itself.

Semantics

  • Resolution per field: model entry > provider entry > global. Unknown provider/model ids fall back up the cascade.
  • maxContextLimit precedence chain: nested override > flat modelMaxLimits > global (enforced explicitly in resolveContextTokenLimit).
  • providers maps deep-merge across the three config file layers per provider/model key (unlike flat maps' replace-inherit).
  • Mechanism: applyCompressOverrides() builds the effective config once per turn (identity return, zero allocation, when nothing applies); injected at the two points where model info is known — nudge injection (inject.ts) and the compress tool (range.ts, after prepareSession).
  • Exception: the system-prompt protected-tools listing always reflects the global value (prompt is built before model info exists).

Validation

  • Per-field type table (OVERRIDE_FIELD_TYPES) covering every overridable field; unknown fields rejected at both levels; dcp.schema.json nested schema with additionalProperties: false at both levels.

Tests — 1062/1062 (typecheck ✓, build ✓)

  • Resolver: all-field cascade precedence, identity swap, maxContextLimit excluded from blanket apply, nested-max beats flat map.
  • Inject gates (§5.7 multi-turn + side-effect assertions): model-level maxContextLimit lowers the over-max band; provider-level nudgeGrowthTokens tightens the growth threshold; model-level 0 disables the floor.
  • Includes reviewer commit 6576fcf (§5.7.1 production-config growth-cycle test with preserveRecentMessages > 0 + PR fix: preserve growth baseline when nothingToCompress #207 regression lock), merged conflict-free-keep-both.
  • Mutation-verified M1–M5 (each mutation kills exactly the expected tests).

Docs

CONFIGURATION.md / CONFIGURATION.zh-CN.md: rewritten compress.providers section + new recipe "Per-model tuning of any compress field". Supersedes #345 (closed).

Stack

Includes #343 (merged). Merge this before #352 (deprecation annotations reference the same family).

ework-agent and others added 12 commits August 28, 2026 20:35
Growth-triggered compression nudges fired well below a configured
minContextLimit because computeShouldNudge() only uses overMinLimit to
pick the tips variant, not to gate the decision. Add an explicit
(overMaxLimit || overMinLimit) gate to the T1 growth path in
injectCompressNudges so a configured minimum acts as a lower bound.

- overMaxLimit and the emergency override bypass the gate
- T2/T3 tier-promotion nudges are unaffected (independent cadence)
- 4 new tests; 2 existing growth-mechanism tests had minContextLimit
  lowered so they still isolate the growth mechanism

devlog: devlog/2026-08-28_min-gate-growth-nudges/
- Gate only applies when minContextLimit resolves (minLimitResolved):
  percent limits with an unknown model context window keep pre-#342
  growth-only behavior instead of suppressing all growth nudges
- Unmask 2 existing tests whose minContextLimit sat above the test
  context (post-compress small growth, baseline init)
- 3 new tests: unresolvable-limit fallback, emergency override bypass,
  T2 tier-promotion independence from the min gate
- Correct stale min/max defaults in README.md and CONFIGURATION.md
  (45%/55% -> 80%/80%, matching lib/config.ts since v1.14.16)
- Devlog: REQ constraints/acceptance, WORKLOG results (1036 tests)
…minContextLimit

Per @Dog's feedback in issue #342: minContextLimit defaults to 80%
(lib/config.ts:200) and is documented as the soft lower threshold for
turn/iteration reminders, so gating growth nudges on it suppresses ALL
growth nudges below 80% for default users — effectively disabling
compression for most of a session. The dual-agent review's minLimitResolved
guard did not address this (a default user with a known context still gets
growth nudges suppressed below 80%).

Use minNudgeContextPercent (default 15, a percent of model context) as the
growth-nudge floor instead. It is the intended 'don't nudge below this'
knob and was previously a no-op (plumbed into computeShouldNudge but
ignored). When the model context limit is unknown the floor is unresolvable
and growth nudges keep pre-#342 behavior. overMaxLimit and the emergency
override bypass the floor; T2/T3 tier-promotion nudges are unaffected.

- Reverts the review commit's minLimitResolved mechanism (dead code here)
- Preserves the review commit's README/CONFIGURATION default fixes (80%/80%)
- Tests: 4 floor tests + 2 new (unresolvable model limit, T2 independence)
  re-pointed at the floor; pre-existing test #27 raised into the [floor, max)
  range. 1035 tests, 0 failures.
…th-floor redesign

Re-point stale references left by the floor redesign (ecfe7f0):
- README.md: minContextLimit governs turn/iteration reminders only;
  growth nudges have their own floor (minNudgeContextPercent)
- CONFIGURATION.md: minContextLimit description (turn/iteration only);
  minNudgeContextPercent description (growth-nudge floor, over-max and
  emergency bypasses, unknown-window fallback)
- lib/messages/inject/utils.ts: fix the @deprecated comment on
  minNudgeContextPercent (field is active; floor computed in inject.ts)
- tests/inject.test.ts: section header re-pointed to the floor
- WORKLOG: record ecfe7f0 hash + this commit's row
A 15% default floor binds on windows >= ~400K (P% x W > baseline + 50K
growth threshold): on 1M-window models every compress cycle is
suppressed until 150K context usage, shifting the steady-state working
range ~2x upward. For the dominant large-window user profile
(baseline 10-50K, cycle ceiling ~110K) this is a silent, bug-level
behavior change.

5% only binds on >= ~2M-class windows, keeping the floor inert for
typical working cycles. Users who want a higher floor set it
explicitly (0 disables).

- lib/config.ts default 15 -> 5
- lib/messages/inject/inject.ts policy passthrough + floor fallback
  15 -> 5, with rationale comment
- dcp.schema.json default + stale description fixed
- CONFIGURATION.md / CONFIGURATION.zh-CN.md defaults + rationale
- tests/inject.test.ts: default-lock test (mutation-sensitive: fails
  if the fallback reverts to 15)
…mantics

The zh description still said 'any nudge' (pre-floor-redesign wording);
mirror the English entry: growth-nudge floor, over-max/emergency
bypasses, unknown-window fallback, minContextLimit governs turn/iteration
reminders, low-default rationale, 0 disables.
…roviders (issue #344)

Replaces the flat modelMinNudgeLimits surface (PR #345, superseded) with
compress.providers.{provider}.models.{model}.minNudgeContextPercent,
cascading field-by-field: model > provider > global. 0 disables explicitly;
unknown ids fall back to global; percent resolves against the active model
window. Deep-merged across the three config file layers per provider/model
key. Validated + schema + CONFIGURATION (en/zh) + devlog + 17 new tests
(mutation-verified M1/M2/M3). 1053/1053 green.
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

📦 Built Plugin Artifact

Branch: 2026-08-29_min-nudge-providers-models (694eb88)

Option A — Install from npm PR tag (recommended)

opencode plugin opencode-acp@pr-351 --global

Each push to this PR publishes a new version under the pr-351 npm tag.

Option B — Install from GitHub

opencode plugin "github:ranxianglei/opencode-acp#2026-08-29_min-nudge-providers-models" --global

Option C — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf opencode-acp-pr351.tgz
cp -r package/dist ~/.cache/opencode/packages/opencode-acp@latest/node_modules/opencode-acp/dist
  1. Restart opencode to pick up changes.

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · vllm-qwen/qwen3.8-27b

[bot] On it — reviewing PR #351 (branch 2026-08-29_min-nudge-providers-models). Checking #343 merge status, the diff, tests, and the mutation-verification claims. Will report findings here.

…ted .gitignore change

Post-PR review of #351:
- §5.7.1 required at least one test per PR using preserveRecentMessages > 0;
  all 17 original tests ran on the preserveRecentMessages: 0 default, which
  disables the protected-zone / nothingToCompress path (the #207 bug scenario).
  New 5-turn test: per-model floor suppression → fire → compress → new
  baseline → fire again, plus a fully-protected turn locking the #207
  baseline-reset regression. Mutation-verified (reintroducing the #207 bug
  fails this test).
- Reverted the node_modules/ → node_modules .gitignore edit bundled into
  e14193c (unsubstantiated 'symlink fix'; no tracked symlink exists).

1054/1054 green (1036 baseline + 18 new). Typecheck + build pass.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · vllm-qwen/qwen3.8-27b

[bot] ## Review complete — PR #351 ✅ (2 findings fixed, branch updated to 6576fcf)

Claim verification (all confirmed against the code)

Check Result
npm run typecheck ✅ clean
npm run test 1053/1053 on PR tip (1036 baseline + 17 new) → 1054/1054 after review fix
npm run build ✅ tsup ESM success
scripts/ci/check-pr.sh ✅ branch name, devlog REQ+WORKLOG, version unchanged (1.14.25)
Mutation M1 (skip model-level in resolveMinNudgeContextPercent) ✅ exactly 5 fail (cascade, 0-disable, clamp, multi-turn model floor, model-level-0 gate)
Mutation M2 (DEFAULT_MIN_NUDGE_CONTEXT_PERCENT 5→15) ✅ exactly 2 fail (#342 default-lock + #344 unset-everywhere)
Mutation M3 (mergeProviderOverrides deep-merge→replace) ✅ exactly 2 fail (no-wipe + cannot-clear)

Design checks all pass: cascade model > provider > global field-by-field (lib/messages/inject/utils.ts), 0 = explicit disable, unknown ids → global fallback, getModelInfo window resolution, per-provider/model deep merge across the 3 config layers (lib/config.ts mergeProviderOverrides/mergeModelOverrides), additionalProperties: false typo-safety in both lib/config-validation.ts and dcp.schema.json, docs complete in both languages.

Findings fixed (commit 6576fcf, pushed)

  1. §5.7.1 gap (mandatory) — none of the 17 new tests used preserveRecentMessages > 0 (all used the buildConfig() default of 0, which disables the nothingToCompress path — exactly the scenario that triggered the fix: preserve growth baseline when nothingToCompress #207 baseline-reset bug). Added a 5-turn production-config test to tests/inject.test.ts (preserveRecentMessages: 2, model-level 30% floor on a 1M window): baseline 100K → 200K suppressed below floor → 320K fires → compress resets baseline to 200K → 400K fires again from the new baseline → fully-protected 2-message turn is silent with baseline and lastNudgeShownTokens preserved (fix: preserve growth baseline when nothingToCompress #207 regression lock). Mutation-verified: reintroducing the fix: preserve growth baseline when nothingToCompress #207 reset fails this test + the 5 pre-existing fix: preserve growth baseline when nothingToCompress #207 tests.
  2. Unrelated .gitignore change (node_modules/node_modules) bundled into e14193c with unsubstantiated "symlink fix" rationale (no tracked symlink exists) — reverted to the f081b28 state.

Dual-agent review (§5.3 code + §5.6 tests)

Two independent agents reviewed the final diff (f081b28..6576fcf). Each raised one P1; both were false positives on verification:

  • Code agent: "deepCloneConfig aliases compress.protectedTools" — it doesn't: lib/config.ts:504 and :532 already clone both commands.protectedTools and compress.protectedTools; every nested mutable field is covered.
  • Test agent: "new feat: per-model growth-nudge floor (modelMinNudgeLimits) for mixed-context installs #344 test lacks lastPerMessageNudgeTokens assertions on turns 2/4" — it has them: tests/inject.test.ts:2448 (100K) and :2484 (200K); every turn asserts all three side-effect fields.

One P2 accepted as-is: as any in tests/config-providers.test.ts:135 — consistent with existing test conventions (tests are excluded from tsconfig typechecking; the legacy experimental.allowSubAgents shape is accepted by the loader at lib/config.ts:602).

Status

Branch 2026-08-29_min-nudge-providers-models @ 6576fcf pushed, working tree clean, 1054/1054 green. Ready for human merge — stacked on #343, so #343 needs to land first (per §5.1.1.2 I won't merge either).

 follow-up)

Every tunable compress field now supports the per-provider/per-model
override cascade (model > provider > global, field-by-field), not just
minNudgeContextPercent.

- CompressOverridableConfig = CompressConfig minus permission, the
  deprecated minContextLimit family, flat model*Limits maps, providers
- resolveCompressOverrides / applyCompressOverrides (identity when
  nothing applies; maxContextLimit excluded from blanket apply)
- inject + compress tool entry points swap in the effective config
  right after model info is known
- resolveContextTokenLimit: nested maxContextLimit beats the flat
  modelMaxLimits map, which beats the global value
- validation: per-field type table for all overridable fields
- dcp.schema.json: full nested property list at both levels
- tests 1061/1061 (+8), mutations M1-M5 verified exact
…evert)

Kept both sides: 2 all-field cascade tests + reviewer's production-config
growth-cycle test. 1062/1062 tests, typecheck clean.
@ranxianglei ranxianglei changed the title feat: per-provider/per-model growth-nudge floor via nested compress.providers (issue #344) feat: per-provider/per-model overrides for ALL compress fields via nested compress.providers (issue #344) Aug 29, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

ework再review下看是否会引入回归问题

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · vllm-qwen/qwen3.8-27b

[bot] ## 回归专项复查(f081b28..6576fcf 逐行 diff 追踪)

结论:对现有用户(配置中无 compress.providers)零行为变化,不会引入回归。 依据如下:

1. 无 providers 时新旧代码逐点等价

代码点 #343 基线 本 PR 等价性
百分比解析(inject.ts:300) config.compress?.minNudgeContextPercent ?? 5 resolveMinNudgeContextPercent(config, providerId, modelId) ?? DEFAULT_MIN_NUDGE_CONTEXT_PERCENT providers 未设置/未知 id 时,resolver 返回值就是 config.compress?.minNudgeContextPercent(lib/messages/inject/utils.ts:274),默认值同为 5 → 输入完全相同
floor 计算(inject.ts:331) Math.round((percent/100) * modelContextLimit) resolveMinNudgeFloorTokens(...),多了 Math.min(100, Math.max(0, percent)) 钳制 对 0–100 范围内的值钳制是 no-op → 逐位相同;窗口未知时两者都返回 undefined(growth-only)
provider/model 来源(inject.ts:90) getModelInfo 已存在(#343 引入) 未改动 无新增每轮开销;无 model 信息时返回 undefined → 回落全局,与基线一致
配置合并(config.ts:417-418) 无此字段 mergeProviderOverrides(undefined, undefined)undefined 合并后配置形状不变(不会凭空多出 providers: {});{} 也不会抹掉浅层 providers
持久化状态 本 PR 未触碰 lib/state/ 任何文件 无状态格式变化、无迁移风险

2. 验证/告警面

  • 全局 minNudgeContextPercent 在基线和 HEAD 都没有范围校验(validatePercentField 只作用于新增的 provider/model 级字段)→ 现有配置不会新增任何告警。
  • 校验本身是 warning-only(toast,不阻断加载,lib/config.ts:151-194)。
  • getConfigKeyPaths 的 skip 只针对 compress.providers 本身;拼错成 compress.provider 仍会触发 unknown-key 告警(typo 安全)。

3. 仅有的两处边界差异(都只影响非法配置,且方向更合理)

  1. 全局 percent 超出 0–100(如手写了 150,该字段两版都不校验):旧代码 floor = 150% 窗口(实际永远打不开)→ 新代码钳到 100%(窗口满时打开)。负值两版行为一致(floor 恒开)。
  2. 理论情况:若用户旧配置里已有 compress.providers 键且是别的项目的结构(如从 billion-context-pi 复制),现在会收到一条非阻断的结构告警。

4. 测试证据

总评:可安全合并(仍需先合 #343;按 §5.1.1.2 合并操作由人工执行)。

@ranxianglei
ranxianglei merged commit 46014bc into master Aug 29, 2026
6 checks passed
ranxianglei added a commit that referenced this pull request Aug 29, 2026
ranxianglei added a commit that referenced this pull request Aug 29, 2026
ranxianglei added a commit that referenced this pull request Aug 29, 2026
release: v1.14.26 — per-provider/per-model compress overrides + nudge floor (#343, #351)
drexb-ops pushed a commit to drexb-ops/opencode-acp that referenced this pull request Sep 3, 2026
…ft, no behavior change)

minContextLimit (default 80%) is the lower bound for turn/iteration
reminder nudges. The growth-nudge floor (minNudgeContextPercent +
nudgeGrowthTokens, ranxianglei#343/ranxianglei#351) is the maintained mechanism — two parallel
'min' knobs confuse users. Soft deprecation: JSDoc @deprecated, schema
[DEPRECATED] prefixes, EN/zh Status DEPRECATED + removal-consequence note,
legend reworded (may still take effect until removed). zh default fixed
45% -> 80%. No behavior change; 1053/1053 unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant