@@ -371,4 +371,110 @@ void testToolCallWithNullArguments() {
371371 assertEquals ("call_null" , toolUse .getId ());
372372 assertEquals ("null_args_tool" , toolUse .getName ());
373373 }
374+
375+ @ Test
376+ @ DisplayName ("Should generate streaming ID when first chunk has no ID" )
377+ void testStreamingToolCallGeneratesIdWhenMissing () {
378+ OpenAIResponse response = new OpenAIResponse ();
379+ response .setId ("chatcmpl-tool" );
380+ response .setObject ("chat.completion.chunk" );
381+
382+ OpenAIChoice choice = new OpenAIChoice ();
383+ choice .setIndex (0 );
384+
385+ OpenAIMessage delta = new OpenAIMessage ();
386+ OpenAIToolCall toolCall = new OpenAIToolCall ();
387+ toolCall .setId (null );
388+ toolCall .setIndex (0 );
389+ toolCall .setType ("function" );
390+
391+ OpenAIFunction function = new OpenAIFunction ();
392+ function .setName ("lookup_weather" );
393+ function .setArguments ("" );
394+ toolCall .setFunction (function );
395+
396+ delta .setToolCalls (List .of (toolCall ));
397+ choice .setDelta (delta );
398+ response .setChoices (List .of (choice ));
399+
400+ ChatResponse chatResponse = parser .parseResponse (response , Instant .now ());
401+
402+ assertNotNull (chatResponse );
403+ ToolUseBlock toolUse = (ToolUseBlock ) chatResponse .getContent ().get (0 );
404+ assertTrue (toolUse .getId ().startsWith ("streaming_" ));
405+ assertEquals ("lookup_weather" , toolUse .getName ());
406+ assertTrue (toolUse .getInput ().isEmpty ());
407+ }
408+
409+ @ Test
410+ @ DisplayName ("Should keep raw content when complete streaming JSON is malformed" )
411+ void testStreamingToolCallMalformedCompleteJsonKeepsRawContent () {
412+ OpenAIResponse response = new OpenAIResponse ();
413+ response .setId ("chatcmpl-tool" );
414+ response .setObject ("chat.completion.chunk" );
415+
416+ OpenAIChoice choice = new OpenAIChoice ();
417+ choice .setIndex (0 );
418+
419+ OpenAIMessage delta = new OpenAIMessage ();
420+ OpenAIToolCall toolCall = new OpenAIToolCall ();
421+ toolCall .setId ("call_bad_json" );
422+ toolCall .setIndex (0 );
423+ toolCall .setType ("function" );
424+
425+ OpenAIFunction function = new OpenAIFunction ();
426+ function .setName ("broken_tool" );
427+ function .setArguments ("{\" city\" :}" );
428+ toolCall .setFunction (function );
429+
430+ delta .setToolCalls (List .of (toolCall ));
431+ choice .setDelta (delta );
432+ response .setChoices (List .of (choice ));
433+
434+ ChatResponse chatResponse = parser .parseResponse (response , Instant .now ());
435+
436+ assertNotNull (chatResponse );
437+ ToolUseBlock toolUse = (ToolUseBlock ) chatResponse .getContent ().get (0 );
438+ assertEquals ("call_bad_json" , toolUse .getId ());
439+ assertEquals ("broken_tool" , toolUse .getName ());
440+ assertEquals ("{\" city\" :}" , toolUse .getContent ());
441+ assertTrue (toolUse .getInput ().isEmpty ());
442+ }
443+
444+ @ Test
445+ @ DisplayName ("Should emit placeholder fragment when chunk only carries thought signature" )
446+ void testStreamingToolCallThoughtSignatureOnlyFragment () {
447+ OpenAIResponse response = new OpenAIResponse ();
448+ response .setId ("chatcmpl-tool" );
449+ response .setObject ("chat.completion.chunk" );
450+
451+ OpenAIChoice choice = new OpenAIChoice ();
452+ choice .setIndex (0 );
453+
454+ OpenAIMessage delta = new OpenAIMessage ();
455+ OpenAIToolCall toolCall = new OpenAIToolCall ();
456+ toolCall .setId ("call_sig_only" );
457+ toolCall .setIndex (0 );
458+ toolCall .setType ("function" );
459+ toolCall .setThoughtSignature ("signature_only" );
460+
461+ OpenAIFunction function = new OpenAIFunction ();
462+ function .setName (null );
463+ function .setArguments ("" );
464+ toolCall .setFunction (function );
465+
466+ delta .setToolCalls (List .of (toolCall ));
467+ choice .setDelta (delta );
468+ response .setChoices (List .of (choice ));
469+
470+ ChatResponse chatResponse = parser .parseResponse (response , Instant .now ());
471+
472+ assertNotNull (chatResponse );
473+ ToolUseBlock toolUse = (ToolUseBlock ) chatResponse .getContent ().get (0 );
474+ assertEquals (OpenAIResponseParser .FRAGMENT_PLACEHOLDER , toolUse .getName ());
475+ assertEquals ("" , toolUse .getId ());
476+ assertEquals (
477+ "signature_only" ,
478+ toolUse .getMetadata ().get (ToolUseBlock .METADATA_THOUGHT_SIGNATURE ));
479+ }
374480}
0 commit comments