diff --git a/eng/Versions.props b/eng/Versions.props
index aed94a55e30..5c3ea3f8b74 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -2,7 +2,7 @@
10
1
- 0
+ 1
preview
1
$(MajorVersion).$(MinorVersion).$(PatchVersion)
diff --git a/eng/packages/General.props b/eng/packages/General.props
index a8de843a353..7565d36bac0 100644
--- a/eng/packages/General.props
+++ b/eng/packages/General.props
@@ -20,7 +20,7 @@
-
+
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md
index b108a2503af..a586329aa33 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md
@@ -1,6 +1,12 @@
# Microsoft.Extensions.AI.Abstractions Release History
-## NOT YET RELEASED
+## 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.
- Added `AIJsonSchemaCreateOptions.ParameterDescriptions`.
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 32faa8a1f4d..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"
@@ -2236,6 +2260,14 @@
{
"Member": "long? Microsoft.Extensions.AI.UsageDetails.TotalTokenCount { get; set; }",
"Stage": "Stable"
+ },
+ {
+ "Member": "long? Microsoft.Extensions.AI.UsageDetails.CachedInputTokenCount { get; set; }",
+ "Stage": "Stable"
+ },
+ {
+ "Member": "long? Microsoft.Extensions.AI.UsageDetails.ReasoningTokenCount { get; set; }",
+ "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.Abstractions/UsageDetails.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/UsageDetails.cs
index b3c62cb67e0..b3edbad5e99 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/UsageDetails.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/UsageDetails.cs
@@ -21,6 +21,23 @@ public class UsageDetails
/// Gets or sets the total number of tokens used to produce the response.
public long? TotalTokenCount { get; set; }
+ ///
+ /// Gets or sets the number of input tokens that were read from a cache.
+ ///
+ ///
+ /// Cached input tokens should be counted as part of .
+ ///
+ public long? CachedInputTokenCount { get; set; }
+
+ ///
+ /// Gets or sets the number of "reasoning" / "thinking" tokens used internally
+ /// by the model.
+ ///
+ ///
+ /// Reasoning tokens should be counted as part of .
+ ///
+ public long? ReasoningTokenCount { get; set; }
+
/// Gets or sets a dictionary of additional usage counts.
///
/// All values set here are assumed to be summable. For example, when middleware makes multiple calls to an underlying
@@ -38,6 +55,8 @@ public void Add(UsageDetails usage)
InputTokenCount = NullableSum(InputTokenCount, usage.InputTokenCount);
OutputTokenCount = NullableSum(OutputTokenCount, usage.OutputTokenCount);
TotalTokenCount = NullableSum(TotalTokenCount, usage.TotalTokenCount);
+ CachedInputTokenCount = NullableSum(CachedInputTokenCount, usage.CachedInputTokenCount);
+ ReasoningTokenCount = NullableSum(ReasoningTokenCount, usage.ReasoningTokenCount);
if (usage.AdditionalCounts is { } countsToAdd)
{
@@ -80,6 +99,16 @@ internal string DebuggerDisplay
parts.Add($"{nameof(TotalTokenCount)} = {total}");
}
+ if (CachedInputTokenCount is { } cached)
+ {
+ parts.Add($"{nameof(CachedInputTokenCount)} = {cached}");
+ }
+
+ if (ReasoningTokenCount is { } reasoning)
+ {
+ parts.Add($"{nameof(ReasoningTokenCount)} = {reasoning}");
+ }
+
if (AdditionalCounts is { } additionalCounts)
{
foreach (var entry in additionalCounts)
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
index 6dbf0d1facb..43238de166c 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
@@ -1,10 +1,20 @@
# Microsoft.Extensions.AI.OpenAI Release History
-## NOT YET RELEASED
+## 10.1.1-preview.1.? (NOT YET RELEASED)
+
+- Updated to depend on OpenAI 2.8.0.
+- Updated public API signatures in `OpenAIClientExtensions` and `MicrosoftExtensionsAIResponsesExtensions` to match the corresponding breaking changes in OpenAI's Responses APIs.
+- 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.
- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`.
- Updated the OpenAI Responses `IChatClient` to ensure all `ResponseItem`s are yielded in `AIContent`.
+- Added workaround to the OpenAI Responses `IChatClient` for OpenAI service sometimes sending error data in a manner different from how it's documented.
## 10.0.1-preview.1.25571.5
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs
index 6d989c0b56d..9c2ed0348fb 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs
@@ -56,12 +56,12 @@ public static IEnumerable AsOpenAIResponseItems(this IEnumerable AsChatMessages(this IEnumerable items) =>
OpenAIResponsesChatClient.ToChatMessages(Throw.IfNull(items));
- /// Creates a Microsoft.Extensions.AI from an .
- /// The to convert to a .
+ /// Creates a Microsoft.Extensions.AI from an .
+ /// The to convert to a .
/// The options employed in the creation of the response.
/// A converted .
/// is .
- public static ChatResponse AsChatResponse(this OpenAIResponse response, ResponseCreationOptions? options = null) =>
+ public static ChatResponse AsChatResponse(this ResponseResult response, CreateResponseOptions? options = null) =>
OpenAIResponsesChatClient.FromOpenAIResponse(Throw.IfNull(response), options, conversationId: null);
///
@@ -74,35 +74,43 @@ public static ChatResponse AsChatResponse(this OpenAIResponse response, Response
/// A sequence of converted instances.
/// is .
public static IAsyncEnumerable AsChatResponseUpdatesAsync(
- this IAsyncEnumerable responseUpdates, ResponseCreationOptions? options = null, CancellationToken cancellationToken = default) =>
+ this IAsyncEnumerable responseUpdates, CreateResponseOptions? options = null, CancellationToken cancellationToken = default) =>
OpenAIResponsesChatClient.FromOpenAIStreamingResponseUpdatesAsync(Throw.IfNull(responseUpdates), options, conversationId: null, cancellationToken: cancellationToken);
- /// Creates an OpenAI from a .
+ /// Creates an OpenAI from a .
/// The response to convert.
/// The options employed in the creation of the response.
- /// The created .
- public static OpenAIResponse AsOpenAIResponse(this ChatResponse response, ChatOptions? options = null)
+ /// The created .
+ public static ResponseResult AsOpenAIResponseResult(this ChatResponse response, ChatOptions? options = null)
{
_ = Throw.IfNull(response);
- if (response.RawRepresentation is OpenAIResponse openAIResponse)
+ if (response.RawRepresentation is ResponseResult openAIResponse)
{
return openAIResponse;
}
- return OpenAIResponsesModelFactory.OpenAIResponse(
- response.ResponseId,
- response.CreatedAt ?? default,
- ResponseStatus.Completed,
- usage: null, // No way to construct a ResponseTokenUsage right now from external to the OpenAI library
- maxOutputTokenCount: options?.MaxOutputTokens,
- outputItems: OpenAIResponsesChatClient.ToOpenAIResponseItems(response.Messages, options),
- parallelToolCallsEnabled: options?.AllowMultipleToolCalls ?? false,
- model: response.ModelId ?? options?.ModelId,
- temperature: options?.Temperature,
- topP: options?.TopP,
- previousResponseId: options?.ConversationId,
- instructions: options?.Instructions);
+ ResponseResult result = new()
+ {
+ 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,
+ Status = ResponseStatus.Completed,
+ Temperature = options?.Temperature,
+ TopP = options?.TopP,
+ Usage = OpenAIResponsesChatClient.ToResponseTokenUsage(response.Usage),
+ };
+
+ foreach (var responseItem in OpenAIResponsesChatClient.ToOpenAIResponseItems(response.Messages, options))
+ {
+ result.OutputItems.Add(responseItem);
+ }
+
+ return result;
}
/// Adds the to the list of s.
@@ -111,7 +119,7 @@ public static OpenAIResponse AsOpenAIResponse(this ChatResponse response, ChatOp
///
/// 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)
@@ -127,7 +135,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.
///
///
@@ -136,7 +144,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/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/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
index 70ef6674bd4..a7ca8c08d95 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
@@ -644,6 +644,8 @@ private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage)
InputTokenCount = tokenUsage.InputTokenCount,
OutputTokenCount = tokenUsage.OutputTokenCount,
TotalTokenCount = tokenUsage.TotalTokenCount,
+ CachedInputTokenCount = tokenUsage.InputTokenDetails?.CachedTokenCount,
+ ReasoningTokenCount = tokenUsage.OutputTokenDetails?.ReasoningTokenCount,
AdditionalCounts = [],
};
@@ -653,13 +655,11 @@ private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage)
{
const string InputDetails = nameof(ChatTokenUsage.InputTokenDetails);
counts.Add($"{InputDetails}.{nameof(ChatInputTokenUsageDetails.AudioTokenCount)}", inputDetails.AudioTokenCount);
- counts.Add($"{InputDetails}.{nameof(ChatInputTokenUsageDetails.CachedTokenCount)}", inputDetails.CachedTokenCount);
}
if (tokenUsage.OutputTokenDetails is ChatOutputTokenUsageDetails outputDetails)
{
const string OutputDetails = nameof(ChatTokenUsage.OutputTokenDetails);
- counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount);
counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.AudioTokenCount)}", outputDetails.AudioTokenCount);
counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.AcceptedPredictionTokenCount)}", outputDetails.AcceptedPredictionTokenCount);
counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.RejectedPredictionTokenCount)}", outputDetails.RejectedPredictionTokenCount);
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
index 285b2c1e7ae..36d8677e70a 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
@@ -25,7 +25,7 @@ namespace Microsoft.Extensions.AI;
public static class OpenAIClientExtensions
{
/// Key into AdditionalProperties used to store a strict option.
- private const string StrictKey = "strictJsonSchema";
+ private const string StrictKey = "strict";
/// Gets the default OpenAI endpoint.
internal static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1");
@@ -111,11 +111,11 @@ static void AppendLine(ref StringBuilder? sb, string propName, JsonNode propNode
public static IChatClient AsIChatClient(this ChatClient chatClient) =>
new OpenAIChatClient(chatClient);
- /// Gets an for use with this .
+ /// Gets an for use with this .
/// The client.
- /// An that can be used to converse via the .
+ /// An that can be used to converse via the .
/// is .
- public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient) =>
+ public static IChatClient AsIChatClient(this ResponsesClient responseClient) =>
new OpenAIResponsesChatClient(responseClient);
/// Gets an for use with this .
@@ -242,6 +242,18 @@ 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;
+
+ /// Gets whether an ID is an OpenAI conversation ID.
+ ///
+ /// Technically, OpenAI's IDs are opaque. However, by convention conversation IDs start with "conv_" and
+ /// we can use that to disambiguate whether we're looking at a conversation ID or something else, like a response ID.
+ ///
+ internal static bool IsConversationId(string? id) =>
+ id?.StartsWith("conv_", StringComparison.OrdinalIgnoreCase) is true;
+
/// 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 eb39754d5fd..e6359cbdd7a 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -5,6 +5,7 @@
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
@@ -23,53 +24,38 @@
namespace Microsoft.Extensions.AI;
-/// Represents an for an .
+/// Represents an for an .
internal sealed class OpenAIResponsesChatClient : IChatClient
{
// These delegate instances are used to call the internal overloads of CreateResponseAsync and CreateResponseStreamingAsync that accept
// a RequestOptions. These should be replaced once a better way to pass RequestOptions is available.
- private static readonly Func, ResponseCreationOptions, RequestOptions, Task>>?
- _createResponseAsync =
- (Func, ResponseCreationOptions, RequestOptions, Task>>?)
- typeof(OpenAIResponseClient).GetMethod(
- nameof(OpenAIResponseClient.CreateResponseAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
- null, [typeof(IEnumerable), typeof(ResponseCreationOptions), typeof(RequestOptions)], null)
- ?.CreateDelegate(typeof(Func, ResponseCreationOptions, RequestOptions, Task>>));
-
- private static readonly Func, ResponseCreationOptions, RequestOptions, AsyncCollectionResult>?
+
+ private static readonly Func>?
_createResponseStreamingAsync =
- (Func, ResponseCreationOptions, RequestOptions, AsyncCollectionResult>?)
- typeof(OpenAIResponseClient).GetMethod(
- nameof(OpenAIResponseClient.CreateResponseStreamingAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
- null, [typeof(IEnumerable), typeof(ResponseCreationOptions), typeof(RequestOptions)], null)
- ?.CreateDelegate(typeof(Func, ResponseCreationOptions, RequestOptions, AsyncCollectionResult>));
-
- private static readonly Func>>?
- _getResponseAsync =
- (Func>>?)
- typeof(OpenAIResponseClient).GetMethod(
- nameof(OpenAIResponseClient.GetResponseAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
- null, [typeof(string), typeof(RequestOptions)], null)
- ?.CreateDelegate(typeof(Func>>));
-
- private static readonly Func>?
+ (Func>?)
+ typeof(ResponsesClient).GetMethod(
+ nameof(ResponsesClient.CreateResponseStreamingAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
+ null, [typeof(CreateResponseOptions), typeof(RequestOptions)], null)
+ ?.CreateDelegate(typeof(Func>));
+
+ private static readonly Func>?
_getResponseStreamingAsync =
- (Func>?)
- typeof(OpenAIResponseClient).GetMethod(
- nameof(OpenAIResponseClient.GetResponseStreamingAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
- null, [typeof(string), typeof(RequestOptions), typeof(int?)], null)
- ?.CreateDelegate(typeof(Func>));
+ (Func>?)
+ typeof(ResponsesClient).GetMethod(
+ nameof(ResponsesClient.GetResponseStreamingAsync), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
+ null, [typeof(GetResponseOptions), typeof(RequestOptions)], null)
+ ?.CreateDelegate(typeof(Func>));
/// Metadata about the client.
private readonly ChatClientMetadata _metadata;
- /// The underlying .
- private readonly OpenAIResponseClient _responseClient;
+ /// The underlying .
+ private readonly ResponsesClient _responseClient;
- /// Initializes a new instance of the class for the specified .
+ /// Initializes a new instance of the class for the specified .
/// The underlying client.
/// is .
- public OpenAIResponsesChatClient(OpenAIResponseClient responseClient)
+ public OpenAIResponsesChatClient(ResponsesClient responseClient)
{
_ = Throw.IfNull(responseClient);
@@ -86,7 +72,7 @@ public OpenAIResponsesChatClient(OpenAIResponseClient responseClient)
return
serviceKey is not null ? null :
serviceType == typeof(ChatClientMetadata) ? _metadata :
- serviceType == typeof(OpenAIResponseClient) ? _responseClient :
+ serviceType == typeof(ResponsesClient) ? _responseClient :
serviceType.IsInstanceOfType(this) ? this :
null;
}
@@ -97,76 +83,79 @@ public async Task GetResponseAsync(
{
_ = Throw.IfNull(messages);
- // Convert the inputs into what OpenAIResponseClient expects.
- var openAIOptions = ToOpenAIResponseCreationOptions(options, out string? openAIConversationId);
+ // Convert the inputs into what ResponsesClient expects.
+ var openAIOptions = AsCreateResponseOptions(options, out string? openAIConversationId);
// Provided continuation token signals that an existing background response should be fetched.
if (GetContinuationToken(messages, options) is { } token)
{
- var getTask = _getResponseAsync is not null ?
- _getResponseAsync(_responseClient, token.ResponseId, cancellationToken.ToRequestOptions(streaming: false)) :
- _responseClient.GetResponseAsync(token.ResponseId, cancellationToken);
- var response = (await getTask.ConfigureAwait(false)).Value;
-
+ var getTask = _responseClient.GetResponseAsync(token.ResponseId, include: null, stream: null, startingAfter: null, includeObfuscation: null, cancellationToken.ToRequestOptions(streaming: false));
+ var response = (ResponseResult)await getTask.ConfigureAwait(false);
return FromOpenAIResponse(response, openAIOptions, openAIConversationId);
}
- var openAIResponseItems = ToOpenAIResponseItems(messages, options);
+ foreach (var responseItem in ToOpenAIResponseItems(messages, options))
+ {
+ openAIOptions.InputItems.Add(responseItem);
+ }
- // Make the call to the OpenAIResponseClient.
- var createTask = _createResponseAsync is not null ?
- _createResponseAsync(_responseClient, openAIResponseItems, openAIOptions, cancellationToken.ToRequestOptions(streaming: false)) :
- _responseClient.CreateResponseAsync(openAIResponseItems, openAIOptions, cancellationToken);
- var openAIResponse = (await createTask.ConfigureAwait(false)).Value;
+ // Make the call to the ResponsesClient.
+ var createTask = _responseClient.CreateResponseAsync((BinaryContent)openAIOptions, cancellationToken.ToRequestOptions(streaming: false));
+ var openAIResponsesResult = (ResponseResult)await createTask.ConfigureAwait(false);
// Convert the response to a ChatResponse.
- return FromOpenAIResponse(openAIResponse, openAIOptions, openAIConversationId);
+ return FromOpenAIResponse(openAIResponsesResult, openAIOptions, openAIConversationId);
}
- internal static ChatResponse FromOpenAIResponse(OpenAIResponse openAIResponse, ResponseCreationOptions? openAIOptions, string? conversationId)
+ internal static ChatResponse FromOpenAIResponse(ResponseResult responseResult, CreateResponseOptions? openAIOptions, string? conversationId)
{
// Convert and return the results.
ChatResponse response = new()
{
- ConversationId = openAIOptions?.StoredOutputEnabled is false ? null : (conversationId ?? openAIResponse.Id),
- CreatedAt = openAIResponse.CreatedAt,
- ContinuationToken = CreateContinuationToken(openAIResponse),
- FinishReason = ToFinishReason(openAIResponse.IncompleteStatusDetails?.Reason),
- ModelId = openAIResponse.Model,
- RawRepresentation = openAIResponse,
- ResponseId = openAIResponse.Id,
- Usage = ToUsageDetails(openAIResponse),
+ ConversationId = openAIOptions?.StoredOutputEnabled is false ? null : (conversationId ?? responseResult.Id),
+ CreatedAt = responseResult.CreatedAt,
+ ContinuationToken = CreateContinuationToken(responseResult),
+ FinishReason = AsFinishReason(responseResult.IncompleteStatusDetails?.Reason),
+ ModelId = responseResult.Model,
+ RawRepresentation = responseResult,
+ ResponseId = responseResult.Id,
+ Usage = ToUsageDetails(responseResult),
};
- if (!string.IsNullOrEmpty(openAIResponse.EndUserId))
+ if (!string.IsNullOrEmpty(responseResult.EndUserId))
{
- (response.AdditionalProperties ??= [])[nameof(openAIResponse.EndUserId)] = openAIResponse.EndUserId;
+ (response.AdditionalProperties ??= [])[nameof(responseResult.EndUserId)] = responseResult.EndUserId;
}
- if (openAIResponse.Error is not null)
+ if (responseResult.Error is not null)
{
- (response.AdditionalProperties ??= [])[nameof(openAIResponse.Error)] = openAIResponse.Error;
+ (response.AdditionalProperties ??= [])[nameof(responseResult.Error)] = responseResult.Error;
}
- if (openAIResponse.OutputItems is not null)
+ if (responseResult.OutputItems is not null)
{
- response.Messages = [.. ToChatMessages(openAIResponse.OutputItems, openAIOptions)];
+ response.Messages = [.. ToChatMessages(responseResult.OutputItems, openAIOptions)];
- if (response.Messages.LastOrDefault() is { } lastMessage && openAIResponse.Error is { } error)
+ if (response.Messages.LastOrDefault() is { } lastMessage && responseResult.Error is { } error)
{
lastMessage.Contents.Add(new ErrorContent(error.Message) { ErrorCode = error.Code.ToString() });
}
foreach (var message in response.Messages)
{
- message.CreatedAt ??= openAIResponse.CreatedAt;
+ message.CreatedAt ??= responseResult.CreatedAt;
}
}
+ if (responseResult.SafetyIdentifier is not null)
+ {
+ (response.AdditionalProperties ??= [])[nameof(responseResult.SafetyIdentifier)] = responseResult.SafetyIdentifier;
+ }
+
return response;
}
- internal static IEnumerable ToChatMessages(IEnumerable items, ResponseCreationOptions? options = null)
+ internal static IEnumerable ToChatMessages(IEnumerable items, CreateResponseOptions? options = null)
{
ChatMessage? message = null;
@@ -185,7 +174,7 @@ internal static IEnumerable ToChatMessages(IEnumerable)message.Contents).AddRange(ToAIContents(messageItem.Content));
break;
@@ -252,30 +241,38 @@ public IAsyncEnumerable GetStreamingResponseAsync(
{
_ = Throw.IfNull(messages);
- var openAIOptions = ToOpenAIResponseCreationOptions(options, out string? openAIConversationId);
+ var openAIOptions = AsCreateResponseOptions(options, out string? openAIConversationId);
+ openAIOptions.StreamingEnabled = true;
// Provided continuation token signals that an existing background response should be fetched.
if (GetContinuationToken(messages, options) is { } token)
{
+ GetResponseOptions getOptions = new(token.ResponseId) { StartingAfter = token.SequenceNumber, StreamingEnabled = true };
+
+ Debug.Assert(_getResponseStreamingAsync is not null, $"Unable to find {nameof(_getResponseStreamingAsync)} method");
IAsyncEnumerable getUpdates = _getResponseStreamingAsync is not null ?
- _getResponseStreamingAsync(_responseClient, token.ResponseId, cancellationToken.ToRequestOptions(streaming: true), token.SequenceNumber) :
- _responseClient.GetResponseStreamingAsync(token.ResponseId, token.SequenceNumber, cancellationToken);
+ _getResponseStreamingAsync(_responseClient, getOptions, cancellationToken.ToRequestOptions(streaming: true)) :
+ _responseClient.GetResponseStreamingAsync(getOptions, cancellationToken);
return FromOpenAIStreamingResponseUpdatesAsync(getUpdates, openAIOptions, openAIConversationId, token.ResponseId, cancellationToken);
}
- var openAIResponseItems = ToOpenAIResponseItems(messages, options);
+ foreach (var responseItem in ToOpenAIResponseItems(messages, options))
+ {
+ openAIOptions.InputItems.Add(responseItem);
+ }
- var createUpdates = _createResponseStreamingAsync is not null ?
- _createResponseStreamingAsync(_responseClient, openAIResponseItems, openAIOptions, cancellationToken.ToRequestOptions(streaming: true)) :
- _responseClient.CreateResponseStreamingAsync(openAIResponseItems, openAIOptions, cancellationToken);
+ Debug.Assert(_createResponseStreamingAsync is not null, $"Unable to find {nameof(_createResponseStreamingAsync)} method");
+ AsyncCollectionResult createUpdates = _createResponseStreamingAsync is not null ?
+ _createResponseStreamingAsync(_responseClient, openAIOptions, cancellationToken.ToRequestOptions(streaming: true)) :
+ _responseClient.CreateResponseStreamingAsync(openAIOptions, cancellationToken);
return FromOpenAIStreamingResponseUpdatesAsync(createUpdates, openAIOptions, openAIConversationId, cancellationToken: cancellationToken);
}
internal static async IAsyncEnumerable FromOpenAIStreamingResponseUpdatesAsync(
IAsyncEnumerable streamingResponseUpdates,
- ResponseCreationOptions? options,
+ CreateResponseOptions? options,
string? conversationId,
string? resumeResponseId = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
@@ -360,7 +357,7 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
latestResponseStatus = completedUpdate.Response?.Status;
var update = CreateUpdate(ToUsageDetails(completedUpdate.Response) is { } usage ? new UsageContent(usage) : null);
update.FinishReason =
- ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ??
+ AsFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ??
(anyFunctions ? ChatFinishReason.ToolCalls :
ChatFinishReason.Stop);
yield return update;
@@ -372,7 +369,7 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) =>
{
case MessageResponseItem mri:
lastMessageId = outputItemAddedUpdate.Item.Id;
- lastRole = ToChatRole(mri.Role);
+ lastRole = AsChatRole(mri.Role);
break;
case FunctionCallResponseItem fcri:
@@ -530,7 +527,7 @@ void UpdateConversationId(string? id)
///
void IDisposable.Dispose()
{
- // Nothing to dispose. Implementation required for the IChatClient interface.
+ // Nothing to dispose.
}
internal static ResponseTool? ToResponseTool(AITool tool, ChatOptions? options = null)
@@ -544,47 +541,60 @@ void IDisposable.Dispose()
return ToResponseTool(aiFunction, options);
case HostedWebSearchTool webSearchTool:
- WebSearchToolLocation? location = null;
- if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation))
+ return new WebSearchTool
{
- 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);
+ Filters = webSearchTool.GetProperty(nameof(WebSearchTool.Filters)),
+ SearchContextSize = webSearchTool.GetProperty(nameof(WebSearchTool.SearchContextSize)),
+ UserLocation = webSearchTool.GetProperty(nameof(WebSearchTool.UserLocation)),
+ };
case HostedFileSearchTool fileSearchTool:
- return ResponseTool.CreateFileSearchTool(
- fileSearchTool.Inputs?.OfType().Select(c => c.VectorStoreId) ?? [],
- fileSearchTool.MaximumResultCount);
-
- case HostedImageGenerationTool imageGenerationTool:
- return ToImageResponseTool(imageGenerationTool);
+ return new FileSearchTool(fileSearchTool.Inputs?.OfType().Select(c => c.VectorStoreId) ?? [])
+ {
+ Filters = fileSearchTool.GetProperty(nameof(FileSearchTool.Filters)),
+ MaxResultCount = fileSearchTool.MaximumResultCount,
+ RankingOptions = fileSearchTool.GetProperty(nameof(FileSearchTool.RankingOptions)),
+ };
case HostedCodeInterpreterTool codeTool:
- return ResponseTool.CreateCodeInterpreterTool(
- new CodeInterpreterToolContainer(codeTool.Inputs?.OfType().Select(f => f.FileId).ToList() is { Count: > 0 } ids ?
+ return new CodeInterpreterTool(
+ new(codeTool.Inputs?.OfType().Select(f => f.FileId).ToList() is { Count: > 0 } ids ?
CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(ids) :
new()));
+ case HostedImageGenerationTool imageGenerationTool:
+ ImageGenerationOptions? igo = imageGenerationTool.Options;
+ return new ImageGenerationTool
+ {
+ Background = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.Background)),
+ InputFidelity = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.InputFidelity)),
+ InputImageMask = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.InputImageMask)),
+ Model = igo?.ModelId,
+ ModerationLevel = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.ModerationLevel)),
+ OutputCompressionFactor = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.OutputCompressionFactor)),
+ OutputFileFormat = igo?.MediaType is { } mediaType ?
+ mediaType switch
+ {
+ "image/png" => ImageGenerationToolOutputFileFormat.Png,
+ "image/jpeg" => ImageGenerationToolOutputFileFormat.Jpeg,
+ "image/webp" => ImageGenerationToolOutputFileFormat.Webp,
+ _ => null,
+ } :
+ null,
+ PartialImageCount = igo?.StreamingCount,
+ Quality = imageGenerationTool.GetProperty(nameof(ImageGenerationTool.Quality)),
+ Size = igo?.ImageSize is { } size ?
+ new ImageGenerationToolSize(size.Width, size.Height) :
+ null,
+ };
+
case HostedMcpServerTool mcpTool:
- McpTool responsesMcpTool = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? url) ?
- ResponseTool.CreateMcpTool(
- mcpTool.ServerName,
- url,
- mcpTool.AuthorizationToken,
- mcpTool.ServerDescription) :
- ResponseTool.CreateMcpTool(
- mcpTool.ServerName,
- new McpToolConnectorId(mcpTool.ServerAddress),
- mcpTool.AuthorizationToken,
- mcpTool.ServerDescription);
+ McpTool responsesMcpTool = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? serverAddressUrl) ?
+ new McpTool(mcpTool.ServerName, serverAddressUrl) :
+ new McpTool(mcpTool.ServerName, new McpToolConnectorId(mcpTool.ServerAddress));
+
+ responsesMcpTool.ServerDescription = mcpTool.ServerDescription;
+ responsesMcpTool.AuthorizationToken = mcpTool.AuthorizationToken;
if (mcpTool.AllowedTools is not null)
{
@@ -629,53 +639,21 @@ void IDisposable.Dispose()
internal static FunctionTool ToResponseTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null)
{
- bool? strict =
+ bool? strictModeEnabled =
OpenAIClientExtensions.HasStrict(aiFunction.AdditionalProperties) ??
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties);
- return ResponseTool.CreateFunctionTool(
+ return new FunctionTool(
aiFunction.Name,
- OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strict),
- strict,
- aiFunction.Description);
- }
-
- 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);
- }
-
- // OutputFileFormat: File output format
- if (imageGenerationOptions?.MediaType is not null)
+ OpenAIClientExtensions.ToOpenAIFunctionParameters(aiFunction, strictModeEnabled),
+ strictModeEnabled)
{
- 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;
+ FunctionDescription = aiFunction.Description,
+ };
}
/// Creates a from a .
- private static ChatRole ToChatRole(MessageRole? role) =>
+ private static ChatRole AsChatRole(MessageRole? role) =>
role switch
{
MessageRole.System => ChatRole.System,
@@ -685,23 +663,26 @@ private static ChatRole ToChatRole(MessageRole? role) =>
};
/// Creates a from a .
- private static ChatFinishReason? ToFinishReason(ResponseIncompleteStatusReason? statusReason) =>
+ private static ChatFinishReason? AsFinishReason(ResponseIncompleteStatusReason? statusReason) =>
statusReason == ResponseIncompleteStatusReason.ContentFilter ? ChatFinishReason.ContentFilter :
statusReason == ResponseIncompleteStatusReason.MaxOutputTokens ? ChatFinishReason.Length :
null;
- /// Converts a to a .
- private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? options, out string? openAIConversationId)
+ /// Converts a to a .
+ private CreateResponseOptions AsCreateResponseOptions(ChatOptions? options, out string? openAIConversationId)
{
openAIConversationId = null;
if (options is null)
{
- return new();
+ return new()
+ {
+ Model = _responseClient.Model,
+ };
}
bool hasRawRco = false;
- if (options.RawRepresentationFactory?.Invoke(this) is ResponseCreationOptions result)
+ if (options.RawRepresentationFactory?.Invoke(this) is CreateResponseOptions result)
{
hasRawRco = true;
}
@@ -710,32 +691,29 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
result = new();
}
+ result.BackgroundModeEnabled ??= options.AllowBackgroundResponses;
result.MaxOutputTokenCount ??= options.MaxOutputTokens;
+ result.Model ??= options.ModelId ?? _responseClient.Model;
result.Temperature ??= options.Temperature;
result.TopP ??= options.TopP;
- result.BackgroundModeEnabled ??= options.AllowBackgroundResponses;
- OpenAIClientExtensions.PatchModelIfNotSet(ref result.Patch, options.ModelId);
- // If the ResponseCreationOptions.PreviousResponseId is already set (likely rare), then we don't need to do
+ // If the CreateResponseOptions.PreviousResponseId is already set (likely rare), then we don't need to do
// anything with regards to Conversation, because they're mutually exclusive and we would want to ignore
- // ChatOptions.ConversationId regardless of its value. If it's null, we want to examine the ResponseCreationOptions
+ // ChatOptions.ConversationId regardless of its value. If it's null, we want to examine the CreateResponseOptions
// instance to see if a conversation ID has already been set on it and use that conversation ID subsequently if
// it has. If one hasn't been set, but ChatOptions.ConversationId has been set, we'll either set
- // ResponseCreationOptions.Conversation if the string represents a conversation ID or else PreviousResponseId.
+ // CreateResponseOptions.Conversation if the string represents a conversation ID or else PreviousResponseId.
if (result.PreviousResponseId is null)
{
- // Technically, OpenAI's IDs are opaque. However, by convention conversation IDs start with "conv_" and
- // we can use that to disambiguate whether we're looking at a conversation ID or a response ID.
- string? chatOptionsConversationId = options.ConversationId;
- bool chatOptionsHasOpenAIConversationId = chatOptionsConversationId?.StartsWith("conv_", StringComparison.OrdinalIgnoreCase) is true;
+ bool chatOptionsHasOpenAIConversationId = OpenAIClientExtensions.IsConversationId(options.ConversationId);
if (hasRawRco || chatOptionsHasOpenAIConversationId)
{
- _ = result.Patch.TryGetValue("$.conversation"u8, out openAIConversationId);
+ openAIConversationId = result.ConversationOptions?.ConversationId;
if (openAIConversationId is null && chatOptionsHasOpenAIConversationId)
{
- result.Patch.Set("$.conversation"u8, chatOptionsConversationId!);
- openAIConversationId = chatOptionsConversationId;
+ result.ConversationOptions = new(options.ConversationId);
+ openAIConversationId = options.ConversationId;
}
}
@@ -778,7 +756,6 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
break;
case AutoChatToolMode:
- case null:
result.ToolChoice = ResponseToolChoice.CreateAutoChoice();
break;
@@ -1076,9 +1053,10 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera
break;
case TextReasoningContent reasoningContent:
- yield return OpenAIResponsesModelFactory.ReasoningResponseItem(
- encryptedContent: reasoningContent.ProtectedData,
- summaryText: reasoningContent.Text);
+ yield return new ReasoningResponseItem(reasoningContent.Text)
+ {
+ EncryptedContent = reasoningContent.ProtectedData,
+ };
break;
case FunctionCallContent callContent:
@@ -1132,33 +1110,55 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera
}
}
- /// Extract usage details from an .
- private static UsageDetails? ToUsageDetails(OpenAIResponse? openAIResponse)
+ /// Extract usage details from a into a .
+ private static UsageDetails? ToUsageDetails(ResponseResult? responseResult)
{
UsageDetails? ud = null;
- if (openAIResponse?.Usage is { } usage)
+ if (responseResult?.Usage is { } usage)
{
ud = new()
{
InputTokenCount = usage.InputTokenCount,
OutputTokenCount = usage.OutputTokenCount,
TotalTokenCount = usage.TotalTokenCount,
+ CachedInputTokenCount = usage.InputTokenDetails?.CachedTokenCount,
+ ReasoningTokenCount = usage.OutputTokenDetails?.ReasoningTokenCount,
};
+ }
+
+ return ud;
+ }
- if (usage.InputTokenDetails is { } inputDetails)
+ /// Converts a to a .
+ internal static ResponseTokenUsage? ToResponseTokenUsage(UsageDetails? usageDetails)
+ {
+ ResponseTokenUsage? rtu = null;
+ if (usageDetails is not null)
+ {
+ rtu = new()
{
- ud.AdditionalCounts ??= [];
- ud.AdditionalCounts.Add($"{nameof(usage.InputTokenDetails)}.{nameof(inputDetails.CachedTokenCount)}", inputDetails.CachedTokenCount);
- }
+ InputTokenCount = (int?)usageDetails.InputTokenCount ?? 0,
+ OutputTokenCount = (int?)usageDetails.OutputTokenCount ?? 0,
+ TotalTokenCount = (int?)usageDetails.TotalTokenCount ?? 0,
+ InputTokenDetails = new(),
+ OutputTokenDetails = new(),
+ };
- if (usage.OutputTokenDetails is { } outputDetails)
+ if (usageDetails.AdditionalCounts is { } additionalCounts)
{
- ud.AdditionalCounts ??= [];
- ud.AdditionalCounts.Add($"{nameof(usage.OutputTokenDetails)}.{nameof(outputDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount);
+ if (additionalCounts.TryGetValue($"{nameof(ResponseTokenUsage.InputTokenDetails)}.{nameof(ResponseInputTokenUsageDetails.CachedTokenCount)}", out int? cachedTokenCount))
+ {
+ rtu.InputTokenDetails.CachedTokenCount = cachedTokenCount.GetValueOrDefault();
+ }
+
+ if (additionalCounts.TryGetValue($"{nameof(ResponseTokenUsage.OutputTokenDetails)}.{nameof(ResponseOutputTokenUsageDetails.ReasoningTokenCount)}", out int? reasoningTokenCount))
+ {
+ rtu.OutputTokenDetails.ReasoningTokenCount = reasoningTokenCount.GetValueOrDefault();
+ }
}
}
- return ud;
+ return rtu;
}
/// Convert a sequence of s to a list of .
@@ -1168,46 +1168,40 @@ private static List ToAIContents(IEnumerable con
foreach (ResponseContentPart part in contents)
{
+ AIContent? content;
switch (part.Kind)
{
case ResponseContentPartKind.InputText or ResponseContentPartKind.OutputText:
- TextContent text = new(part.Text) { RawRepresentation = part };
+ TextContent text = new(part.Text);
PopulateAnnotations(part, text);
- results.Add(text);
+ content = text;
break;
- case ResponseContentPartKind.InputFile:
- if (!string.IsNullOrWhiteSpace(part.InputImageFileId))
- {
- results.Add(new HostedFileContent(part.InputImageFileId) { MediaType = "image/*", RawRepresentation = part });
- }
- else if (!string.IsNullOrWhiteSpace(part.InputFileId))
- {
- results.Add(new HostedFileContent(part.InputFileId) { Name = part.InputFilename, RawRepresentation = part });
- }
- else if (part.InputFileBytes is not null)
- {
- results.Add(new DataContent(part.InputFileBytes, part.InputFileBytesMediaType ?? "application/octet-stream")
- {
- Name = part.InputFilename,
- RawRepresentation = part,
- });
- }
-
+ case ResponseContentPartKind.InputFile or ResponseContentPartKind.InputImage:
+ content =
+ !string.IsNullOrWhiteSpace(part.InputImageFileId) ? new HostedFileContent(part.InputImageFileId) { MediaType = "image/*" } :
+ !string.IsNullOrWhiteSpace(part.InputFileId) ? new HostedFileContent(part.InputFileId) { Name = part.InputFilename } :
+ part.InputFileBytes is not null ? new DataContent(part.InputFileBytes, part.InputFileBytesMediaType ?? "application/octet-stream") { Name = part.InputFilename } :
+ null;
break;
case ResponseContentPartKind.Refusal:
- results.Add(new ErrorContent(part.Refusal)
+ content = new ErrorContent(part.Refusal)
{
ErrorCode = nameof(ResponseContentPartKind.Refusal),
- RawRepresentation = part,
- });
+ };
break;
default:
- results.Add(new() { RawRepresentation = part });
+ content = new();
break;
}
+
+ if (content is not null)
+ {
+ content.RawRepresentation = part;
+ results.Add(content);
+ }
}
return results;
@@ -1311,7 +1305,7 @@ private static void AddCodeInterpreterContents(CodeInterpreterCallResponseItem c
});
}
- private static void AddImageGenerationContents(ImageGenerationCallResponseItem outputItem, ResponseCreationOptions? options, IList contents)
+ private static void AddImageGenerationContents(ImageGenerationCallResponseItem outputItem, CreateResponseOptions? options, IList contents)
{
var imageGenTool = options?.Tools.OfType().FirstOrDefault();
string outputFormat = imageGenTool?.OutputFileFormat?.ToString() ?? "png";
@@ -1325,14 +1319,11 @@ private static void AddImageGenerationContents(ImageGenerationCallResponseItem o
{
ImageId = outputItem.Id,
RawRepresentation = outputItem,
- Outputs = new List
- {
- new DataContent(outputItem.ImageResultBytes, $"image/{outputFormat}")
- }
+ Outputs = [new DataContent(outputItem.ImageResultBytes, $"image/{outputFormat}")]
});
}
- private static ImageGenerationToolResultContent GetImageGenerationResult(StreamingResponseImageGenerationCallPartialImageUpdate update, ResponseCreationOptions? options)
+ private static ImageGenerationToolResultContent GetImageGenerationResult(StreamingResponseImageGenerationCallPartialImageUpdate update, CreateResponseOptions? options)
{
var imageGenTool = options?.Tools.OfType().FirstOrDefault();
var outputType = imageGenTool?.OutputFileFormat?.ToString() ?? "png";
@@ -1356,15 +1347,13 @@ private static ImageGenerationToolResultContent GetImageGenerationResult(Streami
};
}
- private static OpenAIResponsesContinuationToken? CreateContinuationToken(OpenAIResponse openAIResponse)
- {
- return CreateContinuationToken(
- responseId: openAIResponse.Id,
- responseStatus: openAIResponse.Status,
- isBackgroundModeEnabled: openAIResponse.BackgroundModeEnabled);
- }
+ private static ResponsesClientContinuationToken? CreateContinuationToken(ResponseResult responseResult) =>
+ CreateContinuationToken(
+ responseId: responseResult.Id,
+ responseStatus: responseResult.Status,
+ isBackgroundModeEnabled: responseResult.BackgroundModeEnabled);
- private static OpenAIResponsesContinuationToken? CreateContinuationToken(
+ private static ResponsesClientContinuationToken? CreateContinuationToken(
string responseId,
ResponseStatus? responseStatus,
bool? isBackgroundModeEnabled,
@@ -1382,7 +1371,7 @@ private static ImageGenerationToolResultContent GetImageGenerationResult(Streami
if ((responseStatus is ResponseStatus.InProgress or ResponseStatus.Queued) ||
(responseStatus is null && updateSequenceNumber is not null))
{
- return new OpenAIResponsesContinuationToken(responseId)
+ return new ResponsesClientContinuationToken(responseId)
{
SequenceNumber = updateSequenceNumber,
};
@@ -1394,7 +1383,7 @@ private static ImageGenerationToolResultContent GetImageGenerationResult(Streami
return null;
}
- private static OpenAIResponsesContinuationToken? GetContinuationToken(IEnumerable messages, ChatOptions? options = null)
+ private static ResponsesClientContinuationToken? GetContinuationToken(IEnumerable messages, ChatOptions? options = null)
{
if (options?.ContinuationToken is { } token)
{
@@ -1403,7 +1392,7 @@ private static ImageGenerationToolResultContent GetImageGenerationResult(Streami
throw new InvalidOperationException("Messages are not allowed when continuing a background response using a continuation token.");
}
- return OpenAIResponsesContinuationToken.FromToken(token);
+ return ResponsesClientContinuationToken.FromToken(token);
}
return null;
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/ResponsesClientContinuationToken.cs
similarity index 81%
rename from src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs
rename to src/Libraries/Microsoft.Extensions.AI.OpenAI/ResponsesClientContinuationToken.cs
index 8e6f5ffd71c..770f5b10afa 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesContinuationToken.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/ResponsesClientContinuationToken.cs
@@ -13,10 +13,10 @@ namespace Microsoft.Extensions.AI;
/// The token is used for resuming streamed background responses and continuing
/// non-streamed background responses until completion.
///
-internal sealed class OpenAIResponsesContinuationToken : ResponseContinuationToken
+internal sealed class ResponsesClientContinuationToken : ResponseContinuationToken
{
- /// Initializes a new instance of the class.
- internal OpenAIResponsesContinuationToken(string responseId)
+ /// Initializes a new instance of the class.
+ internal ResponsesClientContinuationToken(string responseId)
{
ResponseId = responseId;
}
@@ -49,13 +49,13 @@ public override ReadOnlyMemory ToBytes()
return stream.ToArray();
}
- /// Create a new instance of from the provided .
+ /// Create a new instance of from the provided .
///
- /// The token to create the from.
- /// A equivalent of the provided .
- internal static OpenAIResponsesContinuationToken FromToken(ResponseContinuationToken token)
+ /// The token to create the from.
+ /// A equivalent of the provided .
+ internal static ResponsesClientContinuationToken FromToken(ResponseContinuationToken token)
{
- if (token is OpenAIResponsesContinuationToken openAIResponsesContinuationToken)
+ if (token is ResponsesClientContinuationToken openAIResponsesContinuationToken)
{
return openAIResponsesContinuationToken;
}
diff --git a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md
index 5d918798c86..816e8b92267 100644
--- a/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md
+++ b/src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md
@@ -1,6 +1,10 @@
# Microsoft.Extensions.AI Release History
-## NOT YET RELEASED
+## 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.
- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`.
diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion.Markdig/MarkdownParser.cs b/src/Libraries/Microsoft.Extensions.DataIngestion.Markdig/MarkdownParser.cs
index 8ef2b27d152..5b02f917147 100644
--- a/src/Libraries/Microsoft.Extensions.DataIngestion.Markdig/MarkdownParser.cs
+++ b/src/Libraries/Microsoft.Extensions.DataIngestion.Markdig/MarkdownParser.cs
@@ -228,6 +228,10 @@ private static IngestionDocumentSection MapQuoteBlock(QuoteBlock quoteBlock, boo
{
content.Append(codeInline.Content);
}
+ else if (inline is HtmlInline htmlInline)
+ {
+ content.Append(htmlInline.Tag);
+ }
else
{
throw new NotSupportedException($"Inline type '{inline.GetType().Name}' is not supported.");
@@ -244,8 +248,11 @@ private static IngestionDocumentSection MapQuoteBlock(QuoteBlock quoteBlock, boo
{
int firstRowIndex = SkipFirstRow(table, outputContent) ? 1 : 0;
- // For some reason, table.ColumnDefinitions.Count returns one extra column.
- var cells = new IngestionDocumentElement?[table.Count - firstRowIndex, table.ColumnDefinitions.Count - 1];
+ // Calculate the actual number of columns by examining the rows.
+ // table.ColumnDefinitions.Count can vary: for tables WITH trailing pipes it's (columns + 1),
+ // but for tables WITHOUT trailing pipes it's equal to the actual column count.
+ int columnCount = GetColumnCount(table, firstRowIndex);
+ var cells = new IngestionDocumentElement?[table.Count - firstRowIndex, columnCount];
for (int rowIndex = firstRowIndex; rowIndex < table.Count; rowIndex++)
{
@@ -271,6 +278,25 @@ private static IngestionDocumentSection MapQuoteBlock(QuoteBlock quoteBlock, boo
return cells;
+ static int GetColumnCount(Table table, int firstRowIndex)
+ {
+ int maxColumns = 0;
+ for (int rowIndex = firstRowIndex; rowIndex < table.Count; rowIndex++)
+ {
+ var tableRow = (TableRow)table[rowIndex];
+ int columnCount = 0;
+ for (int cellIndex = 0; cellIndex < tableRow.Count; cellIndex++)
+ {
+ var tableCell = (TableCell)tableRow[cellIndex];
+ columnCount += tableCell.ColumnSpan;
+ }
+
+ maxColumns = Math.Max(maxColumns, columnCount);
+ }
+
+ return maxColumns;
+ }
+
// Some parsers like MarkItDown include a row with invalid markdown before the separator row:
// | | | | |
// | --- | --- | --- | --- |
diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets
index 0aa5afc9b1a..97870dcc6b7 100644
--- a/src/ProjectTemplates/GeneratedContent.targets
+++ b/src/ProjectTemplates/GeneratedContent.targets
@@ -21,11 +21,14 @@
"TemplatePackageVersion_{PackageName}"
where {PackageName} is the package ID with '.' characters removed.
The value of each property will be the computed package version.
+
+ IMPORTANT: Internal packages that ship at a different cadence than these project templates should
+ be referenced using explicit package versions instead (see the "ComputeGeneratedContentProperties"
+ target below).
-->
-
@@ -46,6 +49,7 @@
11.7.0
13.0.0-beta.444
10.0.0
+ 10.1.0
10.0.0
2.0.0
1.67.1
@@ -75,6 +79,7 @@
TemplatePackageVersion_AzureSearchDocuments=$(TemplatePackageVersion_AzureSearchDocuments);
TemplatePackageVersion_CommunityToolkitAspire=$(TemplatePackageVersion_CommunityToolkitAspire);
TemplatePackageVersion_MicrosoftExtensionsHosting=$(TemplatePackageVersion_MicrosoftExtensionsHosting);
+ TemplatePackageVersion_MicrosoftExtensionsHttpResilience=$(TemplatePackageVersion_MicrosoftExtensionsHttpResilience);
TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery=$(TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery);
TemplatePackageVersion_MicrosoftMLTokenizers=$(TemplatePackageVersion_MicrosoftMLTokenizers);
TemplatePackageVersion_MicrosoftSemanticKernel=$(TemplatePackageVersion_MicrosoftSemanticKernel);
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs
index e9984318254..a51a674cf71 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-CSharp.Web/Program.cs
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData-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 // GetOpenAIResponseClient(string) is experimental and subject to change or removal in future updates.
-var chatClient = openAIClient.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient();
+#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 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 GetOpenAIResponseClient(string) are experimental and subject to change or removal in future updates.
+#pragma warning disable OPENAI001 // OpenAIClient(AuthenticationPolicy, OpenAIClientOptions) and GetResponsesClient(string) 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 // GetOpenAIResponseClient(string) is experimental and subject to change or removal in future updates.
+#pragma warning disable OPENAI001 // GetResponsesClient(string) is experimental and subject to change or removal in future updates.
#endif
-var chatClient = azureOpenAi.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient();
+var chatClient = azureOpenAi.GetResponsesClient("gpt-4o-mini").AsIChatClient();
#pragma warning restore OPENAI001
var embeddingGenerator = azureOpenAi.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UsageContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UsageContentTests.cs
index ed268176c5d..d3bf0889821 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UsageContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/UsageContentTests.cs
@@ -66,7 +66,9 @@ public void Serialization_Roundtrips()
{
InputTokenCount = 10,
OutputTokenCount = 20,
- TotalTokenCount = 30
+ TotalTokenCount = 30,
+ CachedInputTokenCount = 5,
+ ReasoningTokenCount = 8
});
var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions);
@@ -77,5 +79,7 @@ public void Serialization_Roundtrips()
Assert.Equal(content.Details.InputTokenCount, deserializedContent.Details.InputTokenCount);
Assert.Equal(content.Details.OutputTokenCount, deserializedContent.Details.OutputTokenCount);
Assert.Equal(content.Details.TotalTokenCount, deserializedContent.Details.TotalTokenCount);
+ Assert.Equal(content.Details.CachedInputTokenCount, deserializedContent.Details.CachedInputTokenCount);
+ Assert.Equal(content.Details.ReasoningTokenCount, deserializedContent.Details.ReasoningTokenCount);
}
}
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.Abstractions.Tests/UsageDetailsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/UsageDetailsTests.cs
new file mode 100644
index 00000000000..d7fcd2545f0
--- /dev/null
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/UsageDetailsTests.cs
@@ -0,0 +1,190 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Text.Json;
+using Xunit;
+
+namespace Microsoft.Extensions.AI;
+
+public class UsageDetailsTests
+{
+ [Fact]
+ public void Constructor_PropsDefault()
+ {
+ UsageDetails details = new();
+ Assert.Null(details.InputTokenCount);
+ Assert.Null(details.OutputTokenCount);
+ Assert.Null(details.TotalTokenCount);
+ Assert.Null(details.CachedInputTokenCount);
+ Assert.Null(details.ReasoningTokenCount);
+ Assert.Null(details.AdditionalCounts);
+ }
+
+ [Fact]
+ public void Properties_Roundtrip()
+ {
+ UsageDetails details = new()
+ {
+ InputTokenCount = 10,
+ OutputTokenCount = 20,
+ TotalTokenCount = 30,
+ CachedInputTokenCount = 5,
+ ReasoningTokenCount = 8,
+ AdditionalCounts = new() { ["custom"] = 100 }
+ };
+
+ Assert.Equal(10, details.InputTokenCount);
+ Assert.Equal(20, details.OutputTokenCount);
+ Assert.Equal(30, details.TotalTokenCount);
+ Assert.Equal(5, details.CachedInputTokenCount);
+ Assert.Equal(8, details.ReasoningTokenCount);
+ Assert.NotNull(details.AdditionalCounts);
+ Assert.Equal(100, details.AdditionalCounts["custom"]);
+ }
+
+ [Fact]
+ public void Add_NullUsage_Throws()
+ {
+ UsageDetails details = new();
+ Assert.Throws("usage", () => details.Add(null!));
+ }
+
+ [Fact]
+ public void Add_SumsAllProperties()
+ {
+ UsageDetails details1 = new()
+ {
+ InputTokenCount = 10,
+ OutputTokenCount = 20,
+ TotalTokenCount = 30,
+ CachedInputTokenCount = 5,
+ ReasoningTokenCount = 8,
+ };
+
+ UsageDetails details2 = new()
+ {
+ InputTokenCount = 15,
+ OutputTokenCount = 25,
+ TotalTokenCount = 40,
+ CachedInputTokenCount = 7,
+ ReasoningTokenCount = 12,
+ };
+
+ details1.Add(details2);
+
+ Assert.Equal(25, details1.InputTokenCount);
+ Assert.Equal(45, details1.OutputTokenCount);
+ Assert.Equal(70, details1.TotalTokenCount);
+ Assert.Equal(12, details1.CachedInputTokenCount);
+ Assert.Equal(20, details1.ReasoningTokenCount);
+ }
+
+ [Fact]
+ public void Add_WithNullValues_HandlesCorrectly()
+ {
+ UsageDetails details1 = new()
+ {
+ InputTokenCount = 10,
+ CachedInputTokenCount = 5,
+ };
+
+ UsageDetails details2 = new()
+ {
+ OutputTokenCount = 25,
+ ReasoningTokenCount = 12,
+ };
+
+ details1.Add(details2);
+
+ Assert.Equal(10, details1.InputTokenCount);
+ Assert.Equal(25, details1.OutputTokenCount);
+ Assert.Null(details1.TotalTokenCount);
+ Assert.Equal(5, details1.CachedInputTokenCount);
+ Assert.Equal(12, details1.ReasoningTokenCount);
+ }
+
+ [Fact]
+ public void Add_FromNullToValue_SetsValue()
+ {
+ UsageDetails details1 = new();
+
+ UsageDetails details2 = new()
+ {
+ CachedInputTokenCount = 5,
+ ReasoningTokenCount = 10,
+ };
+
+ details1.Add(details2);
+
+ Assert.Equal(5, details1.CachedInputTokenCount);
+ Assert.Equal(10, details1.ReasoningTokenCount);
+ }
+
+ [Fact]
+ public void Add_AdditionalCounts_MergesCorrectly()
+ {
+ UsageDetails details1 = new()
+ {
+ AdditionalCounts = new() { ["key1"] = 10, ["key2"] = 20 }
+ };
+
+ UsageDetails details2 = new()
+ {
+ AdditionalCounts = new() { ["key2"] = 30, ["key3"] = 40 }
+ };
+
+ details1.Add(details2);
+
+ Assert.NotNull(details1.AdditionalCounts);
+ Assert.Equal(10, details1.AdditionalCounts["key1"]);
+ Assert.Equal(50, details1.AdditionalCounts["key2"]);
+ Assert.Equal(40, details1.AdditionalCounts["key3"]);
+ }
+
+ [Fact]
+ public void Serialization_Roundtrips()
+ {
+ UsageDetails details = new()
+ {
+ InputTokenCount = 10,
+ OutputTokenCount = 20,
+ TotalTokenCount = 30,
+ CachedInputTokenCount = 5,
+ ReasoningTokenCount = 8,
+ AdditionalCounts = new() { ["custom"] = 100 }
+ };
+
+ string json = JsonSerializer.Serialize(details, AIJsonUtilities.DefaultOptions);
+ UsageDetails? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
+
+ Assert.NotNull(deserialized);
+ Assert.Equal(details.InputTokenCount, deserialized.InputTokenCount);
+ Assert.Equal(details.OutputTokenCount, deserialized.OutputTokenCount);
+ Assert.Equal(details.TotalTokenCount, deserialized.TotalTokenCount);
+ Assert.Equal(details.CachedInputTokenCount, deserialized.CachedInputTokenCount);
+ Assert.Equal(details.ReasoningTokenCount, deserialized.ReasoningTokenCount);
+ Assert.NotNull(deserialized.AdditionalCounts);
+ Assert.Equal(100, deserialized.AdditionalCounts["custom"]);
+ }
+
+ [Fact]
+ public void Serialization_WithNullProperties_Roundtrips()
+ {
+ UsageDetails details = new()
+ {
+ InputTokenCount = 10,
+ OutputTokenCount = 20,
+ };
+
+ string json = JsonSerializer.Serialize(details, AIJsonUtilities.DefaultOptions);
+ UsageDetails? deserialized = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions);
+
+ Assert.NotNull(deserialized);
+ Assert.Equal(10, deserialized.InputTokenCount);
+ Assert.Equal(20, deserialized.OutputTokenCount);
+ Assert.Null(deserialized.TotalTokenCount);
+ Assert.Null(deserialized.CachedInputTokenCount);
+ Assert.Null(deserialized.ReasoningTokenCount);
+ }
+}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs
index 7b1dd10a2bd..ce31abe8bc9 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Integration.Tests/ChatClientIntegrationTests.cs
@@ -432,7 +432,7 @@ private async Task AvailableTools_SchemasAreAccepted(bool strict)
if (strict)
{
- aiFuncOptions.AdditionalProperties = new Dictionary { ["strictJsonSchema"] = true };
+ aiFuncOptions.AdditionalProperties = new Dictionary { ["strict"] = true };
}
return aiFuncOptions;
@@ -444,7 +444,7 @@ private async Task AvailableTools_SchemasAreAccepted(bool strict)
if (strict)
{
- additionalProperties["strictJsonSchema"] = true;
+ additionalProperties["strict"] = true;
}
return new CustomAIFunction($"CustomMethod{methodCount++}", schema, additionalProperties);
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs
index 5e4932c6736..99aa44cdd0f 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs
@@ -171,11 +171,11 @@ public async Task BasicRequestResponse_NonStreaming()
Assert.Equal(8, response.Usage.InputTokenCount);
Assert.Equal(9, response.Usage.OutputTokenCount);
Assert.Equal(17, response.Usage.TotalTokenCount);
+ Assert.Equal(13, response.Usage.CachedInputTokenCount);
+ Assert.Equal(90, response.Usage.ReasoningTokenCount);
Assert.Equal(new Dictionary
{
{ "InputTokenDetails.AudioTokenCount", 0 },
- { "InputTokenDetails.CachedTokenCount", 13 },
- { "OutputTokenDetails.ReasoningTokenCount", 90 },
{ "OutputTokenDetails.AudioTokenCount", 0 },
{ "OutputTokenDetails.AcceptedPredictionTokenCount", 0 },
{ "OutputTokenDetails.RejectedPredictionTokenCount", 0 },
@@ -258,12 +258,12 @@ public async Task BasicRequestResponse_Streaming()
Assert.Equal(8, usage.Details.InputTokenCount);
Assert.Equal(9, usage.Details.OutputTokenCount);
Assert.Equal(17, usage.Details.TotalTokenCount);
+ Assert.Equal(5, usage.Details.CachedInputTokenCount);
+ Assert.Equal(90, usage.Details.ReasoningTokenCount);
Assert.Equal(new AdditionalPropertiesDictionary
{
{ "InputTokenDetails.AudioTokenCount", 123 },
- { "InputTokenDetails.CachedTokenCount", 5 },
- { "OutputTokenDetails.ReasoningTokenCount", 90 },
{ "OutputTokenDetails.AudioTokenCount", 456 },
{ "OutputTokenDetails.AcceptedPredictionTokenCount", 0 },
{ "OutputTokenDetails.RejectedPredictionTokenCount", 0 },
@@ -332,7 +332,7 @@ public async Task ChatOptions_StrictRespected()
Tools = [AIFunctionFactory.Create(() => 42, "GetPersonAge", "Gets the age of the specified person.")],
AdditionalProperties = new()
{
- ["strictJsonSchema"] = true,
+ ["strict"] = true,
},
});
Assert.NotNull(response);
@@ -845,11 +845,11 @@ public async Task MultipleMessages_NonStreaming()
Assert.Equal(42, response.Usage.InputTokenCount);
Assert.Equal(15, response.Usage.OutputTokenCount);
Assert.Equal(57, response.Usage.TotalTokenCount);
+ Assert.Equal(13, response.Usage.CachedInputTokenCount);
+ Assert.Equal(90, response.Usage.ReasoningTokenCount);
Assert.Equal(new Dictionary
{
{ "InputTokenDetails.AudioTokenCount", 123 },
- { "InputTokenDetails.CachedTokenCount", 13 },
- { "OutputTokenDetails.ReasoningTokenCount", 90 },
{ "OutputTokenDetails.AudioTokenCount", 456 },
{ "OutputTokenDetails.AcceptedPredictionTokenCount", 0 },
{ "OutputTokenDetails.RejectedPredictionTokenCount", 0 },
@@ -942,11 +942,11 @@ public async Task MultiPartSystemMessage_NonStreaming()
Assert.Equal(42, response.Usage.InputTokenCount);
Assert.Equal(15, response.Usage.OutputTokenCount);
Assert.Equal(57, response.Usage.TotalTokenCount);
+ Assert.Equal(13, response.Usage.CachedInputTokenCount);
+ Assert.Equal(90, response.Usage.ReasoningTokenCount);
Assert.Equal(new Dictionary
{
{ "InputTokenDetails.AudioTokenCount", 0 },
- { "InputTokenDetails.CachedTokenCount", 13 },
- { "OutputTokenDetails.ReasoningTokenCount", 90 },
{ "OutputTokenDetails.AudioTokenCount", 0 },
{ "OutputTokenDetails.AcceptedPredictionTokenCount", 0 },
{ "OutputTokenDetails.RejectedPredictionTokenCount", 0 },
@@ -1040,11 +1040,11 @@ public async Task EmptyAssistantMessage_NonStreaming()
Assert.Equal(42, response.Usage.InputTokenCount);
Assert.Equal(15, response.Usage.OutputTokenCount);
Assert.Equal(57, response.Usage.TotalTokenCount);
+ Assert.Equal(13, response.Usage.CachedInputTokenCount);
+ Assert.Equal(90, response.Usage.ReasoningTokenCount);
Assert.Equal(new Dictionary
{
{ "InputTokenDetails.AudioTokenCount", 0 },
- { "InputTokenDetails.CachedTokenCount", 13 },
- { "OutputTokenDetails.ReasoningTokenCount", 90 },
{ "OutputTokenDetails.AudioTokenCount", 0 },
{ "OutputTokenDetails.AcceptedPredictionTokenCount", 0 },
{ "OutputTokenDetails.RejectedPredictionTokenCount", 0 },
@@ -1151,12 +1151,12 @@ public async Task FunctionCallContent_NonStreaming()
Assert.Equal(61, response.Usage.InputTokenCount);
Assert.Equal(16, response.Usage.OutputTokenCount);
Assert.Equal(77, response.Usage.TotalTokenCount);
+ Assert.Equal(13, response.Usage.CachedInputTokenCount);
+ Assert.Equal(90, response.Usage.ReasoningTokenCount);
Assert.Equal(new Dictionary
{
{ "InputTokenDetails.AudioTokenCount", 0 },
- { "InputTokenDetails.CachedTokenCount", 13 },
- { "OutputTokenDetails.ReasoningTokenCount", 90 },
{ "OutputTokenDetails.AudioTokenCount", 0 },
{ "OutputTokenDetails.AcceptedPredictionTokenCount", 0 },
{ "OutputTokenDetails.RejectedPredictionTokenCount", 0 },
@@ -1235,12 +1235,12 @@ public async Task UnavailableBuiltInFunctionCall_NonStreaming()
Assert.Equal(61, response.Usage.InputTokenCount);
Assert.Equal(16, response.Usage.OutputTokenCount);
Assert.Equal(77, response.Usage.TotalTokenCount);
+ Assert.Equal(13, response.Usage.CachedInputTokenCount);
+ Assert.Equal(90, response.Usage.ReasoningTokenCount);
Assert.Equal(new Dictionary
{
{ "InputTokenDetails.AudioTokenCount", 0 },
- { "InputTokenDetails.CachedTokenCount", 13 },
- { "OutputTokenDetails.ReasoningTokenCount", 90 },
{ "OutputTokenDetails.AudioTokenCount", 0 },
{ "OutputTokenDetails.AcceptedPredictionTokenCount", 0 },
{ "OutputTokenDetails.RejectedPredictionTokenCount", 0 },
@@ -1351,12 +1351,12 @@ public async Task FunctionCallContent_Streaming()
Assert.Equal(61, usage.Details.InputTokenCount);
Assert.Equal(16, usage.Details.OutputTokenCount);
Assert.Equal(77, usage.Details.TotalTokenCount);
+ Assert.Equal(0, usage.Details.CachedInputTokenCount);
+ Assert.Equal(90, usage.Details.ReasoningTokenCount);
Assert.Equal(new Dictionary
{
{ "InputTokenDetails.AudioTokenCount", 0 },
- { "InputTokenDetails.CachedTokenCount", 0 },
- { "OutputTokenDetails.ReasoningTokenCount", 90 },
{ "OutputTokenDetails.AudioTokenCount", 0 },
{ "OutputTokenDetails.AcceptedPredictionTokenCount", 0 },
{ "OutputTokenDetails.RejectedPredictionTokenCount", 0 },
@@ -1493,11 +1493,11 @@ public async Task AssistantMessageWithBothToolsAndContent_NonStreaming()
Assert.Equal(42, response.Usage.InputTokenCount);
Assert.Equal(15, response.Usage.OutputTokenCount);
Assert.Equal(57, response.Usage.TotalTokenCount);
+ Assert.Equal(20, response.Usage.CachedInputTokenCount);
+ Assert.Equal(90, response.Usage.ReasoningTokenCount);
Assert.Equal(new Dictionary
{
{ "InputTokenDetails.AudioTokenCount", 0 },
- { "InputTokenDetails.CachedTokenCount", 20 },
- { "OutputTokenDetails.ReasoningTokenCount", 90 },
{ "OutputTokenDetails.AudioTokenCount", 0 },
{ "OutputTokenDetails.AcceptedPredictionTokenCount", 0 },
{ "OutputTokenDetails.RejectedPredictionTokenCount", 0 },
@@ -1608,11 +1608,11 @@ private static async Task DataContentMessage_Image_AdditionalPropertyDetail_NonS
Assert.Equal(8513, response.Usage.InputTokenCount);
Assert.Equal(56, response.Usage.OutputTokenCount);
Assert.Equal(8569, response.Usage.TotalTokenCount);
+ Assert.Equal(0, response.Usage.CachedInputTokenCount);
+ Assert.Equal(0, response.Usage.ReasoningTokenCount);
Assert.Equal(new Dictionary
{
{ "InputTokenDetails.AudioTokenCount", 0 },
- { "InputTokenDetails.CachedTokenCount", 0 },
- { "OutputTokenDetails.ReasoningTokenCount", 0 },
{ "OutputTokenDetails.AudioTokenCount", 0 },
{ "OutputTokenDetails.AcceptedPredictionTokenCount", 0 },
{ "OutputTokenDetails.RejectedPredictionTokenCount", 0 },
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
index 9c4ffeefdcd..1a711b7417c 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
@@ -47,7 +47,7 @@ public void AsOpenAIChatResponseFormat_HandlesVariousFormats()
"""), RemoveWhitespace(((IJsonModel)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString()));
jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIChatResponseFormat(
- new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strictJsonSchema"] = true } });
+ new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strict"] = true } });
Assert.NotNull(jsonSchema);
Assert.Equal(RemoveWhitespace("""
{
@@ -82,7 +82,7 @@ public void AsOpenAIResponseTextFormat_HandlesVariousFormats()
"""), RemoveWhitespace(((IJsonModel)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString()));
jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIResponseTextFormat(
- new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strictJsonSchema"] = true } });
+ new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strict"] = true } });
Assert.NotNull(jsonSchema);
Assert.Equal(ResponseTextFormatKind.JsonSchema, jsonSchema.Kind);
Assert.Equal(RemoveWhitespace("""
@@ -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()
{
@@ -671,7 +788,7 @@ static async IAsyncEnumerable CreateUpdates()
[Fact]
public void AsChatResponse_ConvertsOpenAIResponse()
{
- Assert.Throws("response", () => ((OpenAIResponse)null!).AsChatResponse());
+ Assert.Throws("response", () => ((ResponseResult)null!).AsChatResponse());
// The OpenAI library currently doesn't provide any way to create an OpenAIResponse instance,
// as all constructors/factory methods currently are internal. Update this test when such functionality is available.
@@ -1238,32 +1355,32 @@ public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithMultipleUpdate
[Fact]
public void AsOpenAIResponse_WithNullArgument_ThrowsArgumentNullException()
{
- Assert.Throws("response", () => ((ChatResponse)null!).AsOpenAIResponse());
+ Assert.Throws("response", () => ((ChatResponse)null!).AsOpenAIResponseResult());
}
[Fact]
public void AsOpenAIResponse_WithRawRepresentation_ReturnsOriginal()
{
- var originalOpenAIResponse = OpenAIResponsesModelFactory.OpenAIResponse(
- "original-response-id",
- new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
- ResponseStatus.Completed,
- usage: null,
- maxOutputTokenCount: 100,
- outputItems: [],
- parallelToolCallsEnabled: false,
- model: "gpt-4",
- temperature: 0.7f,
- topP: 0.9f,
- previousResponseId: "prev-id",
- instructions: "Test instructions");
+ ResponseResult originalOpenAIResponse = new()
+ {
+ Id = "original-response-id",
+ CreatedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
+ Status = ResponseStatus.Completed,
+ MaxOutputTokenCount = 100,
+ ParallelToolCallsEnabled = false,
+ Model = "gpt-4",
+ Temperature = 0.7f,
+ TopP = 0.9f,
+ PreviousResponseId = "prev-id",
+ Instructions = "Test instructions"
+ };
var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Test"))
{
RawRepresentation = originalOpenAIResponse
};
- var result = chatResponse.AsOpenAIResponse();
+ var result = chatResponse.AsOpenAIResponseResult();
Assert.Same(originalOpenAIResponse, result);
}
@@ -1279,7 +1396,7 @@ public void AsOpenAIResponse_WithBasicChatResponse_CreatesValidOpenAIResponse()
FinishReason = ChatFinishReason.Stop
};
- var openAIResponse = chatResponse.AsOpenAIResponse();
+ var openAIResponse = chatResponse.AsOpenAIResponseResult();
Assert.NotNull(openAIResponse);
Assert.Equal("test-response-id", openAIResponse.Id);
@@ -1298,6 +1415,7 @@ public void AsOpenAIResponse_WithChatOptions_IncludesOptionsInResponse()
{
var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Test message"))
{
+ ConversationId = "conv_123",
ResponseId = "options-test",
ModelId = "gpt-3.5-turbo"
};
@@ -1306,20 +1424,19 @@ public void AsOpenAIResponse_WithChatOptions_IncludesOptionsInResponse()
{
MaxOutputTokens = 500,
AllowMultipleToolCalls = true,
- ConversationId = "conversation-123",
Instructions = "You are a helpful assistant.",
Temperature = 0.8f,
TopP = 0.95f,
ModelId = "override-model"
};
- var openAIResponse = chatResponse.AsOpenAIResponse(options);
+ var openAIResponse = chatResponse.AsOpenAIResponseResult(options);
Assert.Equal("options-test", openAIResponse.Id);
Assert.Equal("gpt-3.5-turbo", openAIResponse.Model);
Assert.Equal(500, openAIResponse.MaxOutputTokenCount);
Assert.True(openAIResponse.ParallelToolCallsEnabled);
- Assert.Equal("conversation-123", openAIResponse.PreviousResponseId);
+ Assert.Equal("conv_123", openAIResponse.ConversationOptions?.ConversationId);
Assert.Equal("You are a helpful assistant.", openAIResponse.Instructions);
Assert.Equal(0.8f, openAIResponse.Temperature);
Assert.Equal(0.95f, openAIResponse.TopP);
@@ -1334,7 +1451,7 @@ public void AsOpenAIResponse_WithEmptyMessages_CreatesResponseWithEmptyOutputIte
ModelId = "gpt-4"
};
- var openAIResponse = chatResponse.AsOpenAIResponse();
+ var openAIResponse = chatResponse.AsOpenAIResponseResult();
Assert.Equal("empty-response", openAIResponse.Id);
Assert.Equal("gpt-4", openAIResponse.Model);
@@ -1360,7 +1477,7 @@ public void AsOpenAIResponse_WithMultipleMessages_ConvertsAllMessages()
ResponseId = "multi-message-response"
};
- var openAIResponse = chatResponse.AsOpenAIResponse();
+ var openAIResponse = chatResponse.AsOpenAIResponseResult();
Assert.Equal(4, openAIResponse.OutputItems.Count);
@@ -1397,7 +1514,7 @@ public void AsOpenAIResponse_WithToolMessages_ConvertsCorrectly()
ResponseId = "tool-message-test"
};
- var openAIResponse = chatResponse.AsOpenAIResponse();
+ var openAIResponse = chatResponse.AsOpenAIResponseResult();
var outputItems = openAIResponse.OutputItems.ToArray();
Assert.Equal(4, outputItems.Length);
@@ -1428,7 +1545,7 @@ public void AsOpenAIResponse_WithSystemAndUserMessages_ConvertsCorrectly()
ResponseId = "system-user-test"
};
- var openAIResponse = chatResponse.AsOpenAIResponse();
+ var openAIResponse = chatResponse.AsOpenAIResponseResult();
var outputItems = openAIResponse.OutputItems.ToArray();
Assert.Equal(3, outputItems.Length);
@@ -1447,15 +1564,15 @@ public void AsOpenAIResponse_WithDefaultValues_UsesExpectedDefaults()
{
var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Default test"));
- var openAIResponse = chatResponse.AsOpenAIResponse();
+ var openAIResponse = chatResponse.AsOpenAIResponseResult();
Assert.NotNull(openAIResponse);
Assert.Equal(ResponseStatus.Completed, openAIResponse.Status);
- Assert.False(openAIResponse.ParallelToolCallsEnabled);
+ Assert.True(openAIResponse.ParallelToolCallsEnabled);
Assert.Null(openAIResponse.MaxOutputTokenCount);
Assert.Null(openAIResponse.Temperature);
Assert.Null(openAIResponse.TopP);
- Assert.Null(openAIResponse.PreviousResponseId);
+ Assert.Null(openAIResponse.ConversationOptions);
Assert.Null(openAIResponse.Instructions);
Assert.NotNull(openAIResponse.OutputItems);
}
@@ -1470,7 +1587,7 @@ public void AsOpenAIResponse_WithOptionsButNoModelId_UsesOptionsModelId()
ModelId = "options-model-id"
};
- var openAIResponse = chatResponse.AsOpenAIResponse(options);
+ var openAIResponse = chatResponse.AsOpenAIResponseResult(options);
Assert.Equal("options-model-id", openAIResponse.Model);
}
@@ -1488,7 +1605,7 @@ public void AsOpenAIResponse_WithBothModelIds_PrefersChatResponseModelId()
ModelId = "options-model-id"
};
- var openAIResponse = chatResponse.AsOpenAIResponse(options);
+ var openAIResponse = chatResponse.AsOpenAIResponseResult(options);
Assert.Equal("response-model-id", openAIResponse.Model);
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
index 830563a60e1..1421e780dca 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs
@@ -14,7 +14,7 @@ public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests
{
protected override IChatClient? CreateChatClient() =>
IntegrationTestHelpers.GetOpenAIClient()
- ?.GetOpenAIResponseClient(TestRunnerConfiguration.Instance["OpenAI:ChatModel"] ?? "gpt-4o-mini")
+ ?.GetResponsesClient(TestRunnerConfiguration.Instance["OpenAI:ChatModel"] ?? "gpt-4o-mini")
.AsIChatClient();
public override bool FunctionInvokingChatClientSetsConversationId => true;
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
index 94d767f67d4..1e19466ee7f 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs
@@ -11,7 +11,6 @@
using System.Net.Http;
using System.Text;
using System.Text.Json;
-using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
@@ -28,7 +27,7 @@ public class OpenAIResponseClientTests
[Fact]
public void AsIChatClient_InvalidArgs_Throws()
{
- Assert.Throws("responseClient", () => ((OpenAIResponseClient)null!).AsIChatClient());
+ Assert.Throws("responseClient", () => ((ResponsesClient)null!).AsIChatClient());
}
[Fact]
@@ -39,7 +38,7 @@ public void AsIChatClient_ProducesExpectedMetadata()
var client = new OpenAIClient(new ApiKeyCredential("key"), new OpenAIClientOptions { Endpoint = endpoint });
- IChatClient chatClient = client.GetOpenAIResponseClient(model).AsIChatClient();
+ IChatClient chatClient = client.GetResponsesClient(model).AsIChatClient();
var metadata = chatClient.GetService();
Assert.Equal("openai", metadata?.ProviderName);
Assert.Equal(endpoint, metadata?.ProviderUri);
@@ -49,11 +48,11 @@ public void AsIChatClient_ProducesExpectedMetadata()
[Fact]
public void GetService_SuccessfullyReturnsUnderlyingClient()
{
- OpenAIResponseClient openAIClient = new OpenAIClient(new ApiKeyCredential("key")).GetOpenAIResponseClient("model");
+ ResponsesClient openAIClient = new OpenAIClient(new ApiKeyCredential("key")).GetResponsesClient("model");
IChatClient chatClient = openAIClient.AsIChatClient();
Assert.Same(chatClient, chatClient.GetService());
- Assert.Same(openAIClient, chatClient.GetService());
+ Assert.Same(openAIClient, chatClient.GetService());
using IChatClient pipeline = chatClient
.AsBuilder()
@@ -67,7 +66,7 @@ public void GetService_SuccessfullyReturnsUnderlyingClient()
Assert.NotNull(pipeline.GetService());
Assert.NotNull(pipeline.GetService());
- Assert.Same(openAIClient, pipeline.GetService());
+ Assert.Same(openAIClient, pipeline.GetService());
Assert.IsType(pipeline.GetService());
}
@@ -295,7 +294,7 @@ 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 ResponseCreationOptions
+ RawRepresentationFactory = options => new CreateResponseOptions
{
ReasoningOptions = new()
{
@@ -428,7 +427,7 @@ public async Task ReasoningTextDelta_Streaming()
List updates = [];
await foreach (var update in client.GetStreamingResponseAsync("Solve this problem step by step.", new()
{
- RawRepresentationFactory = options => new ResponseCreationOptions
+ RawRepresentationFactory = options => new CreateResponseOptions
{
ReasoningOptions = new()
{
@@ -609,7 +608,6 @@ public async Task MissingAbstractionResponse_NonStreaming()
"display_height": 768
}
],
- "tool_choice": "auto",
"input": [
{
"type": "message",
@@ -700,7 +698,7 @@ public async Task MissingAbstractionResponse_NonStreaming()
ChatOptions chatOptions = new()
{
Tools = [ResponseTool.CreateComputerTool(ComputerToolEnvironment.Browser, 1024, 768).AsAITool()],
- RawRepresentationFactory = options => new ResponseCreationOptions
+ RawRepresentationFactory = options => new CreateResponseOptions
{
ReasoningOptions = new() { ReasoningSummaryVerbosity = ResponseReasoningSummaryVerbosity.Concise },
}
@@ -745,7 +743,6 @@ public async Task MissingAbstractionResponse_Streaming()
"display_height": 768
}
],
- "tool_choice": "auto",
"input": [
{
"type": "message",
@@ -789,7 +786,7 @@ public async Task MissingAbstractionResponse_Streaming()
ChatOptions chatOptions = new()
{
Tools = [ResponseTool.CreateComputerTool(ComputerToolEnvironment.Browser, 1024, 768).AsAITool()],
- RawRepresentationFactory = options => new ResponseCreationOptions
+ RawRepresentationFactory = options => new CreateResponseOptions
{
ReasoningOptions = new() { ReasoningSummaryVerbosity = ResponseReasoningSummaryVerbosity.Concise },
}
@@ -843,7 +840,6 @@ public async Task ChatOptions_StrictRespected()
]
}
],
- "tool_choice": "auto",
"tools": [
{
"type": "function",
@@ -894,7 +890,7 @@ public async Task ChatOptions_StrictRespected()
Tools = [AIFunctionFactory.Create(() => 42, "GetPersonAge", "Gets the age of the specified person.")],
AdditionalProperties = new()
{
- ["strictJsonSchema"] = true,
+ ["strict"] = true,
},
});
Assert.NotNull(response);
@@ -1039,7 +1035,7 @@ public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentatio
{
RawRepresentationFactory = (c) =>
{
- ResponseCreationOptions openAIOptions = new()
+ CreateResponseOptions openAIOptions = new()
{
MaxOutputTokenCount = 10,
PreviousResponseId = "resp_42",
@@ -1202,7 +1198,6 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role)
"server_url": "https://mcp.deepwiki.com/mcp"
}
],
- "tool_choice": "auto",
"input": [
{
"type": "message",
@@ -1257,7 +1252,6 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role)
},
"verbosity": "medium"
},
- "tool_choice": "auto",
"tools": [
{
"type": "mcp",
@@ -1317,7 +1311,6 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role)
"server_url": "https://mcp.deepwiki.com/mcp"
}
],
- "tool_choice": "auto",
"input": [
{
"type": "mcp_approval_response",
@@ -1474,7 +1467,6 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool)
"require_approval": "never"
}
],
- "tool_choice": "auto",
"input": [
{
"type": "message",
@@ -1739,7 +1731,6 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming()
"require_approval": "never"
}
],
- "tool_choice": "auto",
"input": [
{
"type": "message",
@@ -2610,7 +2601,6 @@ public async Task CodeInterpreterTool_NonStreaming()
"role":"user",
"content":[{"type":"input_text","text":"Calculate the sum of numbers from 1 to 5"}]
}],
- "tool_choice":"auto",
"tools":[{
"type":"code_interpreter",
"container":{"type":"auto"}
@@ -2739,7 +2729,6 @@ public async Task CodeInterpreterTool_Streaming()
"role":"user",
"content":[{"type":"input_text","text":"Calculate the sum of numbers from 1 to 10 using Python"}]
}],
- "tool_choice":"auto",
"tools":[{
"type":"code_interpreter",
"container":{"type":"auto"}
@@ -3032,7 +3021,7 @@ public async Task ConversationId_AsConversationId_NonStreaming()
{
"temperature":0.5,
"model":"gpt-4o-mini",
- "conversation":"conv_12345",
+ "conversation":{"id":"conv_12345"},
"input": [{
"type":"message",
"role":"user",
@@ -3134,7 +3123,7 @@ public async Task ConversationId_WhenStoreDisabled_ReturnsNull_NonStreaming()
{
MaxOutputTokens = 20,
Temperature = 0.5f,
- RawRepresentationFactory = (c) => new ResponseCreationOptions
+ RawRepresentationFactory = (c) => new CreateResponseOptions
{
StoredOutputEnabled = false
}
@@ -3196,7 +3185,7 @@ public async Task ConversationId_ChatOptionsOverridesRawRepresentationResponseId
MaxOutputTokens = 20,
Temperature = 0.5f,
ConversationId = "resp_override",
- RawRepresentationFactory = (c) => new ResponseCreationOptions
+ RawRepresentationFactory = (c) => new CreateResponseOptions
{
PreviousResponseId = null
}
@@ -3258,7 +3247,7 @@ public async Task ConversationId_RawRepresentationPreviousResponseIdTakesPrecede
MaxOutputTokens = 20,
Temperature = 0.5f,
ConversationId = "conv_ignored",
- RawRepresentationFactory = (c) => new ResponseCreationOptions
+ RawRepresentationFactory = (c) => new CreateResponseOptions
{
PreviousResponseId = "resp_fromraw"
}
@@ -3320,7 +3309,7 @@ public async Task ConversationId_WhenStoreExplicitlyTrue_UsesResponseId_NonStrea
{
MaxOutputTokens = 20,
Temperature = 0.5f,
- RawRepresentationFactory = (c) => new ResponseCreationOptions
+ RawRepresentationFactory = (c) => new CreateResponseOptions
{
StoredOutputEnabled = true
}
@@ -3394,7 +3383,7 @@ public async Task ConversationId_WhenStoreExplicitlyTrue_UsesResponseId_Streamin
{
MaxOutputTokens = 20,
Temperature = 0.5f,
- RawRepresentationFactory = (c) => new ResponseCreationOptions
+ RawRepresentationFactory = (c) => new CreateResponseOptions
{
StoredOutputEnabled = true
}
@@ -3475,7 +3464,7 @@ public async Task ConversationId_WhenStoreDisabled_ReturnsNull_Streaming()
{
MaxOutputTokens = 20,
Temperature = 0.5f,
- RawRepresentationFactory = (c) => new ResponseCreationOptions
+ RawRepresentationFactory = (c) => new CreateResponseOptions
{
StoredOutputEnabled = false
}
@@ -3500,7 +3489,7 @@ public async Task ConversationId_AsConversationId_Streaming()
{
"temperature":0.5,
"model":"gpt-4o-mini",
- "conversation":"conv_12345",
+ "conversation":{"id":"conv_12345"},
"input":[
{
"type":"message",
@@ -3656,7 +3645,7 @@ public async Task ConversationId_RawRepresentationConversationIdTakesPrecedence_
{
"temperature":0.5,
"model":"gpt-4o-mini",
- "conversation":"conv_12345",
+ "conversation":{"id":"conv_12345"},
"input": [{
"type":"message",
"role":"user",
@@ -3695,20 +3684,15 @@ public async Task ConversationId_RawRepresentationConversationIdTakesPrecedence_
using HttpClient httpClient = new(handler);
using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini");
- var rcoJsonModel = (IJsonModel)new ResponseCreationOptions();
- BinaryData rcoJsonBinaryData = rcoJsonModel.Write(ModelReaderWriterOptions.Json);
- JsonObject rcoJsonObject = Assert.IsType(JsonNode.Parse(rcoJsonBinaryData.ToMemory().Span));
- Assert.Null(rcoJsonObject["conversation"]);
- rcoJsonObject["conversation"] = "conv_12345";
-
var response = await client.GetResponseAsync("hello", new()
{
MaxOutputTokens = 20,
Temperature = 0.5f,
ConversationId = "conv_ignored",
- RawRepresentationFactory = (c) => rcoJsonModel.Create(
- new BinaryData(JsonSerializer.SerializeToUtf8Bytes(rcoJsonObject)),
- ModelReaderWriterOptions.Json)
+ RawRepresentationFactory = _ => new CreateResponseOptions
+ {
+ ConversationOptions = new("conv_12345"),
+ }
});
Assert.NotNull(response);
@@ -4584,12 +4568,12 @@ public async Task ResponseWithUsageDetails_ParsesTokenCounts()
var response = await client.GetResponseAsync("test");
Assert.NotNull(response.Usage);
+ Assert.Null(response.Usage.AdditionalCounts);
Assert.Equal(50, response.Usage.InputTokenCount);
Assert.Equal(25, response.Usage.OutputTokenCount);
Assert.Equal(75, response.Usage.TotalTokenCount);
- Assert.NotNull(response.Usage.AdditionalCounts);
- Assert.Equal(10, response.Usage.AdditionalCounts["InputTokenDetails.CachedTokenCount"]);
- Assert.Equal(5, response.Usage.AdditionalCounts["OutputTokenDetails.ReasoningTokenCount"]);
+ Assert.Equal(10, response.Usage.CachedInputTokenCount);
+ Assert.Equal(5, response.Usage.ReasoningTokenCount);
}
[Fact]
@@ -5146,7 +5130,6 @@ public async Task HostedImageGenerationTool_NonStreaming()
"output_format": "png"
}
],
- "tool_choice": "auto",
"input": [
{
"type": "message",
@@ -5240,7 +5223,6 @@ public async Task HostedImageGenerationTool_Streaming()
"output_format": "png"
}
],
- "tool_choice": "auto",
"stream": true,
"input": [
{
@@ -5352,7 +5334,6 @@ public async Task HostedImageGenerationTool_StreamingMultipleImages()
"partial_images": 3
}
],
- "tool_choice": "auto",
"stream": true,
"input": [
{
@@ -5486,7 +5467,7 @@ private static IChatClient CreateResponseClient(HttpClient httpClient, string mo
new OpenAIClient(
new ApiKeyCredential("apikey"),
new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) })
- .GetOpenAIResponseClient(modelId)
+ .GetResponsesClient(modelId)
.AsIChatClient();
private static string ResponseStatusToRequestValue(ResponseStatus status)
diff --git a/test/Libraries/Microsoft.Extensions.DataIngestion.Tests/Readers/MarkdownReaderTests.cs b/test/Libraries/Microsoft.Extensions.DataIngestion.Tests/Readers/MarkdownReaderTests.cs
index dce6d996821..0e0ac10ca91 100644
--- a/test/Libraries/Microsoft.Extensions.DataIngestion.Tests/Readers/MarkdownReaderTests.cs
+++ b/test/Libraries/Microsoft.Extensions.DataIngestion.Tests/Readers/MarkdownReaderTests.cs
@@ -59,6 +59,47 @@ public override async Task SupportsTables()
Assert.Equal(expected, documentTable.Cells.Map(element => element!.GetMarkdown().Trim()));
}
+ [ConditionalFact]
+ public async Task SupportsTablesWithoutTrailingPipes()
+ {
+ // Markdown tables without trailing pipes (|) at the end of each row should be parsed correctly.
+ // This was causing IndexOutOfRangeException before the fix.
+ string markdownContent = """
+ # ReadyToRun Flags
+
+ | Flag | Value | Description
+ |:-------------------------------------------|-----------:|:-----------
+ | READYTORUN_FLAG_PLATFORM_NEUTRAL_SOURCE | 0x00000001 | Set if the original IL image was platform neutral.
+ | READYTORUN_FLAG_COMPOSITE | 0x00000002 | The image represents a composite R2R file.
+ | READYTORUN_FLAG_PARTIAL | 0x00000004 |
+ | READYTORUN_FLAG_NONSHARED_PINVOKE_STUBS | 0x00000008 | PInvoke stubs compiled into image are non-shareable.
+ | READYTORUN_FLAG_EMBEDDED_MSIL | 0x00000010 | Input MSIL is embedded in the R2R image.
+ | READYTORUN_FLAG_COMPONENT | 0x00000020 | This is a component assembly of a composite R2R image
+ | READYTORUN_FLAG_MULTIMODULE_VERSION_BUBBLE | 0x00000040 | This R2R module has multiple modules within its version bubble.
+ | READYTORUN_FLAG_UNRELATED_R2R_CODE | 0x00000080 | This R2R module has code in it that would not be naturally encoded.
+ | READYTORUN_FLAG_PLATFORM_NATIVE_IMAGE | 0x00000100 | The owning composite executable is in the platform native format
+ """;
+
+ IngestionDocument document = await ReadAsync(markdownContent);
+
+ IngestionDocumentTable documentTable = Assert.Single(document.EnumerateContent().OfType());
+ Assert.Equal(10, documentTable.Cells.GetLength(0)); // 10 rows (1 header + 9 data rows)
+ Assert.Equal(3, documentTable.Cells.GetLength(1)); // 3 columns
+
+ // Verify a few key cells
+ Assert.Equal("Flag", documentTable.Cells[0, 0]!.GetMarkdown().Trim());
+ Assert.Equal("Value", documentTable.Cells[0, 1]!.GetMarkdown().Trim());
+ Assert.Equal("Description", documentTable.Cells[0, 2]!.GetMarkdown().Trim());
+
+ Assert.Equal("READYTORUN_FLAG_PLATFORM_NEUTRAL_SOURCE", documentTable.Cells[1, 0]!.GetMarkdown().Trim());
+ Assert.Equal("0x00000001", documentTable.Cells[1, 1]!.GetMarkdown().Trim());
+ Assert.Contains("platform neutral", documentTable.Cells[1, 2]!.GetMarkdown().Trim());
+
+ Assert.Equal("READYTORUN_FLAG_PARTIAL", documentTable.Cells[3, 0]!.GetMarkdown().Trim());
+ Assert.Equal("0x00000004", documentTable.Cells[3, 1]!.GetMarkdown().Trim());
+ Assert.Null(documentTable.Cells[3, 2]); // Empty description cell is null
+ }
+
[ConditionalFact]
public override async Task SupportsImages()
{
@@ -133,6 +174,32 @@ public async Task SupportsTablesWithImages()
Assert.Equal("Latest logo", img.AlternativeText);
}
+ [ConditionalFact]
+ public async Task SupportsInlineHtml()
+ {
+ string markdownContent = "This has [1] inline HTML.";
+
+ IngestionDocument document = await ReadAsync(markdownContent);
+
+ var paragraph = Assert.Single(document.EnumerateContent().OfType());
+ Assert.Equal("This has [1] inline HTML.", paragraph.Text);
+ Assert.Equal(markdownContent, paragraph.GetMarkdown());
+ }
+
+ [ConditionalFact]
+ public async Task SupportsMultipleInlineHtmlElements()
+ {
+ string markdownContent = """
+ Text with bold, italic, subscript, and superscript tags.
+ """;
+
+ IngestionDocument document = await ReadAsync(markdownContent);
+
+ var paragraph = Assert.Single(document.EnumerateContent().OfType());
+ Assert.Equal("Text with bold, italic, subscript, and superscript tags.", paragraph.Text);
+ Assert.Equal(markdownContent, paragraph.GetMarkdown());
+ }
+
private async Task ReadAsync(string content)
{
using MemoryStream stream = new(System.Text.Encoding.UTF8.GetBytes(content));
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj
index dfbeed45dbf..47cf239e07b 100644
--- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj
+++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.AzureOpenAI_Qdrant_Aspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj
@@ -9,11 +9,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj
index 34a192db6cd..77ead7c69ea 100644
--- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj
+++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj
@@ -8,10 +8,10 @@
-
-
-
-
+
+
+
+
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj
index 55455cd7afe..07b23118444 100644
--- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj
+++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.BasicAspire.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj
@@ -9,11 +9,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj
index 4f1a5c24e25..58262329c83 100644
--- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj
+++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Ollama_Qdrant.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj
@@ -10,10 +10,10 @@
-
-
-
-
+
+
+
+
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs
index d7e25135462..4bfb1ca7796 100644
--- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/Program.cs
+++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.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 // GetOpenAIResponseClient(string) is experimental and subject to change or removal in future updates.
-var chatClient = openAIClient.GetOpenAIResponseClient("gpt-4o-mini").AsIChatClient();
+#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 restore OPENAI001
var embeddingGenerator = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj
index c22478b3496..383a6012b2c 100644
--- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj
+++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.OpenAI_AzureAISearch.verified/aichatweb/aichatweb.csproj
@@ -8,11 +8,11 @@
-
+
-
-
-
+
+
+