mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(compression): autoraise gpt-5.3-codex-spark threshold to 70% (#48621)
gpt-5.3-codex-spark has a native 128K context window but the default 50% compaction trigger fires at ~64K, wasting half the usable window before the session has accumulated enough turns to summarize meaningfully. This raises the trigger to 70% (~90K) on the Codex OAuth route only, leaving ~38K headroom for the summary and continued conversation before the 128K hard limit. The override is not gated by allow_codex_gpt55_autoraise because 128K is the model's native window (unlike gpt-5.5's artificial 272K Codex cap). Non-Codex routes are unaffected. Also adds a boundary regression test verifying the short-session scenario from the issue always yields a non-empty compressible window (no silent context wipe).
This commit is contained in:
parent
948993cd62
commit
0b6df665a9
3 changed files with 157 additions and 0 deletions
|
|
@ -325,6 +325,15 @@ def _is_arcee_trinity_thinking(model: Optional[str]) -> bool:
|
|||
# gpt-5.5 sessions use the window they actually have.
|
||||
_CODEX_GPT54_GPT55_COMPACTION_THRESHOLD = 0.85
|
||||
|
||||
# gpt-5.3-codex-spark is Codex-OAuth-only (ChatGPT Pro entitlement) with a
|
||||
# native 128K context window. The default 50% compaction trigger fires at
|
||||
# ~64K — wasting half the usable window, often before the session has enough
|
||||
# turns to summarize meaningfully. We raise the trigger to 70% (~90K) so
|
||||
# spark sessions use more of the window before summarization, while still
|
||||
# leaving ~38K headroom for the summary and continued conversation before
|
||||
# the 128K hard limit.
|
||||
_CODEX_SPARK_COMPACTION_THRESHOLD = 0.70
|
||||
|
||||
|
||||
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.
|
||||
|
|
@ -350,6 +359,21 @@ def _is_codex_gpt54_or_gpt55(model: Optional[str], provider: Optional[str] = Non
|
|||
)
|
||||
|
||||
|
||||
def _is_codex_spark(model: Optional[str], provider: Optional[str] = None) -> bool:
|
||||
"""True for ``gpt-5.3-codex-spark`` on the ChatGPT Codex OAuth backend.
|
||||
|
||||
The model is Codex-OAuth-only (ChatGPT Pro entitlement) with a native
|
||||
128K context window. Only the Codex OAuth route (provider
|
||||
``openai-codex``) is matched — the slug is not available on other
|
||||
routes.
|
||||
"""
|
||||
prov = (provider or "").strip().lower()
|
||||
if prov != "openai-codex":
|
||||
return False
|
||||
bare = (model or "").strip().lower().rsplit("/", 1)[-1]
|
||||
return bare == "gpt-5.3-codex-spark"
|
||||
|
||||
|
||||
def _fixed_temperature_for_model(
|
||||
model: Optional[str],
|
||||
base_url: Optional[str] = None,
|
||||
|
|
@ -391,6 +415,9 @@ def _compression_threshold_for_model(
|
|||
~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).
|
||||
- gpt-5.3-codex-spark on the Codex OAuth route → 0.70, because the model
|
||||
has a native 128K window and the default 50% trigger would compact at
|
||||
~64K — wasting half the usable context. Gated by the same flag.
|
||||
|
||||
Returns a float in (0, 1] to override the global ``compression.threshold``
|
||||
config value, or ``None`` to leave the user's config value unchanged.
|
||||
|
|
@ -399,6 +426,8 @@ def _compression_threshold_for_model(
|
|||
return 0.75
|
||||
if allow_codex_gpt55_autoraise and _is_codex_gpt54_or_gpt55(model, provider):
|
||||
return _CODEX_GPT54_GPT55_COMPACTION_THRESHOLD
|
||||
if allow_codex_gpt55_autoraise and _is_codex_spark(model, provider):
|
||||
return _CODEX_SPARK_COMPACTION_THRESHOLD
|
||||
return None
|
||||
|
||||
# Default auxiliary models for direct API-key providers (cheap/fast for side tasks)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from agent.auxiliary_client import (
|
|||
_fixed_temperature_for_model,
|
||||
_is_arcee_trinity_thinking,
|
||||
_is_codex_gpt54_or_gpt55,
|
||||
_is_codex_spark,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -175,3 +176,75 @@ def test_compression_threshold_opt_out_does_not_disable_trinity() -> None:
|
|||
)
|
||||
== 0.75
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Codex gpt-5.3-codex-spark compaction-threshold autoraise
|
||||
#
|
||||
# gpt-5.3-codex-spark is Codex-OAuth-only (ChatGPT Pro entitlement) with a
|
||||
# native 128K context window. The default 50% compaction trigger would fire
|
||||
# at ~64K — wasting half the usable window, often before the session has
|
||||
# accumulated enough turns to summarize meaningfully. This route raises the
|
||||
# trigger to 70% (~90K) to preserve more raw context while leaving ~38K
|
||||
# headroom before the 128K hard limit.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
[
|
||||
"gpt-5.3-codex-spark",
|
||||
"openai/gpt-5.3-codex-spark", # aggregator-prefixed (still on the codex route)
|
||||
"GPT-5.3-CODEX-SPARK", # case-insensitive
|
||||
" gpt-5.3-codex-spark ", # whitespace tolerant
|
||||
],
|
||||
)
|
||||
def test_is_codex_spark_matches_on_codex_provider(model: str) -> None:
|
||||
assert _is_codex_spark(model, "openai-codex") is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"provider",
|
||||
["openrouter", "openai", "copilot", "openai-api", "", None],
|
||||
)
|
||||
def test_is_codex_spark_rejects_non_codex_providers(provider) -> None:
|
||||
# spark on any non-Codex route is not a real slug — no override.
|
||||
assert _is_codex_spark("gpt-5.3-codex-spark", provider) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
[
|
||||
"gpt-5.5", # different family
|
||||
"gpt-5.3-codex", # sibling, not spark
|
||||
"gpt-5.3", # bare 5.3, not spark
|
||||
"gpt-5.3-codex-spark-mini", # hypothetical variant — not matched yet
|
||||
"", None,
|
||||
],
|
||||
)
|
||||
def test_is_codex_spark_rejects_non_spark_models(model) -> None:
|
||||
assert _is_codex_spark(model, "openai-codex") is False
|
||||
|
||||
|
||||
def test_compression_threshold_for_codex_spark() -> None:
|
||||
assert _compression_threshold_for_model("gpt-5.3-codex-spark", "openai-codex") == 0.70
|
||||
assert _compression_threshold_for_model("openai/gpt-5.3-codex-spark", "openai-codex") == 0.70
|
||||
|
||||
|
||||
def test_compression_threshold_codex_spark_other_routes_unaffected() -> None:
|
||||
# Same slug, different route → no override (keep the user's config value).
|
||||
assert _compression_threshold_for_model("gpt-5.3-codex-spark", "openrouter") is None
|
||||
assert _compression_threshold_for_model("gpt-5.3-codex-spark", "openai") is None
|
||||
assert _compression_threshold_for_model("gpt-5.3-codex-spark") is None # no provider
|
||||
|
||||
|
||||
def test_compression_threshold_codex_spark_not_gated_by_gpt55_optout() -> None:
|
||||
# The spark autoraise is independent of the gpt-5.5 opt-out flag — 128K is
|
||||
# the model's native window, so 70% is unambiguously correct regardless of
|
||||
# whether the user opted out of the (artificial-cap) gpt-5.5 autoraise.
|
||||
assert (
|
||||
_compression_threshold_for_model(
|
||||
"gpt-5.3-codex-spark", "openai-codex", allow_codex_gpt55_autoraise=False
|
||||
)
|
||||
== 0.70
|
||||
)
|
||||
|
|
|
|||
|
|
@ -283,3 +283,58 @@ class TestCooldownGuard:
|
|||
comp.last_prompt_tokens = 65_000
|
||||
assert comp._summary_failure_cooldown_until == 0.0
|
||||
assert comp.should_compress(65_000)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test: #48621 — gpt-5.3-codex-spark short-session boundary
|
||||
#
|
||||
# Issue #48621 Bug 2 claims that a short high-token session (15-20 messages,
|
||||
# ~90k tokens on a 128k model with protect_last_n=20) hits
|
||||
# compress_start >= compress_end, causing a silent context wipe. The
|
||||
# raw-budget fallback added in the #40803 fix already mitigates this: the
|
||||
# boundary logic always exposes a minimal compressible window. This test
|
||||
# locks that behavior in for the exact #48621 parameters.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCodexSparkShortSessionBoundary:
|
||||
"""Verify that gpt-5.3-codex-spark's short-session scenario always yields
|
||||
a non-empty compressible window (no silent wipe)."""
|
||||
|
||||
def test_short_high_token_session_has_compressible_window(self):
|
||||
"""16 messages with large tool outputs on a 128k model must leave
|
||||
a compressible middle (compress_start < compress_end)."""
|
||||
comp = _make_compressor(
|
||||
model="gpt-5.3-codex-spark",
|
||||
threshold_percent=0.70,
|
||||
protect_first_n=3,
|
||||
protect_last_n=20,
|
||||
config_context_length=128000,
|
||||
)
|
||||
# Build system + 3 head pairs + 3 tool groups (large outputs) + tail pair
|
||||
big_tool = "x" * 20000 # ~5k tokens each
|
||||
messages = [{"role": "system", "content": "You are a helpful agent."}]
|
||||
for i in range(3):
|
||||
messages.append({"role": "user", "content": f"Question {i}"})
|
||||
messages.append({"role": "assistant", "content": f"Answer {i}"})
|
||||
for i in range(3):
|
||||
messages.append({"role": "user", "content": f"Run command {i}"})
|
||||
messages.append({
|
||||
"role": "assistant", "content": "",
|
||||
"tool_calls": [{
|
||||
"id": f"tc{i}", "type": "function",
|
||||
"function": {"name": "terminal", "arguments": "{}"},
|
||||
}],
|
||||
})
|
||||
messages.append({"role": "tool", "tool_call_id": f"tc{i}", "content": big_tool})
|
||||
messages.append({"role": "user", "content": "Final question"})
|
||||
messages.append({"role": "assistant", "content": "Final answer"})
|
||||
|
||||
head = comp._protect_head_size(messages)
|
||||
compress_start = comp._align_boundary_forward(messages, head)
|
||||
compress_end = comp._find_tail_cut_by_tokens(messages, compress_start)
|
||||
|
||||
assert compress_start < compress_end, (
|
||||
f"No compressible window: start={compress_start}, end={compress_end}. "
|
||||
f"This would cause the silent context wipe described in #48621."
|
||||
)
|
||||
assert comp.has_content_to_compress(messages) is True
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue