Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down
64 changes: 64 additions & 0 deletions hindsight-api-slim/tests/test_codex_reasoning_effort.py
Original file line number Diff line number Diff line change
@@ -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",
}
Loading