Skip to content

Commit 24188c7

Browse files
SteveSandersonMSjeffhandley
authored andcommitted
Add ChatOptions.AllowMultipleToolCalls (#6326)
* Add ChatOptions.AllowMultipleToolCalls * Use it in OpenAI adapter
1 parent a5e7a69 commit 24188c7

File tree

5 files changed

+32
-15
lines changed

5 files changed

+32
-15
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,23 @@ public string? ChatThreadId
9393
/// </remarks>
9494
public IList<string>? StopSequences { get; set; }
9595

96+
/// <summary>
97+
/// Gets or sets a flag to indicate whether a single response is allowed to include multiple tool calls.
98+
/// If <see langword="false"/>, the <see cref="IChatClient"/> is asked to return a maximum of one tool call per request.
99+
/// If <see langword="true"/>, there is no limit.
100+
/// If <see langword="null"/>, the provider may select its own default.
101+
/// </summary>
102+
/// <remarks>
103+
/// <para>
104+
/// When used with function calling middleware, this does not affect the ability to perform multiple function calls in sequence.
105+
/// It only affects the number of function calls within a single iteration of the function calling loop.
106+
/// </para>
107+
/// <para>
108+
/// 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.
109+
/// </para>
110+
/// </remarks>
111+
public bool? AllowMultipleToolCalls { get; set; }
112+
96113
/// <summary>Gets or sets the tool mode for the chat request.</summary>
97114
/// <remarks>The default value is <see langword="null"/>, which is treated the same as <see cref="ChatToolMode.Auto"/>.</remarks>
98115
public ChatToolMode? ToolMode { get; set; }
@@ -125,6 +142,7 @@ public virtual ChatOptions Clone()
125142
Seed = Seed,
126143
ResponseFormat = ResponseFormat,
127144
ModelId = ModelId,
145+
AllowMultipleToolCalls = AllowMultipleToolCalls,
128146
ToolMode = ToolMode,
129147
AdditionalProperties = AdditionalProperties?.Clone(),
130148
};

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ private static ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
496496
result.TopP = options.TopP;
497497
result.PresencePenalty = options.PresencePenalty;
498498
result.Temperature = options.Temperature;
499+
result.AllowParallelToolCalls = options.AllowMultipleToolCalls;
499500
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
500501
result.Seed = options.Seed;
501502
#pragma warning restore OPENAI001
@@ -510,11 +511,6 @@ private static ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
510511

511512
if (options.AdditionalProperties is { Count: > 0 } additionalProperties)
512513
{
513-
if (additionalProperties.TryGetValue(nameof(result.AllowParallelToolCalls), out bool allowParallelToolCalls))
514-
{
515-
result.AllowParallelToolCalls = allowParallelToolCalls;
516-
}
517-
518514
if (additionalProperties.TryGetValue(nameof(result.AudioOptions), out ChatAudioOptions? audioOptions))
519515
{
520516
result.AudioOptions = audioOptions;

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponseChatClient.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,15 +312,11 @@ private static ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptio
312312
result.PreviousResponseId = options.ConversationId;
313313
result.TopP = options.TopP;
314314
result.Temperature = options.Temperature;
315+
result.ParallelToolCallsEnabled = options.AllowMultipleToolCalls;
315316

316317
// Handle loosely-typed properties from AdditionalProperties.
317318
if (options.AdditionalProperties is { Count: > 0 } additionalProperties)
318319
{
319-
if (additionalProperties.TryGetValue(nameof(result.ParallelToolCallsEnabled), out bool allowParallelToolCalls))
320-
{
321-
result.ParallelToolCallsEnabled = allowParallelToolCalls;
322-
}
323-
324320
if (additionalProperties.TryGetValue(nameof(result.EndUserId), out string? endUserId))
325321
{
326322
result.EndUserId = endUserId;

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/ChatCompletion/ChatOptionsTests.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,24 @@ public void Constructor_Parameterless_PropsDefaulted()
2424
Assert.Null(options.ResponseFormat);
2525
Assert.Null(options.ModelId);
2626
Assert.Null(options.StopSequences);
27+
Assert.Null(options.AllowMultipleToolCalls);
2728
Assert.Null(options.ToolMode);
2829
Assert.Null(options.Tools);
2930
Assert.Null(options.AdditionalProperties);
3031

3132
ChatOptions clone = options.Clone();
32-
Assert.Null(options.ConversationId);
33+
Assert.Null(clone.ConversationId);
3334
Assert.Null(clone.Temperature);
3435
Assert.Null(clone.MaxOutputTokens);
3536
Assert.Null(clone.TopP);
3637
Assert.Null(clone.TopK);
3738
Assert.Null(clone.FrequencyPenalty);
3839
Assert.Null(clone.PresencePenalty);
39-
Assert.Null(options.Seed);
40+
Assert.Null(clone.Seed);
4041
Assert.Null(clone.ResponseFormat);
4142
Assert.Null(clone.ModelId);
4243
Assert.Null(clone.StopSequences);
44+
Assert.Null(clone.AllowMultipleToolCalls);
4345
Assert.Null(clone.ToolMode);
4446
Assert.Null(clone.Tools);
4547
Assert.Null(clone.AdditionalProperties);
@@ -78,6 +80,7 @@ public void Properties_Roundtrip()
7880
options.ResponseFormat = ChatResponseFormat.Json;
7981
options.ModelId = "modelId";
8082
options.StopSequences = stopSequences;
83+
options.AllowMultipleToolCalls = true;
8184
options.ToolMode = ChatToolMode.RequireAny;
8285
options.Tools = tools;
8386
options.AdditionalProperties = additionalProps;
@@ -93,22 +96,24 @@ public void Properties_Roundtrip()
9396
Assert.Same(ChatResponseFormat.Json, options.ResponseFormat);
9497
Assert.Equal("modelId", options.ModelId);
9598
Assert.Same(stopSequences, options.StopSequences);
99+
Assert.True(options.AllowMultipleToolCalls);
96100
Assert.Same(ChatToolMode.RequireAny, options.ToolMode);
97101
Assert.Same(tools, options.Tools);
98102
Assert.Same(additionalProps, options.AdditionalProperties);
99103

100104
ChatOptions clone = options.Clone();
101-
Assert.Equal("12345", options.ConversationId);
105+
Assert.Equal("12345", clone.ConversationId);
102106
Assert.Equal(0.1f, clone.Temperature);
103107
Assert.Equal(2, clone.MaxOutputTokens);
104108
Assert.Equal(0.3f, clone.TopP);
105109
Assert.Equal(42, clone.TopK);
106110
Assert.Equal(0.4f, clone.FrequencyPenalty);
107111
Assert.Equal(0.5f, clone.PresencePenalty);
108-
Assert.Equal(12345, options.Seed);
112+
Assert.Equal(12345, clone.Seed);
109113
Assert.Same(ChatResponseFormat.Json, clone.ResponseFormat);
110114
Assert.Equal("modelId", clone.ModelId);
111115
Assert.Equal(stopSequences, clone.StopSequences);
116+
Assert.True(clone.AllowMultipleToolCalls);
112117
Assert.Same(ChatToolMode.RequireAny, clone.ToolMode);
113118
Assert.Equal(tools, clone.Tools);
114119
Assert.Equal(additionalProps, clone.AdditionalProperties);
@@ -141,6 +146,7 @@ public void JsonSerialization_Roundtrips()
141146
options.ResponseFormat = ChatResponseFormat.Json;
142147
options.ModelId = "modelId";
143148
options.StopSequences = stopSequences;
149+
options.AllowMultipleToolCalls = false;
144150
options.ToolMode = ChatToolMode.RequireAny;
145151
options.Tools =
146152
[
@@ -166,6 +172,7 @@ public void JsonSerialization_Roundtrips()
166172
Assert.Equal("modelId", deserialized.ModelId);
167173
Assert.NotSame(stopSequences, deserialized.StopSequences);
168174
Assert.Equal(stopSequences, deserialized.StopSequences);
175+
Assert.False(deserialized.AllowMultipleToolCalls);
169176
Assert.Equal(ChatToolMode.RequireAny, deserialized.ToolMode);
170177
Assert.Null(deserialized.Tools);
171178

test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ public async Task NonStronglyTypedOptions_AllSent()
319319

320320
Assert.NotNull(await client.GetResponseAsync("hello", new()
321321
{
322+
AllowMultipleToolCalls = false,
322323
AdditionalProperties = new()
323324
{
324325
["StoredOutputEnabled"] = true,
@@ -329,7 +330,6 @@ public async Task NonStronglyTypedOptions_AllSent()
329330
["LogitBiases"] = new Dictionary<int, int> { { 12, 34 } },
330331
["IncludeLogProbabilities"] = true,
331332
["TopLogProbabilityCount"] = 42,
332-
["AllowParallelToolCalls"] = false,
333333
["EndUserId"] = "12345",
334334
},
335335
}));

0 commit comments

Comments
 (0)