.Net: Honor FunctionChoiceBehavior function list in Gemini connector (#14183)

## Motivation and Context

The Gemini connector converts a `FunctionChoiceBehavior` (FCB) into the
connector-internal `GeminiToolCallBehavior` in
`GeminiPromptExecutionSettings`. That conversion only inspected the
`AutoInvoke` flag and evaluated the behavior against an empty kernel, so
the configured function list and choice were dropped:

- `FunctionChoiceBehavior.Auto()` / `Auto([subset])` always provided
**all** kernel functions to the model, regardless of the list supplied.
- `FunctionChoiceBehavior.Auto([])`, which is documented as equivalent
to disabling function calling, still provided all kernel functions.
- Explicit function lists failed to convert because the behavior was
evaluated against an empty kernel.

This diverges from the other connectors, which resolve the behavior
against the request kernel and provide exactly the functions it
specifies.

## Description

- Resolve the `FunctionChoiceBehavior` against the request's kernel and
provide exactly the functions it declares (all, an explicit subset, or
none), matching the shared function-calling semantics used by the other
connectors.
- An empty function list now correctly disables function calling.
- Perform the conversion on a clone so shared or frozen settings
instances are not mutated and the resolved result is not cached across
different kernels.
- Add a kernel-aware `FromExecutionSettings(settings, kernel)` overload
and use it from `GeminiChatCompletionClient`.

## Contribution Checklist

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄

---------

Co-authored-by: Copilot <[email protected]>
Copilot-Session: c0e4edcd-4d36-486f-950b-9c0c36fa68e8
This commit is contained in:
Giles Odigwe
2026-08-11 16:50:26 +00:00
committed by GitHub
co-authored by Copilot
parent 1d336b3731
commit c028a0c7dc
5 changed files with 317 additions and 52 deletions
@@ -159,6 +159,75 @@ public sealed class GeminiChatClientFunctionCallingTests : IDisposable
Assert.IsAssignableFrom<IChatClient>(chatClient);
}
[Fact]
public async Task AutoInvokeDoesNotInvokeFunctionThatWasNotAdvertisedByFunctionChoiceBehaviorAsync()
{
// Arrange - register two functions in the same kernel but only advertise one of them via the
// FunctionChoiceBehavior. The model is then made to request the function that was not advertised.
bool unadvertisedFunctionInvoked = false;
var plugin = KernelPluginFactory.CreateFromFunctions("TestPlugin", new[]
{
KernelFunctionFactory.CreateFromMethod(() => "allowed-result", "AllowedFunction"),
KernelFunctionFactory.CreateFromMethod(() => { unadvertisedFunctionInvoked = true; return "secret"; }, "UnadvertisedFunction"),
});
var kernel = new Kernel();
kernel.Plugins.Add(plugin);
var allowedFunction = kernel.Plugins.GetFunction("TestPlugin", "AllowedFunction");
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions: [allowedFunction], autoInvoke: true)
};
// The model requests the function that was not advertised, then produces a final text answer.
string functionCallResponse = $$"""
{
"candidates": [
{
"content": {
"parts": [ { "functionCall": { "name": "TestPlugin{{GeminiFunction.NameSeparator}}UnadvertisedFunction", "args": {} } } ],
"role": "model"
},
"finishReason": "STOP",
"index": 0
}
],
"usageMetadata": { "promptTokenCount": 1, "candidatesTokenCount": 1, "totalTokenCount": 2 }
}
""";
string finalTextResponse = """
{
"candidates": [
{
"content": { "parts": [ { "text": "done" } ], "role": "model" },
"finishReason": "STOP",
"index": 0
}
],
"usageMetadata": { "promptTokenCount": 1, "candidatesTokenCount": 1, "totalTokenCount": 2 }
}
""";
using var functionCallHttpResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(functionCallResponse) };
this._messageHandlerStub.ResponseQueue.Enqueue(functionCallHttpResponse);
using var finalTextHttpResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(finalTextResponse) };
this._messageHandlerStub.ResponseQueue.Enqueue(finalTextHttpResponse);
var service = this.CreateChatCompletionService();
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage("Do something");
// Act
var result = await service.GetChatMessageContentsAsync(chatHistory, settings, kernel);
// Assert - the unadvertised function must never be invoked, even though it exists in the kernel.
Assert.False(unadvertisedFunctionInvoked);
// The requested tool call was processed and rejected (not silently ignored): both responses were
// consumed and the model's final text answer was returned.
Assert.Empty(this._messageHandlerStub.ResponseQueue);
Assert.Contains(result, m => m.Content is not null && m.Content.Contains("done", StringComparison.Ordinal));
}
private GoogleAIGeminiChatCompletionService CreateChatCompletionService(HttpClient? httpClient = null)
{
return new GoogleAIGeminiChatCompletionService(
@@ -195,78 +195,210 @@ public sealed class GeminiToolCallBehaviorTests
}
[Fact]
public void FunctionChoiceBehaviorAutoConvertsToAutoInvokeKernelFunctions()
public void FunctionChoiceBehaviorAutoConvertsToEnabledFunctionsWithAllKernelFunctions()
{
// Arrange
var kernel = CreateKernelWithFunctions("FunctionA", "FunctionB");
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
// Act
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
// Assert
Assert.NotNull(converted.ToolCallBehavior);
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
var enabledFunctions = Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
Assert.True(converted.ToolCallBehavior.MaximumAutoInvokeAttempts > 0);
// The provided set is validated against the requested functions.
Assert.False(GetAllowAnyRequestedKernelFunction(converted.ToolCallBehavior));
// Auto continues to provide the functions across follow-up requests.
Assert.Equal(int.MaxValue, converted.ToolCallBehavior.MaximumUseAttempts);
Assert.Equal(new[] { $"TestPlugin{GeminiFunction.NameSeparator}FunctionA", $"TestPlugin{GeminiFunction.NameSeparator}FunctionB" }, GetAdvertisedFunctionNames(enabledFunctions, kernel));
}
[Fact]
public void FunctionChoiceBehaviorAutoWithNoAutoInvokeConvertsToEnableKernelFunctions()
public void FunctionChoiceBehaviorAutoWithNoAutoInvokeConvertsToEnabledFunctionsWithoutAutoInvoke()
{
// Arrange
var kernel = CreateKernelWithFunctions("FunctionA");
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
};
// Act
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
// Assert
Assert.NotNull(converted.ToolCallBehavior);
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
}
[Fact]
public void FunctionChoiceBehaviorRequiredConvertsToAutoInvokeKernelFunctions()
public void FunctionChoiceBehaviorRequiredConvertsToEnabledFunctions()
{
// Arrange
var kernel = CreateKernelWithFunctions("FunctionA");
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Required()
};
// Act
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
// Assert
Assert.NotNull(converted.ToolCallBehavior);
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
Assert.True(converted.ToolCallBehavior.MaximumAutoInvokeAttempts > 0);
Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
Assert.False(GetAllowAnyRequestedKernelFunction(converted.ToolCallBehavior));
// Required stops providing the functions after the first request so the model is not
// repeatedly forced to call them on follow-up iterations. Auto-invocation is capped to the same
// value so it does not outlast the advertised tools (MaximumUseAttempts >= MaximumAutoInvokeAttempts).
Assert.Equal(1, converted.ToolCallBehavior.MaximumUseAttempts);
Assert.Equal(1, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
Assert.True(converted.ToolCallBehavior.MaximumUseAttempts >= converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
}
[Fact]
public void FunctionChoiceBehaviorNoneConvertsToEnableKernelFunctions()
public void FunctionChoiceBehaviorNoneConvertsToEnabledFunctionsWithoutAutoInvoke()
{
// Arrange
var kernel = CreateKernelWithFunctions("FunctionA");
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.None()
};
// Act
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings);
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
// Assert
Assert.NotNull(converted.ToolCallBehavior);
Assert.IsType<GeminiToolCallBehavior.KernelFunctions>(converted.ToolCallBehavior);
Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
// None behavior doesn't auto-invoke
Assert.Equal(0, converted.ToolCallBehavior.MaximumAutoInvokeAttempts);
}
[Fact]
public void FunctionChoiceBehaviorAutoWithEmptyFunctionListDisablesFunctionCalling()
{
// Arrange
// An empty function list is documented as being equivalent to disabling function calling.
var kernel = CreateKernelWithFunctions("FunctionA", "FunctionB");
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions: [], autoInvoke: true)
};
// Act
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
// Assert
Assert.Null(converted.ToolCallBehavior);
}
[Fact]
public void FunctionChoiceBehaviorAutoWithSubsetProvidesOnlyThatSubset()
{
// Arrange
var kernel = CreateKernelWithFunctions("First", "Second");
var first = kernel.Plugins.GetFunction("TestPlugin", "First");
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions: [first], autoInvoke: true)
};
// Act
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
// Assert
Assert.NotNull(converted.ToolCallBehavior);
var enabledFunctions = Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
Assert.False(GetAllowAnyRequestedKernelFunction(converted.ToolCallBehavior));
// Only the function specified in the behavior is provided to the model.
Assert.Equal(new[] { $"TestPlugin{GeminiFunction.NameSeparator}First" }, GetAdvertisedFunctionNames(enabledFunctions, kernel));
}
[Fact]
public void FunctionChoiceBehaviorConfigurationErrorsArePropagated()
{
// Arrange - auto-invocation requires the declared function to exist in the kernel; when it does
// not, the behavior resolution throws and the error must surface rather than be swallowed.
var kernelWithFunction = CreateKernelWithFunctions("First");
var first = kernelWithFunction.Plugins.GetFunction("TestPlugin", "First");
var emptyKernel = Kernel.CreateBuilder().Build();
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions: [first], autoInvoke: true)
};
// Act & Assert
Assert.Throws<KernelException>(() => GeminiPromptExecutionSettings.FromExecutionSettings(settings, emptyKernel));
}
[Fact]
public void FromExecutionSettingsDoesNotMutateCallerSettingsWhenConvertingFunctionChoiceBehavior()
{
// Arrange
var kernel = CreateKernelWithFunctions("FunctionA");
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
// Act
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
// Assert - the caller's original settings object must remain untouched.
Assert.NotSame(settings, converted);
Assert.Null(settings.ToolCallBehavior);
Assert.NotNull(converted.ToolCallBehavior);
}
[Fact]
public void FromExecutionSettingsWithFrozenSettingsDoesNotThrowWhenConvertingFunctionChoiceBehavior()
{
// Arrange
var kernel = CreateKernelWithFunctions("FunctionA");
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
settings.Freeze();
// Act - converting a frozen settings object must not attempt to mutate it.
var converted = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel);
// Assert
Assert.NotNull(converted.ToolCallBehavior);
Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(converted.ToolCallBehavior);
}
[Fact]
public void FromExecutionSettingsReResolvesFunctionChoiceBehaviorPerKernel()
{
// Arrange - a single reused settings object must reflect each request's own kernel.
var settings = new GeminiPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
var kernelA = CreateKernelWithFunctions("FunctionA");
var kernelB = CreateKernelWithFunctions("FunctionB");
// Act
var convertedA = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernelA);
var convertedB = GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernelB);
// Assert - each request reflects its own kernel.
var enabledA = Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(convertedA.ToolCallBehavior);
var enabledB = Assert.IsType<GeminiToolCallBehavior.EnabledFunctions>(convertedB.ToolCallBehavior);
Assert.Equal(new[] { $"TestPlugin{GeminiFunction.NameSeparator}FunctionA" }, GetAdvertisedFunctionNames(enabledA, kernelA));
Assert.Equal(new[] { $"TestPlugin{GeminiFunction.NameSeparator}FunctionB" }, GetAdvertisedFunctionNames(enabledB, kernelB));
}
[Fact]
public void GeminiPromptExecutionSettingsWithNoFunctionChoiceBehaviorDoesNotSetToolCallBehavior()
{
@@ -311,6 +443,32 @@ public sealed class GeminiToolCallBehaviorTests
return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]);
}
private static Kernel CreateKernelWithFunctions(params string[] functionNames)
{
var functions = functionNames
.Select(name => KernelFunctionFactory.CreateFromMethod(() => "result", name))
.ToArray();
var kernel = Kernel.CreateBuilder().Build();
kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("TestPlugin", functions));
return kernel;
}
private static bool GetAllowAnyRequestedKernelFunction(GeminiToolCallBehavior behavior)
=> behavior.AllowAnyRequestedKernelFunction;
private static string[] GetAdvertisedFunctionNames(GeminiToolCallBehavior behavior, Kernel kernel)
{
var request = new GeminiRequest();
behavior.ConfigureGeminiRequest(kernel, request);
if (request.Tools is null)
{
return [];
}
return request.Tools[0].Functions.Select(f => f.Name).ToArray();
}
private static void AssertFunctions(GeminiRequest request)
{
Assert.NotNull(request.Tools);
@@ -323,7 +323,7 @@ internal sealed class GeminiChatCompletionClient : ClientBase
{
ValidateChatHistory(chatHistory);
var geminiExecutionSettings = GeminiPromptExecutionSettings.FromExecutionSettings(executionSettings);
var geminiExecutionSettings = GeminiPromptExecutionSettings.FromExecutionSettings(executionSettings, kernel);
ValidateMaxTokens(geminiExecutionSettings.MaxTokens);
if (this.Logger.IsEnabled(LogLevel.Trace))
@@ -355,16 +355,36 @@ public sealed class GeminiPromptExecutionSettings : PromptExecutionSettings
/// is null, an <see cref="ArgumentException"/> is thrown.
/// </returns>
public static GeminiPromptExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings)
=> FromExecutionSettings(executionSettings, kernel: null);
/// <summary>
/// Create a new settings object with the values from another settings object, resolving any
/// <see cref="FunctionChoiceBehavior"/> against the provided <paramref name="kernel"/>.
/// </summary>
/// <param name="executionSettings">The prompt execution settings to convert.</param>
/// <param name="kernel">
/// The <see cref="Kernel"/> used to resolve the functions declared by a <see cref="FunctionChoiceBehavior"/>.
/// This must be the kernel that will actually be used for the request so that the advertised function set
/// matches the caller's intent.
/// </param>
/// <returns>The converted <see cref="GeminiPromptExecutionSettings"/>.</returns>
public static GeminiPromptExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings, Kernel? kernel)
{
switch (executionSettings)
{
case null:
return new GeminiPromptExecutionSettings();
case GeminiPromptExecutionSettings geminiSettings:
// If FunctionChoiceBehavior is set and ToolCallBehavior is not, convert it
// If FunctionChoiceBehavior is set and ToolCallBehavior is not, resolve it against the
// request's kernel. This is done on a clone so we neither mutate the caller's (possibly
// shared or frozen) settings instance nor cache a kernel-specific result that could go
// stale when the same settings object is reused with a different kernel.
if (geminiSettings.FunctionChoiceBehavior is not null && geminiSettings.ToolCallBehavior is null)
{
geminiSettings.ToolCallBehavior = ConvertFunctionChoiceBehaviorToToolCallBehavior(geminiSettings.FunctionChoiceBehavior);
var resolved = (GeminiPromptExecutionSettings)geminiSettings.Clone();
resolved.FunctionChoiceBehavior = geminiSettings.FunctionChoiceBehavior;
resolved.ToolCallBehavior = ConvertFunctionChoiceBehaviorToToolCallBehavior(geminiSettings.FunctionChoiceBehavior, kernel);
return resolved;
}
return geminiSettings;
}
@@ -375,56 +395,62 @@ public sealed class GeminiPromptExecutionSettings : PromptExecutionSettings
// If FunctionChoiceBehavior is set and ToolCallBehavior is not, convert it
if (executionSettings.FunctionChoiceBehavior is not null && settings.ToolCallBehavior is null)
{
settings.ToolCallBehavior = ConvertFunctionChoiceBehaviorToToolCallBehavior(executionSettings.FunctionChoiceBehavior);
settings.ToolCallBehavior = ConvertFunctionChoiceBehaviorToToolCallBehavior(executionSettings.FunctionChoiceBehavior, kernel);
}
return settings;
}
/// <summary>
/// Shared empty kernel instance used for FunctionChoiceBehavior conversion.
/// </summary>
private static readonly Kernel s_emptyKernel = new();
/// <summary>
/// Converts a <see cref="FunctionChoiceBehavior"/> to a <see cref="GeminiToolCallBehavior"/>.
/// </summary>
/// <param name="functionChoiceBehavior">The <see cref="FunctionChoiceBehavior"/> to convert.</param>
/// <param name="kernel">The <see cref="Kernel"/> used to resolve the behavior's declared functions.</param>
/// <returns>The converted <see cref="GeminiToolCallBehavior"/>.</returns>
internal static GeminiToolCallBehavior? ConvertFunctionChoiceBehaviorToToolCallBehavior(FunctionChoiceBehavior? functionChoiceBehavior)
/// <remarks>
/// The conversion honors the exact function set expressed by the <see cref="FunctionChoiceBehavior"/>.
/// The resolved <see cref="FunctionChoiceBehaviorConfiguration.Functions"/> list is the set of functions
/// provided to the model, matching the behavior of the other connectors. A null or empty function set
/// (e.g. <c>FunctionChoiceBehavior.Auto([])</c>, which is documented as being equivalent to disabling
/// function calling) results in no functions being provided. Any configuration errors thrown while
/// resolving the behavior (for example, requesting auto-invocation for a function that is not present in
/// the kernel) are surfaced to the caller rather than being swallowed, matching the other connectors.
/// </remarks>
internal static GeminiToolCallBehavior? ConvertFunctionChoiceBehaviorToToolCallBehavior(FunctionChoiceBehavior? functionChoiceBehavior, Kernel? kernel)
{
if (functionChoiceBehavior is null)
{
return null;
}
// Check the type and determine auto-invoke by reflection or known behavior types
// All FunctionChoiceBehavior types (Auto, Required, None) support auto-invoke
// We use a simple approach: get the configuration with minimal context to check AutoInvoke
try
// Resolve the behavior against the kernel so the provided function set reflects
// exactly what the caller requested (all functions, an explicit subset, or none).
var context = new FunctionChoiceBehaviorConfigurationContext(new ChatHistory())
{
var context = new FunctionChoiceBehaviorConfigurationContext(new ChatHistory())
{
Kernel = s_emptyKernel, // Provide an empty kernel for the configuration
RequestSequenceIndex = 0
};
var config = functionChoiceBehavior.GetConfiguration(context);
Kernel = kernel,
RequestSequenceIndex = 0
};
var config = functionChoiceBehavior.GetConfiguration(context);
// Return appropriate GeminiToolCallBehavior based on AutoInvoke setting
if (config.AutoInvoke)
{
return GeminiToolCallBehavior.AutoInvokeKernelFunctions;
}
return GeminiToolCallBehavior.EnableKernelFunctions;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031
// A null or empty resolved function list means no functions should be provided to the model,
// which preserves the documented "empty list disables function calling" behavior.
if (config.Functions is not { Count: > 0 } functions)
{
// If we can't get configuration (e.g., due to missing dependencies or unexpected state),
// default to EnableKernelFunctions as the safer option that doesn't auto-invoke
return GeminiToolCallBehavior.EnableKernelFunctions;
return null;
}
// For the Required choice, functions should only be provided on the first request to avoid
// repeatedly forcing the model to call them on follow-up iterations, matching the shared
// FunctionChoiceBehavior semantics. Limiting the use attempts to 1 stops the connector from
// re-advertising the tools after the initial request; the auto-invoke attempts are capped to the
// same value (see GeminiToolCallBehavior) so a single forced call is executed and the model's
// final answer is returned.
int maximumUseAttempts = config.Choice == FunctionChoice.Required ? 1 : int.MaxValue;
// Provide exactly the resolved functions.
return new GeminiToolCallBehavior.EnabledFunctions(
functions.Select(f => f.Metadata.ToGeminiFunction()),
autoInvoke: config.AutoInvoke,
maximumUseAttempts: maximumUseAttempts);
}
}
@@ -68,9 +68,15 @@ public abstract class GeminiToolCallBehavior
}
/// <summary>Initializes the instance; prevents external instantiation.</summary>
private GeminiToolCallBehavior(bool autoInvoke)
private GeminiToolCallBehavior(bool autoInvoke, int maximumUseAttempts = int.MaxValue)
{
this.MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0;
this.MaximumUseAttempts = maximumUseAttempts;
// Auto-invocation can never outlast the number of requests that still advertise the tools, otherwise
// the model could request a tool after it has been removed from the request. Cap the auto-invoke
// attempts by the use attempts to preserve the documented invariant (MaximumUseAttempts >= MaximumAutoInvokeAttempts).
var autoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0;
this.MaximumAutoInvokeAttempts = System.Math.Min(autoInvokeAttempts, maximumUseAttempts);
}
/// <summary>Gets how many requests are part of a single interaction should include this tool in the request.</summary>
@@ -80,7 +86,7 @@ public abstract class GeminiToolCallBehavior
/// if this is 1, the first request will include the tools, but the subsequent response sending back the tool's result
/// will not include the tools for further use.
/// </remarks>
public int MaximumUseAttempts { get; } = int.MaxValue;
public int MaximumUseAttempts { get; }
/// <summary>Gets how many tool call request/response roundtrips are supported with auto-invocation.</summary>
/// <remarks>
@@ -137,9 +143,15 @@ public abstract class GeminiToolCallBehavior
/// <summary>
/// Represents a <see cref="GeminiToolCallBehavior"/> that provides a specified list of functions to the model.
/// </summary>
internal sealed class EnabledFunctions(IEnumerable<GeminiFunction> functions, bool autoInvoke) : GeminiToolCallBehavior(autoInvoke)
internal sealed class EnabledFunctions : GeminiToolCallBehavior
{
private readonly GeminiFunction[] _functions = functions.ToArray();
private readonly GeminiFunction[] _functions;
internal EnabledFunctions(IEnumerable<GeminiFunction> functions, bool autoInvoke, int maximumUseAttempts = int.MaxValue)
: base(autoInvoke, maximumUseAttempts)
{
this._functions = functions.ToArray();
}
public override string ToString() =>
$"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): " +