"""Tests for native OpenAI Responses server-side compaction (gpt-5.6 only). Live behavior verified 2026-08-08 against api.openai.com: gpt-5.6 and gpt-5.3-codex accept ``context_management`` and emit compaction items; gpt-5.1/gpt-5.2 fail server-side (HTTP 500 / stream stall) with no structured rejection — hence the hard model-family gate these tests pin. """ from types import SimpleNamespace import pytest from agent.native_compaction import ( DEFAULT_COMPACT_THRESHOLD, is_direct_openai_route, is_native_compaction_model, is_native_compaction_rejection, native_compaction_context_management, resolve_compact_threshold, ) def _agent( model="gpt-5.6", base_url="https://api.openai.com/v1", enabled=True, compression_enabled=True, threshold: object = DEFAULT_COMPACT_THRESHOLD, compressor=None, capabilities=None, ): return SimpleNamespace( model=model, base_url=base_url, codex_responses_native_compaction=enabled, compression_enabled=compression_enabled, codex_responses_compact_threshold=threshold, context_compressor=compressor, capabilities=capabilities or {}, ) class TestModelGate: def test_gpt56_family_eligible(self): assert is_native_compaction_model("gpt-5.6") assert is_native_compaction_model("gpt-5.6-mini") assert is_native_compaction_model("GPT-5.6-2026-07-15") def test_other_models_ineligible(self): # gpt-5.1/5.2 fail server-side on context_management (live-verified); # gpt-5.3-codex works upstream but is outside the supported set. for model in ("gpt-5.1", "gpt-5.2", "gpt-5.3-codex", "gpt-4o", "o3", ""): assert not is_native_compaction_model(model) assert not is_native_compaction_model(None) class TestRouteGate: def test_direct_openai_api(self): assert is_direct_openai_route("https://api.openai.com/v1") def test_codex_backend_flag(self): assert is_direct_openai_route( "https://chatgpt.com/backend-api/codex", is_codex_backend=True ) def test_everything_else_rejected(self): for url in ( "https://openrouter.ai/api/v1", "https://api.x.ai/v1", "https://models.github.ai/inference", "http://localhost:1234/v1", "https://api.openai.com.evil.com/v1", # suffix spoof "", None, ): assert not is_direct_openai_route(url), url class TestRequestGate: def test_eligible_route_gets_payload(self): payload = native_compaction_context_management( _agent(), is_codex_backend=False ) assert payload == [ {"type": "compaction", "compact_threshold": DEFAULT_COMPACT_THRESHOLD} ] def test_codex_backend_gets_payload(self): payload = native_compaction_context_management( _agent(base_url="https://chatgpt.com/backend-api/codex"), is_codex_backend=True, ) assert payload is not None def test_trusted_proxy_capability_gets_payload(self): payload = native_compaction_context_management( _agent( base_url="https://trusted-proxy.example/v1", capabilities={"openai_native_compaction": True}, ), is_codex_backend=False, ) assert payload is not None def test_disabled_by_default_config_value(self): assert ( native_compaction_context_management( _agent(enabled=False), is_codex_backend=False ) is None ) def test_compression_disabled_disables_native(self): assert ( native_compaction_context_management( _agent(compression_enabled=False), is_codex_backend=False ) is None ) def test_wrong_model_never_sends(self): assert ( native_compaction_context_management( _agent(model="gpt-5.1"), is_codex_backend=False ) is None ) def test_xai_and_github_surfaces_never_send(self): agent = _agent() assert ( native_compaction_context_management( agent, is_codex_backend=False, is_xai_responses=True ) is None ) assert ( native_compaction_context_management( agent, is_codex_backend=False, is_github_responses=True ) is None ) def test_non_openai_route_never_sends(self): assert ( native_compaction_context_management( _agent(base_url="https://openrouter.ai/api/v1"), is_codex_backend=False, ) is None ) def test_threshold_clamped_below_local_compressor(self): compressor = SimpleNamespace(threshold_tokens=100_000) payload = native_compaction_context_management( _agent(compressor=compressor), is_codex_backend=False ) assert payload[0]["compact_threshold"] < 100_000 def test_omitted_threshold_tracks_resolved_local_trigger(self): compressor = SimpleNamespace(threshold_tokens=765_000) payload = native_compaction_context_management( _agent(threshold=None, compressor=compressor), is_codex_backend=False ) assert payload == [{"type": "compaction", "compact_threshold": 756_808}] class TestThresholdClamp: def test_omitted_threshold_derives_from_local_trigger(self): assert resolve_compact_threshold(None, 765_000) == 756_808 def test_clamps_below_local_trigger(self): assert resolve_compact_threshold(200_000, 100_000) == 100_000 - 8_192 def test_explicit_threshold_remains_absolute_for_larger_window(self): assert resolve_compact_threshold(200_000, 765_000) == 200_000 def test_no_local_trigger_uses_configured(self): assert resolve_compact_threshold(200_000, None) == 200_000 @pytest.mark.parametrize("configured", ["garbage", True, -5, 1.5]) def test_invalid_configured_uses_automatic_local_threshold(self, configured): assert resolve_compact_threshold(configured, 765_000) == 756_808 def test_automatic_without_local_trigger_uses_documented_fallback(self): assert resolve_compact_threshold(None, None) == DEFAULT_COMPACT_THRESHOLD @pytest.mark.parametrize( ("local_trigger", "expected"), [ (8_193, 1_024), (8_192, 6_553), (4_000, 3_200), (1_024, 1_024), (1_000, 1_024), (1, 1_024), ], ) def test_automatic_tiny_local_trigger_respects_provider_floor( self, local_trigger, expected ): assert resolve_compact_threshold(None, local_trigger) == expected def test_explicit_tiny_local_trigger_stays_positive(self): assert resolve_compact_threshold(200_000, 4_000) >= 1_024 class TestRejectionMatcher: def test_structured_param_rejection_matches(self): assert is_native_compaction_rejection( "Error code: 400 - Unknown parameter: 'context_management'" ) assert is_native_compaction_rejection( "invalid value for compact_threshold" ) def test_generic_errors_do_not_match(self): # Generic failures must take the normal retry path — a transient # 500 or timeout must never permanently disable native compaction. for err in ( "An error occurred while processing your request", "Broken pipe", "Rate limit exceeded", "", None, ): assert not is_native_compaction_rejection(err) def test_field_echo_without_rejection_language_does_not_match(self): # A transient failure body that merely echoes the request (and so # contains the field name) must not downgrade the session (#82777). assert not is_native_compaction_rejection( "upstream timeout while processing request with " "context_management=[{...}]" ) assert not is_native_compaction_rejection( "connection reset; last request included compact_threshold=200000" ) def test_non_400_status_does_not_match(self): msg = "Unknown parameter: 'context_management'" assert not is_native_compaction_rejection(msg, 500) assert not is_native_compaction_rejection(msg, 503) assert not is_native_compaction_rejection(msg, 429) def test_400_status_with_rejection_language_matches(self): msg = "Unknown parameter: 'context_management'" assert is_native_compaction_rejection(msg, 400) def test_unknown_status_preserves_message_only_matching(self): # Transports that surface only a string keep working. assert is_native_compaction_rejection( "Error code: 400 - Unknown parameter: 'context_management'", None ) assert is_native_compaction_rejection( "unsupported field compact_threshold", "not-a-number" ) class TestConfigCoercion: def test_false_like_strings_stay_disabled(self, monkeypatch): from utils import is_truthy_value for raw in ("false", "off", "no", "0", "", "FALSE", " Off "): assert not is_truthy_value(raw, False), raw def test_true_like_strings_enable(self): from utils import is_truthy_value for raw in ("true", "1", "yes", "on", "TRUE"): assert is_truthy_value(raw, False), raw class TestWirePlumbing: """context_management flows through build_kwargs and both preflights.""" def test_transport_build_kwargs_includes_field(self): from agent.transports.codex import ResponsesApiTransport transport = ResponsesApiTransport() kwargs = transport.build_kwargs( model="gpt-5.6", messages=[{"role": "user", "content": "hi"}], context_management=[{"type": "compaction", "compact_threshold": 4000}], ) assert kwargs["context_management"] == [ {"type": "compaction", "compact_threshold": 4000} ] def test_transport_build_kwargs_omits_field_when_none(self): from agent.transports.codex import ResponsesApiTransport transport = ResponsesApiTransport() kwargs = transport.build_kwargs( model="gpt-5.6", messages=[{"role": "user", "content": "hi"}], context_management=None, ) assert "context_management" not in kwargs def test_preflight_preserves_field(self): from agent.codex_responses_adapter import _preflight_codex_api_kwargs normalized = _preflight_codex_api_kwargs( { "model": "gpt-5.6", "instructions": "You are a test.", "input": [{"role": "user", "content": "hi"}], "store": False, "context_management": [ {"type": "compaction", "compact_threshold": 4000} ], } ) assert normalized["context_management"] == [ {"type": "compaction", "compact_threshold": 4000} ] def test_preflight_accepts_replayed_compaction_input_item(self): from agent.codex_responses_adapter import _preflight_codex_input_items items = _preflight_codex_input_items( [ {"type": "compaction", "encrypted_content": "opaque-blob"}, {"role": "user", "content": "hi"}, ] ) assert items[0] == {"type": "compaction", "encrypted_content": "opaque-blob"} def test_preflight_drops_empty_compaction_item(self): from agent.codex_responses_adapter import _preflight_codex_input_items items = _preflight_codex_input_items( [ {"type": "compaction", "encrypted_content": ""}, {"role": "user", "content": "hi"}, ] ) assert all(item.get("type") != "compaction" for item in items) class TestResponseCapture: def test_compaction_output_item_lands_in_reasoning_sidecar(self): from agent.codex_responses_adapter import _normalize_codex_response response = SimpleNamespace( status="completed", output=[ SimpleNamespace( type="compaction", encrypted_content="blob123", status="completed", ), SimpleNamespace( type="message", status="completed", phase="final_answer", content=[SimpleNamespace(type="output_text", text="OK")], id="msg_1", ), ], ) msg, finish_reason = _normalize_codex_response( response, issuer_kind="other:https://api.openai.com/v1" ) assert finish_reason == "stop" compaction_items = [ item for item in (msg.codex_reasoning_items or []) if item.get("type") == "compaction" ] assert len(compaction_items) == 1 assert compaction_items[0]["encrypted_content"] == "blob123" assert compaction_items[0]["_issuer_kind"] == "other:https://api.openai.com/v1" def test_compaction_item_replayed_on_next_turn(self): from agent.codex_responses_adapter import _chat_messages_to_responses_input items = _chat_messages_to_responses_input( [ { "role": "assistant", "content": "OK", "codex_reasoning_items": [ { "type": "compaction", "encrypted_content": "blob123", "_issuer_kind": "codex_backend", } ], }, {"role": "user", "content": "next"}, ], current_issuer_kind="codex_backend", native_compaction_eligible=True, ) replayed = [item for item in items if item.get("type") == "compaction"] assert len(replayed) == 1 assert replayed[0]["encrypted_content"] == "blob123" # Internal stamp must not go over the wire. assert "_issuer_kind" not in replayed[0] def test_foreign_issuer_compaction_item_dropped(self): from agent.codex_responses_adapter import _chat_messages_to_responses_input items = _chat_messages_to_responses_input( [ { "role": "assistant", "content": "OK", "codex_reasoning_items": [ { "type": "compaction", "encrypted_content": "blob123", "_issuer_kind": "codex_backend", } ], }, {"role": "user", "content": "next"}, ], current_issuer_kind="xai_responses", native_compaction_eligible=True, ) assert all(item.get("type") != "compaction" for item in items) class TestAgentInitConfig: def test_defaults_off_and_automatic_threshold(self, monkeypatch): from run_agent import AIAgent agent = AIAgent( api_key="test-key", base_url="https://api.openai.com/v1", api_mode="codex_responses", model="gpt-5.6", provider="openai-api", quiet_mode=True, skip_context_files=True, skip_memory=True, enabled_toolsets=[], ) assert agent.codex_responses_native_compaction is False assert agent.codex_responses_compact_threshold is None def test_public_config_default_selects_automatic_threshold(self): from hermes_cli.config import DEFAULT_CONFIG assert ( DEFAULT_CONFIG["compression"]["codex_responses_compact_threshold"] is None ) @pytest.mark.parametrize( ("threshold_yaml", "configured", "resolved"), [ (None, None, 756_808), ("null", None, 756_808), ("200000", 200_000, 200_000), ("true", None, 756_808), ("-5", None, 756_808), ("1.5", None, 756_808), ('"bad"', None, 756_808), ], ) def test_loaded_config_reaches_request_threshold( self, tmp_path, monkeypatch, threshold_yaml, configured, resolved ): from run_agent import AIAgent home = tmp_path / ".hermes" home.mkdir() lines = ["compression:", " codex_responses_native: true"] if threshold_yaml is not None: lines.append(f" codex_responses_compact_threshold: {threshold_yaml}") (home / "config.yaml").write_text("\n".join(lines) + "\n", encoding="utf-8") monkeypatch.setenv("HERMES_HOME", str(home)) agent = AIAgent( api_key="test-key", base_url="https://api.openai.com/v1", api_mode="codex_responses", model="gpt-5.6", provider="openai-api", quiet_mode=True, skip_context_files=True, skip_memory=True, enabled_toolsets=[], ) compressor = getattr(agent, "context_compressor") compressor.threshold_tokens = 765_000 kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}]) assert getattr(agent, "codex_responses_compact_threshold") == configured assert kwargs["context_management"] == [ {"type": "compaction", "compact_threshold": resolved} ] def test_kwargs_have_no_context_management_by_default(self): from run_agent import AIAgent agent = AIAgent( api_key="test-key", base_url="https://api.openai.com/v1", api_mode="codex_responses", model="gpt-5.6", provider="openai-api", quiet_mode=True, skip_context_files=True, skip_memory=True, enabled_toolsets=[], ) kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}]) assert "context_management" not in kwargs def test_kwargs_include_field_when_enabled_on_eligible_route(self): from run_agent import AIAgent agent = AIAgent( api_key="test-key", base_url="https://api.openai.com/v1", api_mode="codex_responses", model="gpt-5.6", provider="openai-api", quiet_mode=True, skip_context_files=True, skip_memory=True, enabled_toolsets=[], ) agent.codex_responses_native_compaction = True kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}]) assert isinstance(kwargs.get("context_management"), list) def test_kwargs_omit_field_for_ineligible_model_even_when_enabled(self): from run_agent import AIAgent agent = AIAgent( api_key="test-key", base_url="https://api.openai.com/v1", api_mode="codex_responses", model="gpt-5.1", provider="openai-api", quiet_mode=True, skip_context_files=True, skip_memory=True, enabled_toolsets=[], ) agent.codex_responses_native_compaction = True kwargs = agent._build_api_kwargs([{"role": "user", "content": "hi"}]) assert "context_management" not in kwargs class TestPrunePreCheckpointItems: """Wire restructure around a replayed checkpoint (live-verified Aug 2026: the server renders nothing placed before a compaction input item).""" def _items(self): return [ {"role": "user", "content": "goal: build the runner"}, {"role": "assistant", "content": "ack"}, {"role": "user", "content": "constraint: no sleep-polling"}, {"type": "function_call", "call_id": "c1", "name": "t", "arguments": "{}"}, {"type": "function_call_output", "call_id": "c1", "output": "out"}, {"type": "compaction", "encrypted_content": "blobA"}, {"role": "assistant", "content": "post-cp answer"}, {"role": "user", "content": "next ask"}, ] def test_no_checkpoint_is_identity(self): from agent.native_compaction import prune_pre_checkpoint_items items = [i for i in self._items() if i.get("type") != "compaction"] assert prune_pre_checkpoint_items(list(items)) == items def test_checkpoint_leads_and_pre_history_dropped(self): from agent.native_compaction import prune_pre_checkpoint_items out = prune_pre_checkpoint_items(self._items()) assert out[0] == {"type": "compaction", "encrypted_content": "blobA"} # Pre-checkpoint assistant + tool traffic is gone (server never saw it # anyway); post-checkpoint tail is intact and ordered. assert {"role": "assistant", "content": "ack"} not in out assert all(i.get("call_id") != "c1" for i in out if isinstance(i, dict)) assert out[-2:] == self._items()[-2:] def test_pre_checkpoint_user_messages_retained_in_order(self): from agent.native_compaction import prune_pre_checkpoint_items out = prune_pre_checkpoint_items(self._items()) users = [i["content"] for i in out if i.get("role") == "user"] assert users == [ "goal: build the runner", "constraint: no sleep-polling", "next ask", ] # Retained users sit between the checkpoint and the post tail. assert out.index({"role": "user", "content": "goal: build the runner"}) == 1 def test_newest_checkpoint_run_wins(self): from agent.native_compaction import prune_pre_checkpoint_items items = [ {"type": "compaction", "encrypted_content": "old"}, {"role": "user", "content": "mid ask"}, {"type": "compaction", "encrypted_content": "newA"}, {"type": "compaction", "encrypted_content": "newB"}, {"role": "user", "content": "tail ask"}, ] out = prune_pre_checkpoint_items(items) blobs = [i["encrypted_content"] for i in out if i.get("type") == "compaction"] assert blobs == ["newA", "newB"] assert [i["content"] for i in out if i.get("role") == "user"] == [ "mid ask", "tail ask", ] def test_retention_budget_newest_first_with_truncation(self): from agent.native_compaction import prune_pre_checkpoint_items old = {"role": "user", "content": "x" * 4000} # ~1000 tokens newer = {"role": "user", "content": "y" * 2000} # ~500 tokens items = [old, newer, {"type": "compaction", "encrypted_content": "b"}] out = prune_pre_checkpoint_items(items, retained_user_token_budget=600) users = [i["content"] for i in out if i.get("role") == "user"] # Newest kept whole; boundary (older) head-truncated to remaining budget. assert users[-1] == "y" * 2000 assert users[0] == "x" * 400 # (600-500)*4 chars assert out[0]["type"] == "compaction" def test_zero_budget_keeps_only_post_tail(self): from agent.native_compaction import prune_pre_checkpoint_items out = prune_pre_checkpoint_items(self._items(), retained_user_token_budget=0) users = [i["content"] for i in out if i.get("role") == "user"] assert users == ["next ask"] def test_adapter_applies_prune_end_to_end(self): from agent.codex_responses_adapter import _chat_messages_to_responses_input msgs = [ {"role": "user", "content": "the goal"}, { "role": "assistant", "content": "ok", "codex_reasoning_items": [ {"type": "compaction", "encrypted_content": "blob"} ], }, {"role": "user", "content": "follow-up"}, ] items = _chat_messages_to_responses_input(msgs, native_compaction_eligible=True) assert items[0] == {"type": "compaction", "encrypted_content": "blob"} users = [i["content"] for i in items if i.get("role") == "user"] assert users == ["the goal", "follow-up"] # The checkpoint's own turn content is emitted AFTER the checkpoint # in wire order (sidecar items lead the assistant branch), so it # survives in the post tail — call pairing for that turn is intact. assert {"role": "assistant", "content": "ok"} in items assert items.index({"role": "assistant", "content": "ok"}) > 0 def test_adapter_without_checkpoint_unchanged_shape(self): from agent.codex_responses_adapter import _chat_messages_to_responses_input msgs = [ {"role": "user", "content": "hi"}, {"role": "assistant", "content": "hello"}, {"role": "user", "content": "again"}, ] items = _chat_messages_to_responses_input(msgs) assert [i.get("role") for i in items] == ["user", "assistant", "user"] class TestCheckpointGatedOnCurrentEligibility: """A captured checkpoint must not outlive the native gate. The checkpoint is persisted in the ``codex_reasoning_items`` sidecar, so it survives a mid-session model swap, ``compression.enabled: false``, the rejection kill switch and a resumed session. Every one of those closes the gate; if the wire kept being restructured around the stale checkpoint, pre-checkpoint history would be deleted from requests that were never natively compacted — on a model that cannot even decrypt the blob. """ def _history(self): return [ {"role": "user", "content": "goal: ship the migration"}, {"role": "assistant", "content": "on it"}, {"role": "user", "content": "detail A"}, { "role": "assistant", "content": "checkpointed turn", "codex_reasoning_items": [ { "type": "compaction", "encrypted_content": "blob", "_issuer_kind": "codex_backend", } ], }, {"role": "user", "content": "next ask"}, ] def test_ineligible_request_keeps_pre_feature_wire(self): from agent.codex_responses_adapter import _chat_messages_to_responses_input history = self._history() items = _chat_messages_to_responses_input( history, current_issuer_kind="codex_backend", native_compaction_eligible=False, ) pre_feature = _chat_messages_to_responses_input( [ {k: v for k, v in msg.items() if k != "codex_reasoning_items"} for msg in history ], ) assert items == pre_feature # Specifically: no checkpoint on the wire, no deleted history. assert all(i.get("type") != "compaction" for i in items) assert {"role": "assistant", "content": "on it"} in items def test_eligible_request_still_restructures(self): from agent.codex_responses_adapter import _chat_messages_to_responses_input items = _chat_messages_to_responses_input( self._history(), current_issuer_kind="codex_backend", native_compaction_eligible=True, ) assert items[0]["type"] == "compaction" assert {"role": "assistant", "content": "on it"} not in items def test_converter_defaults_to_ineligible(self): from agent.codex_responses_adapter import _chat_messages_to_responses_input items = _chat_messages_to_responses_input(self._history()) assert all(i.get("type") != "compaction" for i in items) assert {"role": "assistant", "content": "on it"} in items def test_build_kwargs_without_field_does_not_prune(self): """Model swapped out of the gpt-5.6 family / kill switch fired: the gate returns None, so the wire must be the pre-feature one.""" from agent.transports.codex import ResponsesApiTransport kwargs = ResponsesApiTransport().build_kwargs( model="gpt-5.2", messages=self._history(), context_management=None, ) assert "context_management" not in kwargs assert all(i.get("type") != "compaction" for i in kwargs["input"]) assert {"role": "assistant", "content": "on it"} in kwargs["input"] def test_build_kwargs_with_field_prunes(self): from agent.transports.codex import ResponsesApiTransport kwargs = ResponsesApiTransport().build_kwargs( model="gpt-5.6", messages=self._history(), is_codex_backend=True, context_management=[{"type": "compaction", "compact_threshold": 4000}], ) assert kwargs["input"][0]["type"] == "compaction" assert {"role": "assistant", "content": "on it"} not in kwargs["input"] def test_convert_messages_defaults_to_ineligible(self): from agent.transports.codex import ResponsesApiTransport items = ResponsesApiTransport().convert_messages( self._history(), is_codex_backend=True ) assert all(i.get("type") != "compaction" for i in items) assert {"role": "assistant", "content": "on it"} in items def test_auxiliary_responses_adapter_never_prunes(self, monkeypatch): """Auxiliary calls (compression, flush_memories, MoA) replay real session history but never send ``context_management`` — so a checkpoint in that history must not restructure their request.""" import agent.codex_responses_adapter as adapter from agent.auxiliary_client import _CodexCompletionsAdapter seen = {} real = adapter._chat_messages_to_responses_input def _spy(messages, **kw): seen.update(kw) return real(messages, **kw) monkeypatch.setattr(adapter, "_chat_messages_to_responses_input", _spy) class _Responses: def create(self, **kwargs): # #93650 routes the bulk input around the SDK transform via # extra_body; accept the payload in either wire shape. seen["input"] = kwargs.get("input") or ( kwargs.get("extra_body") or {} ).get("input") raise RuntimeError("stop before network") class _Client: base_url = "https://chatgpt.com/backend-api/codex" responses = _Responses() with pytest.raises(RuntimeError, match="stop before network"): _CodexCompletionsAdapter(_Client(), "gpt-5.2").create( messages=self._history() ) assert seen.get("native_compaction_eligible") is False assert all(i.get("type") != "compaction" for i in seen["input"]) assert {"role": "assistant", "content": "on it"} in seen["input"]