Skip to content
7 changes: 7 additions & 0 deletions src/strands/models/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ def stream(self, request: dict[str, Any]) -> Iterable[dict[str, Any]]:
if choice.delta.content:
yield {"chunk_type": "content_delta", "data_type": "text", "data": choice.delta.content}

if hasattr(choice.delta, "reasoning_content") and choice.delta.reasoning_content:
yield {
"chunk_type": "content_delta",
"data_type": "reasoning_content",
"data": choice.delta.reasoning_content,
}

for tool_call in choice.delta.tool_calls or []:
tool_calls.setdefault(tool_call.index, []).append(tool_call)

Expand Down
3 changes: 3 additions & 0 deletions src/strands/types/models/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ def format_chunk(self, event: dict[str, Any]) -> StreamEvent:
"contentBlockDelta": {"delta": {"toolUse": {"input": event["data"].function.arguments or ""}}}
}

if event["data_type"] == "reasoning_content":
return {"contentBlockDelta": {"delta": {"reasoningContent": {"text": event["data"]}}}}

return {"contentBlockDelta": {"delta": {"text": event["data"]}}}

case "content_stop":
Expand Down
34 changes: 24 additions & 10 deletions tests/strands/models/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,31 +73,45 @@ def test_stream(openai_client, model):
mock_tool_call_1_part_1 = unittest.mock.Mock(index=0)
mock_tool_call_2_part_1 = unittest.mock.Mock(index=1)
mock_delta_1 = unittest.mock.Mock(
content="I'll calculate", tool_calls=[mock_tool_call_1_part_1, mock_tool_call_2_part_1]
reasoning_content="",
content=None,
tool_calls=None,
)
mock_delta_2 = unittest.mock.Mock(
reasoning_content="\nI'm thinking",
content=None,
tool_calls=None,
)
mock_delta_3 = unittest.mock.Mock(
content="I'll calculate", tool_calls=[mock_tool_call_1_part_1, mock_tool_call_2_part_1], reasoning_content=None
)

mock_tool_call_1_part_2 = unittest.mock.Mock(index=0)
mock_tool_call_2_part_2 = unittest.mock.Mock(index=1)
mock_delta_2 = unittest.mock.Mock(
content="that for you", tool_calls=[mock_tool_call_1_part_2, mock_tool_call_2_part_2]
mock_delta_4 = unittest.mock.Mock(
content="that for you", tool_calls=[mock_tool_call_1_part_2, mock_tool_call_2_part_2], reasoning_content=None
)

mock_delta_3 = unittest.mock.Mock(content="", tool_calls=None)
mock_delta_5 = unittest.mock.Mock(content="", tool_calls=None, reasoning_content=None)

mock_event_1 = unittest.mock.Mock(choices=[unittest.mock.Mock(finish_reason=None, delta=mock_delta_1)])
mock_event_2 = unittest.mock.Mock(choices=[unittest.mock.Mock(finish_reason=None, delta=mock_delta_2)])
mock_event_3 = unittest.mock.Mock(choices=[unittest.mock.Mock(finish_reason="tool_calls", delta=mock_delta_3)])
mock_event_4 = unittest.mock.Mock()
mock_event_3 = unittest.mock.Mock(choices=[unittest.mock.Mock(finish_reason=None, delta=mock_delta_3)])
mock_event_4 = unittest.mock.Mock(choices=[unittest.mock.Mock(finish_reason=None, delta=mock_delta_4)])
mock_event_5 = unittest.mock.Mock(choices=[unittest.mock.Mock(finish_reason="tool_calls", delta=mock_delta_5)])
mock_event_6 = unittest.mock.Mock()

openai_client.chat.completions.create.return_value = iter([mock_event_1, mock_event_2, mock_event_3, mock_event_4])
openai_client.chat.completions.create.return_value = iter(
[mock_event_1, mock_event_2, mock_event_3, mock_event_4, mock_event_5, mock_event_6]
)

request = {"model": "m1", "messages": [{"role": "user", "content": [{"type": "text", "text": "calculate 2+2"}]}]}
response = model.stream(request)

tru_events = list(response)
exp_events = [
{"chunk_type": "message_start"},
{"chunk_type": "content_start", "data_type": "text"},
{"chunk_type": "content_delta", "data_type": "reasoning_content", "data": "\nI'm thinking"},
{"chunk_type": "content_delta", "data_type": "text", "data": "I'll calculate"},
{"chunk_type": "content_delta", "data_type": "text", "data": "that for you"},
{"chunk_type": "content_stop", "data_type": "text"},
Expand All @@ -110,15 +124,15 @@ def test_stream(openai_client, model):
{"chunk_type": "content_delta", "data_type": "tool", "data": mock_tool_call_2_part_2},
{"chunk_type": "content_stop", "data_type": "tool"},
{"chunk_type": "message_stop", "data": "tool_calls"},
{"chunk_type": "metadata", "data": mock_event_4.usage},
{"chunk_type": "metadata", "data": mock_event_6.usage},
]

assert tru_events == exp_events
openai_client.chat.completions.create.assert_called_once_with(**request)


def test_stream_empty(openai_client, model):
mock_delta = unittest.mock.Mock(content=None, tool_calls=None)
mock_delta = unittest.mock.Mock(content=None, tool_calls=None, reasoning_content=None)
mock_usage = unittest.mock.Mock(prompt_tokens=0, completion_tokens=0, total_tokens=0)

mock_event_1 = unittest.mock.Mock(choices=[unittest.mock.Mock(finish_reason=None, delta=mock_delta)])
Expand Down
5 changes: 5 additions & 0 deletions tests/strands/types/models/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@ def test_format_request(model, messages, tool_specs, system_prompt):
},
{"contentBlockDelta": {"delta": {"toolUse": {"input": ""}}}},
),
# Content Delta - Reasoning Text
(
{"chunk_type": "content_delta", "data_type": "reasoning_content", "data": "I'm thinking"},
{"contentBlockDelta": {"delta": {"reasoningContent": {"text": "I'm thinking"}}}},
),
# Content Delta - Text
(
{"chunk_type": "content_delta", "data_type": "text", "data": "hello"},
Expand Down
Loading