Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d31fa94

Browse filesBrowse files
committed
Expose streaming conversion utility methods
We're already exposing the non-streaming response conversions. Expose the streaming ones as well.
1 parent c9584a4 commit d31fa94
Copy full SHA for d31fa94

5 files changed

+535-43Lines changed: 535 additions & 43 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs‎

Copy file name to clipboardExpand all lines: src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs
+90-22Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
using System.Collections.Generic;
77
using System.IO;
88
using System.Linq;
9+
using System.Runtime.CompilerServices;
910
using System.Text.Encodings.Web;
1011
using System.Text.Json;
12+
using System.Threading;
13+
using System.Threading.Tasks;
1114
using Microsoft.Extensions.AI;
1215
using Microsoft.Shared.Diagnostics;
1316

14-
#pragma warning disable S103 // Lines should not be too long
15-
1617
namespace OpenAI.Chat;
1718

1819
/// <summary>Provides extension methods for working with content associated with OpenAI.Chat.</summary>
@@ -27,10 +28,10 @@ public static ChatTool AsOpenAIChatTool(this AIFunction function) =>
2728

2829
/// <summary>Creates a sequence of OpenAI <see cref="ChatMessage"/> instances from the specified input messages.</summary>
2930
/// <param name="messages">The input messages to convert.</param>
31+
/// <param name="options">The options employed while processing <paramref name="messages"/>.</param>
3032
/// <returns>A sequence of OpenAI chat messages.</returns>
31-
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
32-
public static IEnumerable<ChatMessage> AsOpenAIChatMessages(this IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages) =>
33-
OpenAIChatClient.ToOpenAIChatMessages(Throw.IfNull(messages), chatOptions: null);
33+
public static IEnumerable<ChatMessage> AsOpenAIChatMessages(this IEnumerable<Microsoft.Extensions.AI.ChatMessage> messages, ChatOptions? options = null) =>
34+
OpenAIChatClient.ToOpenAIChatMessages(Throw.IfNull(messages), options);
3435

3536
/// <summary>Creates an OpenAI <see cref="ChatCompletion"/> from a <see cref="ChatResponse"/>.</summary>
3637
/// <param name="response">The <see cref="ChatResponse"/> to convert to a <see cref="ChatCompletion"/>.</param>
@@ -47,24 +48,9 @@ public static ChatCompletion AsOpenAIChatCompletion(this ChatResponse response)
4748

4849
var lastMessage = response.Messages.LastOrDefault();
4950

50-
ChatMessageRole role = lastMessage?.Role.Value switch
51-
{
52-
"user" => ChatMessageRole.User,
53-
"function" => ChatMessageRole.Function,
54-
"tool" => ChatMessageRole.Tool,
55-
"developer" => ChatMessageRole.Developer,
56-
"system" => ChatMessageRole.System,
57-
_ => ChatMessageRole.Assistant,
58-
};
51+
ChatMessageRole role = ToChatMessageRole(lastMessage?.Role);
5952

60-
ChatFinishReason finishReason = response.FinishReason?.Value switch
61-
{
62-
"length" => ChatFinishReason.Length,
63-
"content_filter" => ChatFinishReason.ContentFilter,
64-
"tool_calls" => ChatFinishReason.ToolCalls,
65-
"function_call" => ChatFinishReason.FunctionCall,
66-
_ => ChatFinishReason.Stop,
67-
};
53+
ChatFinishReason finishReason = ToChatFinishReason(response.FinishReason);
6854

6955
ChatTokenUsage usage = OpenAIChatModelFactory.ChatTokenUsage(
7056
(int?)response.Usage?.OutputTokenCount ?? 0,
@@ -124,6 +110,52 @@ static IEnumerable<ChatMessageAnnotation> ConvertAnnotations(IEnumerable<AIConte
124110
}
125111
}
126112

113+
/// <summary>
114+
/// Creates a sequence of OpenAI <see cref="StreamingChatCompletionUpdate"/> instances from the specified
115+
/// sequence of <see cref="ChatResponseUpdate"/> instances.
116+
/// </summary>
117+
/// <param name="responseUpdates">The update instances.</param>
118+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
119+
/// <returns>A sequence of converted <see cref="ChatResponseUpdate"/> instances.</returns>
120+
/// <exception cref="ArgumentNullException"><paramref name="responseUpdates"/> is <see langword="null"/>.</exception>
121+
public static async IAsyncEnumerable<StreamingChatCompletionUpdate> AsOpenAIStreamingChatCompletionUpdatesAsync(
122+
this IAsyncEnumerable<ChatResponseUpdate> responseUpdates, [EnumeratorCancellation] CancellationToken cancellationToken = default)
123+
{
124+
_ = Throw.IfNull(responseUpdates);
125+
126+
await foreach (var update in responseUpdates.WithCancellation(cancellationToken).ConfigureAwait(false))
127+
{
128+
if (update.RawRepresentation is StreamingChatCompletionUpdate streamingUpdate)
129+
{
130+
yield return streamingUpdate;
131+
continue;
132+
}
133+
134+
var usage = update.Contents.FirstOrDefault(c => c is UsageContent) is UsageContent usageContent ?
135+
OpenAIChatModelFactory.ChatTokenUsage(
136+
(int?)usageContent.Details.OutputTokenCount ?? 0,
137+
(int?)usageContent.Details.InputTokenCount ?? 0,
138+
(int?)usageContent.Details.TotalTokenCount ?? 0) :
139+
null;
140+
141+
var toolCallUpdates = update.Contents.OfType<FunctionCallContent>().Select((fcc, index) =>
142+
OpenAIChatModelFactory.StreamingChatToolCallUpdate(
143+
index, fcc.CallId, ChatToolCallKind.Function, fcc.Name,
144+
new(JsonSerializer.SerializeToUtf8Bytes(fcc.Arguments, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary<string, object?>))))))
145+
.ToList();
146+
147+
yield return OpenAIChatModelFactory.StreamingChatCompletionUpdate(
148+
update.ResponseId,
149+
new(OpenAIChatClient.ToOpenAIChatContent(update.Contents)),
150+
toolCallUpdates: toolCallUpdates,
151+
role: ToChatMessageRole(update.Role),
152+
finishReason: ToChatFinishReason(update.FinishReason),
153+
createdAt: update.CreatedAt ?? default,
154+
model: update.ModelId,
155+
usage: usage);
156+
}
157+
}
158+
127159
/// <summary>Creates a sequence of <see cref="Microsoft.Extensions.AI.ChatMessage"/> instances from the specified input messages.</summary>
128160
/// <param name="messages">The input messages to convert.</param>
129161
/// <returns>A sequence of Microsoft.Extensions.AI chat messages.</returns>
@@ -205,4 +237,40 @@ static object ToToolResult(ChatMessageContent content)
205237
/// <exception cref="ArgumentNullException"><paramref name="chatCompletion"/> is <see langword="null"/>.</exception>
206238
public static ChatResponse AsChatResponse(this ChatCompletion chatCompletion, ChatCompletionOptions? options = null) =>
207239
OpenAIChatClient.FromOpenAIChatCompletion(Throw.IfNull(chatCompletion), options);
240+
241+
/// <summary>
242+
/// Creates a sequence of Microsoft.Extensions.AI <see cref="ChatResponseUpdate"/> instances from the specified
243+
/// sequence of OpenAI <see cref="StreamingChatCompletionUpdate"/> instances.
244+
/// </summary>
245+
/// <param name="chatCompletionUpdates">The update instances.</param>
246+
/// <param name="options">The options employed in the creation of the response.</param>
247+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
248+
/// <returns>A sequence of converted <see cref="ChatResponseUpdate"/> instances.</returns>
249+
/// <exception cref="ArgumentNullException"><paramref name="chatCompletionUpdates"/> is <see langword="null"/>.</exception>
250+
public static IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesAsync(
251+
this IAsyncEnumerable<StreamingChatCompletionUpdate> chatCompletionUpdates, ChatCompletionOptions? options = null, CancellationToken cancellationToken = default) =>
252+
OpenAIChatClient.FromOpenAIStreamingChatCompletionAsync(Throw.IfNull(chatCompletionUpdates), options, cancellationToken);
253+
254+
/// <summary>Converts the <see cref="ChatRole"/> to a <see cref="ChatMessageRole"/>.</summary>
255+
private static ChatMessageRole ToChatMessageRole(ChatRole? role) =>
256+
role?.Value switch
257+
{
258+
"user" => ChatMessageRole.User,
259+
"function" => ChatMessageRole.Function,
260+
"tool" => ChatMessageRole.Tool,
261+
"developer" => ChatMessageRole.Developer,
262+
"system" => ChatMessageRole.System,
263+
_ => ChatMessageRole.Assistant,
264+
};
265+
266+
/// <summary>Converts the <see cref="Microsoft.Extensions.AI.ChatFinishReason"/> to a <see cref="ChatFinishReason"/>.</summary>
267+
private static ChatFinishReason ToChatFinishReason(Microsoft.Extensions.AI.ChatFinishReason? finishReason) =>
268+
finishReason?.Value switch
269+
{
270+
"length" => ChatFinishReason.Length,
271+
"content_filter" => ChatFinishReason.ContentFilter,
272+
"tool_calls" => ChatFinishReason.ToolCalls,
273+
"function_call" => ChatFinishReason.FunctionCall,
274+
_ => ChatFinishReason.Stop,
275+
};
208276
}
Collapse file

‎src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs‎

Copy file name to clipboardExpand all lines: src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs
+17-2Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Threading;
67
using Microsoft.Extensions.AI;
78
using Microsoft.Shared.Diagnostics;
89

@@ -20,10 +21,11 @@ public static ResponseTool AsOpenAIResponseTool(this AIFunction function) =>
2021

2122
/// <summary>Creates a sequence of OpenAI <see cref="ResponseItem"/> instances from the specified input messages.</summary>
2223
/// <param name="messages">The input messages to convert.</param>
24+
/// <param name="options">The options employed while processing <paramref name="messages"/>.</param>
2325
/// <returns>A sequence of OpenAI response items.</returns>
2426
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
25-
public static IEnumerable<ResponseItem> AsOpenAIResponseItems(this IEnumerable<ChatMessage> messages) =>
26-
OpenAIResponsesChatClient.ToOpenAIResponseItems(Throw.IfNull(messages));
27+
public static IEnumerable<ResponseItem> AsOpenAIResponseItems(this IEnumerable<ChatMessage> messages, ChatOptions? options = null) =>
28+
OpenAIResponsesChatClient.ToOpenAIResponseItems(Throw.IfNull(messages), options);
2729

2830
/// <summary>Creates a sequence of <see cref="ChatMessage"/> instances from the specified input items.</summary>
2931
/// <param name="items">The input messages to convert.</param>
@@ -40,6 +42,19 @@ public static IEnumerable<ChatMessage> AsChatMessages(this IEnumerable<ResponseI
4042
public static ChatResponse AsChatResponse(this OpenAIResponse response, ResponseCreationOptions? options = null) =>
4143
OpenAIResponsesChatClient.FromOpenAIResponse(Throw.IfNull(response), options);
4244

45+
/// <summary>
46+
/// Creates a sequence of Microsoft.Extensions.AI <see cref="ChatResponseUpdate"/> instances from the specified
47+
/// sequence of OpenAI <see cref="StreamingResponseUpdate"/> instances.
48+
/// </summary>
49+
/// <param name="responseUpdates">The update instances.</param>
50+
/// <param name="options">The options employed in the creation of the response.</param>
51+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
52+
/// <returns>A sequence of converted <see cref="ChatResponseUpdate"/> instances.</returns>
53+
/// <exception cref="ArgumentNullException"><paramref name="responseUpdates"/> is <see langword="null"/>.</exception>
54+
public static IAsyncEnumerable<ChatResponseUpdate> AsChatResponseUpdatesAsync(
55+
this IAsyncEnumerable<StreamingResponseUpdate> responseUpdates, ResponseCreationOptions? options = null, CancellationToken cancellationToken = default) =>
56+
OpenAIResponsesChatClient.FromOpenAIStreamingResponseUpdatesAsync(Throw.IfNull(responseUpdates), options, cancellationToken);
57+
4358
/// <summary>Creates an OpenAI <see cref="OpenAIResponse"/> from a <see cref="ChatResponse"/>.</summary>
4459
/// <param name="response">The response to convert.</param>
4560
/// <returns>The created <see cref="OpenAIResponse"/>.</returns>
Collapse file

‎src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs‎

Copy file name to clipboardExpand all lines: src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ internal static List<ChatMessageContentPart> ToOpenAIChatContent(IEnumerable<AIC
303303
return null;
304304
}
305305

306-
private static async IAsyncEnumerable<ChatResponseUpdate> FromOpenAIStreamingChatCompletionAsync(
306+
internal static async IAsyncEnumerable<ChatResponseUpdate> FromOpenAIStreamingChatCompletionAsync(
307307
IAsyncEnumerable<StreamingChatCompletionUpdate> updates,
308308
ChatCompletionOptions? options,
309309
[EnumeratorCancellation] CancellationToken cancellationToken)
Collapse file

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

Copy file name to clipboardExpand all lines: src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+18-9Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public async Task<ChatResponse> GetResponseAsync(
7272
_ = Throw.IfNull(messages);
7373

7474
// Convert the inputs into what OpenAIResponseClient expects.
75-
var openAIResponseItems = ToOpenAIResponseItems(messages);
75+
var openAIResponseItems = ToOpenAIResponseItems(messages, options);
7676
var openAIOptions = ToOpenAIResponseCreationOptions(options);
7777

7878
// Make the call to the OpenAIResponseClient.
@@ -174,16 +174,22 @@ internal static IEnumerable<ChatMessage> ToChatMessages(IEnumerable<ResponseItem
174174
}
175175

176176
/// <inheritdoc />
177-
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
178-
IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
177+
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
178+
IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
179179
{
180180
_ = Throw.IfNull(messages);
181181

182-
// Convert the inputs into what OpenAIResponseClient expects.
183-
var openAIResponseItems = ToOpenAIResponseItems(messages);
182+
var openAIResponseItems = ToOpenAIResponseItems(messages, options);
184183
var openAIOptions = ToOpenAIResponseCreationOptions(options);
185184

186-
// Make the call to the OpenAIResponseClient and process the streaming results.
185+
var streamingUpdates = _responseClient.CreateResponseStreamingAsync(openAIResponseItems, openAIOptions, cancellationToken);
186+
187+
return FromOpenAIStreamingResponseUpdatesAsync(streamingUpdates, openAIOptions, cancellationToken);
188+
}
189+
190+
internal static async IAsyncEnumerable<ChatResponseUpdate> FromOpenAIStreamingResponseUpdatesAsync(
191+
IAsyncEnumerable<StreamingResponseUpdate> streamingResponseUpdates, ResponseCreationOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken = default)
192+
{
187193
DateTimeOffset? createdAt = null;
188194
string? responseId = null;
189195
string? conversationId = null;
@@ -192,14 +198,15 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
192198
ChatRole? lastRole = null;
193199
Dictionary<int, MessageResponseItem> outputIndexToMessages = [];
194200
Dictionary<int, FunctionCallInfo>? functionCallInfos = null;
195-
await foreach (var streamingUpdate in _responseClient.CreateResponseStreamingAsync(openAIResponseItems, openAIOptions, cancellationToken).ConfigureAwait(false))
201+
202+
await foreach (var streamingUpdate in streamingResponseUpdates.WithCancellation(cancellationToken).ConfigureAwait(false))
196203
{
197204
switch (streamingUpdate)
198205
{
199206
case StreamingResponseCreatedUpdate createdUpdate:
200207
createdAt = createdUpdate.Response.CreatedAt;
201208
responseId = createdUpdate.Response.Id;
202-
conversationId = openAIOptions.StoredOutputEnabled is false ? null : responseId;
209+
conversationId = options?.StoredOutputEnabled is false ? null : responseId;
203210
modelId = createdUpdate.Response.Model;
204211
goto default;
205212

@@ -485,8 +492,10 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
485492
}
486493

487494
/// <summary>Convert a sequence of <see cref="ChatMessage"/>s to <see cref="ResponseItem"/>s.</summary>
488-
internal static IEnumerable<ResponseItem> ToOpenAIResponseItems(IEnumerable<ChatMessage> inputs)
495+
internal static IEnumerable<ResponseItem> ToOpenAIResponseItems(IEnumerable<ChatMessage> inputs, ChatOptions? options)
489496
{
497+
_ = options; // currently unused
498+
490499
foreach (ChatMessage input in inputs)
491500
{
492501
if (input.Role == ChatRole.System ||

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.