Skip to content

Commit b4d61c8

Browse files
Ensure all ResponseItems are yielded in AIContent (#7063)
* Ensure all ResponseItems are yielded in AIContent * Restructure switch for clarity --------- Co-authored-by: Stephen Toub <[email protected]>
1 parent 0f5909e commit b4d61c8

File tree

3 files changed

+301
-55
lines changed

3 files changed

+301
-55
lines changed

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs

Lines changed: 68 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -296,17 +296,17 @@ internal static async IAsyncEnumerable<ChatResponseUpdate> FromOpenAIStreamingRe
296296
ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
297297
new(lastRole, content is not null ? [content] : null)
298298
{
299+
ContinuationToken = CreateContinuationToken(
300+
responseId!,
301+
latestResponseStatus,
302+
options?.BackgroundModeEnabled,
303+
streamingUpdate.SequenceNumber),
299304
ConversationId = conversationId,
300305
CreatedAt = createdAt,
301306
MessageId = lastMessageId,
302307
ModelId = modelId,
303308
RawRepresentation = streamingUpdate,
304309
ResponseId = responseId,
305-
ContinuationToken = CreateContinuationToken(
306-
responseId!,
307-
latestResponseStatus,
308-
options?.BackgroundModeEnabled,
309-
streamingUpdate.SequenceNumber)
310310
};
311311

312312
switch (streamingUpdate)
@@ -395,48 +395,74 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
395395
yield return CreateUpdate(new TextReasoningContent(reasoningTextDeltaUpdate.Delta));
396396
break;
397397

398-
case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is FunctionCallResponseItem fcri:
399-
yield return CreateUpdate(OpenAIClientExtensions.ParseCallContent(fcri.FunctionArguments.ToString(), fcri.CallId, fcri.FunctionName));
400-
break;
401-
402-
case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is McpToolCallItem mtci:
403-
var mcpUpdate = CreateUpdate();
404-
AddMcpToolCallContent(mtci, mcpUpdate.Contents);
405-
yield return mcpUpdate;
406-
break;
407-
408-
case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is McpToolDefinitionListItem mtdli:
409-
yield return CreateUpdate(new AIContent { RawRepresentation = mtdli });
410-
break;
411-
412-
case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is McpToolCallApprovalRequestItem mtcari:
413-
yield return CreateUpdate(new McpServerToolApprovalRequestContent(mtcari.Id, new(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel)
414-
{
415-
Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!,
416-
RawRepresentation = mtcari,
417-
})
398+
case StreamingResponseImageGenerationCallInProgressUpdate imageGenInProgress:
399+
yield return CreateUpdate(new ImageGenerationToolCallContent
418400
{
419-
RawRepresentation = mtcari,
401+
ImageId = imageGenInProgress.ItemId,
402+
RawRepresentation = imageGenInProgress,
420403
});
421404
break;
422405

423-
case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is CodeInterpreterCallResponseItem cicri:
424-
var codeUpdate = CreateUpdate();
425-
AddCodeInterpreterContents(cicri, codeUpdate.Contents);
426-
yield return codeUpdate;
406+
case StreamingResponseImageGenerationCallPartialImageUpdate streamingImageGenUpdate:
407+
yield return CreateUpdate(GetImageGenerationResult(streamingImageGenUpdate, options));
427408
break;
428409

429-
case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when
430-
outputItemDoneUpdate.Item is MessageResponseItem mri &&
431-
mri.Content is { Count: > 0 } content &&
432-
content.Any(c => c.OutputTextAnnotations is { Count: > 0 }):
433-
AIContent annotatedContent = new();
434-
foreach (var c in content)
410+
case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate:
411+
switch (outputItemDoneUpdate.Item)
435412
{
436-
PopulateAnnotations(c, annotatedContent);
437-
}
413+
// Translate completed ResponseItems into their corresponding abstraction representations.
414+
case FunctionCallResponseItem fcri:
415+
yield return CreateUpdate(OpenAIClientExtensions.ParseCallContent(fcri.FunctionArguments.ToString(), fcri.CallId, fcri.FunctionName));
416+
break;
417+
418+
case McpToolCallItem mtci:
419+
var mcpUpdate = CreateUpdate();
420+
AddMcpToolCallContent(mtci, mcpUpdate.Contents);
421+
yield return mcpUpdate;
422+
break;
438423

439-
yield return CreateUpdate(annotatedContent);
424+
case McpToolCallApprovalRequestItem mtcari:
425+
yield return CreateUpdate(new McpServerToolApprovalRequestContent(mtcari.Id, new(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel)
426+
{
427+
Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!,
428+
RawRepresentation = mtcari,
429+
})
430+
{
431+
RawRepresentation = mtcari,
432+
});
433+
break;
434+
435+
case CodeInterpreterCallResponseItem cicri:
436+
var codeUpdate = CreateUpdate();
437+
AddCodeInterpreterContents(cicri, codeUpdate.Contents);
438+
yield return codeUpdate;
439+
break;
440+
441+
// MessageResponseItems will have already had their content yielded as part of delta updates.
442+
// However, those deltas didn't yield annotations. If there are any annotations, yield them now.
443+
case MessageResponseItem mri when mri.Content is { Count: > 0 } mriContent && mriContent.Any(c => c.OutputTextAnnotations is { Count: > 0 }):
444+
AIContent annotatedContent = new(); // do not include RawRepresentation to avoid duplication with already yielded deltas
445+
foreach (var c in mriContent)
446+
{
447+
PopulateAnnotations(c, annotatedContent);
448+
}
449+
yield return CreateUpdate(annotatedContent);
450+
break;
451+
452+
// For ResponseItems where we've already yielded partial deltas for the whole content,
453+
// we still want to yield an update, but we don't want it to include the ResponseItem
454+
// as the RawRepresentation, since if it did, when roundtripping we'd end up sending
455+
// the same content twice (first from the deltas, then from the raw response item).
456+
// Just yield an update without AIContent for the ResponseItem.
457+
case MessageResponseItem or ReasoningResponseItem or ImageGenerationCallResponseItem:
458+
yield return CreateUpdate();
459+
break;
460+
461+
// For everything else, yield an AIContent for the ResponseItem.
462+
default:
463+
yield return CreateUpdate(new AIContent { RawRepresentation = outputItemDoneUpdate.Item });
464+
break;
465+
}
440466
break;
441467

442468
case StreamingResponseErrorUpdate errorUpdate:
@@ -454,18 +480,6 @@ outputItemDoneUpdate.Item is MessageResponseItem mri &&
454480
});
455481
break;
456482

457-
case StreamingResponseImageGenerationCallInProgressUpdate imageGenInProgress:
458-
yield return CreateUpdate(new ImageGenerationToolCallContent
459-
{
460-
ImageId = imageGenInProgress.ItemId,
461-
RawRepresentation = imageGenInProgress,
462-
});
463-
goto default;
464-
465-
case StreamingResponseImageGenerationCallPartialImageUpdate streamingImageGenUpdate:
466-
yield return CreateUpdate(GetImageGenerationResult(streamingImageGenUpdate, options));
467-
break;
468-
469483
default:
470484
yield return CreateUpdate();
471485
break;
@@ -1299,8 +1313,8 @@ private static ImageGenerationToolResultContent GetImageGenerationResult(Streami
12991313
{
13001314
ImageId = update.ItemId,
13011315
RawRepresentation = update,
1302-
Outputs = new List<AIContent>
1303-
{
1316+
Outputs =
1317+
[
13041318
new DataContent(update.PartialImageBytes, $"image/{outputType}")
13051319
{
13061320
AdditionalProperties = new()
@@ -1310,7 +1324,7 @@ private static ImageGenerationToolResultContent GetImageGenerationResult(Streami
13101324
[nameof(update.PartialImageIndex)] = update.PartialImageIndex
13111325
}
13121326
}
1313-
}
1327+
]
13141328
};
13151329
}
13161330

test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<RootNamespace>Microsoft.Extensions.AI</RootNamespace>
44
<Description>Unit tests for Microsoft.Extensions.AI.OpenAI</Description>
55
<NoWarn>$(NoWarn);S104</NoWarn>
6-
<NoWarn>$(NoWarn);OPENAI001;MEAI001</NoWarn>
6+
<NoWarn>$(NoWarn);OPENAI001;OPENAICUA001;MEAI001</NoWarn>
77
</PropertyGroup>
88

99
<PropertyGroup>

0 commit comments

Comments
 (0)