Skip to content

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

Open
giles17 wants to merge 5 commits into
microsoft:mainfrom
giles17:giles17/fix-gemini-function-choice-behavior-functions
Open

.Net: Honor FunctionChoiceBehavior function list in Gemini connector#14183
giles17 wants to merge 5 commits into
microsoft:mainfrom
giles17:giles17/fix-gemini-function-choice-behavior-functions

Conversation

@giles17

@giles17 giles17 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

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

  • The code builds clean without any errors or warnings
  • The PR follows the SK Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • I didn't break anyone 😄

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
Copilot AI review requested due to automatic review settings July 24, 2026 00:50
@giles17
giles17 requested a review from a team as a code owner July 24, 2026 00:50
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0e4edcd-4d36-486f-950b-9c0c36fa68e8

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 generic EnabledFunctions instance 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.ConvertFunctionChoiceBehaviorToolCallBehavior now swallows all configuration failures and returns null (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).
  • RequiredFunctionChoiceBehavior explicitly stops advertising functions after the first request (dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs:68-77), but Gemini now snapshots every resolved behavior into GeminiToolCallBehavior.EnableFunctions(...) (dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs:443-445), whose MaximumUseAttempts stays at int.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), so Required() 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Flagged issue

GeminiPromptExecutionSettings.ConvertFunctionChoiceBehaviorToolCallBehavior now swallows all configuration failures and returns null (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).


Source: automated DevFlow PR review

@github-actions

Copy link
Copy Markdown
Contributor

Flagged issue

RequiredFunctionChoiceBehavior explicitly stops advertising functions after the first request (dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/RequiredFunctionChoiceBehavior.cs:68-77), but Gemini now snapshots every resolved behavior into GeminiToolCallBehavior.EnableFunctions(...) (dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs:443-445), whose MaximumUseAttempts stays at int.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), so Required() no longer gets its one-request-only semantics.


Source: automated DevFlow PR review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FunctionChoiceBehaviorGeminiToolCallBehavior conversion 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.

Comment thread dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs Outdated
- 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
@giles17
giles17 marked this pull request as draft July 24, 2026 01:08
@giles17
giles17 marked this pull request as ready for review July 24, 2026 01:08

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 and MaximumUseAttempts, 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

@github-actions

Copy link
Copy Markdown
Contributor

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants