diff --git a/hindsight-api-slim/hindsight_api/engine/providers/codex_llm.py b/hindsight-api-slim/hindsight_api/engine/providers/codex_llm.py index fe40576e8b..4e1780e1e4 100644 --- a/hindsight-api-slim/hindsight_api/engine/providers/codex_llm.py +++ b/hindsight-api-slim/hindsight_api/engine/providers/codex_llm.py @@ -172,8 +172,8 @@ def __init__( if self.model.startswith("openai/"): self.model = self.model[len("openai/") :] - # Map reasoning effort to Codex reasoning summary format - # Codex supports: "auto", "concise", "detailed" + # Reasoning summary controls presentation separately from the backend's + # reasoning effort, which is sent unchanged in each request payload. self.reasoning_summary = self._map_reasoning_effort(reasoning_effort) # HTTP client for SSE streaming @@ -448,7 +448,7 @@ async def call( "tools": [], "tool_choice": "auto", "parallel_tool_calls": True, - "reasoning": {"summary": reasoning_summary}, + "reasoning": {"effort": self.reasoning_effort, "summary": reasoning_summary}, "store": False, # Codex uses stateless mode "stream": True, # SSE streaming "include": ["reasoning.encrypted_content"], @@ -831,7 +831,7 @@ async def call_with_tools( else tool_choice.mode.value ), "parallel_tool_calls": True, - "reasoning": {"summary": reasoning_summary}, + "reasoning": {"effort": self.reasoning_effort, "summary": reasoning_summary}, "store": False, "stream": True, "include": ["reasoning.encrypted_content"], diff --git a/hindsight-api-slim/tests/test_codex_reasoning_effort.py b/hindsight-api-slim/tests/test_codex_reasoning_effort.py new file mode 100644 index 0000000000..a34162eb19 --- /dev/null +++ b/hindsight-api-slim/tests/test_codex_reasoning_effort.py @@ -0,0 +1,64 @@ +"""Regression tests for Codex reasoning-effort request serialization.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from hindsight_api.engine.providers.codex_llm import CodexLLM + + +def build_llm(reasoning_effort: str = "high") -> CodexLLM: + with ( + patch.object(CodexLLM, "_load_codex_auth", return_value=("token", "account")), + patch.object(CodexLLM, "_load_codex_refresh_token", return_value=None), + ): + return CodexLLM( + provider="openai-codex", + api_key="ignored", + base_url="https://chatgpt.com/backend-api", + model="gpt-5.6-luna", + reasoning_effort=reasoning_effort, + ) + + +@pytest.mark.asyncio +async def test_call_sends_reasoning_effort_separately_from_summary() -> None: + llm = build_llm("high") + response = MagicMock() + response.raise_for_status.return_value = None + + with ( + patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post, + patch.object(llm, "_parse_sse_stream", new_callable=AsyncMock, return_value="ok"), + ): + mock_post.return_value = response + await llm.call(messages=[{"role": "user", "content": "hello"}], max_retries=0) + + assert mock_post.call_args.kwargs["json"]["reasoning"] == { + "effort": "high", + "summary": "detailed", + } + + +@pytest.mark.asyncio +async def test_call_with_tools_sends_reasoning_effort_separately_from_summary() -> None: + llm = build_llm("low") + response = MagicMock() + response.status_code = 200 + response.raise_for_status.return_value = None + + with ( + patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post, + patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock, return_value=(None, [])), + ): + mock_post.return_value = response + await llm.call_with_tools( + messages=[{"role": "user", "content": "hello"}], + tools=[], + max_retries=0, + ) + + assert mock_post.call_args.kwargs["json"]["reasoning"] == { + "effort": "low", + "summary": "concise", + }