@@ -296,6 +296,111 @@ data: {"type":"message_stop"}
296296 require .NotNil (t , bm )
297297 bodyStr := string (bm .GetBody ())
298298
299+ // Parse all streaming events to verify the event flow
300+ var chunks []openai.ChatCompletionResponseChunk
301+ var textChunks []string
302+ var toolCallStarted bool
303+ var hasRole bool
304+ var toolCallCompleted bool
305+ var finalFinishReason openai.ChatCompletionChoicesFinishReason
306+ var finalUsageChunk * openai.ChatCompletionResponseChunk
307+ var toolCallChunks []string // Track partial JSON chunks
308+
309+ lines := strings .SplitSeq (strings .TrimSpace (bodyStr ), "\n \n " )
310+ for line := range lines {
311+ if ! strings .HasPrefix (line , "data: " ) || strings .Contains (line , "[DONE]" ) {
312+ continue
313+ }
314+ jsonBody := strings .TrimPrefix (line , "data: " )
315+
316+ var chunk openai.ChatCompletionResponseChunk
317+ err = json .Unmarshal ([]byte (jsonBody ), & chunk )
318+ require .NoError (t , err , "Failed to unmarshal chunk: %s" , jsonBody )
319+ chunks = append (chunks , chunk )
320+
321+ // Check if this is the final usage chunk
322+ if strings .Contains (jsonBody , `"usage"` ) {
323+ finalUsageChunk = & chunk
324+ }
325+
326+ if len (chunk .Choices ) > 0 {
327+ choice := chunk .Choices [0 ]
328+ // Check for role in first content chunk
329+ if choice .Delta != nil && choice .Delta .Content != nil && * choice .Delta .Content != "" && ! hasRole {
330+ require .NotNil (t , choice .Delta .Role , "Role should be present on first content chunk" )
331+ require .Equal (t , openai .ChatMessageRoleAssistant , choice .Delta .Role )
332+ hasRole = true
333+ }
334+
335+ // Collect text content
336+ if choice .Delta != nil && choice .Delta .Content != nil {
337+ textChunks = append (textChunks , * choice .Delta .Content )
338+ }
339+
340+ // Check tool calls - start and accumulate partial JSON
341+ if choice .Delta != nil && len (choice .Delta .ToolCalls ) > 0 {
342+ toolCall := choice .Delta .ToolCalls [0 ]
343+
344+ // Check tool call initiation
345+ if toolCall .Function .Name == "get_weather" && ! toolCallStarted {
346+ require .Equal (t , "get_weather" , toolCall .Function .Name )
347+ require .NotNil (t , toolCall .ID )
348+ require .Equal (t , "toolu_01T1x1fJ34qAmk2tNTrN7Up6" , * toolCall .ID )
349+ require .Equal (t , int64 (0 ), toolCall .Index , "Tool call should be at index 1 (after text content at index 0)" )
350+ toolCallStarted = true
351+ }
352+
353+ // Accumulate partial JSON arguments - these should also be at index 1
354+ if toolCall .Function .Arguments != "" {
355+ toolCallChunks = append (toolCallChunks , toolCall .Function .Arguments )
356+
357+ // Verify the index remains consistent at 1 for all tool call chunks
358+ require .Equal (t , int64 (0 ), toolCall .Index , "Tool call argument chunks should be at index 1" )
359+ }
360+ }
361+
362+ // Track finish reason
363+ if choice .FinishReason != "" {
364+ finalFinishReason = choice .FinishReason
365+ if finalFinishReason == "tool_calls" {
366+ toolCallCompleted = true
367+ }
368+ }
369+ }
370+ }
371+
372+ // Check the final usage chunk for accumulated tool call arguments
373+ if finalUsageChunk != nil {
374+ require .Equal (t , 472 , finalUsageChunk .Usage .PromptTokens )
375+ require .Equal (t , 89 , finalUsageChunk .Usage .CompletionTokens )
376+ }
377+
378+ // Verify partial JSON accumulation in streaming chunks
379+ if len (toolCallChunks ) > 0 {
380+ // Verify we got multiple partial JSON chunks during streaming
381+ require .True (t , len (toolCallChunks ) >= 2 , "Should receive multiple partial JSON chunks for tool arguments" )
382+
383+ // Verify some expected partial content appears in the chunks
384+ fullPartialJSON := strings .Join (toolCallChunks , "" )
385+ require .Contains (t , fullPartialJSON , `"location":` , "Partial JSON should contain location field" )
386+ require .Contains (t , fullPartialJSON , `"unit":` , "Partial JSON should contain unit field" )
387+ require .Contains (t , fullPartialJSON , "San Francisco" , "Partial JSON should contain location value" )
388+ require .Contains (t , fullPartialJSON , "fahrenheit" , "Partial JSON should contain unit value" )
389+ }
390+
391+ // Verify streaming event assertions
392+ require .True (t , len (chunks ) >= 5 , "Should have multiple streaming chunks" )
393+ require .True (t , hasRole , "Should have role in first content chunk" )
394+ require .True (t , toolCallStarted , "Tool call should have been initiated" )
395+ require .True (t , toolCallCompleted , "Tool call should have complete arguments in final chunk" )
396+ require .Equal (t , openai .ChatCompletionChoicesFinishReasonToolCalls , finalFinishReason , "Final finish reason should be tool_calls" )
397+
398+ // Verify text content was streamed correctly
399+ fullText := strings .Join (textChunks , "" )
400+ require .Contains (t , fullText , "Okay, let's check the weather for San Francisco, CA:" )
401+ require .True (t , len (textChunks ) >= 3 , "Text should be streamed in multiple chunks" )
402+
403+ // Original aggregate response assertions
299404 require .Contains (t , bodyStr , `"content":"Okay"` )
300405 require .Contains (t , bodyStr , `"name":"get_weather"` )
301406 require .Contains (t , bodyStr , "\" arguments\" :\" {\\ \" location\\ \" :" )
0 commit comments