From 26530e7df5d5e367098b11f4a2ccdbddb2b6c62a Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:06:11 +0530 Subject: [PATCH] fix(codex): strip nested extra_body retention at the consumer Codex wire The wire guard only removed the top-level prompt_cache_retention kwarg, but the OpenAI SDK merges extra_body into the outgoing JSON body, so a nested extra_body.prompt_cache_retention reaches chatgpt.com/backend-api/codex just the same and still triggers the non-retryable HTTP 400. Both injection vectors are real and probe-verified: the Relay overlay's 'key not in baseline' arm admits an interceptor-added extra_body, and request_overrides={'extra_body': {...}} lands verbatim in build_kwargs output. Close the gap in the same helper: strip the nested field too (copy-on-write, never mutating the caller's mapping), drop extra_body entirely when it empties, and log the same warning. Compatible endpoints keep nested retention untouched. Mutation-verified: removing the extra_body leg fails both new nested tests. Reported by egilewski's review on #89969. --- agent/codex_runtime.py | 24 +++++++- .../test_run_agent_codex_responses.py | 59 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/agent/codex_runtime.py b/agent/codex_runtime.py index 5f4bc2e939..dffb0340d6 100644 --- a/agent/codex_runtime.py +++ b/agent/codex_runtime.py @@ -1319,7 +1319,9 @@ def _sanitize_consumer_codex_request( Explicit ``request_overrides`` are subject to the same endpoint contract: unsupported retention is dropped with a warning instead of being sent and - rejected by the provider. + rejected by the provider. The check covers both the top-level kwarg and a + nested ``extra_body`` entry — the OpenAI SDK merges ``extra_body`` into + the outgoing JSON body, so either shape reaches the endpoint. """ sanitized = dict(request) # Resolved defensively on purpose: run_codex_stream is also driven with @@ -1330,8 +1332,26 @@ def _sanitize_consumer_codex_request( is_consumer_codex = ( bool(backend_predicate()) if callable(backend_predicate) else False ) - if is_consumer_codex and "prompt_cache_retention" in sanitized: + if not is_consumer_codex: + return sanitized + dropped = False + if "prompt_cache_retention" in sanitized: sanitized.pop("prompt_cache_retention") + dropped = True + # The OpenAI SDK merges ``extra_body`` into the outgoing JSON body, so a + # nested ``extra_body.prompt_cache_retention`` reaches the endpoint just + # like the top-level field would. Copy before editing — the caller's + # mapping must not be mutated — and drop the mapping when it empties. + extra_body = sanitized.get("extra_body") + if isinstance(extra_body, dict) and "prompt_cache_retention" in extra_body: + extra_body = dict(extra_body) + extra_body.pop("prompt_cache_retention") + if extra_body: + sanitized["extra_body"] = extra_body + else: + sanitized.pop("extra_body") + dropped = True + if dropped: logger.warning( "Dropped unsupported prompt_cache_retention at consumer Codex " "wire boundary (model=%s).", diff --git a/tests/run_agent/test_run_agent_codex_responses.py b/tests/run_agent/test_run_agent_codex_responses.py index bbe14ae79d..671fd8424b 100644 --- a/tests/run_agent/test_run_agent_codex_responses.py +++ b/tests/run_agent/test_run_agent_codex_responses.py @@ -602,6 +602,65 @@ def test_consumer_codex_wire_guard_preserves_compatible_endpoint_retention(): assert sanitized is not request +def test_consumer_codex_wire_guard_strips_nested_extra_body_retention(caplog): + """The SDK merges ``extra_body`` into the JSON body, so a nested + ``extra_body.prompt_cache_retention`` reaches the endpoint exactly like the + top-level field — the guard must strip both shapes (#89897).""" + from agent.codex_runtime import _sanitize_consumer_codex_request + + agent = SimpleNamespace(_is_codex_backend=lambda: True, model="gpt-5.6-sol") + extra_body = {"prompt_cache_retention": "24h", "unrelated": "keep"} + request = { + "model": "gpt-5.6-sol", + "prompt_cache_key": "cache-key-sentinel", + "extra_body": extra_body, + } + + with caplog.at_level("WARNING", logger="agent.codex_runtime"): + sanitized = _sanitize_consumer_codex_request(agent, request) + + assert "prompt_cache_retention" not in sanitized.get("extra_body", {}) + # Unrelated extra_body entries survive; the caller's mapping is untouched. + assert sanitized["extra_body"]["unrelated"] == "keep" + assert extra_body["prompt_cache_retention"] == "24h" + assert sanitized["prompt_cache_key"] == "cache-key-sentinel" + assert any( + "Dropped unsupported prompt_cache_retention" in record.message + for record in caplog.records + ) + + +def test_consumer_codex_wire_guard_drops_emptied_extra_body(): + """When retention was extra_body's only entry, the emptied mapping is + removed rather than sent as ``extra_body={}``.""" + from agent.codex_runtime import _sanitize_consumer_codex_request + + agent = SimpleNamespace(_is_codex_backend=lambda: True, model="gpt-5.6-sol") + request = { + "model": "gpt-5.6-sol", + "extra_body": {"prompt_cache_retention": "24h"}, + } + + sanitized = _sanitize_consumer_codex_request(agent, request) + + assert "extra_body" not in sanitized + + +def test_consumer_codex_wire_guard_preserves_nested_retention_on_compatible_endpoint(): + """Compatible endpoints keep a nested extra_body retention untouched.""" + from agent.codex_runtime import _sanitize_consumer_codex_request + + agent = SimpleNamespace(_is_codex_backend=lambda: False) + request = { + "model": "openai.gpt-5.5", + "extra_body": {"prompt_cache_retention": "24h"}, + } + + sanitized = _sanitize_consumer_codex_request(agent, request) + + assert sanitized["extra_body"]["prompt_cache_retention"] == "24h" + + @pytest.mark.parametrize( "base_url,model,expect_dropped", [