Skip to content

Commit c21e89d

Browse files
committed
Update M.E.AI.OpenAI for latest OpenAI release (#6577)
1 parent 207fa90 commit c21e89d

18 files changed

+186
-227
lines changed

eng/packages/General.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<PackageVersion Include="Microsoft.ML.Tokenizers" Version="$(MicrosoftMLTokenizersVersion)" />
1717
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
1818
<PackageVersion Include="OllamaSharp" Version="5.1.9" />
19-
<PackageVersion Include="OpenAI" Version="2.2.0-beta.4" />
19+
<PackageVersion Include="OpenAI" Version="2.2.0" />
2020
<PackageVersion Include="Polly" Version="8.4.2" />
2121
<PackageVersion Include="Polly.Core" Version="8.4.2" />
2222
<PackageVersion Include="Polly.Extensions" Version="8.4.2" />

src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ namespace Microsoft.Extensions.AI;
1111
[Experimental("MEAI001")]
1212
public class SpeechToTextOptions
1313
{
14+
/// <summary>Gets or sets any additional properties associated with the options.</summary>
15+
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
16+
1417
/// <summary>Gets or sets the model ID for the speech to text.</summary>
1518
public string? ModelId { get; set; }
1619

1720
/// <summary>Gets or sets the language of source speech.</summary>
1821
public string? SpeechLanguage { get; set; }
1922

20-
/// <summary>Gets or sets the language for the target generated text.</summary>
21-
public string? TextLanguage { get; set; }
22-
2323
/// <summary>Gets or sets the sample rate of the speech input audio.</summary>
2424
public int? SpeechSampleRate { get; set; }
2525

26-
/// <summary>Gets or sets any additional properties associated with the options.</summary>
27-
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
26+
/// <summary>Gets or sets the language for the target generated text.</summary>
27+
public string? TextLanguage { get; set; }
2828

2929
/// <summary>
3030
/// Gets or sets a callback responsible for creating the raw representation of the embedding generation options from an underlying implementation.
@@ -51,11 +51,11 @@ public virtual SpeechToTextOptions Clone()
5151
{
5252
SpeechToTextOptions options = new()
5353
{
54+
AdditionalProperties = AdditionalProperties?.Clone(),
5455
ModelId = ModelId,
5556
SpeechLanguage = SpeechLanguage,
56-
TextLanguage = TextLanguage,
5757
SpeechSampleRate = SpeechSampleRate,
58-
AdditionalProperties = AdditionalProperties?.Clone(),
58+
TextLanguage = TextLanguage,
5959
};
6060

6161
return options;

src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<RootNamespace>Microsoft.Extensions.AI</RootNamespace>
@@ -15,8 +15,8 @@
1515

1616
<PropertyGroup>
1717
<TargetFrameworks>$(TargetFrameworks);netstandard2.0</TargetFrameworks>
18-
<NoWarn>$(NoWarn);CA1063;CA1508;CA2227;SA1316;S1121;S3358;EA0002;OPENAI002</NoWarn>
19-
<NoWarn>$(NoWarn);MEAI001</NoWarn>
18+
<NoWarn>$(NoWarn);CA1063;CA1508;CA2227;SA1316;S1121;S3358;EA0002</NoWarn>
19+
<NoWarn>$(NoWarn);OPENAI001;OPENAI002;MEAI001</NoWarn>
2020
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
2121
<DisableNETStandardCompatErrors>true</DisableNETStandardCompatErrors>
2222
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Diagnostics.CodeAnalysis;
76
using System.Linq;
87
using System.Reflection;
98
using System.Runtime.CompilerServices;
@@ -28,7 +27,6 @@
2827
namespace Microsoft.Extensions.AI;
2928

3029
/// <summary>Represents an <see cref="IChatClient"/> for an Azure.AI.Agents.Persistent <see cref="AssistantClient"/>.</summary>
31-
[Experimental("OPENAI001")]
3230
internal sealed class OpenAIAssistantChatClient : IChatClient
3331
{
3432
/// <summary>The underlying <see cref="AssistantClient" />.</summary>

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

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
9191
// Make the call to OpenAI.
9292
var chatCompletionUpdates = _chatClient.CompleteChatStreamingAsync(openAIChatMessages, openAIOptions, cancellationToken);
9393

94-
return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, cancellationToken);
94+
return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, openAIOptions, cancellationToken);
9595
}
9696

9797
/// <inheritdoc />
@@ -290,7 +290,8 @@ private static List<ChatMessageContentPart> ToOpenAIChatContent(IList<AIContent>
290290

291291
private static async IAsyncEnumerable<ChatResponseUpdate> FromOpenAIStreamingChatCompletionAsync(
292292
IAsyncEnumerable<StreamingChatCompletionUpdate> updates,
293-
[EnumeratorCancellation] CancellationToken cancellationToken = default)
293+
ChatCompletionOptions? options,
294+
[EnumeratorCancellation] CancellationToken cancellationToken)
294295
{
295296
Dictionary<int, FunctionCallInfo>? functionCallInfos = null;
296297
ChatRole? streamedRole = null;
@@ -334,6 +335,14 @@ private static async IAsyncEnumerable<ChatResponseUpdate> FromOpenAIStreamingCha
334335
}
335336
}
336337

338+
if (update.OutputAudioUpdate is { } audioUpdate)
339+
{
340+
responseUpdate.Contents.Add(new DataContent(audioUpdate.AudioBytesUpdate.ToMemory(), GetOutputAudioMimeType(options))
341+
{
342+
RawRepresentation = audioUpdate,
343+
});
344+
}
345+
337346
// Transfer over refusal updates.
338347
if (update.RefusalUpdate is not null)
339348
{
@@ -363,8 +372,10 @@ private static async IAsyncEnumerable<ChatResponseUpdate> FromOpenAIStreamingCha
363372
// Transfer over usage updates.
364373
if (update.Usage is ChatTokenUsage tokenUsage)
365374
{
366-
var usageDetails = FromOpenAIUsage(tokenUsage);
367-
responseUpdate.Contents.Add(new UsageContent(usageDetails));
375+
responseUpdate.Contents.Add(new UsageContent(FromOpenAIUsage(tokenUsage))
376+
{
377+
RawRepresentation = tokenUsage,
378+
});
368379
}
369380

370381
// Now yield the item.
@@ -408,6 +419,17 @@ private static async IAsyncEnumerable<ChatResponseUpdate> FromOpenAIStreamingCha
408419
}
409420
}
410421

422+
private static string GetOutputAudioMimeType(ChatCompletionOptions? options) =>
423+
options?.AudioOptions?.OutputAudioFormat.ToString()?.ToLowerInvariant() switch
424+
{
425+
"opus" => "audio/opus",
426+
"aac" => "audio/aac",
427+
"flac" => "audio/flac",
428+
"wav" => "audio/wav",
429+
"pcm" => "audio/pcm",
430+
"mp3" or _ => "audio/mpeg",
431+
};
432+
411433
private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompletion, ChatOptions? options, ChatCompletionOptions chatCompletionOptions)
412434
{
413435
_ = Throw.IfNull(openAICompletion);
@@ -432,19 +454,10 @@ private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAIComple
432454
// Output audio is handled separately from message content parts.
433455
if (openAICompletion.OutputAudio is ChatOutputAudio audio)
434456
{
435-
string mimeType = chatCompletionOptions?.AudioOptions?.OutputAudioFormat.ToString()?.ToLowerInvariant() switch
457+
returnMessage.Contents.Add(new DataContent(audio.AudioBytes.ToMemory(), GetOutputAudioMimeType(chatCompletionOptions))
436458
{
437-
"opus" => "audio/opus",
438-
"aac" => "audio/aac",
439-
"flac" => "audio/flac",
440-
"wav" => "audio/wav",
441-
"pcm" => "audio/pcm",
442-
"mp3" or _ => "audio/mpeg",
443-
};
444-
445-
var dc = new DataContent(audio.AudioBytes.ToMemory(), mimeType);
446-
447-
returnMessage.Contents.Add(dc);
459+
RawRepresentation = audio,
460+
});
448461
}
449462

450463
// Also manufacture function calling content items from any tool calls in the response.
@@ -505,9 +518,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
505518
result.PresencePenalty ??= options.PresencePenalty;
506519
result.Temperature ??= options.Temperature;
507520
result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls;
508-
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
509521
result.Seed ??= options.Seed;
510-
#pragma warning restore OPENAI001
511522

512523
if (options.StopSequences is { Count: > 0 } stopSequences)
513524
{

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
using OpenAI.Audio;
1515
using OpenAI.Chat;
1616
using OpenAI.Embeddings;
17-
using OpenAI.RealtimeConversation;
17+
using OpenAI.Realtime;
1818
using OpenAI.Responses;
1919

2020
#pragma warning disable S103 // Lines should not be too long
@@ -134,7 +134,6 @@ public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient
134134
/// <exception cref="ArgumentNullException"><paramref name="assistantClient"/> is <see langword="null"/>.</exception>
135135
/// <exception cref="ArgumentNullException"><paramref name="assistantId"/> is <see langword="null"/>.</exception>
136136
/// <exception cref="ArgumentException"><paramref name="assistantId"/> is empty or composed entirely of whitespace.</exception>
137-
[Experimental("OPENAI001")] // AssistantClient itself is experimental with this ID
138137
public static IChatClient AsIChatClient(this AssistantClient assistantClient, string assistantId, string? threadId = null) =>
139138
new OpenAIAssistantChatClient(assistantClient, assistantId, threadId);
140139

@@ -165,7 +164,6 @@ public static ChatTool AsOpenAIChatTool(this AIFunction function) =>
165164
/// <param name="function">The function to convert.</param>
166165
/// <returns>An OpenAI <see cref="FunctionToolDefinition"/> representing <paramref name="function"/>.</returns>
167166
/// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>
168-
[Experimental("OPENAI001")] // AssistantClient itself is experimental with this ID
169167
public static FunctionToolDefinition AsOpenAIAssistantsFunctionToolDefinition(this AIFunction function) =>
170168
OpenAIAssistantChatClient.ToOpenAIAssistantsFunctionToolDefinition(Throw.IfNull(function));
171169

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using OpenAI.RealtimeConversation;
4+
using OpenAI.Realtime;
55

66
namespace Microsoft.Extensions.AI;
77

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

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ public async Task<ChatResponse> GetResponseAsync(
117117
((List<AIContent>)message.Contents).AddRange(ToAIContents(messageItem.Content));
118118
break;
119119

120+
case ReasoningResponseItem reasoningItem when reasoningItem.GetSummaryText() is string summary && !string.IsNullOrWhiteSpace(summary):
121+
message.Contents.Add(new TextReasoningContent(summary)
122+
{
123+
RawRepresentation = reasoningItem
124+
});
125+
break;
126+
120127
case FunctionCallResponseItem functionCall:
121128
response.FinishReason ??= ChatFinishReason.ToolCalls;
122129
var fcc = FunctionCallContent.CreateFromParsedArguments(
@@ -139,7 +146,7 @@ public async Task<ChatResponse> GetResponseAsync(
139146

140147
if (openAIResponse.Error is { } error)
141148
{
142-
message.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code });
149+
message.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code.ToString() });
143150
}
144151
}
145152

@@ -367,10 +374,11 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
367374

368375
// Handle strongly-typed properties.
369376
result.MaxOutputTokenCount ??= options.MaxOutputTokens;
377+
result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls;
370378
result.PreviousResponseId ??= options.ConversationId;
371-
result.TopP ??= options.TopP;
372379
result.Temperature ??= options.Temperature;
373-
result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls;
380+
result.TopP ??= options.TopP;
381+
374382
if (options.Instructions is { } instructions)
375383
{
376384
result.Instructions = string.IsNullOrEmpty(result.Instructions) ?
@@ -386,22 +394,21 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
386394
switch (tool)
387395
{
388396
case AIFunction aiFunction:
389-
ResponseTool rtool = ToResponseTool(aiFunction, options);
390-
result.Tools.Add(rtool);
397+
result.Tools.Add(ToResponseTool(aiFunction, options));
391398
break;
392399

393400
case HostedWebSearchTool:
394-
WebSearchToolLocation? location = null;
395-
if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation))
401+
WebSearchUserLocation? location = null;
402+
if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchUserLocation), out object? objLocation))
396403
{
397-
location = objLocation as WebSearchToolLocation;
404+
location = objLocation as WebSearchUserLocation;
398405
}
399406

400-
WebSearchToolContextSize? size = null;
401-
if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) &&
402-
objSize is WebSearchToolContextSize)
407+
WebSearchContextSize? size = null;
408+
if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchContextSize), out object? objSize) &&
409+
objSize is WebSearchContextSize)
403410
{
404-
size = (WebSearchToolContextSize)objSize;
411+
size = (WebSearchContextSize)objSize;
405412
}
406413

407414
result.Tools.Add(ResponseTool.CreateWebSearchTool(location, size));
@@ -522,6 +529,10 @@ private static IEnumerable<ResponseItem> ToOpenAIResponseItems(
522529
yield return ResponseItem.CreateAssistantMessageItem(textContent.Text);
523530
break;
524531

532+
case TextReasoningContent reasoningContent:
533+
yield return ResponseItem.CreateReasoningItem(reasoningContent.Text);
534+
break;
535+
525536
case FunctionCallContent callContent:
526537
yield return ResponseItem.CreateFunctionCallItem(
527538
callContent.CallId,
@@ -555,12 +566,16 @@ private static IEnumerable<ResponseItem> ToOpenAIResponseItems(
555566
TotalTokenCount = usage.TotalTokenCount,
556567
};
557568

558-
if (usage.OutputTokenDetails is { } outputDetails)
569+
if (usage.InputTokenDetails is { } inputDetails)
559570
{
560571
ud.AdditionalCounts ??= [];
572+
ud.AdditionalCounts.Add($"{nameof(usage.InputTokenDetails)}.{nameof(inputDetails.CachedTokenCount)}", inputDetails.CachedTokenCount);
573+
}
561574

562-
const string OutputDetails = nameof(usage.OutputTokenDetails);
563-
ud.AdditionalCounts.Add($"{OutputDetails}.{nameof(outputDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount);
575+
if (usage.OutputTokenDetails is { } outputDetails)
576+
{
577+
ud.AdditionalCounts ??= [];
578+
ud.AdditionalCounts.Add($"{nameof(usage.OutputTokenDetails)}.{nameof(outputDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount);
564579
}
565580
}
566581

@@ -624,8 +639,7 @@ private static List<ResponseContentPart> ToOpenAIResponsesContent(IList<AIConten
624639
break;
625640

626641
case DataContent dataContent when dataContent.MediaType.StartsWith("application/pdf", StringComparison.OrdinalIgnoreCase):
627-
parts.Add(ResponseContentPart.CreateInputFilePart(null, $"{Guid.NewGuid():N}.pdf",
628-
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(dataContent.Uri, OpenAIJsonContext.Default.String))));
642+
parts.Add(ResponseContentPart.CreateInputFilePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType, $"{Guid.NewGuid():N}.pdf"));
629643
break;
630644

631645
case ErrorContent errorContent when errorContent.ErrorCode == nameof(ResponseContentPartKind.Refusal):

0 commit comments

Comments
 (0)