Skip to content
Open
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
6 changes: 6 additions & 0 deletions src/tau_ai/openai_codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,12 @@ async def _codex_provider_events(
thinking_parts.append(delta)
yield ProviderThinkingDeltaEvent(delta=delta)

elif event_type == "response.reasoning_summary_part.done":
if thinking_parts:
separator = "\n\n"
thinking_parts.append(separator)
yield ProviderThinkingDeltaEvent(delta=separator)

elif event_type in {
"response.output_item.done",
"response.output_item.completed",
Expand Down
6 changes: 6 additions & 0 deletions src/tau_ai/openai_compatible.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,12 @@ def feed(self, event: str) -> tuple[list[ProviderEvent], bool]:
self._thinking_parts.append(delta)
return [ProviderThinkingDeltaEvent(delta=delta)], False

elif chunk_type == "response.reasoning_summary_part.done":
if self._thinking_parts:
separator = "\n\n"
self._thinking_parts.append(separator)
return [ProviderThinkingDeltaEvent(delta=separator)], False

elif chunk_type == "response.output_item.added":
item = chunk.get("item")
_register_reasoning_item(self._reasoning_items, item)
Expand Down
104 changes: 104 additions & 0 deletions tests/test_tau_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,59 @@ def handler(_request: httpx.Request) -> httpx.Response:
assert events[-1].message.text == "Done"


@pytest.mark.anyio
async def test_openai_codex_provider_preserves_reasoning_summary_part_boundaries() -> None:
async def credentials() -> OpenAICodexCredentials:
return OpenAICodexCredentials(access_token="access-token", account_id="account-1")

def handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
text=(
'data: {"type":"response.reasoning_summary_text.delta",'
'"delta":"**First step**"}\n\n'
'data: {"type":"response.reasoning_summary_part.done"}\n\n'
'data: {"type":"response.reasoning_summary_text.delta",'
'"delta":"**Second step**"}\n\n'
'data: {"type":"response.reasoning_summary_part.done"}\n\n'
'data: {"type":"response.output_text.delta","delta":"Done"}\n\n'
'data: {"type":"response.completed","response":{"status":"completed"}}\n\n'
),
headers={"content-type": "text/event-stream"},
)

async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
provider = OpenAICodexProvider(
OpenAICodexConfig(
credential_resolver=credentials,
base_url="https://chatgpt.test/backend-api",
),
client=client,
)

events = await _collect(
provider.stream_response(
model="gpt-5.5",
system="You are Tau.",
messages=[UserMessage(content="Say done")],
tools=[],
)
)

thinking_events = [event for event in events if isinstance(event, ThinkingDeltaEvent)]
assert [event.delta for event in thinking_events] == [
"**First step**",
"\n\n",
"**Second step**",
"\n\n",
]
end = events[-1]
assert isinstance(end, AssistantDoneEvent)
thinking = end.message.content[0]
assert isinstance(thinking, ThinkingContent)
assert thinking.thinking == "**First step**\n\n**Second step**\n\n"


@pytest.mark.anyio
async def test_openai_codex_provider_streams_tool_calls() -> None:
async def credentials() -> OpenAICodexCredentials:
Expand Down Expand Up @@ -2053,6 +2106,57 @@ def handler(_request: httpx.Request) -> httpx.Response:
assert thinking.delta == "Considering"


@pytest.mark.anyio
async def test_responses_api_preserves_reasoning_summary_part_boundaries() -> None:
def handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
text=(
'data: {"type":"response.reasoning_summary_text.delta",'
'"delta":"**First step**"}\n\n'
'data: {"type":"response.reasoning_summary_part.done"}\n\n'
'data: {"type":"response.reasoning_summary_text.delta",'
'"delta":"**Second step**"}\n\n'
'data: {"type":"response.reasoning_summary_part.done"}\n\n'
'data: {"type":"response.output_text.delta","delta":"Answer"}\n\n'
'data: {"type":"response.completed","response":{"status":"completed"}}\n\n'
),
headers={"content-type": "text/event-stream"},
)

async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
provider = OpenAICompatibleProvider(
OpenAICompatibleConfig(
api_key="test-key",
base_url="https://example.test/v1",
reasoning_effort="high",
),
client=client,
)

events = await _collect(
provider.stream_response(
model="gpt-5.5",
system="You are Tau.",
messages=[UserMessage(content="think")],
tools=[],
)
)

thinking_events = [event for event in events if isinstance(event, ThinkingDeltaEvent)]
assert [event.delta for event in thinking_events] == [
"**First step**",
"\n\n",
"**Second step**",
"\n\n",
]
end = events[-1]
assert isinstance(end, AssistantDoneEvent)
thinking = end.message.content[0]
assert isinstance(thinking, ThinkingContent)
assert thinking.thinking == "**First step**\n\n**Second step**\n\n"


@pytest.mark.anyio
async def test_responses_api_omits_reasoning_when_effort_is_none() -> None:
requests: list[httpx.Request] = []
Expand Down