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.
This commit is contained in:
@@ -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).",
|
||||
|
||||
@@ -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",
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user