.Net: Honor FunctionChoiceBehavior function list in Gemini connector#14183
.Net: Honor FunctionChoiceBehavior function list in Gemini connector#14183giles17 wants to merge 5 commits into
Conversation
The Gemini connector converted a FunctionChoiceBehavior into a GeminiToolCallBehavior by inspecting only the AutoInvoke flag and evaluating the behavior against an empty kernel. As a result the configured function list and choice were ignored: every conversion provided all kernel functions to the model, and an explicit function list produced an incorrect result. Resolve the behavior against the request's kernel and provide exactly the functions it specifies, consistent with the other connectors. An empty function list now correctly disables function calling. The conversion is performed on a clone so shared or frozen settings are not mutated and the result is not cached across different kernels. Also add a kernel-aware FromExecutionSettings overload and update the chat completion client to use it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c0e4edcd-4d36-486f-950b-9c0c36fa68e8
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c0e4edcd-4d36-486f-950b-9c0c36fa68e8
There was a problem hiding this comment.
Automated Code Review
Reviewers: 5 | Confidence: 87%
✓ Correctness
The PR correctly resolves FunctionChoiceBehavior against the actual request kernel, clones settings to avoid mutation, and properly handles edge cases (empty function list, frozen settings, kernel reuse). The code is well-structured with comprehensive test coverage. No correctness bugs found.
✓ Security Reliability
This PR improves both security and reliability of the Gemini connector's function-calling behavior. It correctly resolves FunctionChoiceBehavior against the request's kernel, provides only the declared functions (not all kernel functions), uses cloning to prevent mutation of shared settings, and defaults to disabling function calling on resolution failure. No security or reliability issues found.
✓ Test Coverage
The PR has good test coverage for the happy paths of the new kernel-aware FunctionChoiceBehavior resolution, including empty lists, subsets, immutability, frozen settings, and per-kernel resolution. However, there are two notable test coverage gaps: (1) the catch/exception path where GetConfiguration throws (e.g., a function specified in the behavior doesn't exist in the kernel) now silently disables function calling instead of falling back to EnableKernelFunctions, but this path has no explicit test; (2) the Required() and None() tests don't verify the actual function list provided to the model, unlike the Auto() tests which do.
✓ Failure Modes
The PR correctly resolves FunctionChoiceBehavior against the request's kernel and clones settings to avoid mutation. The main concern is the broad catch-all exception handler (line 448) which now silently disables function calling on any unexpected error (e.g., a bug in ToGeminiFunction metadata conversion), with no logging or user-visible indication. This is a pre-existing pattern but the failure mode changed from 'provide all functions' to 'provide no functions', which could make debugging harder for users whose tool-calling silently stops working.
✗ Design Approach
I found two design issues in the new FunctionChoiceBehavior conversion. First, the new catch-all fallback now turns shared FunctionChoiceBehavior configuration errors into a silent "no tools" request instead of preserving the existing fatal behavior for invalid auto-invoke setups. Second, converting
Required()into a genericEnabledFunctionsinstance loses the shared invariant that required tools must only be advertised on the first request, so Gemini can keep re-advertising required tools across auto-invoke follow-ups.
Flagged Issues
-
GeminiPromptExecutionSettings.ConvertFunctionChoiceBehaviorToolCallBehaviornow swallows all configuration failures and returnsnull(dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs:448-453), but the shared FunctionChoiceBehavior contract treats missing kernels / missing required functions as fatal (dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/FunctionChoiceBehavior.cs:117-147) and existing tests assert those exceptions (dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/AutoFunctionChoiceBehaviorTests.cs:183-215,dotnet/src/SemanticKernel.UnitTests/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehaviorTests.cs:221-247). -
RequiredFunctionChoiceBehaviorexplicitly stops advertising functions after the first request (dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs:68-77), but Gemini now snapshots every resolved behavior intoGeminiToolCallBehavior.EnableFunctions(...)(dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs:443-445), whoseMaximumUseAttemptsstays atint.MaxValue(dotnet/src/Connectors/Connectors.Google/GeminiToolCallBehavior.cs:76-83). Gemini reuses that same tool behavior on later iterations (dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs:538-552), soRequired()no longer gets its one-request-only semantics.
Automated review by giles17's agents
| autoInvoke: config.AutoInvoke); | ||
| } | ||
| #pragma warning disable CA1031 // Do not catch general exception types | ||
| catch |
There was a problem hiding this comment.
This catch-all returns null (disabling function calling) for any exception—including unexpected bugs in ToGeminiFunction() (e.g., NullReferenceException from malformed metadata) and configuration errors that the shared FunctionChoiceBehavior contract explicitly throws (missing kernel, missing function in FunctionChoiceBehavior.cs:117-147). Since this is a static method with no logger, the user gets no indication their functions were silently dropped. Consider narrowing the catch to KernelException and letting other exceptions propagate, so metadata conversion bugs and invalid auto-invoke configurations surface rather than appearing as mysteriously non-functional tool calling.
|
Flagged issue
Source: automated DevFlow PR review |
|
Flagged issue
Source: automated DevFlow PR review |
There was a problem hiding this comment.
Pull request overview
This PR fixes Gemini connector function-calling parity by resolving FunctionChoiceBehavior (including its function list) against the request kernel, ensuring the connector advertises exactly the intended functions (all, subset, or none) and avoiding mutation/caching of kernel-specific results on shared settings instances.
Changes:
- Added a kernel-aware
GeminiPromptExecutionSettings.FromExecutionSettings(settings, kernel)overload and updated the chat client to use it. - Updated
FunctionChoiceBehavior→GeminiToolCallBehaviorconversion to honor the resolved function list (including “empty list disables function calling”). - Expanded unit tests to validate subset/all/none behavior and non-mutation across frozen/reused settings.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs | Adds kernel-aware conversion and resolves FunctionChoiceBehavior into an explicit function allow-list for Gemini. |
| dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs | Passes the request kernel into settings conversion so function resolution matches the actual request kernel. |
| dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiToolCallBehaviorTests.cs | Updates and adds tests for correct function list resolution, empty-list disabling, and non-mutation behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Stop swallowing exceptions during behavior resolution. Configuration errors thrown by FunctionChoiceBehavior (e.g. auto-invocation requested for a function not present in the kernel) now surface to the caller instead of silently disabling function calling, matching the other connectors' fail-fast behavior. - Honor the Required choice's request-sequence semantics: functions are now only provided on the first request (MaximumUseAttempts = 1) so the model is not repeatedly forced to call them on follow-up iterations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c0e4edcd-4d36-486f-950b-9c0c36fa68e8
There was a problem hiding this comment.
Automated Code Review
Reviewers: 5 | Confidence: 84%
✓ Correctness
The PR correctly implements FunctionChoiceBehavior resolution against the request kernel in the Gemini connector. The Required semantics are properly preserved via maximumUseAttempts=1, the clone approach prevents mutation of shared/frozen settings, and configuration errors now propagate rather than being swallowed. Both previously unresolved review concerns are addressed. No significant correctness issues found.
✗ Security Reliability
The PR correctly addresses the stated goals of honoring FunctionChoiceBehavior function lists and properly cloning settings. However, for Required behaviors, it creates an EnabledFunctions with MaximumUseAttempts=1 while MaximumAutoInvokeAtempts remains at 5, violating the documented invariant at GeminiToolCallBehavior.cs:79. This can cause a NullReferenceException in the auto-invoke loop if Gemini returns tool calls after tools have been removed from the request.
✓ Test Coverage
The test coverage for this PR is generally strong, covering the main conversion paths (Auto, Required, None, empty list, subset), immutability, frozen settings, and per-kernel re-resolution. However, the test for
FunctionChoiceBehavior.None()is notably less thorough than the other behavior tests—it only asserts type and auto-invoke count but omits verification of the advertised function set andMaximumUseAttempts, which are both asserted in the analogous Auto and Required tests.
✓ Failure Modes
The PR correctly resolves FunctionChoiceBehavior against the request kernel, removes the problematic catch-all, and handles cloning to avoid mutation. The main concern is that for FunctionChoiceBehavior.Required(), setting MaximumUseAttempts=1 while MaximumAutoInvokeAttempts=128 violates the documented invariant at GeminiToolCallBehavior.cs:79 and creates a narrow crash path if the model returns unexpected tool calls after tools are removed from the request.
✓ Design Approach
The PR mostly fixes the kernel-aware function resolution problem, but it still collapses
FunctionChoiceBehavior.None()into ordinary enabled tools instead of preserving the shared "advertise functions, but instruct the model not to call them" contract. That is a design-level semantic gap, and the new tests now codify it.
Flagged Issues
- For FunctionChoiceBehavior.Required(), MaximumUseAtempts (1) < MaximumAutoInvokeAtempts (5/128) violates the documented invariant at GeminiToolCallBehavior.cs:79 and creates a NullReferenceException path at GeminiChatCompletionClient.cs:576 when Gemini returns unexpected tool calls after tools are removed from the request.
Suggestions
- For FunctionChoiceBehavior.Required(), cap MaximumAutoInvokeAtempts to not exceed MaximumUseAttempts (both = 1), since after the one forced tool-call + execution round-trip the intent is to return the model's final text answer. This satisfies the documented invariant and prevents the NRE crash path.
Automated review by giles17's agents
|
Flagged issue For FunctionChoiceBehavior.Required(), MaximumUseAtempts (1) < MaximumAutoInvokeAtempts (5/128) violates the documented invariant at GeminiToolCallBehavior.cs:79 and creates a NullReferenceException path at GeminiChatCompletionClient.cs:576 when Gemini returns unexpected tool calls after tools are removed from the request. Source: automated DevFlow PR review |
For FunctionChoiceBehavior.Required(), MaximumUseAttempts was set to 1 while MaximumAutoInvokeAttempts remained at the default, violating the documented MaximumUseAttempts >= MaximumAutoInvokeAttempts invariant. Because auto-invocation outlasted the requests that advertised the tools, an unexpected follow-up tool call could dereference a null tool list in the chat completion client. Cap MaximumAutoInvokeAttempts by MaximumUseAttempts in the tool call behavior constructor so auto-invocation never outlasts the advertised tools. For Required this results in a single forced tool call followed by the model's final text answer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c0e4edcd-4d36-486f-950b-9c0c36fa68e8
…tion Verifies that when a FunctionChoiceBehavior advertises only a subset of the kernel's functions, an auto-invoked tool call for a registered-but-not-advertised function is rejected rather than executed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c0e4edcd-4d36-486f-950b-9c0c36fa68e8
Motivation and Context
The Gemini connector converts a
FunctionChoiceBehavior(FCB) into the connector-internalGeminiToolCallBehaviorinGeminiPromptExecutionSettings. That conversion only inspected theAutoInvokeflag 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.This diverges from the other connectors, which resolve the behavior against the request kernel and provide exactly the functions it specifies.
Description
FunctionChoiceBehavioragainst 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.FromExecutionSettings(settings, kernel)overload and use it fromGeminiChatCompletionClient.Contribution Checklist