feat(compression): extend Codex 272K compaction autoraise to gpt-5.4

The ChatGPT Codex OAuth backend caps both gpt-5.4 and gpt-5.5 at a 272K
context window, but the autoraise that lifts the compaction trigger to 85%
only matched gpt-5.5. On gpt-5.4 the global 50% threshold fired at ~136K —
half the usable window — compacting far earlier than necessary.

Rename _is_codex_gpt55 -> _is_codex_gpt54_or_gpt55 and match both families.
The one-time user notice is now model-aware (shows the actual slug). The
config key codex_gpt55_autoraise is kept as-is for backward compatibility.
Adds gpt-5.4 coverage to the autoraise tests.
This commit is contained in:
Tanmay Choudhary 2026-06-27 01:46:35 +02:00 committed by Teknium
parent 370a489fb4
commit 948993cd62
5 changed files with 135 additions and 70 deletions

View file

@ -68,18 +68,19 @@ def _ra():
return run_agent
def _build_codex_gpt55_autoraise_notice(autoraise: Dict[str, float]) -> str:
"""Build the one-time notice shown when Codex gpt-5.5 raises compaction.
def _build_codex_gpt5_autoraise_notice(autoraise: Dict[str, float]) -> str:
"""Build the one-time notice shown when Codex gpt-5.x raises compaction.
``autoraise`` is ``{"from": <old_ratio>, "to": <new_ratio>}``. The same
text is printed inline for CLI users and replayed via ``status_callback``
for gateway users, so it must be self-contained and include the exact
opt-back-out command.
``autoraise`` is ``{"model": <slug>, "from": <old_ratio>, "to": <new_ratio>}``.
The same text is printed inline for CLI users and replayed via
``status_callback`` for gateway users, so it must be self-contained and
include the exact opt-back-out command.
"""
model = str(autoraise.get("model") or "gpt-5.4/5.5").strip().lower().rsplit("/", 1)[-1]
from_pct = int(round(autoraise["from"] * 100))
to_pct = int(round(autoraise["to"] * 100))
return (
f" Codex gpt-5.5 caps context at 272K, so auto-compaction was raised "
f" Codex {model} caps context at 272K, so auto-compaction was raised "
f"to {to_pct}% (from {from_pct}%) to use more of the window before "
f"summarizing.\n"
f" Opt back out: hermes config set compression.codex_gpt55_autoraise false"
@ -1409,14 +1410,14 @@ def init_agent(
if not isinstance(_compression_cfg, dict):
_compression_cfg = {}
compression_threshold = float(_compression_cfg.get("threshold", 0.50))
# Per-model/route compaction-threshold override. Codex gpt-5.5 raises to
# 85% (the Codex backend caps the window at 272K, so the default 50% would
# compact at ~136K — half the usable context). Gated by an opt-out config
# flag so the user can fall back to the global threshold; when the override
# fires we stash a one-time notification (replayed on the first turn) that
# tells the user what changed and how to revert. The notice has its own
# display gate so users can keep the threshold autoraise without getting
# the banner on gateway turns.
# Per-model/route compaction-threshold override. Codex gpt-5.4 / gpt-5.5
# raise to 85% (the Codex backend caps both families at 272K, so the
# default 50% would compact at ~136K — half the usable context). Gated by
# an opt-out config flag so the user can fall back to the global threshold;
# when the override fires we stash a one-time notification (replayed on the
# first turn) that tells the user what changed and how to revert. The
# notice has its own display gate so users can keep the threshold
# autoraise without getting the banner on gateway turns.
_codex_gpt55_autoraise = str(
_compression_cfg.get("codex_gpt55_autoraise", True)
).lower() in {"true", "1", "yes"}
@ -1427,7 +1428,7 @@ def init_agent(
try:
from agent.auxiliary_client import (
_compression_threshold_for_model as _cthresh_fn,
_is_codex_gpt55 as _is_codex_gpt55_fn,
_is_codex_gpt54_or_gpt55 as _is_codex_gpt54_or_gpt55_fn,
)
_model_cthresh = _cthresh_fn(
agent.model,
@ -1437,15 +1438,16 @@ def init_agent(
if _model_cthresh is not None:
_prev_threshold = compression_threshold
compression_threshold = _model_cthresh
# Notify only for the Codex gpt-5.5 autoraise (the Arcee Trinity
# override is a long-standing silent default). Skip the notice when
# the user's global threshold already meets/exceeds the raised
# value, since nothing actually changed for them.
# Notify only for the Codex gpt-5.4 / gpt-5.5 autoraise (the Arcee
# Trinity override is a long-standing silent default). Skip the
# notice when the user's global threshold already meets/exceeds the
# raised value, since nothing actually changed for them.
if (
_is_codex_gpt55_fn(agent.model, agent.provider)
_is_codex_gpt54_or_gpt55_fn(agent.model, agent.provider)
and _model_cthresh > _prev_threshold + 1e-9
):
agent._compression_threshold_autoraised = {
"model": agent.model,
"from": _prev_threshold,
"to": _model_cthresh,
}
@ -1910,7 +1912,7 @@ def init_agent(
# turn 1 (set below, after the warning slot is initialized).
_autoraise = getattr(agent, "_compression_threshold_autoraised", None)
if _autoraise and compression_enabled and _codex_gpt55_autoraise_notice:
print(_build_codex_gpt55_autoraise_notice(_autoraise))
print(_build_codex_gpt5_autoraise_notice(_autoraise))
# Check immediately so CLI users see the warning at startup.
# Gateway status_callback is not yet wired, so any warning is stored
@ -1921,7 +1923,7 @@ def init_agent(
# through status_callback on the first turn (Telegram/Discord/Slack/etc.).
_autoraise = getattr(agent, "_compression_threshold_autoraised", None)
if _autoraise and compression_enabled and _codex_gpt55_autoraise_notice:
agent._compression_warning = _build_codex_gpt55_autoraise_notice(_autoraise)
agent._compression_warning = _build_codex_gpt5_autoraise_notice(_autoraise)
# Lazy feasibility check: deferred to the first turn that approaches the
# compression threshold. Running it eagerly here costs ~400ms cold (network
# probe of the auxiliary provider chain + /models lookup) on every agent

View file

@ -314,33 +314,40 @@ def _is_arcee_trinity_thinking(model: Optional[str]) -> bool:
return bare == "trinity-large-thinking"
# Context window enforced by ChatGPT's Codex OAuth backend for gpt-5.5.
# Context window enforced by ChatGPT's Codex OAuth backend for gpt-5.4/5.5.
# The raw OpenAI API and OpenRouter expose 1.05M for the same slug, but the
# Codex backend hard-caps at 272K (verified live: a ~330K-token request to
# chatgpt.com/backend-api/codex/responses is rejected with
# ``context_length_exceeded`` while ~250K succeeds). With a 272K ceiling the
# default 50% compaction trigger fires at ~136K — wasteful, since the model
# can hold far more raw context before summarization actually buys anything.
# We raise the trigger to 85% (~231K) on this exact route so Codex gpt-5.5
# sessions use the window they actually have.
_CODEX_GPT55_COMPACTION_THRESHOLD = 0.85
# We raise the trigger to 85% (~231K) on this exact route so Codex gpt-5.4/
# gpt-5.5 sessions use the window they actually have.
_CODEX_GPT54_GPT55_COMPACTION_THRESHOLD = 0.85
def _is_codex_gpt55(model: Optional[str], provider: Optional[str] = None) -> bool:
"""True for gpt-5.5 accessed through the ChatGPT Codex OAuth backend.
def _is_codex_gpt54_or_gpt55(model: Optional[str], provider: Optional[str] = None) -> bool:
"""True for gpt-5.4 / gpt-5.5 on the ChatGPT Codex OAuth backend.
Matches only the Codex OAuth route (provider ``openai-codex``), not the
direct OpenAI API, OpenRouter, or GitHub Copilot paths those expose a
larger context window for the same slug and must keep the user's default
compaction threshold. ``gpt-5.5-pro`` and dated snapshots
(``gpt-5.5-2026-04-23``) are matched via prefix so the override tracks the
family without re-listing every variant.
compaction threshold. ``gpt-5.4-pro`` / ``gpt-5.5-pro`` and dated snapshots
are matched via prefix so the override tracks both 272K-capped families
without re-listing every variant.
"""
prov = (provider or "").strip().lower()
if prov != "openai-codex":
return False
bare = (model or "").strip().lower().rsplit("/", 1)[-1]
return bare == "gpt-5.5" or bare.startswith("gpt-5.5-") or bare.startswith("gpt-5.5.")
return (
bare == "gpt-5.4"
or bare.startswith("gpt-5.4-")
or bare.startswith("gpt-5.4.")
or bare == "gpt-5.5"
or bare.startswith("gpt-5.5-")
or bare.startswith("gpt-5.5.")
)
def _fixed_temperature_for_model(
@ -379,18 +386,19 @@ def _compression_threshold_for_model(
Per-model/route overrides:
- Arcee Trinity Large Thinking 0.75 (preserve reasoning context).
- gpt-5.5 on the Codex OAuth route 0.85, because Codex caps the window
at 272K and the default 50% trigger would compact at ~136K. Gated by
``allow_codex_gpt55_autoraise`` so the user can opt back down to the
global default (the caller passes the config flag through here).
- gpt-5.4 / gpt-5.5 on the Codex OAuth route 0.85, because Codex caps
both families at 272K and the default 50% trigger would compact at
~136K. Gated by ``allow_codex_gpt55_autoraise`` (historical config-key
name kept for backward compatibility) so the user can opt back down to
the global default (the caller passes the config flag through here).
Returns a float in (0, 1] to override the global ``compression.threshold``
config value, or ``None`` to leave the user's config value unchanged.
"""
if _is_arcee_trinity_thinking(model):
return 0.75
if allow_codex_gpt55_autoraise and _is_codex_gpt55(model, provider):
return _CODEX_GPT55_COMPACTION_THRESHOLD
if allow_codex_gpt55_autoraise and _is_codex_gpt54_or_gpt55(model, provider):
return _CODEX_GPT54_GPT55_COMPACTION_THRESHOLD
return None
# Default auxiliary models for direct API-key providers (cheap/fast for side tasks)
@ -4151,7 +4159,15 @@ def resolve_provider_client(
# main_model also empty), the branches still hit their own
# missing-credentials returns and ``_resolve_auto`` falls through to
# the Step-2 chain as before.
if not model:
#
# Prefer explicit caller model, then provider-scoped aux model, then main model.
# Do NOT pre-fill a blank ``auto`` request from the config/main default here.
# ``auto`` has its own main-runtime resolver below; pre-filling first can pair
# a stale configured model with a live fallback provider (e.g. Claude model
# sent to Codex after the main lane fell back to gpt-5.5). Let _resolve_auto()
# return the actual current runtime model when the caller did not explicitly
# request one. (# compression-current-model)
if not model and provider != "auto":
model = _get_aux_model_for_provider(provider) or _read_main_model() or model
def _needs_codex_wrap(client_obj, base_url_str: str, model_str: str) -> bool:

View file

@ -1362,17 +1362,18 @@ DEFAULT_CONFIG = {
# Default False matches historical behavior; set to
# True if you'd rather pause than silently lose
# context turns when your aux model is flaky.
"codex_gpt55_autoraise": True, # When True, gpt-5.5 on the ChatGPT Codex OAuth
# route raises its compaction trigger to 85% (vs the
# global `threshold` above). Codex hard-caps gpt-5.5
# at a 272K window, so the default 50% would compact
# at ~136K and waste half the usable context. Set to
# False to opt back down to the global threshold
# (e.g. 0.50) for Codex gpt-5.5 sessions. Only this
# exact route is affected — gpt-5.5 on OpenAI's
# direct API, OpenRouter, and Copilot keep the
# global threshold regardless.
"codex_gpt55_autoraise_notice": True, # Display the one-time Codex gpt-5.5
"codex_gpt55_autoraise": True, # Historical key name kept for compatibility.
# When True, gpt-5.4 / gpt-5.5 on the ChatGPT Codex
# OAuth route raise their compaction trigger to 85%
# (vs the global `threshold` above). Codex hard-caps
# both families at a 272K window, so the default 50%
# would compact at ~136K and waste half the usable
# context. Set to False to opt back down to the global
# threshold (e.g. 0.50) for those Codex sessions.
# Only this exact route is affected — gpt-5.4 / 5.5
# on OpenAI's direct API, OpenRouter, and Copilot keep
# the global threshold regardless.
"codex_gpt55_autoraise_notice": True, # Display the one-time Codex gpt-5.4/5.5
# autoraise banner. Set False to keep the
# 85% threshold autoraise but suppress the
# user-facing notice in CLI/gateway output.

View file

@ -17,7 +17,7 @@ from agent.auxiliary_client import (
_compression_threshold_for_model,
_fixed_temperature_for_model,
_is_arcee_trinity_thinking,
_is_codex_gpt55,
_is_codex_gpt54_or_gpt55,
)
@ -78,14 +78,14 @@ def test_compression_threshold_default_none_for_other_models() -> None:
# ---------------------------------------------------------------------------
# Codex gpt-5.5 compaction-threshold autoraise
# Codex gpt-5.4 / gpt-5.5 compaction-threshold autoraise
#
# ChatGPT's Codex OAuth backend caps gpt-5.5 at a 272K window (verified live:
# ~330K-token request rejected with context_length_exceeded, ~250K accepted).
# The default 50% compaction trigger would fire at ~136K — half the usable
# window — so this route raises the trigger to 85%. Only the Codex OAuth route
# is affected; the same slug on OpenAI direct / OpenRouter / Copilot exposes a
# larger window and keeps the user's global threshold.
# ChatGPT's Codex OAuth backend caps both families at a 272K window (verified
# live via the Codex /models resolver and per-slug fallback table). The default
# 50% compaction trigger would fire at ~136K — half the usable window — so this
# route raises the trigger to 85%. Only the Codex OAuth route is affected; the
# same slugs on OpenAI direct / OpenRouter / Copilot expose a larger window and
# keep the user's global threshold.
# ---------------------------------------------------------------------------
@ -99,32 +99,40 @@ def test_compression_threshold_default_none_for_other_models() -> None:
"openai/gpt-5.5", # aggregator-prefixed (still on the codex route)
"GPT-5.5", # case-insensitive
" gpt-5.5 ", # whitespace tolerant
"gpt-5.4", # base 5.4 (272K-capped)
"gpt-5.4-pro", # pro 5.4 variant (272K-capped)
"gpt-5.4-2026-01-01", # dated 5.4 snapshot
"openai/gpt-5.4", # aggregator-prefixed 5.4
],
)
def test_is_codex_gpt55_matches_on_codex_provider(model: str) -> None:
assert _is_codex_gpt55(model, "openai-codex") is True
def test_is_codex_gpt54_or_gpt55_matches_on_codex_provider(model: str) -> None:
assert _is_codex_gpt54_or_gpt55(model, "openai-codex") is True
@pytest.mark.parametrize(
"provider",
["openrouter", "openai", "copilot", "openai-api", "", None],
)
def test_is_codex_gpt55_rejects_non_codex_providers(provider) -> None:
# gpt-5.5 on any non-Codex route keeps the larger window — no override.
assert _is_codex_gpt55("gpt-5.5", provider) is False
def test_is_codex_gpt54_or_gpt55_rejects_non_codex_providers(provider) -> None:
# gpt-5.4 / gpt-5.5 on any non-Codex route keep the larger window.
assert _is_codex_gpt54_or_gpt55("gpt-5.5", provider) is False
assert _is_codex_gpt54_or_gpt55("gpt-5.4", provider) is False
@pytest.mark.parametrize(
"model",
["gpt-5.4", "gpt-5", "gpt-5.55", "gpt-5.50", "", None],
["gpt-5", "gpt-5.55", "gpt-5.50", "gpt-5.45", "gpt-5.40", "", None],
)
def test_is_codex_gpt55_rejects_non_55_models(model) -> None:
# gpt-5.55 / gpt-5.50 are different families and must NOT match — the
# "gpt-5.5-" / "gpt-5.5." prefix guards require a separator after "5.5".
assert _is_codex_gpt55(model, "openai-codex") is False
def test_is_codex_gpt54_or_gpt55_rejects_non_54_55_models(model) -> None:
# Close numeric neighbours must NOT match — the prefix guards require a
# separator after "5.4" / "5.5" so e.g. gpt-5.45 and gpt-5.55 stay out.
assert _is_codex_gpt54_or_gpt55(model, "openai-codex") is False
def test_compression_threshold_for_codex_gpt55() -> None:
assert _compression_threshold_for_model("gpt-5.4", "openai-codex") == 0.85
assert _compression_threshold_for_model("gpt-5.4-pro", "openai-codex") == 0.85
assert _compression_threshold_for_model("openai/gpt-5.4", "openai-codex") == 0.85
assert _compression_threshold_for_model("gpt-5.5", "openai-codex") == 0.85
assert _compression_threshold_for_model("gpt-5.5-pro", "openai-codex") == 0.85
assert _compression_threshold_for_model("openai/gpt-5.5", "openai-codex") == 0.85
@ -132,14 +140,24 @@ def test_compression_threshold_for_codex_gpt55() -> None:
def test_compression_threshold_codex_gpt55_other_routes_unaffected() -> None:
# Same slug, different route → no override (keep the user's config value).
assert _compression_threshold_for_model("gpt-5.4", "openrouter") is None
assert _compression_threshold_for_model("gpt-5.4", "openai") is None
assert _compression_threshold_for_model("gpt-5.4", "copilot") is None
assert _compression_threshold_for_model("gpt-5.5", "openrouter") is None
assert _compression_threshold_for_model("gpt-5.5", "openai") is None
assert _compression_threshold_for_model("gpt-5.5", "copilot") is None
assert _compression_threshold_for_model("openai/gpt-5.4") is None # no provider
assert _compression_threshold_for_model("openai/gpt-5.5") is None # no provider
def test_compression_threshold_codex_gpt55_opt_out() -> None:
# allow_codex_gpt55_autoraise=False reverts to the global default (None).
# Historical flag name still governs both Codex families.
assert (
_compression_threshold_for_model(
"gpt-5.4", "openai-codex", allow_codex_gpt55_autoraise=False
)
is None
)
assert (
_compression_threshold_for_model(
"gpt-5.5", "openai-codex", allow_codex_gpt55_autoraise=False

View file

@ -279,6 +279,34 @@ class TestResolveAutoMainFirst:
assert mock_resolve.call_args.args[0] == "anthropic"
assert mock_resolve.call_args.args[1] == "runtime-model"
def test_resolve_provider_auto_returns_runtime_model_not_stale_config_default(self):
"""Blank auto aux requests must not pair a stale config model with live fallback provider."""
runtime_client = MagicMock()
with patch(
"agent.auxiliary_client._read_main_model",
return_value="claude-opus-4-8",
) as mock_read_main_model, patch(
"agent.auxiliary_client._resolve_auto",
return_value=(runtime_client, "gpt-5.5"),
) as mock_resolve_auto:
from agent.auxiliary_client import resolve_provider_client
client, model = resolve_provider_client(
"auto",
main_runtime={
"provider": "openai-codex",
"model": "gpt-5.5",
"base_url": "",
"api_key": "",
"api_mode": "codex_responses",
},
)
assert client is runtime_client
assert model == "gpt-5.5"
mock_read_main_model.assert_not_called()
mock_resolve_auto.assert_called_once()
def test_runtime_base_url_passed_for_named_api_key_provider(self):
"""Named API-key providers inherit the live session endpoint for aux work."""
token_plan_url = "https://token-plan-sgp.xiaomimimo.com/v1"