Fix Codex OAuth reasoning effort forwarding

Normalize the legacy reasoning_effort field at the Codex Responses proxy boundary so it cannot reach the upstream endpoint in an unsupported shape. Preserve an explicit native reasoning object and cover alias conversion and precedence with regression tests.

Alessandro committed Jul 27, 2026 at 12:08 UTC ed7b0800011df2bea5b1e2e9dae93780351fe4bc
3 files changed +28
plugins/_oauth/AGENTS.md
+1
@@ -44,6 +44,7 @@
44 - Browser callback providers must support manual callback paste when the browser cannot reach the local callback route.
45 - Local proxy routes must remain loopback or token protected and must not add broad CORS access.
46 - 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.
47 +- Codex Responses proxy requests must translate the legacy top-level `reasoning_effort` field to `reasoning.effort`; an explicit native `reasoning` field takes precedence.
48
49 ## Work Guidance
50
plugins/_oauth/helpers/codex.py
+3
@@ -666,6 +666,9 @@ 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 + reasoning_effort = normalized.pop("reasoning_effort", None)
670 + if reasoning_effort is not None and "reasoning" not in normalized:
671 + normalized["reasoning"] = {"effort": reasoning_effort}
672 input_value = normalized.get("input")
673 if isinstance(input_value, str):
674 normalized["input"] = (
tests/test_oauth_codex.py
+24
@@ -207,6 +207,30 @@ def test_prepare_responses_body_adds_codex_client_metadata(monkeypatch):
207 assert body["include"] == ["output_text", "reasoning.encrypted_content"]
208
209
210 +@pytest.mark.parametrize(
211 + ("request_reasoning", "expected"),
212 + [
213 + ({"reasoning_effort": "xhigh"}, {"effort": "xhigh"}),
214 + (
215 + {"reasoning": {"effort": "medium"}, "reasoning_effort": "xhigh"},
216 + {"effort": "medium"},
217 + ),
218 + ],
219 +)
220 +def test_prepare_responses_body_normalizes_reasoning_effort(
221 + monkeypatch, request_reasoning, expected
222 +):
223 + monkeypatch.setattr(codex, "build_client_metadata", lambda: {})
224 +
225 + body = codex.prepare_responses_body(
226 + {"model": "gpt-5.5", "input": "hello", **request_reasoning},
227 + force_stream=True,
228 + )
229 +
230 + assert body["reasoning"] == expected
231 + assert "reasoning_effort" not in body
232 +
233 +
234 def test_prepare_responses_body_sends_empty_continuation_input_as_list(monkeypatch):
235 monkeypatch.setattr(codex, "build_client_metadata", lambda: {})
236