diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md index 8e1b571967e..a586329aa33 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md @@ -1,5 +1,11 @@ # Microsoft.Extensions.AI.Abstractions Release History +## 10.1.1 (NOT YET RELEASED) + +- Added `InputCachedTokenCount` and `ReasoningTokenCount` to `UsageDetails`. +- Added constructors to `HostedCodeInterpreterTool`, `HostedFileSearchTool`, `HostedImageGeneratorTool`, `HostedMcpServerTool`, + and `HostedWebSearchTool` that accept a dictionary for `AdditionalProperties`. + ## 10.1.0 - Fixed package references for net10.0 asset. diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json index b0b47c4ef6a..e401502d82b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json @@ -1918,9 +1918,17 @@ { "Member": "Microsoft.Extensions.AI.HostedCodeInterpreterTool.HostedCodeInterpreterTool();", "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.HostedCodeInterpreterTool.HostedCodeInterpreterTool(System.Collections.Generic.IReadOnlyDictionary? additionalProperties);", + "Stage": "Stable" } ], "Properties": [ + { + "Member": "override System.Collections.Generic.IReadOnlyDictionary Microsoft.Extensions.AI.HostedCodeInterpreterTool.AdditionalProperties { get; }", + "Stage": "Stable" + }, { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedCodeInterpreterTool.Inputs { get; set; }", "Stage": "Stable" @@ -1938,9 +1946,17 @@ { "Member": "Microsoft.Extensions.AI.HostedFileSearchTool.HostedFileSearchTool();", "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.HostedFileSearchTool.HostedFileSearchTool(System.Collections.Generic.IReadOnlyDictionary? additionalProperties);", + "Stage": "Stable" } ], "Properties": [ + { + "Member": "override System.Collections.Generic.IReadOnlyDictionary Microsoft.Extensions.AI.HostedFileSearchTool.AdditionalProperties { get; }", + "Stage": "Stable" + }, { "Member": "System.Collections.Generic.IList? Microsoft.Extensions.AI.HostedFileSearchTool.Inputs { get; set; }", "Stage": "Stable" @@ -1962,9 +1978,17 @@ { "Member": "Microsoft.Extensions.AI.HostedWebSearchTool.HostedWebSearchTool();", "Stage": "Stable" + }, + { + "Member": "Microsoft.Extensions.AI.HostedWebSearchTool.HostedWebSearchTool(System.Collections.Generic.IReadOnlyDictionary? additionalProperties);", + "Stage": "Stable" } ], "Properties": [ + { + "Member": "override System.Collections.Generic.IReadOnlyDictionary Microsoft.Extensions.AI.HostedWebSearchTool.AdditionalProperties { get; }", + "Stage": "Stable" + }, { "Member": "override string Microsoft.Extensions.AI.HostedWebSearchTool.Name { get; }", "Stage": "Stable" diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedCodeInterpreterTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedCodeInterpreterTool.cs index 4bd63a0df75..f0ab845a110 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedCodeInterpreterTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedCodeInterpreterTool.cs @@ -12,14 +12,27 @@ namespace Microsoft.Extensions.AI; /// public class HostedCodeInterpreterTool : AITool { + /// Any additional properties associated with the tool. + private IReadOnlyDictionary? _additionalProperties; + /// Initializes a new instance of the class. public HostedCodeInterpreterTool() { } + /// Initializes a new instance of the class. + /// Any additional properties associated with the tool. + public HostedCodeInterpreterTool(IReadOnlyDictionary? additionalProperties) + { + _additionalProperties = additionalProperties; + } + /// public override string Name => "code_interpreter"; + /// + public override IReadOnlyDictionary AdditionalProperties => _additionalProperties ?? base.AdditionalProperties; + /// Gets or sets a collection of to be used as input to the code interpreter tool. /// /// Services support different varied kinds of inputs. Most support the IDs of files that are hosted by the service, diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedFileSearchTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedFileSearchTool.cs index b130e26b647..3456c301f17 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedFileSearchTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedFileSearchTool.cs @@ -12,14 +12,27 @@ namespace Microsoft.Extensions.AI; /// public class HostedFileSearchTool : AITool { + /// Any additional properties associated with the tool. + private IReadOnlyDictionary? _additionalProperties; + /// Initializes a new instance of the class. public HostedFileSearchTool() { } + /// Initializes a new instance of the class. + /// Any additional properties associated with the tool. + public HostedFileSearchTool(IReadOnlyDictionary? additionalProperties) + { + _additionalProperties = additionalProperties; + } + /// public override string Name => "file_search"; + /// + public override IReadOnlyDictionary AdditionalProperties => _additionalProperties ?? base.AdditionalProperties; + /// Gets or sets a collection of to be used as input to the file search tool. /// /// If no explicit inputs are provided, the service determines what inputs should be searched. Different services diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedImageGenerationTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedImageGenerationTool.cs index aca072653ab..4b75d2d5f08 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedImageGenerationTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedImageGenerationTool.cs @@ -1,6 +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 System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Microsoft.Extensions.AI; @@ -13,6 +14,9 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class HostedImageGenerationTool : AITool { + /// Any additional properties associated with the tool. + private IReadOnlyDictionary? _additionalProperties; + /// /// Initializes a new instance of the class with the specified options. /// @@ -20,6 +24,19 @@ public HostedImageGenerationTool() { } + /// Initializes a new instance of the class. + /// Any additional properties associated with the tool. + public HostedImageGenerationTool(IReadOnlyDictionary? additionalProperties) + { + _additionalProperties = additionalProperties; + } + + /// + public override string Name => "image_generation"; + + /// + public override IReadOnlyDictionary AdditionalProperties => _additionalProperties ?? base.AdditionalProperties; + /// /// Gets or sets the options used to configure image generation. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs index aa33a581710..fbc80fe4d59 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -14,6 +14,9 @@ namespace Microsoft.Extensions.AI; [Experimental("MEAI001")] public class HostedMcpServerTool : AITool { + /// Any additional properties associated with the tool. + private IReadOnlyDictionary? _additionalProperties; + /// /// Initializes a new instance of the class. /// @@ -27,6 +30,20 @@ public HostedMcpServerTool(string serverName, string serverAddress) ServerAddress = Throw.IfNullOrWhitespace(serverAddress); } + /// + /// Initializes a new instance of the class. + /// + /// The name of the remote MCP server. + /// The address of the remote MCP server. This may be a URL, or in the case of a service providing built-in MCP servers with known names, it can be such a name. + /// Any additional properties associated with the tool. + /// or is . + /// or is empty or composed entirely of whitespace. + public HostedMcpServerTool(string serverName, string serverAddress, IReadOnlyDictionary? additionalProperties) + : this(serverName, serverAddress) + { + _additionalProperties = additionalProperties; + } + /// /// Initializes a new instance of the class. /// @@ -40,6 +57,21 @@ public HostedMcpServerTool(string serverName, Uri serverUrl) { } + /// + /// Initializes a new instance of the class. + /// + /// The name of the remote MCP server. + /// The URL of the remote MCP server. + /// Any additional properties associated with the tool. + /// or is . + /// is empty or composed entirely of whitespace. + /// is not an absolute URL. + public HostedMcpServerTool(string serverName, Uri serverUrl, IReadOnlyDictionary? additionalProperties) + : this(serverName, ValidateUrl(serverUrl)) + { + _additionalProperties = additionalProperties; + } + private static string ValidateUrl(Uri serverUrl) { _ = Throw.IfNull(serverUrl); @@ -55,6 +87,9 @@ private static string ValidateUrl(Uri serverUrl) /// public override string Name => "mcp"; + /// + public override IReadOnlyDictionary AdditionalProperties => _additionalProperties ?? base.AdditionalProperties; + /// /// Gets the name of the remote MCP server that is used to identify it. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedWebSearchTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedWebSearchTool.cs index 19d25510d19..c107473eb49 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedWebSearchTool.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedWebSearchTool.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; + namespace Microsoft.Extensions.AI; /// Represents a hosted tool that can be specified to an AI service to enable it to perform web searches. @@ -10,11 +12,24 @@ namespace Microsoft.Extensions.AI; /// public class HostedWebSearchTool : AITool { + /// Any additional properties associated with the tool. + private IReadOnlyDictionary? _additionalProperties; + /// Initializes a new instance of the class. public HostedWebSearchTool() { } + /// Initializes a new instance of the class. + /// Any additional properties associated with the tool. + public HostedWebSearchTool(IReadOnlyDictionary? additionalProperties) + { + _additionalProperties = additionalProperties; + } + /// public override string Name => "web_search"; + + /// + public override IReadOnlyDictionary AdditionalProperties => _additionalProperties ?? base.AdditionalProperties; } diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md index 607fe6228d6..6e087263cca 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md @@ -1,5 +1,12 @@ # Microsoft.Extensions.AI.OpenAI Release History +## 10.1.1-preview.1.? (NOT YET RELEASED) + +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. +- Updated the OpenAI Responses and Chat Completion `IChatClient`s to populate `UsageDetails`'s `InputCachedTokenCount` and `ReasoningTokenCount`. +- Updated handling of `HostedWebSearchTool`, `HostedFileSearchTool`, and `HostedImageGenerationTool` to pull OpenAI-specific + options from `AdditionalProperties`. + ## 10.1.0-preview.1.25608.1 - Fixed package references for net10.0 asset. diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index 065ad80d23a..96f3d9113c2 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -409,7 +409,10 @@ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition( break; case HostedFileSearchTool fileSearchTool: - _ = toolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount)); + var fst = ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount); + fst.RankingOptions = fileSearchTool.GetProperty(nameof(FileSearchToolDefinition.RankingOptions)); + _ = toolsOverride.Add(fst); + if (fileSearchTool.Inputs is { Count: > 0 } fileSearchInputs) { foreach (var input in fileSearchInputs) diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs index 285b2c1e7ae..f696e394b44 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs @@ -242,6 +242,10 @@ internal static void PatchModelIfNotSet(ref JsonPatch patch, string? modelId) } } + /// Gets the typed property of the specified name from the tool's . + internal static T? GetProperty(this AITool tool, string name) => + tool.AdditionalProperties?.TryGetValue(name, out object? value) is true && value is T tValue ? tValue : default; + /// Used to create the JSON payload for an OpenAI tool description. internal sealed class ToolJson { diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 1c172db283a..98c30b73526 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -544,25 +544,17 @@ void IDisposable.Dispose() return ToResponseTool(aiFunction, options); case HostedWebSearchTool webSearchTool: - WebSearchToolLocation? location = null; - if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation)) - { - location = objLocation as WebSearchToolLocation; - } - - WebSearchToolContextSize? size = null; - if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) && - objSize is WebSearchToolContextSize) - { - size = (WebSearchToolContextSize)objSize; - } - - return ResponseTool.CreateWebSearchTool(location, size); + return ResponseTool.CreateWebSearchTool( + webSearchTool.GetProperty(nameof(WebSearchTool.UserLocation)), + webSearchTool.GetProperty(nameof(WebSearchTool.SearchContextSize)), + webSearchTool.GetProperty(nameof(WebSearchTool.Filters))); case HostedFileSearchTool fileSearchTool: return ResponseTool.CreateFileSearchTool( fileSearchTool.Inputs?.OfType().Select(c => c.VectorStoreId) ?? [], - fileSearchTool.MaximumResultCount); + fileSearchTool.MaximumResultCount, + fileSearchTool.GetProperty(nameof(FileSearchTool.RankingOptions)), + fileSearchTool.GetProperty(nameof(FileSearchTool.Filters))); case HostedImageGenerationTool imageGenerationTool: return ToImageResponseTool(imageGenerationTool); @@ -642,36 +634,31 @@ internal static FunctionTool ToResponseTool(AIFunctionDeclaration aiFunction, Ch internal static ImageGenerationTool ToImageResponseTool(HostedImageGenerationTool imageGenerationTool) { - ImageGenerationTool result = new(); - ImageGenerationOptions? imageGenerationOptions = imageGenerationTool.Options; - - // Model: Image generation model - result.Model = imageGenerationOptions?.ModelId; - - // Size: Image dimensions (e.g., 1024x1024, 1024x1536) - if (imageGenerationOptions?.ImageSize is not null) - { - result.Size = new ImageGenerationToolSize( - imageGenerationOptions.ImageSize.Value.Width, - imageGenerationOptions.ImageSize.Value.Height); - } + ImageGenerationOptions? options = imageGenerationTool.Options; - // OutputFileFormat: File output format - if (imageGenerationOptions?.MediaType is not null) + return new() { - result.OutputFileFormat = imageGenerationOptions.MediaType switch - { - "image/png" => ImageGenerationToolOutputFileFormat.Png, - "image/jpeg" => ImageGenerationToolOutputFileFormat.Jpeg, - "image/webp" => ImageGenerationToolOutputFileFormat.Webp, - _ => null, - }; - } - - // PartialImageCount: Whether to return partial images during generation - result.PartialImageCount ??= imageGenerationOptions?.StreamingCount; - - return result; + Background = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.Background)), + InputFidelity = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.InputFidelity)), + InputImageMask = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.InputImageMask)), + Model = options?.ModelId, + ModerationLevel = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.ModerationLevel)), + OutputCompressionFactor = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.OutputCompressionFactor)), + OutputFileFormat = options?.MediaType is not null ? + options.MediaType switch + { + "image/png" => ImageGenerationToolOutputFileFormat.Png, + "image/jpeg" => ImageGenerationToolOutputFileFormat.Jpeg, + "image/webp" => ImageGenerationToolOutputFileFormat.Webp, + _ => null, + } : + null, + PartialImageCount = options?.StreamingCount, + Quality = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.Quality)), + Size = options?.ImageSize is not null ? + new ImageGenerationToolSize(options.ImageSize.Value.Width, options.ImageSize.Value.Height) : + null + }; } /// Creates a from a . diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md index 4f06605b47f..816e8b92267 100644 --- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md +++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md @@ -1,5 +1,9 @@ # Microsoft.Extensions.AI Release History +## 10.1.1 (NOT YET RELEASED) + +- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. + ## 10.1.0 - Fixed package references for net10.0 asset. diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedCodeInterpreterToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedCodeInterpreterToolTests.cs index 19044a6a295..34f6dd32f1e 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedCodeInterpreterToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedCodeInterpreterToolTests.cs @@ -1,6 +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 System.Collections.Generic; using Xunit; namespace Microsoft.Extensions.AI; @@ -18,6 +19,24 @@ public void Constructor_Roundtrips() Assert.Equal(tool.Name, tool.ToString()); } + [Fact] + public void Constructor_AdditionalProperties_Roundtrips() + { + var props = new Dictionary { ["key"] = "value" }; + var tool = new HostedCodeInterpreterTool(props); + + Assert.Equal("code_interpreter", tool.Name); + Assert.Same(props, tool.AdditionalProperties); + } + + [Fact] + public void Constructor_NullAdditionalProperties_UsesEmpty() + { + var tool = new HostedCodeInterpreterTool(null); + + Assert.Empty(tool.AdditionalProperties); + } + [Fact] public void Properties_Roundtrip() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedFileSearchToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedFileSearchToolTests.cs index e2d71a65013..cffa5b418b1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedFileSearchToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedFileSearchToolTests.cs @@ -1,6 +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 System.Collections.Generic; using Xunit; namespace Microsoft.Extensions.AI; @@ -19,6 +20,24 @@ public void Constructor_Roundtrips() Assert.Equal(tool.Name, tool.ToString()); } + [Fact] + public void Constructor_AdditionalProperties_Roundtrips() + { + var props = new Dictionary { ["key"] = "value" }; + var tool = new HostedFileSearchTool(props); + + Assert.Equal("file_search", tool.Name); + Assert.Same(props, tool.AdditionalProperties); + } + + [Fact] + public void Constructor_NullAdditionalProperties_UsesEmpty() + { + var tool = new HostedFileSearchTool(null); + + Assert.Empty(tool.AdditionalProperties); + } + [Fact] public void Properties_Roundtrip() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedImageGenerationToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedImageGenerationToolTests.cs new file mode 100644 index 00000000000..1f14ca7175d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedImageGenerationToolTests.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class HostedImageGenerationToolTests +{ + [Fact] + public void Constructor_Roundtrips() + { + var tool = new HostedImageGenerationTool(); + Assert.Equal("image_generation", tool.Name); + Assert.Empty(tool.Description); + Assert.Empty(tool.AdditionalProperties); + Assert.Null(tool.Options); + Assert.Equal(tool.Name, tool.ToString()); + } + + [Fact] + public void Constructor_AdditionalProperties_Roundtrips() + { + var props = new Dictionary { ["key"] = "value" }; + var tool = new HostedImageGenerationTool(props); + + Assert.Equal("image_generation", tool.Name); + Assert.Same(props, tool.AdditionalProperties); + } + + [Fact] + public void Constructor_NullAdditionalProperties_UsesEmpty() + { + var tool = new HostedImageGenerationTool(null); + + Assert.Empty(tool.AdditionalProperties); + } + + [Fact] + public void Options_Roundtrip() + { + var options = new ImageGenerationOptions(); + var tool = new HostedImageGenerationTool + { + Options = options + }; + + Assert.Same(options, tool.Options); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs index ec1dc407973..aa23cfd3ff4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -26,6 +26,36 @@ public void Constructor_PropsDefault() Assert.Null(tool.ApprovalMode); } + [Fact] + public void Constructor_AdditionalProperties_String_Roundtrips() + { + var props = new Dictionary { ["key"] = "value" }; + HostedMcpServerTool tool = new("serverName", "connector_id", props); + + Assert.Equal("serverName", tool.ServerName); + Assert.Equal("connector_id", tool.ServerAddress); + Assert.Same(props, tool.AdditionalProperties); + } + + [Fact] + public void Constructor_AdditionalProperties_Uri_Roundtrips() + { + var props = new Dictionary { ["key"] = "value" }; + HostedMcpServerTool tool = new("serverName", new Uri("https://localhost/"), props); + + Assert.Equal("serverName", tool.ServerName); + Assert.Equal("https://localhost/", tool.ServerAddress); + Assert.Same(props, tool.AdditionalProperties); + } + + [Fact] + public void Constructor_NullAdditionalProperties_UsesEmpty() + { + HostedMcpServerTool tool = new("serverName", "connector_id", null); + + Assert.Empty(tool.AdditionalProperties); + } + [Fact] public void Constructor_Roundtrips() { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedWebSearchToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedWebSearchToolTests.cs index 4bb6ca4b847..7040289a210 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedWebSearchToolTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedWebSearchToolTests.cs @@ -1,6 +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 System.Collections.Generic; using Xunit; namespace Microsoft.Extensions.AI; @@ -16,4 +17,22 @@ public void Constructor_Roundtrips() Assert.Empty(tool.AdditionalProperties); Assert.Equal(tool.Name, tool.ToString()); } + + [Fact] + public void Constructor_AdditionalProperties_Roundtrips() + { + var props = new Dictionary { ["key"] = "value" }; + var tool = new HostedWebSearchTool(props); + + Assert.Equal("web_search", tool.Name); + Assert.Same(props, tool.AdditionalProperties); + } + + [Fact] + public void Constructor_NullAdditionalProperties_UsesEmpty() + { + var tool = new HostedWebSearchTool(null); + + Assert.Empty(tool.AdditionalProperties); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs index 9c4ffeefdcd..0ab4ea2c6b1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs @@ -141,8 +141,8 @@ public void AsOpenAIResponseTool_WithHostedWebSearchToolWithAdditionalProperties var location = WebSearchToolLocation.CreateApproximateLocation("US", "Region", "City", "UTC"); var webSearchTool = new HostedWebSearchToolWithProperties(new Dictionary { - [nameof(WebSearchToolLocation)] = location, - [nameof(WebSearchToolContextSize)] = WebSearchToolContextSize.High + [nameof(WebSearchTool.UserLocation)] = location, + [nameof(WebSearchTool.SearchContextSize)] = WebSearchToolContextSize.High }); var result = webSearchTool.AsOpenAIResponseTool(); @@ -205,6 +205,30 @@ public void AsOpenAIResponseTool_WithHostedFileSearchToolWithMaxResults_Produces Assert.Equal(10, tool.MaxResultCount); } + [Fact] + public void AsOpenAIResponseTool_WithHostedFileSearchToolWithAdditionalProperties_ProducesValidFileSearchTool() + { + var rankingOptions = new FileSearchToolRankingOptions { ScoreThreshold = 0.5f }; + var filters = BinaryData.FromString("{\"type\":\"eq\",\"key\":\"status\",\"value\":\"published\"}"); + var fileSearchTool = new HostedFileSearchTool(new Dictionary + { + [nameof(FileSearchTool.RankingOptions)] = rankingOptions, + [nameof(FileSearchTool.Filters)] = filters + }) + { + MaximumResultCount = 15 + }; + + var result = fileSearchTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.NotNull(tool.RankingOptions); + Assert.Equal(0.5f, tool.RankingOptions.ScoreThreshold); + Assert.NotNull(tool.Filters); + Assert.Equal(15, tool.MaxResultCount); + } + [Fact] public void AsOpenAIResponseTool_WithHostedCodeInterpreterTool_ProducesValidCodeInterpreterTool() { @@ -236,6 +260,99 @@ public void AsOpenAIResponseTool_WithHostedCodeInterpreterToolWithFiles_Produces Assert.Equal(fileContent.FileId, autoContainerConfig.FileIds[0]); } + [Fact] + public void AsOpenAIResponseTool_WithHostedImageGenerationTool_ProducesValidImageGenerationTool() + { + var imageGenTool = new HostedImageGenerationTool + { + Options = new ImageGenerationOptions { MediaType = "image/png" } + }; + + var result = imageGenTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.NotNull(tool); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedImageGenerationToolWithOptions_ProducesValidImageGenerationTool() + { + var imageGenTool = new HostedImageGenerationTool + { + Options = new ImageGenerationOptions + { + ModelId = "gpt-image-1", + MediaType = "image/png", + ImageSize = new System.Drawing.Size(1024, 1024), + StreamingCount = 2 + } + }; + + var result = imageGenTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.Equal("gpt-image-1", tool.Model); + Assert.Equal(ImageGenerationToolOutputFileFormat.Png, tool.OutputFileFormat); + Assert.NotNull(tool.Size); + Assert.Equal(2, tool.PartialImageCount); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedImageGenerationToolWithAdditionalProperties_ProducesValidImageGenerationTool() + { + var imageGenTool = new HostedImageGenerationTool(new Dictionary + { + [nameof(ImageGenerationTool.Background)] = ImageGenerationToolBackground.Transparent, + [nameof(ImageGenerationTool.InputFidelity)] = ImageGenerationToolInputFidelity.High, + [nameof(ImageGenerationTool.ModerationLevel)] = ImageGenerationToolModerationLevel.Low, + [nameof(ImageGenerationTool.OutputCompressionFactor)] = 50, + [nameof(ImageGenerationTool.Quality)] = ImageGenerationToolQuality.High + }) + { + Options = new ImageGenerationOptions + { + ModelId = "gpt-image-1", + MediaType = "image/jpeg", + } + }; + + var result = imageGenTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.Equal("gpt-image-1", tool.Model); + Assert.Equal(ImageGenerationToolOutputFileFormat.Jpeg, tool.OutputFileFormat); + Assert.Equal(ImageGenerationToolBackground.Transparent, tool.Background); + Assert.Equal(ImageGenerationToolInputFidelity.High, tool.InputFidelity); + Assert.Equal(ImageGenerationToolModerationLevel.Low, tool.ModerationLevel); + Assert.Equal(50, tool.OutputCompressionFactor); + Assert.Equal(ImageGenerationToolQuality.High, tool.Quality); + } + + [Fact] + public void AsOpenAIResponseTool_WithHostedImageGenerationToolWithInputImageMask_ProducesValidImageGenerationTool() + { + var inputImageMask = new ImageGenerationToolInputImageMask( + BinaryData.FromBytes([0x89, 0x50, 0x4E, 0x47]), + "image/png"); + + var imageGenTool = new HostedImageGenerationTool(new Dictionary + { + [nameof(ImageGenerationTool.InputImageMask)] = inputImageMask + }) + { + Options = new ImageGenerationOptions { MediaType = "image/png" } + }; + + var result = imageGenTool.AsOpenAIResponseTool(); + + Assert.NotNull(result); + var tool = Assert.IsType(result); + Assert.NotNull(tool.InputImageMask); + } + [Fact] public void AsOpenAIResponseTool_WithHostedMcpServerTool_ProducesValidMcpTool() {