From 4d39a603d197a6ad2da483430446b96a1a9b0aea Mon Sep 17 00:00:00 2001 From: kyssta-exe Date: Tue, 16 Jun 2026 18:32:27 +0000 Subject: [PATCH] fix(codex): restore session_id/x-client-request-id HTTP headers for cache routing (#47335) --- agent/transports/codex.py | 26 ++++++++++++++++--- .../agent/transports/test_codex_transport.py | 25 +++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/agent/transports/codex.py b/agent/transports/codex.py index 1d24ac3355a7..eaf6160ae1d3 100644 --- a/agent/transports/codex.py +++ b/agent/transports/codex.py @@ -218,10 +218,28 @@ class ResponsesApiTransport(ProviderTransport): kwargs.pop("timeout", None) if is_codex_backend: - # chatgpt.com/backend-api/codex rejects body-level - # ``extra_headers`` with HTTP 400. Correlation/cache routing for - # this backend must not be sent through the Responses payload. - kwargs.pop("extra_headers", None) + # The Codex backend rejects body-level ``extra_headers`` with + # HTTP 400, but the OpenAI SDK's ``extra_headers`` kwarg maps + # to actual HTTP request headers (not body fields). We need + # these headers for cache-scope routing so prompt cache hits + # 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() + if cache_scope_id: + existing_extra_headers = kwargs.get("extra_headers") + merged_extra_headers: Dict[str, str] = {} + if isinstance(existing_extra_headers, dict): + merged_extra_headers.update( + { + str(key): str(value) + for key, value in existing_extra_headers.items() + if key and value is not None + } + ) + merged_extra_headers["session_id"] = cache_scope_id + merged_extra_headers["x-client-request-id"] = cache_scope_id + kwargs["extra_headers"] = merged_extra_headers max_tokens = params.get("max_tokens") if max_tokens is not None and not is_codex_backend: diff --git a/tests/agent/transports/test_codex_transport.py b/tests/agent/transports/test_codex_transport.py index e028f4344462..9355a7869649 100644 --- a/tests/agent/transports/test_codex_transport.py +++ b/tests/agent/transports/test_codex_transport.py @@ -155,7 +155,9 @@ class TestCodexBuildKwargs: ) assert "max_output_tokens" not in kw - def test_codex_backend_does_not_set_extra_headers(self, transport): + def test_codex_backend_sets_cache_routing_headers(self, transport): + """Codex backend sends session_id / x-client-request-id as HTTP + headers (via extra_headers) for cache-scope routing.""" messages = [{"role": "user", "content": "Hi"}] kw = transport.build_kwargs( @@ -166,9 +168,23 @@ class TestCodexBuildKwargs: is_codex_backend=True, ) + headers = kw.get("extra_headers", {}) + assert headers.get("session_id") == "conv-codex-1" + assert headers.get("x-client-request-id") == "conv-codex-1" + + def test_codex_backend_no_headers_without_session_id(self, transport): + messages = [{"role": "user", "content": "Hi"}] + + kw = transport.build_kwargs( + model="gpt-5.4", + messages=messages, + tools=[], + is_codex_backend=True, + ) + assert "extra_headers" not in kw - def test_codex_backend_strips_caller_extra_headers(self, transport): + def test_codex_backend_preserves_caller_extra_headers(self, transport): messages = [{"role": "user", "content": "Hi"}] kw = transport.build_kwargs( @@ -180,7 +196,10 @@ class TestCodexBuildKwargs: request_overrides={"extra_headers": {"x-test": "1"}}, ) - assert "extra_headers" not in kw + headers = kw.get("extra_headers", {}) + assert headers.get("x-test") == "1" + assert headers.get("session_id") == "conv-codex-1" + assert headers.get("x-client-request-id") == "conv-codex-1" def test_non_codex_responses_preserves_caller_extra_headers(self, transport): messages = [{"role": "user", "content": "Hi"}]