Skip to content

fix: reconcile modelContextLimit on model switch (#312) - #314

Merged
ranxianglei merged 1 commit into
masterfrom
2026-08-16_model-switch-limit-catalog
Aug 16, 2026
Merged

fix: reconcile modelContextLimit on model switch (#312)#314
ranxianglei merged 1 commit into
masterfrom
2026-08-16_model-switch-limit-catalog

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Problem (issue #312)

切换模型导致上下文水平计算错误:配置了 50% 紧急压缩阈值,但从 200K 切到 1M 模型后,在 26% 实际用量时就触发了紧急告警。

Root cause(host 源码证实):一次 LLM 请求内 opencode 先触发 experimental.chat.messages.transformsession/prompt.ts:1255),后触发 experimental.chat.system.transformllm/request.ts:69,在 handle.process 内部)。而 state.modelContextLimit 只有 system hook 写入 —— 切换模型后的第一个请求,所有百分比阈值仍按旧模型窗口计算:

  • 200K → 1M:50% 阈值 = 50% × 200K = 100K,260K 上下文(真实 26%)立即触发
  • 1M → 200K:反向切换会漏报真实紧急

Fix

SessionStateRegistry 新增 ${providerID}/${modelID} → context limit 目录:

文件 改动
lib/state/state.ts recordModelLimit / resolveModelLimit / hydrateModelLimitsFromClient(best-effort,从不抛出)
lib/hooks.ts system hook 每次请求把 model.limit.context 记入目录(在 session-state 守卫之前);messages hook 在 getOrCreate 后按请求 user message 上的模型名同步 state.modelContextLimit
index.ts 插件启动时 fire-and-forget 从 GET /config/providers 预填目录

未知模型保持原回退路径(旧值一个请求,system hook 同请求内刷新)—— 修复纯增量,无 schema/持久化/API 变更。热路径开销:每次 messages.transform 一次 Map.get

Verification

  • npx tsc --noEmit
  • npm run build
  • 新增 tests/model-switch-limits.test.ts 7 项回归:
    • 26% 用量 + 50% 阈值 + 200K→1M 切换:不触发紧急(issue 原场景)
    • 未知模型回退路径(记录 pre-fix 行为)
    • 1M→200K 反向切换:75% 真实超限时正确触发
    • 目录录入(无 session state 时也可写入)/ 非法条目容错 / /config/providers 预填 / throwing client 容错
  • 全量套件 node --import tsx --test tests/*.test.ts988 pass, 0 fail

Devlog: devlog/2026-08-16_model-switch-limit-catalog/(REQ.md + WORKLOG.md)

Fixes #312

Within one LLM request the host fires experimental.chat.messages.transform
BEFORE experimental.chat.system.transform (sst/opencode: session/prompt.ts
triggers messages.transform, then llm/request.ts triggers system.transform
inside handle.process). state.modelContextLimit is written only by the
system hook, so the first request after a model switch computed every
percentage threshold against the PREVIOUS model's window — e.g. a 50%
emergencyThresholdPercent fired at ~26% real usage after switching 200K
-> 1M (issue #312), and 1M -> 200K missed a real emergency.

Fix: SessionStateRegistry keeps a `${providerID}/${modelID}` -> context
limit catalog. The system hook records every observed model's limit into
it (before the session-state guard, so it works even without state); the
messages hook reconciles state.modelContextLimit from the catalog entry
for the model named on the request's user message; plugin init seeds the
catalog best-effort from GET /config/providers. Unknown models keep the
previous fallback (stale limit for one request, corrected by the system
hook later in the same request).

Tests: 7 new regression tests in tests/model-switch-limits.test.ts;
full suite 988 pass, 0 fail.
@github-actions

Copy link
Copy Markdown

📦 Built Plugin Artifact

Branch: 2026-08-16_model-switch-limit-catalog (4a833fc)

Option A — Install from npm PR tag (recommended)

opencode plugin opencode-acp@pr-314 --global

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

Option B — Install from GitHub

opencode plugin "github:ranxianglei/opencode-acp#2026-08-16_model-switch-limit-catalog" --global

Option C — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf opencode-acp-pr314.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
ranxianglei merged commit 48b941e into master Aug 16, 2026
6 checks passed
ranxianglei pushed a commit that referenced this pull request Aug 16, 2026
Two follow-ups from PR review:

1. index.ts — hydration was fire-and-forget with .catch(() => {}): a silent
   degrade (host without /config/providers, failed fetch, empty catalog) was
   invisible. Now logs: info with entry count on success, warn on zero
   entries / failure. Still fire-and-forget, never blocks init.

2. Extract the model-limit catalog into lib/state/model-limits.ts
   (createModelLimitCatalog). SessionStateRegistry delegates to it; the
   test registry stub composes the SAME factory instead of hand-rolling a
   copy that could drift. Public registry API unchanged.

Verified: tsc clean, 988/988 tests pass, prettier clean on touched files
(unrelated pre-existing format dirt in state.ts/registry-stub.ts left
as-is to keep the diff surgical).
ranxianglei added a commit that referenced this pull request Aug 17, 2026
Completes the #314 fix with #313's fail-safe as the fallback path.

#314 (merged) reconciles state.modelContextLimit from the model-limit
catalog when the request's user message names a known model. When the
catalog MISSES (fresh instance + failed hydration + never-used model),
the stale limit survived and every percentage threshold still ran against
the previous model's window — the exact #312 false positive, narrowed to
a rare corner but not eliminated.

This PR records the identity pair (modelProviderID/modelID) alongside
the limit whenever the system hook writes it, and on a catalog miss the
messages hook compares the request's model against that identity:
- match   → keep the limit (no needless blindness when the catalog
            simply lacks an entry for the CURRENT model)
- mismatch→ invalidate (limit = undefined); consumers already tolerate
            undefined (fresh sessions run with it until the first
            system.transform), and this same request's system.transform
            refreshes the pair anyway
- legacy persisted states carry no identity → treated as stale (one
  blind turn per upgraded session, same as #313)

The identity pair is persisted/restored together with the limit.

Tests: legacy-state invalidation, identity-mismatch invalidation,
same-identity keep (emergency still fires at 130%), system-hook identity
recording, persistence round-trip. 991/991 pass, tsc clean.

Co-authored-by: ranxianglei <ranxianglei@users.noreply.github.com>
@ranxianglei ranxianglei mentioned this pull request Aug 17, 2026
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