diff --git a/eng/packages/General.props b/eng/packages/General.props index 68e004a9275..044a0cd6b7d 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -17,7 +17,7 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs index f54da262d22..428950fcf23 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIRealtimeExtensions.cs @@ -13,10 +13,10 @@ namespace OpenAI.Realtime; [Experimental(DiagnosticIds.Experiments.AIOpenAIRealtime)] public static class MicrosoftExtensionsAIRealtimeExtensions { - /// Creates an OpenAI from an . + /// Creates an OpenAI from an . /// The function to convert. - /// An OpenAI representing . + /// An OpenAI representing . /// is . - public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunctionDeclaration function) => - OpenAIRealtimeConversationClient.ToOpenAIConversationFunctionTool(Throw.IfNull(function)); + public static RealtimeFunctionTool AsOpenAIRealtimeFunctionTool(this AIFunctionDeclaration function) => + OpenAIRealtimeConversationClient.ToOpenAIRealtimeFunctionTool(Throw.IfNull(function)); } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index 1fbf28268cb..419b65aaecc 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -98,7 +98,6 @@ public static ResponseResult AsOpenAIResponseResult(this ChatResponse response, ConversationOptions = OpenAIClientExtensions.IsConversationId(response.ConversationId) ? new(response.ConversationId) : null, CreatedAt = response.CreatedAt ?? default, Id = response.ResponseId, - Instructions = options?.Instructions, MaxOutputTokenCount = options?.MaxOutputTokens, Model = response.ModelId ?? options?.ModelId, ParallelToolCallsEnabled = options?.AllowMultipleToolCalls ?? true, @@ -108,6 +107,11 @@ public static ResponseResult AsOpenAIResponseResult(this ChatResponse response, Usage = OpenAIResponsesChatClient.ToResponseTokenUsage(response.Usage), }; + if (options?.Instructions is { Length: > 0 }) + { + result.Instructions.Add(ResponseItem.CreateDeveloperMessageItem(options.Instructions)); + } + foreach (var responseItem in OpenAIResponsesChatClient.ToOpenAIResponseItems(response.Messages, options)) { result.OutputItems.Add(responseItem); @@ -122,7 +126,7 @@ public static ResponseResult AsOpenAIResponseResult(this ChatResponse response, /// /// does not derive from , so it cannot be added directly to a list of s. /// Instead, this method wraps the provided in an and adds that to the list. - /// The returned by will + /// The returned by will /// be able to unwrap the when it processes the list of tools and use the provided as-is. /// public static void Add(this IList tools, ResponseTool tool) @@ -138,7 +142,7 @@ public static void Add(this IList tools, ResponseTool tool) /// /// /// The returned tool is only suitable for use with the returned by - /// (or s that delegate + /// (or s that delegate /// to such an instance). It is likely to be ignored by any other implementation. /// /// @@ -147,7 +151,7 @@ public static void Add(this IList tools, ResponseTool tool) /// , those types should be preferred instead of this method, as they are more portable, /// capable of being respected by any implementation. This method does not attempt to /// map the supplied to any of those types, it simply wraps it as-is: - /// the returned by will + /// the returned by will /// be able to unwrap the when it processes the list of tools. /// /// diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index b3e03d01fa2..80ed42ffb35 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -681,7 +681,7 @@ ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransf private static ChatReasoningEffortLevel? ToOpenAIChatReasoningEffortLevel(ReasoningEffort? effort) => effort switch { - ReasoningEffort.None => new ChatReasoningEffortLevel("none"), + ReasoningEffort.None => ChatReasoningEffortLevel.None, ReasoningEffort.Low => ChatReasoningEffortLevel.Low, ReasoningEffort.Medium => ChatReasoningEffortLevel.Medium, ReasoningEffort.High => ChatReasoningEffortLevel.High, diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index e4008e4380f..c65a55e4472 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -119,11 +119,12 @@ public static IChatClient AsIChatClient(this ChatClient chatClient) => /// Gets an for use with this . /// The client. + /// The default model ID to use for the chat client. /// An that can be used to converse via the . /// is . [Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] - public static IChatClient AsIChatClient(this ResponsesClient responseClient) => - new OpenAIResponsesChatClient(responseClient); + public static IChatClient AsIChatClient(this ResponsesClient responseClient, string? defaultModelId = null) => + new OpenAIResponsesChatClient(responseClient, defaultModelId); /// Gets an for use with this . /// The instance to be accessed as an . diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs index dab7f82b5a6..9a7aebbaac3 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs @@ -8,10 +8,7 @@ using System.IO; using System.Linq; using System.Net.Mime; -using System.Reflection; using System.Runtime.InteropServices; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.DiagnosticIds; @@ -19,8 +16,6 @@ using OpenAI; using OpenAI.Images; -#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields - namespace Microsoft.Extensions.AI; /// Represents an for an OpenAI or . @@ -110,20 +105,11 @@ void IDisposable.Dispose() /// Converts a to a . private static ImageGenerationResponse ToImageGenerationResponse(GeneratedImageCollection generatedImages) { - string contentType = "image/png"; // Default content type for images - - // OpenAI doesn't expose the content type, so we need to read from the internal JSON representation. - // https://github.com/openai/openai-dotnet/issues/561 - var additionalRawData = typeof(GeneratedImageCollection) - .GetProperty("SerializedAdditionalRawData", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(generatedImages) as IDictionary; - - if (additionalRawData?.TryGetValue("output_format", out var outputFormat) ?? false) - { - var stringJsonTypeInfo = (JsonTypeInfo)AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(string)); - var outputFormatString = JsonSerializer.Deserialize(outputFormat, stringJsonTypeInfo); - contentType = $"image/{outputFormatString}"; - } +#pragma warning disable OPENAI001 + string contentType = generatedImages.OutputFileFormat?.ToString() is { } outputFormat ? + $"image/{outputFormat}" : + "image/png"; // Default content type for images +#pragma warning restore OPENAI001 List contents = []; @@ -175,15 +161,15 @@ private OpenAI.Images.ImageGenerationOptions ToOpenAIImageGenerationOptions(Imag if (result.OutputFileFormat is null) { - if (options?.MediaType?.Equals("image/png", StringComparison.OrdinalIgnoreCase) == true) + if (options?.MediaType?.Equals("image/png", StringComparison.OrdinalIgnoreCase) is true) { result.OutputFileFormat = GeneratedImageFileFormat.Png; } - else if (options?.MediaType?.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase) == true) + else if (options?.MediaType?.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase) is true) { result.OutputFileFormat = GeneratedImageFileFormat.Jpeg; } - else if (options?.MediaType?.Equals("image/webp", StringComparison.OrdinalIgnoreCase) == true) + else if (options?.MediaType?.Equals("image/webp", StringComparison.OrdinalIgnoreCase) is true) { result.OutputFileFormat = GeneratedImageFileFormat.Webp; } @@ -208,6 +194,22 @@ private ImageEditOptions ToOpenAIImageEditOptions(ImageGenerationOptions? option { ImageEditOptions result = options?.RawRepresentationFactory?.Invoke(this) as ImageEditOptions ?? new(); + if (result.OutputFileFormat is null) + { + if (options?.MediaType?.Equals("image/png", StringComparison.OrdinalIgnoreCase) is true) + { + result.OutputFileFormat = GeneratedImageFileFormat.Png; + } + else if (options?.MediaType?.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase) is true) + { + result.OutputFileFormat = GeneratedImageFileFormat.Jpeg; + } + else if (options?.MediaType?.Equals("image/webp", StringComparison.OrdinalIgnoreCase) is true) + { + result.OutputFileFormat = GeneratedImageFileFormat.Webp; + } + } + result.ResponseFormat ??= options?.ResponseFormat switch { ImageGenerationResponseFormat.Uri => GeneratedImageFormat.Uri, diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs index c40a7c525ff..83d1bf2ca98 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIRealtimeConversationClient.cs @@ -11,16 +11,16 @@ namespace Microsoft.Extensions.AI; [Experimental(DiagnosticIds.Experiments.AIOpenAIRealtime)] internal sealed class OpenAIRealtimeConversationClient { - public static ConversationFunctionTool ToOpenAIConversationFunctionTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null) + public static RealtimeFunctionTool ToOpenAIRealtimeFunctionTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null) { bool? strict = OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? OpenAIClientExtensions.HasStrict(options?.AdditionalProperties); - return new ConversationFunctionTool(aiFunction.Name) + return new RealtimeFunctionTool(aiFunction.Name) { - Description = aiFunction.Description, - Parameters = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict), + FunctionDescription = aiFunction.Description, + FunctionParameters = OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict), }; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 1d1c5d37674..bfdb4843c95 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Net.Mime; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; @@ -49,28 +50,27 @@ private static readonly Func>)); - // Workaround for https://github.com/openai/openai-dotnet/pull/874. - // The OpenAI library doesn't yet expose InputImageUrl as a public property, so we access it via reflection. - // Replace this with the actual public property once it's available (e.g., part.InputImageUrl). - private static readonly PropertyInfo? _inputImageUrlProperty = - Type.GetType("OpenAI.Responses.InternalItemContentInputImage, OpenAI")?.GetProperty("ImageUrl"); - /// Metadata about the client. private readonly ChatClientMetadata _metadata; /// The underlying . private readonly ResponsesClient _responseClient; + /// The default model ID to use for the chat client. + private readonly string? _defaultModelId; + /// Initializes a new instance of the class for the specified . /// The underlying client. + /// The default model ID to use for the chat client. /// is . - public OpenAIResponsesChatClient(ResponsesClient responseClient) + public OpenAIResponsesChatClient(ResponsesClient responseClient, string? defaultModelId) { _ = Throw.IfNull(responseClient); _responseClient = responseClient; + _defaultModelId = defaultModelId; - _metadata = new("openai", responseClient.Endpoint, responseClient.Model); + _metadata = new("openai", responseClient.Endpoint, defaultModelId); } /// @@ -737,7 +737,7 @@ private CreateResponseOptions AsCreateResponseOptions(ChatOptions? options, out { return new() { - Model = _responseClient.Model, + Model = _defaultModelId, }; } @@ -753,7 +753,7 @@ private CreateResponseOptions AsCreateResponseOptions(ChatOptions? options, out result.BackgroundModeEnabled ??= options.AllowBackgroundResponses; result.MaxOutputTokenCount ??= options.MaxOutputTokens; - result.Model ??= options.ModelId ?? _responseClient.Model; + result.Model ??= options.ModelId ?? _defaultModelId; result.Temperature ??= options.Temperature; result.TopP ??= options.TopP; result.ReasoningOptions ??= ToOpenAIResponseReasoningOptions(options.Reasoning); @@ -864,7 +864,7 @@ ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransf ResponseReasoningEffortLevel? effortLevel = reasoning.Effort switch { - ReasoningEffort.None => new ResponseReasoningEffortLevel("none"), + ReasoningEffort.None => ResponseReasoningEffortLevel.None, ReasoningEffort.Low => ResponseReasoningEffortLevel.Low, ReasoningEffort.Medium => ResponseReasoningEffortLevel.Medium, ReasoningEffort.High => ResponseReasoningEffortLevel.High, @@ -965,7 +965,7 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable ToAIContents(IEnumerable con { content = new DataContent(part.InputFileBytes, part.InputFileBytesMediaType ?? "application/octet-stream") { Name = part.InputFilename }; } - else if (_inputImageUrlProperty?.GetValue(part) is string inputImageUrl) + else if (part.InputImageUri is { } inputImageUrl) { - if (inputImageUrl.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) - { - content = new DataContent(inputImageUrl); - } - else if (Uri.TryCreate(inputImageUrl, UriKind.Absolute, out Uri? imageUri)) - { - content = new UriContent(imageUri, "image/*"); - } - else - { - content = null; - } + content = inputImageUrl.Scheme.Equals("data", StringComparison.OrdinalIgnoreCase) ? + new DataContent(inputImageUrl) : + new UriContent(inputImageUrl, MediaTypeMap.GetMediaType(inputImageUrl.AbsoluteUri) ?? "image/*"); } else { diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs index 4b041f4a15f..89a231105e9 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs @@ -49,8 +49,8 @@ var openAIClient = new OpenAIClient( new ApiKeyCredential(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: OpenAI:Key. See the README for details."))); -#pragma warning disable OPENAI001 // GetResponsesClient(string) is experimental and subject to change or removal in future updates. -var chatClient = openAIClient.GetResponsesClient("gpt-4o-mini").AsIChatClient(); +#pragma warning disable OPENAI001 // GetResponsesClient() is experimental and subject to change or removal in future updates. +var chatClient = openAIClient.GetResponsesClient().AsIChatClient("gpt-4o-mini"); #pragma warning restore OPENAI001 var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); @@ -66,7 +66,7 @@ #endif var azureOpenAIEndpoint = new Uri(new Uri(builder.Configuration["AzureOpenAI:Endpoint"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAi:Endpoint. See the README for details.")), "/openai/v1"); #if (IsManagedIdentity) -#pragma warning disable OPENAI001 // OpenAIClient(AuthenticationPolicy, OpenAIClientOptions) and GetResponsesClient(string) are experimental and subject to change or removal in future updates. +#pragma warning disable OPENAI001 // OpenAIClient(AuthenticationPolicy, OpenAIClientOptions) and GetResponsesClient() are experimental and subject to change or removal in future updates. var azureOpenAi = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions { Endpoint = azureOpenAIEndpoint }); @@ -75,9 +75,9 @@ var openAIOptions = new OpenAIClientOptions { Endpoint = azureOpenAIEndpoint }; var azureOpenAi = new OpenAIClient(new ApiKeyCredential(builder.Configuration["AzureOpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: AzureOpenAi:Key. See the README for details.")), openAIOptions); -#pragma warning disable OPENAI001 // GetResponsesClient(string) is experimental and subject to change or removal in future updates. +#pragma warning disable OPENAI001 // GetResponsesClient() is experimental and subject to change or removal in future updates. #endif -var chatClient = azureOpenAi.GetResponsesClient("gpt-4o-mini").AsIChatClient(); +var chatClient = azureOpenAi.GetResponsesClient().AsIChatClient("gpt-4o-mini"); #pragma warning restore OPENAI001 var embeddingGenerator = azureOpenAi.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs index 42a0be5c416..51fba1d7582 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/IntegrationTestHelpers.cs @@ -19,7 +19,7 @@ internal static class IntegrationTestHelpers { var configuration = TestRunnerConfiguration.Instance; - string? apiKey = configuration["OpenAI:Key"]; + string? apiKey = configuration["OpenAI:Key"] ?? configuration["AI:OpenAI:ApiKey"]; string? mode = configuration["OpenAI:Mode"]; if (string.Equals(mode, "AzureOpenAI", StringComparison.OrdinalIgnoreCase)) diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index be9201eed56..26f18be1677 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -1697,9 +1697,9 @@ public async Task ChatOptions_ModelId_OverridesClientModel_Streaming() "temperature":0.5, "messages":[{"role":"user","content":"hello"}], "model":"gpt-4o", + "max_completion_tokens":20, "stream":true, - "stream_options":{"include_usage":true}, - "max_completion_tokens":20 + "stream_options":{"include_usage":true} } """; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index d2e4dd39867..0eb093518e2 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -380,8 +380,7 @@ public void AsOpenAIResponseTool_WithHostedImageGenerationToolWithAdditionalProp public void AsOpenAIResponseTool_WithHostedImageGenerationToolWithInputImageMask_ProducesValidImageGenerationTool() { var inputImageMask = new ImageGenerationToolInputImageMask( - BinaryData.FromBytes([0x89, 0x50, 0x4E, 0x47]), - "image/png"); + new Uri(new DataContent((byte[])[0x89, 0x50, 0x4E, 0x47], "image/png").Uri)); var imageGenTool = new HostedImageGenerationTool(new Dictionary { @@ -595,12 +594,12 @@ public void AsOpenAIResponseTool_WithNullTool_ThrowsArgumentNullException() [Fact] public void AsOpenAIConversationFunctionTool_ProducesValidInstance() { - var tool = _testFunction.AsOpenAIConversationFunctionTool(); + var tool = _testFunction.AsOpenAIRealtimeFunctionTool(); Assert.NotNull(tool); - Assert.Equal("test_function", tool.Name); - Assert.Equal("A test function for conversion", tool.Description); - ValidateSchemaParameters(tool.Parameters); + Assert.Equal("test_function", tool.FunctionName); + Assert.Equal("A test function for conversion", tool.FunctionDescription); + ValidateSchemaParameters(tool.FunctionParameters); } [Fact] @@ -1458,7 +1457,6 @@ public void AsOpenAIResponse_WithRawRepresentation_ReturnsOriginal() Temperature = 0.7f, TopP = 0.9f, PreviousResponseId = "prev-id", - Instructions = "Test instructions" }; var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Test")) @@ -1523,7 +1521,7 @@ public void AsOpenAIResponse_WithChatOptions_IncludesOptionsInResponse() Assert.Equal(500, openAIResponse.MaxOutputTokenCount); Assert.True(openAIResponse.ParallelToolCallsEnabled); Assert.Equal("conv_123", openAIResponse.ConversationOptions?.ConversationId); - Assert.Equal("You are a helpful assistant.", openAIResponse.Instructions); + Assert.Equal("You are a helpful assistant.", Assert.IsAssignableFrom(openAIResponse.Instructions.Single()).Content.Single().Text); Assert.Equal(0.8f, openAIResponse.Temperature); Assert.Equal(0.95f, openAIResponse.TopP); } @@ -1659,7 +1657,7 @@ public void AsOpenAIResponse_WithDefaultValues_UsesExpectedDefaults() Assert.Null(openAIResponse.Temperature); Assert.Null(openAIResponse.TopP); Assert.Null(openAIResponse.ConversationOptions); - Assert.Null(openAIResponse.Instructions); + Assert.Empty(openAIResponse.Instructions); Assert.NotNull(openAIResponse.OutputItems); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs index ce0cdb7cf82..bd969aca969 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIImageGeneratorIntegrationTests.cs @@ -7,6 +7,6 @@ public class OpenAIImageGeneratorIntegrationTests : ImageGeneratorIntegrationTes { protected override IImageGenerator? CreateGenerator() => IntegrationTestHelpers.GetOpenAIClient()? - .GetImageClient(TestRunnerConfiguration.Instance["OpenAI:ImageModel"] ?? "dall-e-3") + .GetImageClient(TestRunnerConfiguration.Instance["OpenAI:ImageModel"] ?? "gpt-image-1-mini") .AsIImageGenerator(); } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 0bf3f7fcf0f..56d8135da62 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -19,8 +19,10 @@ public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests { protected override IChatClient? CreateChatClient() => IntegrationTestHelpers.GetOpenAIClient() - ?.GetResponsesClient(TestRunnerConfiguration.Instance["OpenAI:ChatModel"] ?? "gpt-4o-mini") - .AsIChatClient(); + ?.GetResponsesClient() + .AsIChatClient(TestRunnerConfiguration.Instance["OpenAI:ChatModel"] ?? "gpt-4o-mini"); + + private static string ReasoningModel => "gpt-5-nano"; public override bool FunctionInvokingChatClientSetsConversationId => true; @@ -563,13 +565,14 @@ public async Task ReasoningContent_NonStreaming_RoundtripsEncryptedContent() ChatOptions chatOptions = new() { + ModelId = ReasoningModel, + Reasoning = new() + { + Effort = ReasoningEffort.Low, + Output = ReasoningOutput.Full, + }, RawRepresentationFactory = _ => new CreateResponseOptions { - ReasoningOptions = new ResponseReasoningOptions - { - ReasoningEffortLevel = ResponseReasoningEffortLevel.Low, - ReasoningSummaryVerbosity = ResponseReasoningSummaryVerbosity.Detailed - }, StoredOutputEnabled = false, IncludedProperties = { IncludedResponseProperty.ReasoningEncryptedContent }, }, @@ -598,7 +601,7 @@ public async Task ReasoningContent_NonStreaming_RoundtripsEncryptedContent() var response2 = await ChatClient.GetResponseAsync(chatHistory, chatOptions); Assert.NotNull(response2); - Assert.Contains("6", response2.Text); + Assert.True(response2.Text.Contains("6") || response2.Text.Contains("six")); // 3. Serialize/deserialize to drop RawRepresentations, then make third request string json = JsonSerializer.Serialize(chatHistory, AIJsonUtilities.DefaultOptions); @@ -643,13 +646,14 @@ public async Task ReasoningContent_Streaming_RoundtripsEncryptedContent() ChatOptions chatOptions = new() { + ModelId = ReasoningModel, + Reasoning = new() + { + Effort = ReasoningEffort.Low, + Output = ReasoningOutput.Full, + }, RawRepresentationFactory = _ => new CreateResponseOptions { - ReasoningOptions = new ResponseReasoningOptions - { - ReasoningEffortLevel = ResponseReasoningEffortLevel.Low, - ReasoningSummaryVerbosity = ResponseReasoningSummaryVerbosity.Detailed - }, StoredOutputEnabled = false, IncludedProperties = { IncludedResponseProperty.ReasoningEncryptedContent }, }, diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 5b8aa030e71..14f272498cd 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -40,7 +40,7 @@ public void AsIChatClient_ProducesExpectedMetadata() var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint }); - IChatClient chatClient = client.GetResponsesClient(model).AsIChatClient(); + IChatClient chatClient = client.GetResponsesClient().AsIChatClient(model); var metadata = chatClient.GetService(); Assert.Equal("openai", metadata?.ProviderName); Assert.Equal(endpoint, metadata?.ProviderUri); @@ -50,8 +50,8 @@ public void AsIChatClient_ProducesExpectedMetadata() [Fact] public void GetService_SuccessfullyReturnsUnderlyingClient() { - ResponsesClient openAIClient = new OpenAIClient(new ApiKeyCredential("key")).GetResponsesClient("model"); - IChatClient chatClient = openAIClient.AsIChatClient(); + ResponsesClient openAIClient = new OpenAIClient(new ApiKeyCredential("key")).GetResponsesClient(); + IChatClient chatClient = openAIClient.AsIChatClient("model"); Assert.Same(chatClient, chatClient.GetService()); Assert.Same(openAIClient, chatClient.GetService()); @@ -296,14 +296,11 @@ public async Task BasicReasoningResponse_Streaming() List updates = []; await foreach (var update in client.GetStreamingResponseAsync("Calculate the sum of the first 5 positive integers.", new() { - RawRepresentationFactory = options => new CreateResponseOptions + Reasoning = new() { - ReasoningOptions = new() - { - ReasoningEffortLevel = ResponseReasoningEffortLevel.Low, - ReasoningSummaryVerbosity = ResponseReasoningSummaryVerbosity.Detailed - } - } + Effort = ReasoningEffort.Low, + Output = ReasoningOutput.Full, + }, })) { updates.Add(update); @@ -5596,7 +5593,7 @@ public async Task ResponseWithInputImageHttpUrl_ParsesAsUriContent() var imageContent = userMessage.Contents.OfType().FirstOrDefault(); Assert.NotNull(imageContent); Assert.Equal("https://example.com/image.png", imageContent.Uri.ToString()); - Assert.Equal("image/*", imageContent.MediaType); + Assert.Equal("image/png", imageContent.MediaType); var assistantMessage = response.Messages.LastOrDefault(m => m.Role == ChatRole.Assistant); Assert.NotNull(assistantMessage); @@ -6121,8 +6118,8 @@ public async Task OpenAIApiTypeTag_SetToResponses(bool streaming) using VerbatimHttpHandler handler = new(new HttpHandlerExpectedInput(), Output); using HttpClient httpClient = new(handler); using IChatClient client = new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }) - .GetResponsesClient("gpt-4o-mini") - .AsIChatClient() + .GetResponsesClient() + .AsIChatClient("gpt-4o-mini") .AsBuilder() .UseOpenTelemetry(sourceName: sourceName) .Build(); @@ -6147,8 +6144,8 @@ private static IChatClient CreateResponseClient(HttpClient httpClient, string mo new OpenAIClient( new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }) - .GetResponsesClient(modelId) - .AsIChatClient(); + .GetResponsesClient() + .AsIChatClient(modelId); private static string ResponseStatusToRequestValue(ResponseStatus status) { diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.oai_aais.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.oai_aais.verified/aichatweb/Program.cs index 4bfb1ca7796..bc156c8638d 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.oai_aais.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.oai_aais.verified/aichatweb/Program.cs @@ -17,8 +17,8 @@ var openAIClient = new OpenAIClient( new ApiKeyCredential(builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException("Missing configuration: OpenAI:Key. See the README for details."))); -#pragma warning disable OPENAI001 // GetResponsesClient(string) is experimental and subject to change or removal in future updates. -var chatClient = openAIClient.GetResponsesClient("gpt-4o-mini").AsIChatClient(); +#pragma warning disable OPENAI001 // GetResponsesClient() is experimental and subject to change or removal in future updates. +var chatClient = openAIClient.GetResponsesClient().AsIChatClient("gpt-4o-mini"); #pragma warning restore OPENAI001 var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();