feat: give Codex and OpenCode the same compaction limits as Claude Code - #171
Merged
Conversation
The gateway model has a 196608-token hard window, but only Claude Code was told about it. Codex and OpenCode each had a real overrun gap on the unrecognized gateway model: - Codex assumes a 272K fallback window for unknown models — larger than the real 196608 ceiling — so it never compacts in time. Pin `model_context_window = 196608` and `model_auto_compact_token_limit = 167117` (~85%, the same trigger Claude Code uses). Accepts the known auto-compaction edge case in openai/codex#16068. - OpenCode defaults a custom-provider model with no `limit` to context 0, which silently disables auto-compaction entirely. Declare `limit: { context: 196608, output: 65536 }` per model and a top-level `compaction: { auto: true, reserved: 29491 }` block — the reserve lands the trigger at ~85% of the window, matching the other agents. Window/percentage are now shared constants (GATEWAY_CONTEXT_WINDOW etc.) with the Claude env-var strings derived from them, so all three agents stay in sync. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stants CLAUDE_AUTO_COMPACT_WINDOW / CLAUDE_AUTOCOMPACT_PCT were just String() wrappers around the GATEWAY_* numeric constants, each used at a single call site. Inline String(GATEWAY_CONTEXT_WINDOW) / String(GATEWAY_COMPACT_PCT) in configureClaudeCode and remove the indirection. The Claude env-block output is still covered by the configureClaudeCode test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
quickbeard
added a commit
that referenced
this pull request
Aug 2, 2026
* feat: make context windows and auto-compaction per-model
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>
* fix: respect Claude Code's compaction ceilings instead of writing past them
Verified CLAUDE_CODE_AUTO_COMPACT_WINDOW and CLAUDE_AUTOCOMPACT_PCT_OVERRIDE
against the shipped binary (2.1.220). Both are real and read on the
production path, but three undocumented ceilings mean the per-model
windows cannot reach Claude Code the way they reach the other agents.
1. `nc()` resolves the window as `Math.min(nativeWindow, envValue)`, so
the env var can only ever SHRINK it. An unrecognized model - every
model the gateway serves - falls through `w37()` to 200000. Writing
1000000 for MiniMax-M3 was silently reduced to 200000.
2. `Rzq = Math.min(T - round(T * precomputeBufferFraction), qB6(T, opts))`
with the fraction defaulting to 0.2, so the trigger is capped at 80% of
the effective window and any percentage above 80 is discarded.
3. Pinning a window BELOW 200000 sets `source: "env"`, which puts `aiK` on
the branch reading `if (window < 200000) return false` - auto-compaction
stops firing entirely. The pre-existing 196608 was tripping exactly
this, so Claude Code has not been auto-compacting at all.
claudeWindow() therefore pins 200000 and returns null below the ceiling so
the writer omits the variable (source stays "auto", which skips the gate
and resolves to the same 200000). claudeCompactPct() takes the percentage
against the clamped window rather than the model's true one - 800000/200000
is 400, not 80 - and bounds it to [1, 80].
Net: Claude Code compacts at 0.8 x (200000 - min(modelMaxOutput, 20000)),
~144-160K regardless of the model. `S$H` (the model's max output) is not
statically resolvable in the binary, so the exact point in that range is
unverified and documented as such.
Also fixes logout() dropping provider_id/provider_name, and documents all
of the above plus the OpenCode limit.input finding in AGENTS.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The self-hosted gateway model has a 196,608-token hard window, but only Claude Code was configured to respect it (
CLAUDE_CODE_AUTO_COMPACT_WINDOW+CLAUDE_AUTOCOMPACT_PCT_OVERRIDE). Codex and OpenCode each had a real overrun gap on the unrecognized gateway model:model_context_window = 196608andmodel_auto_compact_token_limit = 167117(~85%, the same trigger point Claude Code uses).limittocontext: 0, which silently disables auto-compaction entirely. Now declareslimit: { context: 196608, output: 65536 }per model and a top-levelcompaction: { auto: true, reserved: 29491 }block — OpenCode has no percentage knob, so the reserve is the headroom that lands the trigger at ~85% of the window, matching the other agents.Window/percentage are now shared constants (
GATEWAY_CONTEXT_WINDOW,GATEWAY_COMPACT_PCT, …) with the trigger/reserve derived from them and the Claude env-var strings derived too, so all three agents stay in sync from one source.Notes / trade-offs
limit.output = 65536for all models (chosen value — OpenCode requiresoutputwhenever alimitobject is present).model_context_windowis pinned deliberately, accepting the known auto-compaction edge case in openai/codex#16068 — staying under the gateway wall is the priority.Testing
pnpm fix/pnpm typecheckcleanpnpm test— 750 pass (4 new): Codex window/trigger pins, OpenCode per-modellimit+compactionblock, and the derived-constant math. The Codex test round-trips throughTOML.parseand asserts the scalars land at top level (not nested into the provider table).pnpm build && node dist/index.js --version— bundles and runs under Node🤖 Generated with Claude Code