Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions agent/transports/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@
from agent.transports.types import NormalizedResponse, ToolCall


def _bounded_prompt_cache_key(value: Any) -> Optional[str]:
"""Return a provider-safe cache key without changing session identity."""
if value is None:
return None
key = str(value).strip()
if not key:
return None
if len(key) <= 64:
return key
# Match _content_cache_key's compact, collision-resistant routing-key shape.
digest = hashlib.sha256(key.encode("utf-8", errors="replace")).hexdigest()[:24]
return f"pck_{digest}"


def _content_cache_key(instructions: str, tools: Optional[List[Dict[str, Any]]]) -> Optional[str]:
"""Content-address the prompt cache key from the static request prefix.

Expand Down Expand Up @@ -304,6 +318,13 @@ def build_kwargs(
if request_overrides:
kwargs.update(request_overrides)

if "prompt_cache_key" in kwargs:
bounded_cache_key = _bounded_prompt_cache_key(kwargs["prompt_cache_key"])
if bounded_cache_key:
kwargs["prompt_cache_key"] = bounded_cache_key
else:
kwargs.pop("prompt_cache_key", None)

# xAI Responses API rejects ``service_tier`` (HTTP 400 "Argument not
# supported: service_tier") — hit when ``/fast`` priority-processing
# mode lingers from a prior model in the same session, or when a
Expand Down Expand Up @@ -337,7 +358,7 @@ def build_kwargs(
# remain high. Send session_id / x-client-request-id as HTTP
# headers while keeping ``prompt_cache_key`` in the body for
# standard OpenAI routing as a belt-and-braces fallback.
cache_scope_id = str(session_id or "").strip()
cache_scope_id = _bounded_prompt_cache_key(session_id)
if cache_scope_id:
existing_extra_headers = kwargs.get("extra_headers")
merged_extra_headers: Dict[str, str] = {}
Expand Down Expand Up @@ -382,6 +403,14 @@ def build_kwargs(
merged_extra_body.setdefault("prompt_cache_key", cache_key)
kwargs["extra_body"] = merged_extra_body

extra_body = kwargs.get("extra_body")
if isinstance(extra_body, dict) and "prompt_cache_key" in extra_body:
bounded_cache_key = _bounded_prompt_cache_key(extra_body["prompt_cache_key"])
if bounded_cache_key:
extra_body["prompt_cache_key"] = bounded_cache_key
else:
extra_body.pop("prompt_cache_key", None)

return kwargs

def normalize_response(self, response: Any, **kwargs) -> NormalizedResponse:
Expand Down Expand Up @@ -470,11 +499,26 @@ def preflight_kwargs(
Normalizes input items, strips unsupported fields, validates structure.
"""
from agent.codex_responses_adapter import _preflight_codex_api_kwargs
return _preflight_codex_api_kwargs(

normalized = _preflight_codex_api_kwargs(
api_kwargs,
allow_stream=allow_stream,
is_github_responses=is_github_responses,
)
if "prompt_cache_key" in normalized:
bounded = _bounded_prompt_cache_key(normalized["prompt_cache_key"])
if bounded:
normalized["prompt_cache_key"] = bounded
else:
normalized.pop("prompt_cache_key", None)
extra_body = normalized.get("extra_body")
if isinstance(extra_body, dict) and "prompt_cache_key" in extra_body:
bounded = _bounded_prompt_cache_key(extra_body["prompt_cache_key"])
if bounded:
extra_body["prompt_cache_key"] = bounded
else:
extra_body.pop("prompt_cache_key", None)
return normalized

def map_finish_reason(self, raw_reason: str) -> str:
"""Map Codex response.status to OpenAI finish_reason.
Expand Down
1 change: 1 addition & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@
"dirtyren@users.noreply.github.com": "dirtyren",
"krowd3v@users.noreply.github.com": "krowd3v",
"dfein38347g@users.noreply.github.com": "dfein38347g",
"nicktaylor@TheWorldofNick-Lappy.local": "thegoodguysla",
"s96919@gmail.com": "s96919",
"rasitakyol@hotmail.com": "rasitakyol",
"thatgfsj@gmail.com": "Thatgfsj",
Expand Down
90 changes: 90 additions & 0 deletions tests/agent/transports/test_codex_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,96 @@ def test_codex_backend_sets_cache_routing_headers(self, transport):
assert headers.get("session_id") == "conv-codex-1"
assert headers.get("x-client-request-id") == "conv-codex-1"

def test_codex_backend_hashes_overlength_cache_routing_headers(self, transport):
messages = [{"role": "user", "content": "Hi"}]
long_session_id = "paperclip:company:" + "a" * 80

kw = transport.build_kwargs(
model="gpt-5.4",
messages=messages,
tools=[],
session_id=long_session_id,
is_codex_backend=True,
)

headers = kw["extra_headers"]
cache_scope = headers["session_id"]
assert cache_scope == headers["x-client-request-id"]
assert cache_scope.startswith("pck_")
assert len(cache_scope) <= 64
assert cache_scope != long_session_id
assert kw["prompt_cache_key"].startswith("pck_")
assert len(kw["prompt_cache_key"]) <= 64

@pytest.mark.parametrize("length", [64, 65])
def test_codex_cache_scope_boundary(self, transport, length):
session_id = "s" * length
scope = transport.build_kwargs(
model="gpt-5.4",
messages=[{"role": "user", "content": "Hi"}],
tools=[],
session_id=session_id,
is_codex_backend=True,
request_overrides={"extra_headers": {"x-test": "1"}},
)["extra_headers"]

assert scope["x-test"] == "1"
assert len(scope["session_id"]) <= 64
assert scope["x-client-request-id"] == scope["session_id"]
if length == 64:
assert scope["session_id"] == session_id
else:
assert scope["session_id"].startswith("pck_")
assert scope["session_id"] != session_id

def test_codex_backend_overlength_cache_scope_is_stable_and_collision_resistant(self, transport):
common = "paperclip:company:" + "a" * 80

def cache_scope(session_id):
return transport.build_kwargs(
model="gpt-5.4",
messages=[{"role": "user", "content": "Hi"}],
tools=[],
session_id=session_id,
is_codex_backend=True,
)["extra_headers"]["session_id"]

assert cache_scope(common + "1") == cache_scope(common + "1")
assert cache_scope(common + "1") != cache_scope(common + "2")

def test_long_override_keys_are_bounded_at_build_and_preflight(self, transport):
long_key = "paperclip:" + "x" * 130
kwargs = transport.build_kwargs(
model="gpt-5.4",
messages=[{"role": "user", "content": "Hi"}],
tools=[],
request_overrides={"prompt_cache_key": long_key},
)
assert len(kwargs["prompt_cache_key"]) <= 64

middleware_payload = dict(kwargs)
middleware_payload["prompt_cache_key"] = long_key
preflight = transport.preflight_kwargs(middleware_payload)
assert preflight["prompt_cache_key"].startswith("pck_")
assert len(preflight["prompt_cache_key"]) <= 64

def test_xai_long_override_key_is_bounded_at_build_and_preflight(self, transport):
long_key = "paperclip:" + "x" * 130
kwargs = transport.build_kwargs(
model="grok-4.3",
messages=[{"role": "user", "content": "Hi"}],
tools=[],
is_xai_responses=True,
request_overrides={"extra_body": {"prompt_cache_key": long_key}},
)
assert len(kwargs["extra_body"]["prompt_cache_key"]) <= 64

middleware_payload = dict(kwargs)
middleware_payload["extra_body"] = {"prompt_cache_key": long_key}
preflight = transport.preflight_kwargs(middleware_payload)
assert preflight["extra_body"]["prompt_cache_key"].startswith("pck_")
assert len(preflight["extra_body"]["prompt_cache_key"]) <= 64

def test_codex_backend_no_headers_without_session_id(self, transport):
messages = [{"role": "user", "content": "Hi"}]

Expand Down
29 changes: 29 additions & 0 deletions tests/run_agent/test_run_agent_codex_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,35 @@ def _capture_api_call(api_kwargs):
]


def test_codex_final_preflight_bounds_middleware_cache_key(monkeypatch):
"""Execution middleware cannot reintroduce an over-length provider key."""
agent = _build_agent(monkeypatch)
setattr(agent, "_disable_streaming", True)
captured = {}
long_key = "paperclip:" + "x" * 130

def _execution_middleware(request, next_call, **_context):
replacement = dict(request)
replacement["prompt_cache_key"] = long_key
return next_call(replacement)

def _capture_api_call(api_kwargs):
captured.update(api_kwargs)
return _codex_message_response("OK")

monkeypatch.setattr(
"hermes_cli.middleware.run_llm_execution_middleware",
_execution_middleware,
)
monkeypatch.setattr(agent, "_interruptible_api_call", _capture_api_call)

result = agent.run_conversation("Say OK")

assert result["completed"] is True
assert captured["prompt_cache_key"].startswith("pck_")
assert len(captured["prompt_cache_key"]) <= 64


def test_run_conversation_codex_empty_output_with_output_text(monkeypatch):
"""Regression: empty response.output + valid output_text should succeed,
not trigger retry/fallback. The validation stage must defer to
Expand Down
Loading