The gateway now serves models with very different windows (MiniMax-M3 at
1M, GLM-4.7-cc at 200K), but every agent was configured from one flat
196608-token constant. Replace it with a per-model source of truth.
src/lib/model-limits.ts owns `{context, trigger, output?}` per model and
resolves remote -> static table -> 200K/160K default. The four writers in
configure.ts translate it into each agent's own knob and hold no window
constants of their own; GATEWAY_CONTEXT_WINDOW / GATEWAY_COMPACT_* are
gone from const.ts.
- Claude Code: WINDOW + PCT_OVERRIDE (single-model, so exact)
- Codex: model_context_window + model_auto_compact_token_limit
- Continue: per-model defaultCompletionOptions.contextLength (new -
it previously declared no window at all)
- OpenCode/CoDev Code: per-model limit.input, see below
Fixes a live bug in the OpenCode family. Its threshold is
`limit.input - compaction.reserved`, falling back to
`limit.context - maxOutputTokens` when limit.input is absent - a branch
that discards `reserved` entirely. CoDev wrote {context, output} only, so
the configured reserve had been dead since #171 and compaction fired at
~67% of the window rather than the intended 85%.
Writing limit.input also solves the multi-model problem: `reserved` is a
single top-level value with no per-model variant, so it alone cannot put
a 1M and a 200K model on different triggers. Setting input to
`trigger + reserved` lands each model exactly on target off one shared
reserve, while limit.context stays the true window that the TUI's
"% context used" gauge divides by. Clamped to context so the budget is
never overstated. Verified against the shipped opencode and codev
binaries, not the published schema, which documents neither branch.
Also:
- FALLBACK_MODEL -> MiniMax/MiniMax-M3
- backend.ts#fetchModelWindows reads LiteLLM /model_group/info so windows
become gateway-driven once an admin populates max_input_tokens (null on
every model today). Never throws - a window is an optimization over a
sane default, and install must not break on a metadata endpoint.
- Cached in a new top-level `model_limits` block in auth.json, kept out
of the api-key block so saveApiKey's whole-block rewrite can't clear it
- logout() was dropping provider_id/provider_name, silently reverting a
manually-named provider to netGate on the next config write
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What
The gateway serves models with very different windows —
MiniMax/MiniMax-M3at ~1M tokens andzai-org/GLM-4.7-ccat 200K — but every agent was configured from one flat196608constant. This makes windows and compaction triggers per-model.Targets: M3 compacts at 800K, GLM-4.7-cc at 160K.
src/lib/model-limits.tsowns{context, trigger, output?}per model and resolves remote → static table → 200K/160K default. The four writers inconfigure.tstranslate it into each agent's own knob and hold no window constants of their own;GATEWAY_CONTEXT_WINDOW/GATEWAY_COMPACT_*are gone fromconst.ts.triggeris explicit rather than a percentage — the gap between window and trigger is a per-model judgement call, not a constant.Bug fixed: OpenCode/CoDev Code have been compacting early since #171
The threshold, decompiled from the shipped binaries (identical in
opencodeandcodev):CoDev wrote
limit: {context, output}with noinput, so the else-branch ran andreserved: 29491was dead. The real trigger was196608 − maxOutputTokens≈ 131072 (67%), not the intended 167117 (85%).Writing
limit.inputalso solves the multi-model problem.reservedis a single top-level value with no per-model variant, so it alone cannot put a 1M and a 200K model on different triggers: sized for the big one it drives the small one's trigger negative; sized for the small one the big one fires at ~96%. Settinginput = trigger + reservedlands each model exactly on target off one shared reserve, whilelimit.contextstays the true window that the TUI's "% context used" gauge divides by. Clamped tocontextso the budget is never overstated.Per-agent
CLAUDE_CODE_AUTO_COMPACT_WINDOW+CLAUDE_AUTOCOMPACT_PCT_OVERRIDE(single-model, so exact)model_context_window+model_auto_compact_token_limitdefaultCompletionOptions.contextLength/maxTokens— new, it previously declared no window at alllimit.context(true) +limit.input(= trigger + reserved), one globalcompaction.reservedVerified end-to-end
Real configs written to a throwaway
$HOMEwith all three models selected:Also in this PR
FALLBACK_MODEL→MiniMax/MiniMax-M3(wasMiniMax-M2.7).backend.ts#fetchModelWindowsreads LiteLLM's/model_group/info(gateway root, next to/key/info, not under/v1). The live gateway reportsmax_input_tokens: nullfor every model, so it returns{}and the static table carries everything — the moment an admin populates the field, that model becomes gateway-driven with no CoDev release. UnlikefetchModelsit never throws: a window is an optimization over a sane default, and install must not break because a metadata endpoint 404s.ModelSelectrefreshes it fire-and-forget so it can never delay or fail the picker.model_limitsblock inauth.jsonwith its own save/load, deliberately kept out of the api-key block sosaveApiKey's whole-block rewrite can't clear it.logout()was droppingprovider_id/provider_name— it rebuilds the surviving file field-by-field, and the provider pair wasn't listed, so signing out silently reverted a manually-named provider to netGate on the next config write. Fixed, with a test.ModelSelecttests were about to issue real HTTPS requests tomy-gw.example.com, and nothing there stubs$HOME— a non-empty result would have written to the developer's actual~/.codev-hub/auth.json. Stubbed in all four affected files.AGENTS.md: new "Context windows and auto-compaction" section covering the per-agent shapes, thelimit.inputfinding, and the verify-against-the-binary rule.Migration
None. Existing installs keep
196608until their nextinstall/config/modelswitch or launch-time key refresh, all of which rewrite the config.Validation
pnpm fix,pnpm typecheck,pnpm test(1326 passed, 2 skipped, 74 files),pnpm build && node dist/index.js --version→0.5.4. New:tests/lib/model-limits.test.ts,fetchModelWindowscases inbackend.test.ts, per-model pins for all four writers, and the twologout()preservation tests.🤖 Generated with Claude Code