@@ -625,6 +625,103 @@ async def mock_stream():
625625 # Should have multiple partial responses plus final response
626626 assert len (responses ) > 1
627627
628+ async def test_generate_async_streaming_preserves_text_with_native_tool_call (self ):
629+ """Final streaming response keeps regular text alongside native tool calls."""
630+ model = OpenAIModel (model_name = "gpt-4" , api_key = "test_key" )
631+ content = Content (parts = [Part .from_text (text = "What's the weather?" )], role = "user" )
632+ request = LlmRequest (contents = [content ], config = None , tools_dict = {})
633+ text_chunk = Mock ()
634+ text_chunk .model_dump .return_value = {
635+ "choices" : [{
636+ "delta" : {
637+ "content" : "I'll check the weather first."
638+ },
639+ "finish_reason" : None ,
640+ "index" : 0 ,
641+ }],
642+ "usage" : None ,
643+ }
644+ tool_chunk = Mock ()
645+ tool_chunk .model_dump .return_value = {
646+ "choices" : [{
647+ "delta" : {
648+ "tool_calls" : [{
649+ "index" : 0 ,
650+ "id" : "call_weather" ,
651+ "type" : "function" ,
652+ "function" : {
653+ "name" : "get_weather" ,
654+ "arguments" : '{"city": "Beijing"}' ,
655+ },
656+ }]
657+ },
658+ "finish_reason" : "tool_calls" ,
659+ "index" : 0 ,
660+ }],
661+ "usage" : None ,
662+ }
663+ async def mock_stream ():
664+ yield text_chunk
665+ yield tool_chunk
666+ with patch .object (model , '_create_async_client' ) as mock_client_factory :
667+ mock_client = AsyncMock ()
668+ mock_client .chat .completions .create = AsyncMock (return_value = mock_stream ())
669+ mock_client .close = AsyncMock ()
670+ mock_client_factory .return_value = mock_client
671+ responses = []
672+ async for response in model .generate_async (request , stream = True ):
673+ responses .append (response )
674+ final_response = responses [- 1 ]
675+ assert final_response .partial is False
676+ assert final_response .content is not None
677+ text_parts = [part .text for part in final_response .content .parts if part .text ]
678+ function_parts = [part .function_call for part in final_response .content .parts if part .function_call ]
679+ assert text_parts == ["I'll check the weather first." ]
680+ assert len (function_parts ) == 1
681+ assert function_parts [0 ].name == "get_weather"
682+ assert function_parts [0 ].args == {"city" : "Beijing" }
683+
684+ @pytest .mark .asyncio
685+ async def test_generate_async_streaming_suppresses_tool_prompt_markup_but_keeps_visible_text (self ):
686+ """Provider tool-prompt markup is hidden from final text while visible text is preserved."""
687+ model = OpenAIModel (model_name = "hy3-preview" , api_key = "test_key" , add_tools_to_prompt = True )
688+ content = Content (parts = [Part .from_text (text = "What's the weather?" )], role = "user" )
689+ request = LlmRequest (contents = [content ], config = None , tools_dict = {})
690+ tool_prompt_chunk = Mock ()
691+ tool_prompt_chunk .model_dump .return_value = {
692+ "choices" : [{
693+ "delta" : {
694+ "content" : ("I'll check the weather first. "
695+ "<tool_call>get_weather<tool_sep>"
696+ "<arg_key>city</arg_key><arg_value>Beijing</arg_value>"
697+ "</tool_call>" )
698+ },
699+ "finish_reason" : "stop" ,
700+ "index" : 0 ,
701+ }],
702+ "usage" : None ,
703+ }
704+ async def mock_stream ():
705+ yield tool_prompt_chunk
706+ with patch .object (model , '_create_async_client' ) as mock_client_factory :
707+ mock_client = AsyncMock ()
708+ mock_client .chat .completions .create = AsyncMock (return_value = mock_stream ())
709+ mock_client .close = AsyncMock ()
710+ mock_client_factory .return_value = mock_client
711+ responses = []
712+ async for response in model .generate_async (request , stream = True ):
713+ responses .append (response )
714+ final_response = responses [- 1 ]
715+ assert final_response .partial is False
716+ assert final_response .content is not None
717+ text = "" .join (part .text for part in final_response .content .parts if part .text )
718+ function_parts = [part .function_call for part in final_response .content .parts if part .function_call ]
719+ assert text == "I'll check the weather first. "
720+ assert "<tool_call>" not in text
721+ assert len (function_parts ) == 1
722+ assert function_parts [0 ].name == "get_weather"
723+ assert function_parts [0 ].args == {"city" : "Beijing" }
724+
628725 @pytest .mark .asyncio
629726 async def test_generate_async_error_handling (self ):
630727 """Test generate_async handles API errors gracefully."""
0 commit comments