diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs index aab2f53a28d..8b3bef838dd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs @@ -93,6 +93,23 @@ public string? ChatThreadId /// public IList? StopSequences { get; set; } + /// + /// Gets or sets a flag to indicate whether a single response is allowed to include multiple tool calls. + /// If , the is asked to return a maximum of one tool call per request. + /// If , there is no limit. + /// If , the provider may select its own default. + /// + /// + /// + /// When used with function calling middleware, this does not affect the ability to perform multiple function calls in sequence. + /// It only affects the number of function calls within a single iteration of the function calling loop. + /// + /// + /// The underlying provider is not guaranteed to support or honor this flag. For example it may choose to ignore it and return multiple tool calls regardless. + /// + /// + public bool? AllowMultipleToolCalls { get; set; } + /// Gets or sets the tool mode for the chat request. /// The default value is , which is treated the same as . public ChatToolMode? ToolMode { get; set; } @@ -125,6 +142,7 @@ public virtual ChatOptions Clone() Seed = Seed, ResponseFormat = ResponseFormat, ModelId = ModelId, + AllowMultipleToolCalls = AllowMultipleToolCalls, ToolMode = ToolMode, AdditionalProperties = AdditionalProperties?.Clone(), }; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs index 24387aceb62..2aa4f5817fb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs @@ -496,6 +496,7 @@ private static ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) result.TopP = options.TopP; result.PresencePenalty = options.PresencePenalty; result.Temperature = options.Temperature; + result.AllowParallelToolCalls = options.AllowMultipleToolCalls; #pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. result.Seed = options.Seed; #pragma warning restore OPENAI001 @@ -510,11 +511,6 @@ private static ChatCompletionOptions ToOpenAIOptions(ChatOptions? options) if (options.AdditionalProperties is { Count: > 0 } additionalProperties) { - if (additionalProperties.TryGetValue(nameof(result.AllowParallelToolCalls), out bool allowParallelToolCalls)) - { - result.AllowParallelToolCalls = allowParallelToolCalls; - } - if (additionalProperties.TryGetValue(nameof(result.AudioOptions), out ChatAudioOptions? audioOptions)) { result.AudioOptions = audioOptions; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs index b0498f55e5d..355b3955f85 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs @@ -312,15 +312,11 @@ private static ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptio result.PreviousResponseId = options.ConversationId; result.TopP = options.TopP; result.Temperature = options.Temperature; + result.ParallelToolCallsEnabled = options.AllowMultipleToolCalls; // Handle loosely-typed properties from AdditionalProperties. if (options.AdditionalProperties is { Count: > 0 } additionalProperties) { - if (additionalProperties.TryGetValue(nameof(result.ParallelToolCallsEnabled), out bool allowParallelToolCalls)) - { - result.ParallelToolCallsEnabled = allowParallelToolCalls; - } - if (additionalProperties.TryGetValue(nameof(result.EndUserId), out string? endUserId)) { result.EndUserId = endUserId; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs index 858e81a459f..67bbfb6d3db 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs @@ -24,22 +24,24 @@ public void Constructor_Parameterless_PropsDefaulted() Assert.Null(options.ResponseFormat); Assert.Null(options.ModelId); Assert.Null(options.StopSequences); + Assert.Null(options.AllowMultipleToolCalls); Assert.Null(options.ToolMode); Assert.Null(options.Tools); Assert.Null(options.AdditionalProperties); ChatOptions clone = options.Clone(); - Assert.Null(options.ConversationId); + Assert.Null(clone.ConversationId); Assert.Null(clone.Temperature); Assert.Null(clone.MaxOutputTokens); Assert.Null(clone.TopP); Assert.Null(clone.TopK); Assert.Null(clone.FrequencyPenalty); Assert.Null(clone.PresencePenalty); - Assert.Null(options.Seed); + Assert.Null(clone.Seed); Assert.Null(clone.ResponseFormat); Assert.Null(clone.ModelId); Assert.Null(clone.StopSequences); + Assert.Null(clone.AllowMultipleToolCalls); Assert.Null(clone.ToolMode); Assert.Null(clone.Tools); Assert.Null(clone.AdditionalProperties); @@ -78,6 +80,7 @@ public void Properties_Roundtrip() options.ResponseFormat = ChatResponseFormat.Json; options.ModelId = "modelId"; options.StopSequences = stopSequences; + options.AllowMultipleToolCalls = true; options.ToolMode = ChatToolMode.RequireAny; options.Tools = tools; options.AdditionalProperties = additionalProps; @@ -93,22 +96,24 @@ public void Properties_Roundtrip() Assert.Same(ChatResponseFormat.Json, options.ResponseFormat); Assert.Equal("modelId", options.ModelId); Assert.Same(stopSequences, options.StopSequences); + Assert.True(options.AllowMultipleToolCalls); Assert.Same(ChatToolMode.RequireAny, options.ToolMode); Assert.Same(tools, options.Tools); Assert.Same(additionalProps, options.AdditionalProperties); ChatOptions clone = options.Clone(); - Assert.Equal("12345", options.ConversationId); + Assert.Equal("12345", clone.ConversationId); Assert.Equal(0.1f, clone.Temperature); Assert.Equal(2, clone.MaxOutputTokens); Assert.Equal(0.3f, clone.TopP); Assert.Equal(42, clone.TopK); Assert.Equal(0.4f, clone.FrequencyPenalty); Assert.Equal(0.5f, clone.PresencePenalty); - Assert.Equal(12345, options.Seed); + Assert.Equal(12345, clone.Seed); Assert.Same(ChatResponseFormat.Json, clone.ResponseFormat); Assert.Equal("modelId", clone.ModelId); Assert.Equal(stopSequences, clone.StopSequences); + Assert.True(clone.AllowMultipleToolCalls); Assert.Same(ChatToolMode.RequireAny, clone.ToolMode); Assert.Equal(tools, clone.Tools); Assert.Equal(additionalProps, clone.AdditionalProperties); @@ -141,6 +146,7 @@ public void JsonSerialization_Roundtrips() options.ResponseFormat = ChatResponseFormat.Json; options.ModelId = "modelId"; options.StopSequences = stopSequences; + options.AllowMultipleToolCalls = false; options.ToolMode = ChatToolMode.RequireAny; options.Tools = [ @@ -166,6 +172,7 @@ public void JsonSerialization_Roundtrips() Assert.Equal("modelId", deserialized.ModelId); Assert.NotSame(stopSequences, deserialized.StopSequences); Assert.Equal(stopSequences, deserialized.StopSequences); + Assert.False(deserialized.AllowMultipleToolCalls); Assert.Equal(ChatToolMode.RequireAny, deserialized.ToolMode); Assert.Null(deserialized.Tools); diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs index 2d7ec89b149..e32b522c9d5 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs @@ -319,6 +319,7 @@ public async Task NonStronglyTypedOptions_AllSent() Assert.NotNull(await client.GetResponseAsync("hello", new() { + AllowMultipleToolCalls = false, AdditionalProperties = new() { ["StoredOutputEnabled"] = true, @@ -329,7 +330,6 @@ public async Task NonStronglyTypedOptions_AllSent() ["LogitBiases"] = new Dictionary { { 12, 34 } }, ["IncludeLogProbabilities"] = true, ["TopLogProbabilityCount"] = 42, - ["AllowParallelToolCalls"] = false, ["EndUserId"] = "12345", }, }));