diff --git a/src/strands/models/openai.py b/src/strands/models/openai.py index 7ec16efe..6f761c7c 100644 --- a/src/strands/models/openai.py +++ b/src/strands/models/openai.py @@ -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) diff --git a/src/strands/types/models/openai.py b/src/strands/types/models/openai.py index e5a8ce6b..bfd5e5d1 100644 --- a/src/strands/types/models/openai.py +++ b/src/strands/types/models/openai.py @@ -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": diff --git a/tests/strands/models/test_openai.py b/tests/strands/models/test_openai.py index ae033286..63226bd2 100644 --- a/tests/strands/models/test_openai.py +++ b/tests/strands/models/test_openai.py @@ -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"}, @@ -110,7 +124,7 @@ 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 @@ -118,7 +132,7 @@ def test_stream(openai_client, model): 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)]) @@ -145,7 +159,7 @@ def test_stream_empty(openai_client, model): def test_stream_with_empty_choices(openai_client, model): - mock_delta = unittest.mock.Mock(content="content", tool_calls=None) + mock_delta = unittest.mock.Mock(content="content", tool_calls=None, reasoning_content=None) mock_usage = unittest.mock.Mock(prompt_tokens=10, completion_tokens=20, total_tokens=30) # Event with no choices attribute diff --git a/tests/strands/types/models/test_openai.py b/tests/strands/types/models/test_openai.py index 3a1a940b..a17294fa 100644 --- a/tests/strands/types/models/test_openai.py +++ b/tests/strands/types/models/test_openai.py @@ -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"},