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..359d47bb5 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,55 @@ 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 + 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: @@ -344,17 +395,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,