From 948993cd62e93a0aa51074c880fb76e7a8e3d673 Mon Sep 17 00:00:00 2001 From: Tanmay Choudhary Date: Sat, 27 Jun 2026 01:46:35 +0200 Subject: [PATCH] feat(compression): extend Codex 272K compaction autoraise to gpt-5.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- agent/agent_init.py | 48 +++++++++--------- agent/auxiliary_client.py | 50 +++++++++++------- hermes_cli/config.py | 23 +++++---- tests/agent/test_arcee_trinity_overrides.py | 56 ++++++++++++++------- tests/agent/test_auxiliary_main_first.py | 28 +++++++++++ 5 files changed, 135 insertions(+), 70 deletions(-) diff --git a/agent/agent_init.py b/agent/agent_init.py index 5d65c4176e3..188060bd7f2 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -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": , "to": }``. 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": , "from": , "to": }``. + 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 diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 22d410b3ffe..be02c2aedd1 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -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: diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 3801ebd4d94..81d66e99eba 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -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. diff --git a/tests/agent/test_arcee_trinity_overrides.py b/tests/agent/test_arcee_trinity_overrides.py index 91a5fa743bb..965e9150a6c 100644 --- a/tests/agent/test_arcee_trinity_overrides.py +++ b/tests/agent/test_arcee_trinity_overrides.py @@ -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 diff --git a/tests/agent/test_auxiliary_main_first.py b/tests/agent/test_auxiliary_main_first.py index 0b8b0a04462..935bb04371d 100644 --- a/tests/agent/test_auxiliary_main_first.py +++ b/tests/agent/test_auxiliary_main_first.py @@ -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"