mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
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:
parent
370a489fb4
commit
948993cd62
5 changed files with 135 additions and 70 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue