Skip to content

feat: give Codex and OpenCode the same compaction limits as Claude Code - #171

Merged
quickbeard merged 2 commits into
mainfrom
feat/codex-opencode-compaction-limits
Jun 30, 2026
Merged

feat: give Codex and OpenCode the same compaction limits as Claude Code#171
quickbeard merged 2 commits into
mainfrom
feat/codex-opencode-compaction-limits

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

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:

  • Codex assumes a 272K fallback window for models not in its catalog — larger than the real 196,608 ceiling — so it would keep packing context and never compact in time. Now pins model_context_window = 196608 and model_auto_compact_token_limit = 167117 (~85%, the same trigger point Claude Code uses).
  • OpenCode defaults a custom-provider model with no limit to context: 0, which silently disables auto-compaction entirely. Now declares limit: { context: 196608, output: 65536 } per model and a top-level compaction: { 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 = 65536 for all models (chosen value — OpenCode requires output whenever a limit object is present).
  • Codex model_context_window is 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 typecheck clean
  • pnpm test750 pass (4 new): Codex window/trigger pins, OpenCode per-model limit + compaction block, and the derived-constant math. The Codex test round-trips through TOML.parse and 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

quickbeard and others added 2 commits June 30, 2026 10:00
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
quickbeard merged commit a5b3656 into main Jun 30, 2026
4 checks passed
@quickbeard
quickbeard deleted the feat/codex-opencode-compaction-limits branch June 30, 2026 03:16
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>
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