diff --git a/eng/packages/General.props b/eng/packages/General.props index 253fb51ce1b..b7066789238 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -16,7 +16,7 @@ - + diff --git a/eng/packages/TestOnly.props b/eng/packages/TestOnly.props index dcfc7c03525..d8748a4ae9c 100644 --- a/eng/packages/TestOnly.props +++ b/eng/packages/TestOnly.props @@ -2,7 +2,7 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index 1c1c175ccd4..15859a5744d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -4,6 +4,7 @@ - Updated tool mappings to recognize any `AIFunctionDeclaration`. - Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. +- Updated to depend on OpenAI 2.4.0 ## 9.8.0-preview.1.25412.6 diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs index d6b290f431b..51df2653695 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs @@ -18,7 +18,7 @@ public static class MicrosoftExtensionsAIResponsesExtensions /// The function to convert. /// An OpenAI representing . /// is . - public static ResponseTool AsOpenAIResponseTool(this AIFunctionDeclaration function) => + public static FunctionTool AsOpenAIResponseTool(this AIFunctionDeclaration function) => OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(function)); /// Creates a sequence of OpenAI instances from the specified input messages. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index 01b78994f38..424185db887 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -50,17 +49,9 @@ public OpenAIAssistantsChatClient(AssistantClient assistantClient, string assist { _client = Throw.IfNull(assistantClient); _assistantId = Throw.IfNullOrWhitespace(assistantId); - _defaultThreadId = defaultThreadId; - // https://github.com/openai/openai-dotnet/issues/215 - // The endpoint isn't currently exposed, so use reflection to get at it, temporarily. Once packages - // 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(AssistantClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(assistantClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; - - _metadata = new("openai", providerUrl); + _metadata = new("openai", assistantClient.Endpoint); } /// Initializes a new instance of the class for the specified . diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 415aef5901e..8c65766413f 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -37,18 +36,9 @@ internal sealed class OpenAIChatClient : IChatClient /// is . public OpenAIChatClient(ChatClient chatClient) { - _ = Throw.IfNull(chatClient); + _chatClient = Throw.IfNull(chatClient); - _chatClient = chatClient; - - // https://github.com/openai/openai-dotnet/issues/215 - // The endpoint and model aren't currently exposed, so use reflection to get at them, temporarily. Once packages - // 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(ChatClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(chatClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; - - _metadata = new("openai", providerUrl, _chatClient.Model); + _metadata = new("openai", chatClient.Endpoint, _chatClient.Model); } /// diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs index 84f3c5966b8..dbe9bf10237 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; @@ -18,9 +17,6 @@ namespace Microsoft.Extensions.AI; /// An for an OpenAI . internal sealed class OpenAIEmbeddingGenerator : IEmbeddingGenerator> { - /// Default OpenAI endpoint. - private const string DefaultOpenAIEndpoint = "https://api.openai.com/v1"; - /// Metadata about the embedding generator. private readonly EmbeddingGeneratorMetadata _metadata; @@ -37,24 +33,15 @@ internal sealed class OpenAIEmbeddingGenerator : IEmbeddingGenerator is not positive. public OpenAIEmbeddingGenerator(EmbeddingClient embeddingClient, int? defaultModelDimensions = null) { - _ = Throw.IfNull(embeddingClient); + _embeddingClient = Throw.IfNull(embeddingClient); + _dimensions = defaultModelDimensions; + if (defaultModelDimensions < 1) { Throw.ArgumentOutOfRangeException(nameof(defaultModelDimensions), "Value must be greater than 0."); } - _embeddingClient = embeddingClient; - _dimensions = defaultModelDimensions; - - // https://github.com/openai/openai-dotnet/issues/215 - // The endpoint and model aren't currently exposed, so use reflection to get at them, temporarily. Once packages - // 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. - string providerUrl = (typeof(EmbeddingClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(embeddingClient) as Uri)?.ToString() ?? - DefaultOpenAIEndpoint; - - _metadata = CreateMetadata("openai", providerUrl, _embeddingClient.Model, defaultModelDimensions); + _metadata = new("openai", embeddingClient.Endpoint, _embeddingClient.Model, defaultModelDimensions); } /// @@ -98,10 +85,6 @@ void IDisposable.Dispose() null; } - /// Creates the for this instance. - private static EmbeddingGeneratorMetadata CreateMetadata(string providerName, string providerUrl, string? defaultModelId, int? defaultModelDimensions) => - new(providerName, Uri.TryCreate(providerUrl, UriKind.Absolute, out Uri? providerUri) ? providerUri : null, defaultModelId, defaultModelDimensions); - /// Converts an extensions options instance to an OpenAI options instance. private OpenAI.Embeddings.EmbeddingGenerationOptions ToOpenAIOptions(EmbeddingGenerationOptions? options) { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs index fe1cb399e0d..b2ceb5cb317 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs @@ -45,18 +45,9 @@ internal sealed class OpenAIImageGenerator : IImageGenerator /// is . public OpenAIImageGenerator(ImageClient imageClient) { - _ = Throw.IfNull(imageClient); + _imageClient = Throw.IfNull(imageClient); - _imageClient = imageClient; - - // https://github.com/openai/openai-dotnet/issues/215 - // The endpoint and model aren't currently exposed, so use reflection to get at them, temporarily. Once packages - // 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(ImageClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(imageClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; - - _metadata = new("openai", providerUrl, _imageClient.Model); + _metadata = new("openai", imageClient.Endpoint, _imageClient.Model); } /// @@ -143,7 +134,7 @@ private static ImageGenerationResponse ToImageGenerationResponse(GeneratedImageC // 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 - IDictionary? additionalRawData = typeof(GeneratedImageCollection) + var additionalRawData = typeof(GeneratedImageCollection) .GetProperty("SerializedAdditionalRawData", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(generatedImages) as IDictionary; @@ -154,7 +145,7 @@ private static ImageGenerationResponse ToImageGenerationResponse(GeneratedImageC contentType = $"image/{outputFormatString}"; } - List contents = new(); + List contents = []; foreach (GeneratedImage image in generatedImages) { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index b5d3c09de32..0acfef52eb0 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -47,16 +46,12 @@ public OpenAIResponsesChatClient(OpenAIResponseClient responseClient) _responseClient = responseClient; - // https://github.com/openai/openai-dotnet/issues/215 - // The endpoint and model aren't currently exposed, so use reflection to get at them, temporarily. Once packages - // 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(OpenAIResponseClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(responseClient) as Uri ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; + // https://github.com/openai/openai-dotnet/issues/662 + // Update to avoid reflection once OpenAIResponseClient.Model is exposed publicly. string? model = typeof(OpenAIResponseClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?.GetValue(responseClient) as string; - _metadata = new("openai", providerUrl, model); + _metadata = new("openai", responseClient.Endpoint, model); } /// @@ -204,7 +199,7 @@ internal static async IAsyncEnumerable FromOpenAIStreamingRe string? lastMessageId = null; ChatRole? lastRole = null; Dictionary outputIndexToMessages = []; - Dictionary? functionCallInfos = null; + Dictionary? functionCallItems = null; await foreach (var streamingUpdate in streamingResponseUpdates.WithCancellation(cancellationToken).ConfigureAwait(false)) { @@ -234,7 +229,7 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => var update = CreateUpdate(ToUsageDetails(completedUpdate.Response) is { } usage ? new UsageContent(usage) : null); update.FinishReason = ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ?? - (functionCallInfos is not null ? ChatFinishReason.ToolCalls : + (functionCallItems is not null ? ChatFinishReason.ToolCalls : ChatFinishReason.Stop); yield return update; break; @@ -248,7 +243,7 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => break; case FunctionCallResponseItem fcri: - (functionCallInfos ??= [])[outputItemAddedUpdate.OutputIndex] = new(fcri); + (functionCallItems ??= [])[outputItemAddedUpdate.OutputIndex] = fcri; break; } @@ -283,28 +278,18 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => break; } - case StreamingResponseFunctionCallArgumentsDeltaUpdate functionCallArgumentsDeltaUpdate: - { - if (functionCallInfos?.TryGetValue(functionCallArgumentsDeltaUpdate.OutputIndex, out FunctionCallInfo? callInfo) is true) - { - _ = (callInfo.Arguments ??= new()).Append(functionCallArgumentsDeltaUpdate.Delta); - } - - goto default; - } - case StreamingResponseFunctionCallArgumentsDoneUpdate functionCallOutputDoneUpdate: { - if (functionCallInfos?.TryGetValue(functionCallOutputDoneUpdate.OutputIndex, out FunctionCallInfo? callInfo) is true) + if (functionCallItems?.TryGetValue(functionCallOutputDoneUpdate.OutputIndex, out FunctionCallResponseItem? callInfo) is true) { - _ = functionCallInfos.Remove(functionCallOutputDoneUpdate.OutputIndex); + _ = functionCallItems.Remove(functionCallOutputDoneUpdate.OutputIndex); var fcc = OpenAIClientExtensions.ParseCallContent( - callInfo.Arguments?.ToString() ?? string.Empty, - callInfo.ResponseItem.CallId, - callInfo.ResponseItem.FunctionName); + functionCallOutputDoneUpdate.FunctionArguments.ToString(), + callInfo.CallId, + callInfo.FunctionName); - lastMessageId = callInfo.ResponseItem.Id; + lastMessageId = callInfo.Id; lastRole = ChatRole.Assistant; yield return CreateUpdate(fcc); @@ -329,18 +314,16 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => }); break; - default: - { - if (streamingUpdate.GetType() == _internalResponseReasoningSummaryTextDeltaEventType && - _summaryTextDeltaProperty?.GetValue(streamingUpdate) is string delta) - { - yield return CreateUpdate(new TextReasoningContent(delta)); - break; - } + // Replace with public StreamingResponseReasoningSummaryTextDelta when available + case StreamingResponseUpdate when + streamingUpdate.GetType() == _internalResponseReasoningSummaryTextDeltaEventType && + _summaryTextDeltaProperty?.GetValue(streamingUpdate) is string delta: + yield return CreateUpdate(new TextReasoningContent(delta)); + break; + default: yield return CreateUpdate(); break; - } } } } @@ -351,7 +334,7 @@ void IDisposable.Dispose() // Nothing to dispose. Implementation required for the IChatClient interface. } - internal static ResponseTool ToResponseTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null) + internal static FunctionTool ToResponseTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null) { bool? strict = OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ?? @@ -359,9 +342,9 @@ internal static ResponseTool ToResponseTool(AIFunctionDeclaration aiFunction, Ch return ResponseTool.CreateFunctionTool( aiFunction.Name, - aiFunction.Description, OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict), - strict ?? false); + strict, + aiFunction.Description); } /// Creates a from a . @@ -419,17 +402,17 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt break; case HostedWebSearchTool webSearchTool: - WebSearchUserLocation? location = null; - if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchUserLocation), out object? objLocation)) + WebSearchToolLocation? location = null; + if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation)) { - location = objLocation as WebSearchUserLocation; + location = objLocation as WebSearchToolLocation; } - WebSearchContextSize? size = null; - if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchContextSize), out object? objSize) && - objSize is WebSearchContextSize) + WebSearchToolContextSize? size = null; + if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) && + objSize is WebSearchToolContextSize) { - size = (WebSearchContextSize)objSize; + size = (WebSearchToolContextSize)objSize; } result.Tools.Add(ResponseTool.CreateWebSearchTool(location, size)); @@ -690,14 +673,29 @@ private static void PopulateAnnotations(ResponseContentPart source, AIContent de { foreach (var ota in source.OutputTextAnnotations) { - (destination.Annotations ??= []).Add(new CitationAnnotation + CitationAnnotation ca = new() { RawRepresentation = ota, - AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = ota.UriCitationStartIndex, EndIndex = ota.UriCitationEndIndex }], - Title = ota.UriCitationTitle, - Url = ota.UriCitationUri, - FileId = ota.FileCitationFileId ?? ota.FilePathFileId, - }); + }; + + switch (ota) + { + case UriCitationMessageAnnotation ucma: + ca.AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = ucma.StartIndex, EndIndex = ucma.EndIndex }]; + ca.Title = ucma.Title; + ca.Url = ucma.Uri; + break; + + case FilePathMessageAnnotation fpma: + ca.FileId = fpma.FileId; + break; + + case FileCitationMessageAnnotation fcma: + ca.FileId = fcma.FileId; + break; + } + + (destination.Annotations ??= []).Add(ca); } } } @@ -747,12 +745,4 @@ private static List ToResponseContentParts(IList return parts; } - - /// POCO representing function calling info. - /// Used to concatenation information for a single function call from across multiple streaming updates. - private sealed class FunctionCallInfo(FunctionCallResponseItem item) - { - public readonly FunctionCallResponseItem ResponseItem = item; - public StringBuilder? Arguments; - } } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs index 2e2503335f3..06051a54c26 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAISpeechToTextClient.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -37,18 +36,9 @@ internal sealed class OpenAISpeechToTextClient : ISpeechToTextClient /// The underlying client. public OpenAISpeechToTextClient(AudioClient audioClient) { - _ = Throw.IfNull(audioClient); + _audioClient = Throw.IfNull(audioClient); - _audioClient = audioClient; - - // https://github.com/openai/openai-dotnet/issues/215 - // The endpoint and model aren't currently exposed, so use reflection to get at them, temporarily. Once packages - // 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 ?? OpenAIClientExtensions.DefaultOpenAIEndpoint; - - _metadata = new("openai", providerUrl, _audioClient.Model); + _metadata = new("openai", audioClient.Endpoint, _audioClient.Model); } /// diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs index c87625cf143..448de8d11df 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs @@ -942,7 +942,7 @@ public virtual async Task OpenTelemetry_CanEmitTracesAndMetrics() var activity = Assert.Single(activities); Assert.StartsWith("chat", activity.DisplayName); - Assert.StartsWith("http", (string)activity.GetTagItem("server.address")!); + Assert.Contains(".", (string)activity.GetTagItem("server.address")!); Assert.Equal(chatClient.GetService()?.ProviderUri?.Port, (int)activity.GetTagItem("server.port")!); Assert.NotNull(activity.Id); Assert.NotEmpty(activity.Id); @@ -1276,7 +1276,7 @@ public virtual async Task SummarizingChatReducer_Streaming() new(ChatRole.Assistant, "That sounds impactful! AI in education has so much potential."), new(ChatRole.User, "Yes, we focus on personalized learning experiences."), new(ChatRole.Assistant, "Personalized learning is the future of education!"), - new(ChatRole.User, "What's my name and profession?") + new(ChatRole.User, "Was anyone named in the conversation? Provide their name and job.") ]; StringBuilder sb = new(); @@ -1296,7 +1296,7 @@ public virtual async Task SummarizingChatReducer_Streaming() Assert.Contains("Bob", m.Text); }, m => Assert.StartsWith("Personalized learning", m.Text, StringComparison.Ordinal), - m => Assert.Equal("What's my name and profession?", m.Text)); + m => Assert.Equal("Was anyone named in the conversation? Provide their name and job.", m.Text)); string responseText = sb.ToString(); Assert.Contains("Bob", responseText); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index 640dab54f8e..bdf3b2e7c0a 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -8,8 +8,6 @@ using System.ComponentModel; using System.Linq; using System.Net.Http; -using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; @@ -655,22 +653,6 @@ public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_Stream Assert.Equal("Hello! How can I assist you today?", responseText); } - /// Used to create the JSON payload for an OpenAI chat tool description. - internal sealed class ChatToolJson - { - [JsonPropertyName("type")] - public string Type { get; set; } = "object"; - - [JsonPropertyName("required")] - public HashSet Required { get; set; } = []; - - [JsonPropertyName("properties")] - public Dictionary Properties { get; set; } = []; - - [JsonPropertyName("additionalProperties")] - public bool AdditionalProperties { get; set; } - } - [Fact] public async Task StronglyTypedOptions_AllSent() { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 630af9e34b0..007e86288f5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -27,7 +27,7 @@ public async Task UseWebSearch_AnnotationsReflectResults() SkipIfNotEnabled(); var response = await ChatClient.GetResponseAsync( - "Write a paragraph about the three most recent blog posts on the .NET blog. Cite your sources.", + "Write a paragraph about .NET based on at least three recent news articles. Cite your sources.", new() { Tools = [new HostedWebSearchTool()] }); ChatMessage m = Assert.Single(response.Messages); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index ca71aee550f..014fae0d39f 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -8,7 +8,6 @@ using System.ComponentModel; using System.Linq; using System.Net.Http; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; @@ -543,20 +542,70 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio { const string Input = """ { - "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}], - "model":"gpt-4o-mini", - "max_output_tokens":10, - "previous_response_id":"resp_42", - "top_p":0.5, - "temperature":0.5, - "parallel_tool_calls":true, - "text": {"format": {"type": "text"} - }, - "tool_choice":"auto", - "tools":[ - {"description":"Gets the age of the specified person.","name":"GetPersonAge","parameters":{"additionalProperties":false,"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}},"required":["personName"],"type":"object"},"strict":false,"type":"function"}, - {"description":"Gets the age of the specified person.","name":"GetPersonAge","parameters":{"additionalProperties":false,"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}},"required":["personName"],"type":"object"},"strict":false,"type":"function"} - ] + "temperature": 0.5, + "top_p": 0.5, + "previous_response_id": "resp_42", + "model": "gpt-4o-mini", + "max_output_tokens": 10, + "text": { + "format": { + "type": "text" + } + }, + "tools": [ + { + "type": "function", + "name": "GetPersonAge", + "description": "Gets the age of the specified person.", + "parameters": { + "type": "object", + "required": [ + "personName" + ], + "properties": { + "personName": { + "description": "The person whose age is being requested", + "type": "string" + } + }, + "additionalProperties": false + }, + "strict": null + }, + { + "type": "function", + "name": "GetPersonAge", + "description": "Gets the age of the specified person.", + "parameters": { + "type": "object", + "required": [ + "personName" + ], + "properties": { + "personName": { + "description": "The person whose age is being requested", + "type": "string" + } + }, + "additionalProperties": false + }, + "strict": null + } + ], + "tool_choice": "auto", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "hello" + } + ] + } + ], + "parallel_tool_calls": true } """; @@ -640,7 +689,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio TextFormat = ResponseTextFormat.CreateTextFormat() }, }; - openAIOptions.Tools.Add(ToOpenAIResponseChatTool(tool)); + openAIOptions.Tools.Add(tool.AsOpenAIResponseTool()); return openAIOptions; }, ModelId = null, @@ -775,14 +824,6 @@ public async Task MultipleOutputItems_NonStreaming() Assert.Equal(36, response.Usage.TotalTokenCount); } - /// Converts an Extensions function to an OpenAI response chat tool. - private static ResponseTool ToOpenAIResponseChatTool(AIFunction aiFunction) - { - var tool = JsonSerializer.Deserialize(aiFunction.JsonSchema)!; - var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool)); - return ResponseTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters); - } - private static IChatClient CreateResponseClient(HttpClient httpClient, string modelId) => new OpenAIClient( new ApiKeyCredential("apikey"),