diff --git a/eng/packages/General.props b/eng/packages/General.props
index fa2d51de886..aa9771bfb4a 100644
--- a/eng/packages/General.props
+++ b/eng/packages/General.props
@@ -16,7 +16,7 @@
-
+
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs
index 5ff0135cec7..8efbf510164 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/SpeechToText/SpeechToTextOptions.cs
@@ -11,20 +11,20 @@ namespace Microsoft.Extensions.AI;
[Experimental("MEAI001")]
public class SpeechToTextOptions
{
+ /// Gets or sets any additional properties associated with the options.
+ public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
+
/// Gets or sets the model ID for the speech to text.
public string? ModelId { get; set; }
/// Gets or sets the language of source speech.
public string? SpeechLanguage { get; set; }
- /// Gets or sets the language for the target generated text.
- public string? TextLanguage { get; set; }
-
/// Gets or sets the sample rate of the speech input audio.
public int? SpeechSampleRate { get; set; }
- /// Gets or sets any additional properties associated with the options.
- public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
+ /// Gets or sets the language for the target generated text.
+ public string? TextLanguage { get; set; }
///
/// 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()
{
SpeechToTextOptions options = new()
{
+ AdditionalProperties = AdditionalProperties?.Clone(),
ModelId = ModelId,
SpeechLanguage = SpeechLanguage,
- TextLanguage = TextLanguage,
SpeechSampleRate = SpeechSampleRate,
- AdditionalProperties = AdditionalProperties?.Clone(),
+ TextLanguage = TextLanguage,
};
return options;
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj
index 552d45f0fc6..a135ee011ea 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj
@@ -1,4 +1,4 @@
-
+
Microsoft.Extensions.AI
@@ -15,8 +15,8 @@
$(TargetFrameworks);netstandard2.0
- $(NoWarn);CA1063;CA1508;CA2227;SA1316;S1121;S3358;EA0002;OPENAI002
- $(NoWarn);MEAI001
+ $(NoWarn);CA1063;CA1508;CA2227;SA1316;S1121;S3358;EA0002
+ $(NoWarn);OPENAI001;OPENAI002;MEAI001
true
true
true
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs
index 0b6c5f5122f..c1d59e30a68 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
@@ -28,7 +27,6 @@
namespace Microsoft.Extensions.AI;
/// Represents an for an Azure.AI.Agents.Persistent .
-[Experimental("OPENAI001")]
internal sealed class OpenAIAssistantChatClient : IChatClient
{
/// The underlying .
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
index c051550d493..3be0a1cc1ee 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
@@ -91,7 +91,7 @@ public IAsyncEnumerable GetStreamingResponseAsync(
// Make the call to OpenAI.
var chatCompletionUpdates = _chatClient.CompleteChatStreamingAsync(openAIChatMessages, openAIOptions, cancellationToken);
- return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, cancellationToken);
+ return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, openAIOptions, cancellationToken);
}
///
@@ -290,7 +290,8 @@ private static List ToOpenAIChatContent(IList
private static async IAsyncEnumerable FromOpenAIStreamingChatCompletionAsync(
IAsyncEnumerable updates,
- [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ ChatCompletionOptions? options,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
{
Dictionary? functionCallInfos = null;
ChatRole? streamedRole = null;
@@ -334,6 +335,14 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha
}
}
+ if (update.OutputAudioUpdate is { } audioUpdate)
+ {
+ responseUpdate.Contents.Add(new DataContent(audioUpdate.AudioBytesUpdate.ToMemory(), GetOutputAudioMimeType(options))
+ {
+ RawRepresentation = audioUpdate,
+ });
+ }
+
// Transfer over refusal updates.
if (update.RefusalUpdate is not null)
{
@@ -363,8 +372,10 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha
// Transfer over usage updates.
if (update.Usage is ChatTokenUsage tokenUsage)
{
- var usageDetails = FromOpenAIUsage(tokenUsage);
- responseUpdate.Contents.Add(new UsageContent(usageDetails));
+ responseUpdate.Contents.Add(new UsageContent(FromOpenAIUsage(tokenUsage))
+ {
+ RawRepresentation = tokenUsage,
+ });
}
// Now yield the item.
@@ -408,6 +419,17 @@ private static async IAsyncEnumerable FromOpenAIStreamingCha
}
}
+ private static string GetOutputAudioMimeType(ChatCompletionOptions? options) =>
+ options?.AudioOptions?.OutputAudioFormat.ToString()?.ToLowerInvariant() switch
+ {
+ "opus" => "audio/opus",
+ "aac" => "audio/aac",
+ "flac" => "audio/flac",
+ "wav" => "audio/wav",
+ "pcm" => "audio/pcm",
+ "mp3" or _ => "audio/mpeg",
+ };
+
private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompletion, ChatOptions? options, ChatCompletionOptions chatCompletionOptions)
{
_ = Throw.IfNull(openAICompletion);
@@ -432,19 +454,10 @@ private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAIComple
// Output audio is handled separately from message content parts.
if (openAICompletion.OutputAudio is ChatOutputAudio audio)
{
- string mimeType = chatCompletionOptions?.AudioOptions?.OutputAudioFormat.ToString()?.ToLowerInvariant() switch
+ returnMessage.Contents.Add(new DataContent(audio.AudioBytes.ToMemory(), GetOutputAudioMimeType(chatCompletionOptions))
{
- "opus" => "audio/opus",
- "aac" => "audio/aac",
- "flac" => "audio/flac",
- "wav" => "audio/wav",
- "pcm" => "audio/pcm",
- "mp3" or _ => "audio/mpeg",
- };
-
- var dc = new DataContent(audio.AudioBytes.ToMemory(), mimeType);
-
- returnMessage.Contents.Add(dc);
+ RawRepresentation = audio,
+ });
}
// Also manufacture function calling content items from any tool calls in the response.
@@ -505,9 +518,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
result.PresencePenalty ??= options.PresencePenalty;
result.Temperature ??= options.Temperature;
result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls;
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
result.Seed ??= options.Seed;
-#pragma warning restore OPENAI001
if (options.StopSequences is { Count: > 0 } stopSequences)
{
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
index b20769c0dc4..dccddf3038e 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
@@ -14,7 +14,7 @@
using OpenAI.Audio;
using OpenAI.Chat;
using OpenAI.Embeddings;
-using OpenAI.RealtimeConversation;
+using OpenAI.Realtime;
using OpenAI.Responses;
#pragma warning disable S103 // Lines should not be too long
@@ -134,7 +134,6 @@ public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient
/// is .
/// is .
/// is empty or composed entirely of whitespace.
- [Experimental("OPENAI001")] // AssistantClient itself is experimental with this ID
public static IChatClient AsIChatClient(this AssistantClient assistantClient, string assistantId, string? threadId = null) =>
new OpenAIAssistantChatClient(assistantClient, assistantId, threadId);
@@ -165,7 +164,6 @@ public static ChatTool AsOpenAIChatTool(this AIFunction function) =>
/// The function to convert.
/// An OpenAI representing .
/// is .
- [Experimental("OPENAI001")] // AssistantClient itself is experimental with this ID
public static FunctionToolDefinition AsOpenAIAssistantsFunctionToolDefinition(this AIFunction function) =>
OpenAIAssistantChatClient.ToOpenAIAssistantsFunctionToolDefinition(Throw.IfNull(function));
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs
index abfebd99f34..7c944ac5edb 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using OpenAI.RealtimeConversation;
+using OpenAI.Realtime;
namespace Microsoft.Extensions.AI;
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs
index 46019166719..6aee4bc77e4 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs
@@ -117,6 +117,13 @@ public async Task GetResponseAsync(
((List)message.Contents).AddRange(ToAIContents(messageItem.Content));
break;
+ case ReasoningResponseItem reasoningItem when reasoningItem.GetSummaryText() is string summary && !string.IsNullOrWhiteSpace(summary):
+ message.Contents.Add(new TextReasoningContent(summary)
+ {
+ RawRepresentation = reasoningItem
+ });
+ break;
+
case FunctionCallResponseItem functionCall:
response.FinishReason ??= ChatFinishReason.ToolCalls;
var fcc = FunctionCallContent.CreateFromParsedArguments(
@@ -139,7 +146,7 @@ public async Task GetResponseAsync(
if (openAIResponse.Error is { } error)
{
- message.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code });
+ message.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code.ToString() });
}
}
@@ -367,10 +374,11 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
// Handle strongly-typed properties.
result.MaxOutputTokenCount ??= options.MaxOutputTokens;
+ result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls;
result.PreviousResponseId ??= options.ConversationId;
- result.TopP ??= options.TopP;
result.Temperature ??= options.Temperature;
- result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls;
+ result.TopP ??= options.TopP;
+
if (options.Instructions is { } instructions)
{
result.Instructions = string.IsNullOrEmpty(result.Instructions) ?
@@ -386,22 +394,21 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
switch (tool)
{
case AIFunction aiFunction:
- ResponseTool rtool = ToResponseTool(aiFunction, options);
- result.Tools.Add(rtool);
+ result.Tools.Add(ToResponseTool(aiFunction, options));
break;
case HostedWebSearchTool:
- WebSearchToolLocation? location = null;
- if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation))
+ WebSearchUserLocation? location = null;
+ if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchUserLocation), out object? objLocation))
{
- location = objLocation as WebSearchToolLocation;
+ location = objLocation as WebSearchUserLocation;
}
- WebSearchToolContextSize? size = null;
- if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) &&
- objSize is WebSearchToolContextSize)
+ WebSearchContextSize? size = null;
+ if (tool.AdditionalProperties.TryGetValue(nameof(WebSearchContextSize), out object? objSize) &&
+ objSize is WebSearchContextSize)
{
- size = (WebSearchToolContextSize)objSize;
+ size = (WebSearchContextSize)objSize;
}
result.Tools.Add(ResponseTool.CreateWebSearchTool(location, size));
@@ -522,6 +529,10 @@ private static IEnumerable ToOpenAIResponseItems(
yield return ResponseItem.CreateAssistantMessageItem(textContent.Text);
break;
+ case TextReasoningContent reasoningContent:
+ yield return ResponseItem.CreateReasoningItem(reasoningContent.Text);
+ break;
+
case FunctionCallContent callContent:
yield return ResponseItem.CreateFunctionCallItem(
callContent.CallId,
@@ -555,12 +566,16 @@ private static IEnumerable ToOpenAIResponseItems(
TotalTokenCount = usage.TotalTokenCount,
};
- if (usage.OutputTokenDetails is { } outputDetails)
+ if (usage.InputTokenDetails is { } inputDetails)
{
ud.AdditionalCounts ??= [];
+ ud.AdditionalCounts.Add($"{nameof(usage.InputTokenDetails)}.{nameof(inputDetails.CachedTokenCount)}", inputDetails.CachedTokenCount);
+ }
- const string OutputDetails = nameof(usage.OutputTokenDetails);
- ud.AdditionalCounts.Add($"{OutputDetails}.{nameof(outputDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount);
+ if (usage.OutputTokenDetails is { } outputDetails)
+ {
+ ud.AdditionalCounts ??= [];
+ ud.AdditionalCounts.Add($"{nameof(usage.OutputTokenDetails)}.{nameof(outputDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount);
}
}
@@ -624,8 +639,7 @@ private static List ToOpenAIResponsesContent(IListDefault OpenAI endpoint.
- private static readonly Uri _defaultOpenAIEndpoint = new("https://api.openai.com/v1");
+ /// Filename to use when audio lacks a name.
+ /// This information internally is required but is only being used to create a header name in the multipart request.
+ private const string Filename = "audio.mp3";
/// Metadata about the client.
private readonly SpeechToTextClientMetadata _metadata;
@@ -45,7 +46,7 @@ public OpenAISpeechToTextClient(AudioClient audioClient)
// implement the abstractions directly rather than providing adapters on top of the public APIs,
// the package can provide such implementations separate from what's exposed in the public API.
Uri providerUrl = typeof(AudioClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
- ?.GetValue(audioClient) as Uri ?? _defaultOpenAIEndpoint;
+ ?.GetValue(audioClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint;
string? model = typeof(AudioClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
?.GetValue(audioClient) as string;
@@ -65,20 +66,6 @@ public OpenAISpeechToTextClient(AudioClient audioClient)
null;
}
- ///
- public async IAsyncEnumerable GetStreamingTextAsync(
- Stream audioSpeechStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
- {
- _ = Throw.IfNull(audioSpeechStream);
-
- var speechResponse = await GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false);
-
- foreach (var update in speechResponse.ToSpeechToTextResponseUpdates())
- {
- yield return update;
- }
- }
-
///
public async Task GetTextAsync(
Stream audioSpeechStream, SpeechToTextOptions? options = null, CancellationToken cancellationToken = default)
@@ -87,140 +74,126 @@ public async Task GetTextAsync(
SpeechToTextResponse response = new();
- // A translation is triggered when the target text language is specified and the source language is not provided or different.
- static bool IsTranslationRequest(SpeechToTextOptions? options)
- => options is not null && options.TextLanguage is not null
- && (options.SpeechLanguage is null || options.SpeechLanguage != options.TextLanguage);
+ string filename = audioSpeechStream is FileStream fileStream ?
+ Path.GetFileName(fileStream.Name) : // Use the file name if we can get one from the stream.
+ Filename; // Otherwise, use a default name; this is only used to create a header name in the multipart request.
if (IsTranslationRequest(options))
{
- _ = Throw.IfNull(options);
+ var translation = (await _audioClient.TranslateAudioAsync(audioSpeechStream, filename, ToOpenAITranslationOptions(options), cancellationToken).ConfigureAwait(false)).Value;
- var openAIOptions = ToOpenAITranslationOptions(options);
- AudioTranslation translationResult;
+ response.Contents = [new TextContent(translation.Text)];
+ response.RawRepresentation = translation;
-#if NET
- await using (audioSpeechStream.ConfigureAwait(false))
-#else
- using (audioSpeechStream)
-#endif
+ int segmentCount = translation.Segments.Count;
+ if (segmentCount > 0)
{
- translationResult = (await _audioClient.TranslateAudioAsync(
- audioSpeechStream,
- "file.wav", // this information internally is required but is only being used to create a header name in the multipart request.
- openAIOptions, cancellationToken).ConfigureAwait(false)).Value;
+ response.StartTime = translation.Segments[0].StartTime;
+ response.EndTime = translation.Segments[segmentCount - 1].EndTime;
}
-
- UpdateResponseFromOpenAIAudioTranslation(response, translationResult);
}
else
{
- var openAIOptions = ToOpenAITranscriptionOptions(options);
+ var transcription = (await _audioClient.TranscribeAudioAsync(audioSpeechStream, filename, ToOpenAITranscriptionOptions(options), cancellationToken).ConfigureAwait(false)).Value;
- // Transcription request
- AudioTranscription transcriptionResult;
+ response.Contents = [new TextContent(transcription.Text)];
+ response.RawRepresentation = transcription;
-#if NET
- await using (audioSpeechStream.ConfigureAwait(false))
-#else
- using (audioSpeechStream)
-#endif
+ int segmentCount = transcription.Segments.Count;
+ if (segmentCount > 0)
{
- transcriptionResult = (await _audioClient.TranscribeAudioAsync(
- audioSpeechStream,
- "file.wav", // this information internally is required but is only being used to create a header name in the multipart request.
- openAIOptions, cancellationToken).ConfigureAwait(false)).Value;
+ response.StartTime = transcription.Segments[0].StartTime;
+ response.EndTime = transcription.Segments[segmentCount - 1].EndTime;
+ }
+ else
+ {
+ int wordCount = transcription.Words.Count;
+ if (wordCount > 0)
+ {
+ response.StartTime = transcription.Words[0].StartTime;
+ response.EndTime = transcription.Words[wordCount - 1].EndTime;
+ }
}
-
- UpdateResponseFromOpenAIAudioTranscription(response, transcriptionResult);
}
return response;
}
///
- void IDisposable.Dispose()
- {
- // Nothing to dispose. Implementation required for the IAudioTranscriptionClient interface.
- }
-
- /// Updates a from an OpenAI .
- /// The response to update.
- /// The OpenAI audio transcription.
- private static void UpdateResponseFromOpenAIAudioTranscription(SpeechToTextResponse response, AudioTranscription audioTranscription)
+ public async IAsyncEnumerable GetStreamingTextAsync(
+ Stream audioSpeechStream, SpeechToTextOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- _ = Throw.IfNull(audioTranscription);
+ _ = Throw.IfNull(audioSpeechStream);
- var segmentCount = audioTranscription.Segments.Count;
- var wordCount = audioTranscription.Words.Count;
+ string filename = audioSpeechStream is FileStream fileStream ?
+ Path.GetFileName(fileStream.Name) : // Use the file name if we can get one from the stream.
+ Filename; // Otherwise, use a default name; this is only used to create a header name in the multipart request.
- TimeSpan? endTime = null;
- TimeSpan? startTime = null;
- if (segmentCount > 0)
+ if (IsTranslationRequest(options))
{
- endTime = audioTranscription.Segments[segmentCount - 1].EndTime;
- startTime = audioTranscription.Segments[0].StartTime;
+ foreach (var update in (await GetTextAsync(audioSpeechStream, options, cancellationToken).ConfigureAwait(false)).ToSpeechToTextResponseUpdates())
+ {
+ yield return update;
+ }
}
- else if (wordCount > 0)
+ else
{
- endTime = audioTranscription.Words[wordCount - 1].EndTime;
- startTime = audioTranscription.Words[0].StartTime;
+ await foreach (var update in _audioClient.TranscribeAudioStreamingAsync(
+ audioSpeechStream,
+ filename,
+ ToOpenAITranscriptionOptions(options),
+ cancellationToken).ConfigureAwait(false))
+ {
+ SpeechToTextResponseUpdate result = new()
+ {
+ ModelId = options?.ModelId,
+ RawRepresentation = update,
+ };
+
+ switch (update)
+ {
+ case StreamingAudioTranscriptionTextDeltaUpdate deltaUpdate:
+ result.Kind = SpeechToTextResponseUpdateKind.TextUpdated;
+ result.Contents = [new TextContent(deltaUpdate.Delta)];
+ break;
+
+ case StreamingAudioTranscriptionTextDoneUpdate doneUpdate:
+ result.Kind = SpeechToTextResponseUpdateKind.SessionClose;
+ break;
+ }
+
+ yield return result;
+ }
}
+ }
- // Update the response
- response.RawRepresentation = audioTranscription;
- response.Contents = [new TextContent(audioTranscription.Text)];
- response.StartTime = startTime;
- response.EndTime = endTime;
- response.AdditionalProperties = new AdditionalPropertiesDictionary
- {
- [nameof(audioTranscription.Language)] = audioTranscription.Language,
- [nameof(audioTranscription.Duration)] = audioTranscription.Duration
- };
+ ///
+ void IDisposable.Dispose()
+ {
+ // Nothing to dispose. Implementation required for the IAudioTranscriptionClient interface.
}
- /// Converts an extensions options instance to an OpenAI options instance.
+ // A translation is triggered when the target text language is specified and the source language is not provided or different.
+ private static bool IsTranslationRequest(SpeechToTextOptions? options) =>
+ options is not null &&
+ options.TextLanguage is not null &&
+ (options.SpeechLanguage is null || options.SpeechLanguage != options.TextLanguage);
+
+ /// Converts an extensions options instance to an OpenAI transcription options instance.
private AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTextOptions? options)
{
- if (options?.RawRepresentationFactory?.Invoke(this) is not AudioTranscriptionOptions result)
- {
- result = new AudioTranscriptionOptions();
- }
+ AudioTranscriptionOptions result = options?.RawRepresentationFactory?.Invoke(this) as AudioTranscriptionOptions ?? new();
result.Language ??= options?.SpeechLanguage;
+
return result;
}
- /// Updates a from an OpenAI .
- /// The response to update.
- /// The OpenAI audio translation.
- private static void UpdateResponseFromOpenAIAudioTranslation(SpeechToTextResponse response, AudioTranslation audioTranslation)
+ /// Converts an extensions options instance to an OpenAI translation options instance.
+ private AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options)
{
- _ = Throw.IfNull(audioTranslation);
-
- var segmentCount = audioTranslation.Segments.Count;
-
- TimeSpan? endTime = null;
- TimeSpan? startTime = null;
- if (segmentCount > 0)
- {
- endTime = audioTranslation.Segments[segmentCount - 1].EndTime;
- startTime = audioTranslation.Segments[0].StartTime;
- }
+ AudioTranslationOptions result = options?.RawRepresentationFactory?.Invoke(this) as AudioTranslationOptions ?? new();
- // Update the response
- response.RawRepresentation = audioTranslation;
- response.Contents = [new TextContent(audioTranslation.Text)];
- response.StartTime = startTime;
- response.EndTime = endTime;
- response.AdditionalProperties = new AdditionalPropertiesDictionary
- {
- [nameof(audioTranslation.Language)] = audioTranslation.Language,
- [nameof(audioTranslation.Duration)] = audioTranslation.Duration
- };
+ return result;
}
-
- /// Converts an extensions options instance to an OpenAI options instance.
- private AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options)
- => options?.RawRepresentationFactory?.Invoke(this) as AudioTranslationOptions ?? new AudioTranslationOptions();
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj
index 60b6f8c84ab..536c250cb47 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/Microsoft.Extensions.AI.OpenAI.Tests.csproj
@@ -2,7 +2,8 @@
Microsoft.Extensions.AI
Unit tests for Microsoft.Extensions.AI.OpenAI
- $(NoWarn);OPENAI002;MEAI001;S104
+ $(NoWarn);S104
+ $(NoWarn);OPENAI001;MEAI001
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs
index f2f0c9d8a3f..ce458473c59 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAIFunctionConversionTests.cs
@@ -6,12 +6,10 @@
using System.Text.Json;
using OpenAI.Assistants;
using OpenAI.Chat;
-using OpenAI.RealtimeConversation;
+using OpenAI.Realtime;
using OpenAI.Responses;
using Xunit;
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
-
namespace Microsoft.Extensions.AI;
public class OpenAIAIFunctionConversionTests
@@ -24,7 +22,7 @@ public class OpenAIAIFunctionConversionTests
[Fact]
public void AsOpenAIChatTool_ProducesValidInstance()
{
- ChatTool tool = _testFunction.AsOpenAIChatTool();
+ var tool = _testFunction.AsOpenAIChatTool();
Assert.NotNull(tool);
Assert.Equal("test_function", tool.FunctionName);
@@ -35,7 +33,7 @@ public void AsOpenAIChatTool_ProducesValidInstance()
[Fact]
public void AsOpenAIResponseTool_ProducesValidInstance()
{
- ResponseTool tool = _testFunction.AsOpenAIResponseTool();
+ var tool = _testFunction.AsOpenAIResponseTool();
Assert.NotNull(tool);
}
@@ -43,7 +41,7 @@ public void AsOpenAIResponseTool_ProducesValidInstance()
[Fact]
public void AsOpenAIConversationFunctionTool_ProducesValidInstance()
{
- ConversationFunctionTool tool = _testFunction.AsOpenAIConversationFunctionTool();
+ var tool = _testFunction.AsOpenAIConversationFunctionTool();
Assert.NotNull(tool);
Assert.Equal("test_function", tool.Name);
@@ -54,7 +52,7 @@ public void AsOpenAIConversationFunctionTool_ProducesValidInstance()
[Fact]
public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance()
{
- FunctionToolDefinition tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition();
+ var tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition();
Assert.NotNull(tool);
Assert.Equal("test_function", tool.FunctionName);
@@ -62,7 +60,7 @@ public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance()
ValidateSchemaParameters(tool.Parameters);
}
- /// Helper method to validate function parameters match our schema
+ /// Helper method to validate function parameters match our schema.
private static void ValidateSchemaParameters(BinaryData parameters)
{
Assert.NotNull(parameters);
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs
index e616d5fb87b..90bcf9f2632 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable CA1822 // Mark members as static
#pragma warning disable CA2000 // Dispose objects before losing scope
#pragma warning disable S1135 // Track uses of "TODO" tags
@@ -62,7 +61,7 @@ public async Task DeleteAllThreads()
client.DefaultRequestHeaders.Add("openai-organization", "org-ENTERYOURORGID");
client.DefaultRequestHeaders.Add("openai-project", "proj_ENTERYOURPROJECTID");
- AssistantClient ac = new AssistantClient(Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!);
+ AssistantClient ac = new(Environment.GetEnvironmentVariable("AI:OpenAI:ApiKey")!);
while (true)
{
string listing = await client.GetStringAsync("https://api.openai.com/v1/threads?limit=100");
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs
index 6d3a02a08ec..3b084b5ec8a 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientTests.cs
@@ -3,7 +3,6 @@
using System;
using System.ClientModel;
-using Azure.AI.OpenAI;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using OpenAI;
@@ -11,7 +10,6 @@
using Xunit;
#pragma warning disable S103 // Lines should not be too long
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
namespace Microsoft.Extensions.AI;
@@ -24,16 +22,12 @@ public void AsIChatClient_InvalidArgs_Throws()
Assert.Throws("assistantId", () => new AssistantClient("ignored").AsIChatClient(null!));
}
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI)
+ [Fact]
+ public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata()
{
Uri endpoint = new("http://localhost/some/endpoint");
- var client = useAzureOpenAI ?
- new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) :
- new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
+ var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
IChatClient[] clients =
[
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs
index edb5d9fab07..d06d8f520be 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs
@@ -11,7 +11,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
-using Azure.AI.OpenAI;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using OpenAI;
@@ -30,17 +29,13 @@ public void AsIChatClient_InvalidArgs_Throws()
Assert.Throws("chatClient", () => ((ChatClient)null!).AsIChatClient());
}
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI)
+ [Fact]
+ public void AsIChatClient_OpenAIClient_ProducesExpectedMetadata()
{
Uri endpoint = new("http://localhost/some/endpoint");
string model = "amazingModel";
- var client = useAzureOpenAI ?
- new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) :
- new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
+ var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
IChatClient chatClient = client.GetChatClient(model).AsIChatClient();
var metadata = chatClient.GetService();
@@ -398,9 +393,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio
TopP = 0.5f,
PresencePenalty = 0.5f,
Temperature = 0.5f,
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
Seed = 42,
-#pragma warning restore OPENAI001
ToolChoice = ChatToolChoice.CreateAutoChoice(),
ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat()
};
@@ -477,9 +470,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio
TopP = 0.5f,
PresencePenalty = 0.5f,
Temperature = 0.5f,
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
Seed = 42,
-#pragma warning restore OPENAI001
ToolChoice = ChatToolChoice.CreateAutoChoice(),
ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat()
};
@@ -561,9 +552,7 @@ public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_NonStr
Assert.Null(openAIOptions.TopP);
Assert.Null(openAIOptions.PresencePenalty);
Assert.Null(openAIOptions.Temperature);
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
Assert.Null(openAIOptions.Seed);
-#pragma warning restore OPENAI001
Assert.Empty(openAIOptions.StopSequences);
Assert.Empty(openAIOptions.Tools);
Assert.Null(openAIOptions.ToolChoice);
@@ -637,9 +626,7 @@ public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_Stream
Assert.Null(openAIOptions.TopP);
Assert.Null(openAIOptions.PresencePenalty);
Assert.Null(openAIOptions.Temperature);
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
Assert.Null(openAIOptions.Seed);
-#pragma warning restore OPENAI001
Assert.Empty(openAIOptions.StopSequences);
Assert.Empty(openAIOptions.Tools);
Assert.Null(openAIOptions.ToolChoice);
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs
index 9d8a1219ea7..43112fa88e3 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIEmbeddingGeneratorTests.cs
@@ -6,7 +6,6 @@
using System.ClientModel.Primitives;
using System.Net.Http;
using System.Threading.Tasks;
-using Azure.AI.OpenAI;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using OpenAI;
@@ -25,17 +24,13 @@ public void AsIEmbeddingGenerator_InvalidArgs_Throws()
Assert.Throws("embeddingClient", () => ((EmbeddingClient)null!).AsIEmbeddingGenerator());
}
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public void AsIEmbeddingGenerator_OpenAIClient_ProducesExpectedMetadata(bool useAzureOpenAI)
+ [Fact]
+ public void AsIEmbeddingGenerator_OpenAIClient_ProducesExpectedMetadata()
{
Uri endpoint = new("http://localhost/some/endpoint");
string model = "amazingModel";
- var client = useAzureOpenAI ?
- new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) :
- new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
+ var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
IEmbeddingGenerator> embeddingGenerator = client.GetEmbeddingClient(model).AsIEmbeddingGenerator();
var metadata = embeddingGenerator.GetService();
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
index 28125e462b7..b98eb89197f 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
@@ -10,7 +10,6 @@
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
-using Azure.AI.OpenAI;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using OpenAI;
@@ -29,17 +28,13 @@ public void AsIChatClient_InvalidArgs_Throws()
Assert.Throws("responseClient", () => ((OpenAIResponseClient)null!).AsIChatClient());
}
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public void AsIChatClient_ProducesExpectedMetadata(bool useAzureOpenAI)
+ [Fact]
+ public void AsIChatClient_ProducesExpectedMetadata()
{
Uri endpoint = new("http://localhost/some/endpoint");
string model = "amazingModel";
- var client = useAzureOpenAI ?
- new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) :
- new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
+ var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
IChatClient chatClient = client.GetOpenAIResponseClient(model).AsIChatClient();
var metadata = chatClient.GetService();
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs
index c80b37c865e..bb721806ff6 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientIntegrationTests.cs
@@ -7,6 +7,6 @@ public class OpenAISpeechToTextClientIntegrationTests : SpeechToTextClientIntegr
{
protected override ISpeechToTextClient? CreateClient()
=> IntegrationTestHelpers.GetOpenAIClient()?
- .GetAudioClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "whisper-1")
+ .GetAudioClient(TestRunnerConfiguration.Instance["OpenAI:AudioTranscriptionModel"] ?? "gpt-4o-mini-transcribe")
.AsISpeechToTextClient();
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs
index c92d9627968..1252a20741b 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAISpeechToTextClientTests.cs
@@ -8,7 +8,6 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using Azure.AI.OpenAI;
using Microsoft.Extensions.Logging;
using OpenAI;
using OpenAI.Audio;
@@ -26,17 +25,13 @@ public void AsISpeechToTextClient_InvalidArgs_Throws()
Assert.Throws("audioClient", () => ((AudioClient)null!).AsISpeechToTextClient());
}
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public void AsISpeechToTextClient_AudioClient_ProducesExpectedMetadata(bool useAzureOpenAI)
+ [Fact]
+ public void AsISpeechToTextClient_AudioClient_ProducesExpectedMetadata()
{
Uri endpoint = new("http://localhost/some/endpoint");
string model = "amazingModel";
- var client = useAzureOpenAI ?
- new AzureOpenAIClient(endpoint, new ApiKeyCredential("key")) :
- new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
+ var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
ISpeechToTextClient speechToTextClient = client.GetAudioClient(model).AsISpeechToTextClient();
var metadata = speechToTextClient.GetService();
@@ -148,7 +143,8 @@ public async Task GetStreamingTextAsync_BasicRequestResponse(string? speechLangu
string input = $$"""
{
"model": "whisper-1",
- "language": "{{speechLanguage}}"
+ "language": "{{speechLanguage}}",
+ "stream":true
}
""";