From bae1a8f6ada5587eecccc083d428e1afe2560ff6 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 26 Aug 2025 20:27:17 +0900 Subject: [PATCH 1/2] Add raesoning text delta event support for gpt-oss models --- examples/reasoning_content/gpt_oss_stream.py | 54 +++++++++++ src/agents/models/chatcmpl_stream_handler.py | 94 ++++++++++++++++---- 2 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 examples/reasoning_content/gpt_oss_stream.py diff --git a/examples/reasoning_content/gpt_oss_stream.py b/examples/reasoning_content/gpt_oss_stream.py new file mode 100644 index 000000000..963f5ebe4 --- /dev/null +++ b/examples/reasoning_content/gpt_oss_stream.py @@ -0,0 +1,54 @@ +import asyncio +import os + +from openai import AsyncOpenAI +from openai.types.shared import Reasoning + +from agents import ( + Agent, + ModelSettings, + OpenAIChatCompletionsModel, + Runner, + set_tracing_disabled, +) + +set_tracing_disabled(True) + +# import logging +# logging.basicConfig(level=logging.DEBUG) + +gpt_oss_model = OpenAIChatCompletionsModel( + model="openai/gpt-oss-20b", + openai_client=AsyncOpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=os.getenv("OPENROUTER_API_KEY"), + ), +) + + +async def main(): + agent = Agent( + name="Assistant", + instructions="You're a helpful assistant. You provide a concise answer to the user's question.", + model=gpt_oss_model, + model_settings=ModelSettings( + reasoning=Reasoning(effort="high", summary="detailed"), + ), + ) + + result = Runner.run_streamed(agent, "Tell me about recursion in programming.") + print("=== Run starting ===") + print("\n") + async for event in result.stream_events(): + if event.type == "raw_response_event": + if event.data.type == "response.reasoning_text.delta": + print(f"\033[33m{event.data.delta}\033[0m", end="", flush=True) + elif event.data.type == "response.output_text.delta": + print(f"\033[32m{event.data.delta}\033[0m", end="", flush=True) + + print("\n") + print("=== Run complete ===") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/agents/models/chatcmpl_stream_handler.py b/src/agents/models/chatcmpl_stream_handler.py index 3c3ec06bb..9f1f77e10 100644 --- a/src/agents/models/chatcmpl_stream_handler.py +++ b/src/agents/models/chatcmpl_stream_handler.py @@ -28,11 +28,17 @@ ResponseTextDeltaEvent, ResponseUsage, ) -from openai.types.responses.response_reasoning_item import Summary +from openai.types.responses.response_reasoning_item import Content, Summary from openai.types.responses.response_reasoning_summary_part_added_event import ( Part as AddedEventPart, ) from openai.types.responses.response_reasoning_summary_part_done_event import Part as DoneEventPart +from openai.types.responses.response_reasoning_text_delta_event import ( + ResponseReasoningTextDeltaEvent, +) +from openai.types.responses.response_reasoning_text_done_event import ( + ResponseReasoningTextDoneEvent, +) from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails from ..items import TResponseStreamEvent @@ -95,7 +101,7 @@ async def handle_stream( delta = chunk.choices[0].delta - # Handle reasoning content + # Handle reasoning content for reasoning summaries if hasattr(delta, "reasoning_content"): reasoning_content = delta.reasoning_content if reasoning_content and not state.reasoning_content_index_and_output: @@ -138,10 +144,51 @@ async def handle_stream( ) # Create a new summary with updated text - current_summary = state.reasoning_content_index_and_output[1].summary[0] - updated_text = current_summary.text + reasoning_content - new_summary = Summary(text=updated_text, type="summary_text") - state.reasoning_content_index_and_output[1].summary[0] = new_summary + current_content = state.reasoning_content_index_and_output[1].summary[0] + updated_text = current_content.text + reasoning_content + new_content = Summary(text=updated_text, type="summary_text") + state.reasoning_content_index_and_output[1].summary[0] = new_content + + # Handle reasoning content from 3rd party platforms + if hasattr(delta, "reasoning"): + reasoning_text = delta.reasoning + if reasoning_text and not state.reasoning_content_index_and_output: + state.reasoning_content_index_and_output = ( + 0, + ResponseReasoningItem( + id=FAKE_RESPONSES_ID, + summary=[], + content=[Content(text="", type="reasoning_text")], + type="reasoning", + ), + ) + yield ResponseOutputItemAddedEvent( + item=ResponseReasoningItem( + id=FAKE_RESPONSES_ID, + summary=[], + content=[Content(text="", type="reasoning_text")], + type="reasoning", + ), + output_index=0, + type="response.output_item.added", + sequence_number=sequence_number.get_and_increment(), + ) + + if reasoning_text and state.reasoning_content_index_and_output: + yield ResponseReasoningTextDeltaEvent( + delta=reasoning_text, + item_id=FAKE_RESPONSES_ID, + output_index=0, + content_index=0, + type="response.reasoning_text.delta", + sequence_number=sequence_number.get_and_increment(), + ) + + # Create a new summary with updated text + current_content = state.reasoning_content_index_and_output[1].content[0] + updated_text = current_content.text + reasoning_text + new_content = Content(text=updated_text, type="reasoning_text") + state.reasoning_content_index_and_output[1].content[0] = new_content # Handle regular content if delta.content is not None: @@ -344,17 +391,30 @@ async def handle_stream( ) if state.reasoning_content_index_and_output: - yield ResponseReasoningSummaryPartDoneEvent( - item_id=FAKE_RESPONSES_ID, - output_index=0, - summary_index=0, - part=DoneEventPart( - text=state.reasoning_content_index_and_output[1].summary[0].text, - type="summary_text", - ), - type="response.reasoning_summary_part.done", - sequence_number=sequence_number.get_and_increment(), - ) + if ( + state.reasoning_content_index_and_output[1].summary + and len(state.reasoning_content_index_and_output[1].summary) > 0 + ): + yield ResponseReasoningSummaryPartDoneEvent( + item_id=FAKE_RESPONSES_ID, + output_index=0, + summary_index=0, + part=DoneEventPart( + text=state.reasoning_content_index_and_output[1].summary[0].text, + type="summary_text", + ), + type="response.reasoning_summary_part.done", + sequence_number=sequence_number.get_and_increment(), + ) + elif state.reasoning_content_index_and_output[1].content is not None: + yield ResponseReasoningTextDoneEvent( + item_id=FAKE_RESPONSES_ID, + output_index=0, + content_index=0, + text=state.reasoning_content_index_and_output[1].content[0].text, + type="response.reasoning_text.done", + sequence_number=sequence_number.get_and_increment(), + ) yield ResponseOutputItemDoneEvent( item=state.reasoning_content_index_and_output[1], output_index=0, From aeac7c21b3ab78d9dda50db2b6c468ec0fe476b1 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 26 Aug 2025 20:37:29 +0900 Subject: [PATCH 2/2] fix mypy errors --- src/agents/models/chatcmpl_stream_handler.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/agents/models/chatcmpl_stream_handler.py b/src/agents/models/chatcmpl_stream_handler.py index 9f1f77e10..359d47bb5 100644 --- a/src/agents/models/chatcmpl_stream_handler.py +++ b/src/agents/models/chatcmpl_stream_handler.py @@ -185,10 +185,14 @@ async def handle_stream( ) # Create a new summary with updated text - current_content = state.reasoning_content_index_and_output[1].content[0] - updated_text = current_content.text + reasoning_text - new_content = Content(text=updated_text, type="reasoning_text") - state.reasoning_content_index_and_output[1].content[0] = new_content + if state.reasoning_content_index_and_output[1].content is None: + state.reasoning_content_index_and_output[1].content = [ + Content(text="", type="reasoning_text") + ] + current_text = state.reasoning_content_index_and_output[1].content[0] + updated_text = current_text.text + reasoning_text + new_text_content = Content(text=updated_text, type="reasoning_text") + state.reasoning_content_index_and_output[1].content[0] = new_text_content # Handle regular content if delta.content is not None: