{"repo":"NousResearch/hermes-agent","public_id":"26599","display_label":"Codex backend extra_headers handling fixes","summary":"These issues and PRs address problems with extra_headers being incorrectly sent to the Codex/chatgpt.com backend, causing HTTP 400 errors, alongside related work on session and cache-routing headers.","subsystem":"agent","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":45633},"resolution":{"state":"resolved","by":"pr-45633"},"freshness":{"state":"verified","last_verified_at":"2026-06-17T12:13:13+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.4},"fix_edges":[{"issue_number":26599,"pr_number":45633,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":26599,"pr_number":47836,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":47335,"pr_number":47836,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":26599,"title":"[Bug]: Codex backend rejects 'extra_headers' field in Responses payload (HTTP 400)","url":null,"state":"closed"},{"kind":"pr","number":26749,"title":"fix(codex): drop extra_headers injection for chatgpt.com Codex backend (#26599)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4fd9397ae","number":0},{"kind":"pr","value":"#45633","number":45633}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":26767,"title":"fix(codex): avoid extra_headers on Codex backend","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#45633","number":45633},{"kind":"pr","value":"#26749","number":26749},{"kind":"pr","value":"#26599","number":26599},{"kind":"pr","value":"#45633","number":45633}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":45633,"title":"fix(codex): drop extra_headers for chatgpt.com backend","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/transports/codex.py","tests/agent/transports/test_codex_transport.py"]},{"kind":"pr","number":47836,"title":"fix(codex): restore session_id/x-client-request-id HTTP headers for cache routing (#47335)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/transports/codex.py","tests/agent/transports/test_codex_transport.py"],"fix_hunks":[{"file":"agent/transports/codex.py","hunks":["@@ -218,10 +218,28 @@ def build_kwargs(\n             kwargs.pop(\"timeout\", None)\n \n         if is_codex_backend:\n-            # chatgpt.com/backend-api/codex rejects body-level\n-            # ``extra_headers`` with HTTP 400. Correlation/cache routing for\n-            # this backend must not be sent through the Responses payload.\n-            kwargs.pop(\"extra_headers\", None)\n+            # The Codex backend rejects body-level ``extra_headers`` with\n+            # HTTP 400, but the OpenAI SDK's ``extra_headers`` kwarg maps\n+            # to actual HTTP request headers (not body fields).  We need\n+            # these headers for cache-scope routing so prompt cache hits\n+            # remain high.  Send session_id / x-client-request-id as HTTP\n+            # headers while keeping ``prompt_cache_key`` in the body for\n+            # standard OpenAI routing as a belt-and-braces fallback.\n+            cache_scope_id = str(session_id or \"\").strip()\n+            if cache_scope_id:\n+                existing_extra_headers = kwargs.get(\"extra_headers\")\n+                merged_extra_headers: Dict[str, str] = {}\n+                if isinstance(existing_extra_headers, dict):\n+                    merged_extra_headers.update(\n+                        {\n+                            str(key): str(value)\n+                            for key, value in existing_extra_headers.items()\n+                            if key and value is not None\n+                        }\n+                    )\n+                merged_extra_headers[\"session_id\"] = cache_scope_id\n+                merged_extra_headers[\"x-client-request-id\"] = cache_scope_id\n+                kwargs[\"extra_headers\"] = merged_extra_headers\n \n         max_tokens = [redacted:secret-assignment]\"max_tokens\")\n         if max_tokens is not None and not is_codex_backend:"]},{"file":"tests/agent/transports/test_codex_transport.py","hunks":["@@ -155,7 +155,9 @@ def test_codex_backend_no_max_output_tokens(self, transport):\n         )\n         assert \"max_output_tokens\" not in kw\n \n-    def test_codex_backend_does_not_set_extra_headers(self, transport):\n+    def test_codex_backend_sets_cache_routing_headers(self, transport):\n+        \"\"\"Codex backend sends session_id / x-client-request-id as HTTP\n+        headers (via extra_headers) for cache-scope routing.\"\"\"\n         messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n \n         kw = transport.build_kwargs(","@@ -166,9 +168,23 @@ def test_codex_backend_does_not_set_extra_headers(self, transport):\n             is_codex_backend=True,\n         )\n \n+        headers = kw.get(\"extra_headers\", {})\n+        assert headers.get(\"session_id\") == \"conv-codex-1\"\n+        assert headers.get(\"x-client-request-id\") == \"conv-codex-1\"\n+\n+    def test_codex_backend_no_headers_without_session_id(self, transport):\n+        messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n+\n+        kw = transport.build_kwargs(\n+            model=\"gpt-5.4\",\n+            messages=messages,\n+            tools=[],\n+            is_codex_backend=True,\n+        )\n+\n         assert \"extra_headers\" not in kw\n \n-    def test_codex_backend_strips_caller_extra_headers(self, transport):\n+    def test_codex_backend_preserves_caller_extra_headers(self, transport):\n         messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n \n         kw = transport.build_kwargs(","@@ -180,7 +196,10 @@ def test_codex_backend_strips_caller_extra_headers(self, transport):\n             request_overrides={\"extra_headers\": {\"x-test\": \"1\"}},\n         )\n \n-        assert \"extra_headers\" not in kw\n+        headers = kw.get(\"extra_headers\", {})\n+        assert headers.get(\"x-test\") == \"1\"\n+        assert headers.get(\"session_id\") == \"conv-codex-1\"\n+        assert headers.get(\"x-client-request-id\") == \"conv-codex-1\"\n \n     def test_non_codex_responses_preserves_caller_extra_headers(self, transport):\n         messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}