Normalize Codex Responses input shape

Ensure the Codex OAuth Responses proxy forwards list-shaped input for empty continuation turns while preserving string prompts as user input items. Add regression coverage for the gpt-5.5 continuation case and document the proxy contract.

Alessandro committed Jul 9, 2026 at 02:41 UTC 5f144350936c58f07cb1aef8d0b5b83babe94786
3 files changed +25 -1
plugins/_oauth/AGENTS.md
+1 -1
@@ -40,7 +40,7 @@
40 - Stored upstream base URLs and OAuth token endpoints must be validated against provider-owned allowlists before sending bearer or refresh tokens.
41 - Browser callback providers must support manual callback paste when the browser cannot reach the local callback route.
42 - Local proxy routes must remain loopback or token protected and must not add broad CORS access.
43 -- Codex Responses proxy requests must include Codex client metadata and compatibility headers such as `client_metadata`, `x-codex-installation-id`, `originator`, `session-id`, and `thread-id`.
43 +- Codex Responses proxy requests must include Codex client metadata and compatibility headers such as `client_metadata`, `x-codex-installation-id`, `originator`, `session-id`, and `thread-id`, and must forward `input` as a list for upstream Codex compatibility.
44
45 ## Work Guidance
46
plugins/_oauth/helpers/codex.py
+7
@@ -666,6 +666,13 @@ def fetch_models() -> list[str]:
666
667 def prepare_responses_body(body: dict[str, Any], *, force_stream: bool) -> dict[str, Any]:
668 normalized = dict(body)
669 + input_value = normalized.get("input")
670 + if isinstance(input_value, str):
671 + normalized["input"] = (
672 + [{"role": "user", "content": input_value}] if input_value else []
673 + )
674 + elif not isinstance(input_value, list):
675 + normalized["input"] = []
676 normalized.setdefault("instructions", "")
677 normalized.setdefault("store", False)
678 normalized["client_metadata"] = merge_client_metadata(normalized.get("client_metadata"))
tests/test_oauth_codex.py
+17
@@ -202,10 +202,27 @@ def test_prepare_responses_body_adds_codex_client_metadata(monkeypatch):
202 "thread_id": "thread-1",
203 "x-codex-window-id": "agent-zero",
204 }
205 + assert body["input"] == [{"role": "user", "content": "hello"}]
206 assert body["stream"] is True
207 assert body["include"] == ["output_text", "reasoning.encrypted_content"]
208
209
210 +def test_prepare_responses_body_sends_empty_continuation_input_as_list(monkeypatch):
211 + monkeypatch.setattr(codex, "build_client_metadata", lambda: {})
212 +
213 + body = codex.prepare_responses_body(
214 + {
215 + "model": "gpt-5.5",
216 + "input": "",
217 + "previous_response_id": "resp_1",
218 + },
219 + force_stream=True,
220 + )
221 +
222 + assert body["input"] == []
223 + assert body["previous_response_id"] == "resp_1"
224 +
225 +
226 def test_request_codex_sends_current_codex_headers_from_body(monkeypatch):
227 calls = []
228 body = json.dumps(