diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatGenerationFunctionCallingTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatGenerationFunctionCallingTests.cs
index ef711c3586f6..f4008897c790 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatGenerationFunctionCallingTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatGenerationFunctionCallingTests.cs
@@ -626,6 +626,83 @@ public async Task IfAutoInvokeShouldAllowFilterToModifyFunctionResultAsync()
Assert.Contains(ModifiedResult, secondRequestContent);
}
+ [Fact]
+ public async Task FunctionCallWithThoughtSignatureIsCapturedInToolCallAsync()
+ {
+ // Arrange
+ var responseWithThoughtSignature = File.ReadAllText("./TestData/chat_function_with_thought_signature_response.json")
+ .Replace("%nameSeparator%", GeminiFunction.NameSeparator, StringComparison.Ordinal);
+ this._messageHandlerStub.ResponseToReturn.Content = new StringContent(responseWithThoughtSignature);
+
+ var client = this.CreateChatCompletionClient();
+ var chatHistory = CreateSampleChatHistory();
+ var executionSettings = new GeminiPromptExecutionSettings
+ {
+ ToolCallBehavior = GeminiToolCallBehavior.EnableFunctions([this._timePluginNow])
+ };
+
+ // Act
+ var messages = await client.GenerateChatMessageAsync(chatHistory, executionSettings: executionSettings, kernel: this._kernelWithFunctions);
+
+ // Assert
+ Assert.Single(messages);
+ var geminiMessage = messages[0] as GeminiChatMessageContent;
+ Assert.NotNull(geminiMessage);
+ Assert.NotNull(geminiMessage.ToolCalls);
+ Assert.Single(geminiMessage.ToolCalls);
+ Assert.Equal("test-thought-signature-abc123", geminiMessage.ToolCalls[0].ThoughtSignature);
+ }
+
+ [Fact]
+ public async Task TextResponseWithThoughtSignatureIsCapturedInMetadataAsync()
+ {
+ // Arrange
+ var responseWithThoughtSignature = File.ReadAllText("./TestData/chat_text_with_thought_signature_response.json");
+ this._messageHandlerStub.ResponseToReturn.Content = new StringContent(responseWithThoughtSignature);
+
+ var client = this.CreateChatCompletionClient();
+ var chatHistory = CreateSampleChatHistory();
+
+ // Act
+ var messages = await client.GenerateChatMessageAsync(chatHistory);
+
+ // Assert
+ Assert.Single(messages);
+ var geminiMessage = messages[0] as GeminiChatMessageContent;
+ Assert.NotNull(geminiMessage);
+ Assert.NotNull(geminiMessage.Metadata);
+ var metadata = geminiMessage.Metadata as GeminiMetadata;
+ Assert.NotNull(metadata);
+ Assert.Equal("text-response-thought-signature-xyz789", metadata.ThoughtSignature);
+ }
+
+ [Fact]
+ public async Task ThoughtSignatureIsIncludedInSubsequentRequestAsync()
+ {
+ // Arrange - First response has function call with ThoughtSignature
+ var responseWithThoughtSignature = File.ReadAllText("./TestData/chat_function_with_thought_signature_response.json")
+ .Replace("%nameSeparator%", GeminiFunction.NameSeparator, StringComparison.Ordinal);
+ using var handlerStub = new MultipleHttpMessageHandlerStub();
+ handlerStub.AddJsonResponse(responseWithThoughtSignature);
+ handlerStub.AddJsonResponse(this._responseContent); // Second response is text
+
+ using var httpClient = new HttpClient(handlerStub, false);
+ var client = this.CreateChatCompletionClient(httpClient: httpClient);
+ var chatHistory = CreateSampleChatHistory();
+ var executionSettings = new GeminiPromptExecutionSettings
+ {
+ ToolCallBehavior = GeminiToolCallBehavior.AutoInvokeKernelFunctions
+ };
+
+ // Act
+ await client.GenerateChatMessageAsync(chatHistory, executionSettings: executionSettings, kernel: this._kernelWithFunctions);
+
+ // Assert - Check that the second request includes the ThoughtSignature
+ var secondRequestContent = handlerStub.GetRequestContentAsString(1);
+ Assert.NotNull(secondRequestContent);
+ Assert.Contains("test-thought-signature-abc123", secondRequestContent);
+ }
+
private static ChatHistory CreateSampleChatHistory()
{
var chatHistory = new ChatHistory();
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatStreamingFunctionCallingTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatStreamingFunctionCallingTests.cs
index cf7994d861f2..7a6e478cbc4e 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatStreamingFunctionCallingTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatStreamingFunctionCallingTests.cs
@@ -417,6 +417,63 @@ await client.StreamGenerateChatMessageAsync(chatHistory, executionSettings: exec
c is GeminiChatMessageContent gm && gm.Role == AuthorRole.Tool && gm.CalledToolResult is not null);
}
+ [Fact]
+ public async Task StreamingTextResponseWithAutoInvokeAndEmptyToolCallsDoesNotEnterToolCallingBranchAsync()
+ {
+ // Arrange - This tests the Phase 6 bug fix: empty ToolCalls list should not trigger tool calling
+ var client = this.CreateChatCompletionClient();
+ var chatHistory = CreateSampleChatHistory();
+ var executionSettings = new GeminiPromptExecutionSettings
+ {
+ ToolCallBehavior = GeminiToolCallBehavior.AutoInvokeKernelFunctions
+ };
+
+ // Response is text-only (no function calls), so ToolCalls will be empty list
+ this._messageHandlerStub.ResponseToReturn.Content = new StringContent(this._responseContent);
+
+ // Act
+ var messages = await client.StreamGenerateChatMessageAsync(
+ chatHistory,
+ executionSettings: executionSettings,
+ kernel: this._kernelWithFunctions).ToListAsync();
+
+ // Assert - Should yield text response without entering tool-calling branch
+ Assert.NotEmpty(messages);
+ Assert.All(messages, m =>
+ {
+ var geminiMessage = m as GeminiStreamingChatMessageContent;
+ Assert.NotNull(geminiMessage);
+ // ToolCalls should be null or empty for text responses
+ Assert.True(geminiMessage.ToolCalls is null || geminiMessage.ToolCalls.Count == 0);
+ });
+ }
+
+ [Fact]
+ public async Task StreamingTextResponseWithAutoInvokeAndNullToolCallsDoesNotEnterToolCallingBranchAsync()
+ {
+ // Arrange - This tests that pattern `is { Count: > 0 }` handles null safely
+ var client = this.CreateChatCompletionClient();
+ var chatHistory = CreateSampleChatHistory();
+ var executionSettings = new GeminiPromptExecutionSettings
+ {
+ ToolCallBehavior = GeminiToolCallBehavior.AutoInvokeKernelFunctions
+ };
+
+ // Response is text-only
+ this._messageHandlerStub.ResponseToReturn.Content = new StringContent(this._responseContent);
+
+ // Act
+ var messages = await client.StreamGenerateChatMessageAsync(
+ chatHistory,
+ executionSettings: executionSettings,
+ kernel: this._kernelWithFunctions).ToListAsync();
+
+ // Assert - Should complete without errors
+ Assert.NotEmpty(messages);
+ // Verify we got text content
+ Assert.Contains(messages, m => !string.IsNullOrEmpty(m.Content));
+ }
+
private static ChatHistory CreateSampleChatHistory()
{
var chatHistory = new ChatHistory();
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiFunctionToolCallTests.cs
index ea361f35ca26..0bee8d86add7 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiFunctionToolCallTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiFunctionToolCallTests.cs
@@ -68,4 +68,71 @@ public void ToStringReturnsCorrectValue()
// Act & Assert
Assert.Equal("MyPlugin_MyFunction(location:San Diego, max_price:300)", functionToolCall.ToString());
}
+
+ [Fact]
+ public void ThoughtSignatureIsNullWhenCreatedFromFunctionCallPart()
+ {
+ // Arrange - Using the FunctionCallPart constructor (no ThoughtSignature)
+ var toolCallPart = new GeminiPart.FunctionCallPart { FunctionName = "MyFunction" };
+ var functionToolCall = new GeminiFunctionToolCall(toolCallPart);
+
+ // Act & Assert
+ Assert.Null(functionToolCall.ThoughtSignature);
+ }
+
+ [Fact]
+ public void ThoughtSignatureIsCapturedWhenCreatedFromGeminiPart()
+ {
+ // Arrange - Using the GeminiPart constructor (with ThoughtSignature)
+ var part = new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "MyFunction" },
+ ThoughtSignature = "test-thought-signature-123"
+ };
+ var functionToolCall = new GeminiFunctionToolCall(part);
+
+ // Act & Assert
+ Assert.Equal("test-thought-signature-123", functionToolCall.ThoughtSignature);
+ }
+
+ [Fact]
+ public void ThoughtSignatureIsNullWhenGeminiPartHasNoSignature()
+ {
+ // Arrange
+ var part = new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "MyFunction" },
+ ThoughtSignature = null
+ };
+ var functionToolCall = new GeminiFunctionToolCall(part);
+
+ // Act & Assert
+ Assert.Null(functionToolCall.ThoughtSignature);
+ }
+
+ [Fact]
+ public void ArgumentsArePreservedWhenCreatedFromGeminiPart()
+ {
+ // Arrange
+ var part = new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart
+ {
+ FunctionName = "MyPlugin_MyFunction",
+ Arguments = new JsonObject
+ {
+ { "location", "San Diego" },
+ { "max_price", 300 }
+ }
+ },
+ ThoughtSignature = "signature-abc"
+ };
+ var functionToolCall = new GeminiFunctionToolCall(part);
+
+ // Act & Assert
+ Assert.NotNull(functionToolCall.Arguments);
+ Assert.Equal(2, functionToolCall.Arguments.Count);
+ Assert.Equal("San Diego", functionToolCall.Arguments["location"]!.ToString());
+ Assert.Equal("signature-abc", functionToolCall.ThoughtSignature);
+ }
}
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiMetadataTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiMetadataTests.cs
new file mode 100644
index 000000000000..a0c97a79ac79
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiMetadataTests.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel.Connectors.Google;
+using Xunit;
+
+namespace SemanticKernel.Connectors.Google.UnitTests.Core.Gemini;
+
+///
+/// Unit tests for class.
+///
+public sealed class GeminiMetadataTests
+{
+ [Fact]
+ public void ThoughtSignatureCanBeSetAndRetrieved()
+ {
+ // Arrange & Act
+ var metadata = new GeminiMetadata { ThoughtSignature = "test-signature-123" };
+
+ // Assert
+ Assert.Equal("test-signature-123", metadata.ThoughtSignature);
+ }
+
+ [Fact]
+ public void ThoughtSignatureIsNullByDefault()
+ {
+ // Arrange & Act
+ var metadata = new GeminiMetadata();
+
+ // Assert
+ Assert.Null(metadata.ThoughtSignature);
+ }
+
+ [Fact]
+ public void ThoughtSignatureIsStoredInDictionary()
+ {
+ // Arrange
+ var metadata = new GeminiMetadata { ThoughtSignature = "dict-signature" };
+
+ // Act
+ var hasKey = metadata.TryGetValue("ThoughtSignature", out var value);
+
+ // Assert
+ Assert.True(hasKey);
+ Assert.Equal("dict-signature", value);
+ }
+
+ [Fact]
+ public void ThoughtSignatureCanBeRetrievedFromDictionary()
+ {
+ // Arrange - This simulates deserialized metadata
+ var metadata = new GeminiMetadata { ThoughtSignature = "from-dict" };
+
+ // Act
+ var signature = metadata.ThoughtSignature;
+
+ // Assert
+ Assert.Equal("from-dict", signature);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiPartTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiPartTests.cs
index c2414968edfd..f562a3249eb5 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiPartTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiPartTests.cs
@@ -137,4 +137,124 @@ public GeminiPartTestData()
this.Add(new() { Text = "text", InlineData = new(), FunctionCall = new(), FunctionResponse = new(), FileData = new() });
}
}
+
+ [Fact]
+ public void ThoughtSignatureDoesNotAffectIsValid()
+ {
+ // Arrange - ThoughtSignature is metadata, not content, so it shouldn't affect IsValid
+ var sut = new GeminiPart { ThoughtSignature = "test-signature" };
+
+ // Act
+ var result = sut.IsValid();
+
+ // Assert - Should be invalid because no content type is set
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void ThoughtSignatureWithFunctionCallIsValid()
+ {
+ // Arrange
+ var sut = new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "test" },
+ ThoughtSignature = "test-signature"
+ };
+
+ // Act
+ var result = sut.IsValid();
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ThoughtSignatureWithTextIsValid()
+ {
+ // Arrange
+ var sut = new GeminiPart
+ {
+ Text = "Hello",
+ ThoughtSignature = "test-signature"
+ };
+
+ // Act
+ var result = sut.IsValid();
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void ThoughtSignatureSerializesToJson()
+ {
+ // Arrange
+ var sut = new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "test_function" },
+ ThoughtSignature = "abc123-signature"
+ };
+
+ // Act
+ var json = System.Text.Json.JsonSerializer.Serialize(sut);
+
+ // Assert
+ Assert.Contains("\"thoughtSignature\":\"abc123-signature\"", json);
+ }
+
+ [Fact]
+ public void ThoughtSignatureDeserializesFromJson()
+ {
+ // Arrange
+ var json = """
+ {
+ "functionCall": { "name": "test_function" },
+ "thoughtSignature": "xyz789-signature"
+ }
+ """;
+
+ // Act
+ var sut = System.Text.Json.JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(sut);
+ Assert.Equal("xyz789-signature", sut.ThoughtSignature);
+ }
+
+ [Fact]
+ public void ThoughtSignatureNullIsNotSerializedToJson()
+ {
+ // Arrange
+ var sut = new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "test_function" },
+ ThoughtSignature = null
+ };
+
+ // Act
+ var json = System.Text.Json.JsonSerializer.Serialize(sut);
+
+ // Assert
+ Assert.DoesNotContain("thoughtSignature", json);
+ }
+
+ [Fact]
+ public void ThoughtSignatureEmptyStringIsPreserved()
+ {
+ // Arrange - Empty string should be preserved (defensive coding)
+ var sut = new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "test_function" },
+ ThoughtSignature = ""
+ };
+
+ // Act
+ var json = System.Text.Json.JsonSerializer.Serialize(sut);
+ var deserialized = System.Text.Json.JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.Contains("\"thoughtSignature\":\"\"", json);
+ Assert.NotNull(deserialized);
+ Assert.Equal("", deserialized.ThoughtSignature);
+ }
}
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs
index 142f0b20e4c7..c1492b79ed64 100644
--- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs
@@ -781,6 +781,199 @@ public void FromChatHistoryMultiTurnConversationPreservesAllRoles()
Assert.Equal("assistant-message-2", request.Contents[3].Parts![0].Text);
}
+ [Fact]
+ public void FromChatHistoryToolCallsWithThoughtSignatureIncludesSignatureInRequest()
+ {
+ // Arrange
+ ChatHistory chatHistory = [];
+ var inputPart = new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart
+ {
+ FunctionName = "function-name",
+ Arguments = new JsonObject { ["key"] = "value" }
+ },
+ ThoughtSignature = "thought-signature-abc123"
+ };
+ chatHistory.Add(new GeminiChatMessageContent(AuthorRole.Assistant, "tool-message", "model-id", [inputPart]));
+ var executionSettings = new GeminiPromptExecutionSettings();
+
+ // Act
+ var request = GeminiRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings);
+
+ // Assert
+ Assert.Single(request.Contents);
+ var requestParts = request.Contents[0].Parts;
+ Assert.NotNull(requestParts);
+ var requestPart = Assert.Single(requestParts);
+ Assert.NotNull(requestPart.FunctionCall);
+ Assert.Equal("thought-signature-abc123", requestPart.ThoughtSignature);
+ }
+
+ [Fact]
+ public void FromChatHistoryToolCallsWithoutThoughtSignatureDoesNotIncludeSignature()
+ {
+ // Arrange
+ ChatHistory chatHistory = [];
+ var functionCallPart = new GeminiPart.FunctionCallPart
+ {
+ FunctionName = "function-name",
+ Arguments = new JsonObject { ["key"] = "value" }
+ };
+ chatHistory.Add(new GeminiChatMessageContent(AuthorRole.Assistant, "tool-message", "model-id", functionsToolCalls: [functionCallPart]));
+ var executionSettings = new GeminiPromptExecutionSettings();
+
+ // Act
+ var request = GeminiRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings);
+
+ // Assert
+ Assert.Single(request.Contents);
+ var requestParts = request.Contents[0].Parts;
+ Assert.NotNull(requestParts);
+ var requestPart = Assert.Single(requestParts);
+ Assert.Null(requestPart.ThoughtSignature);
+ }
+
+ [Fact]
+ public void FromChatHistoryParallelToolCallsOnlyFirstHasThoughtSignature()
+ {
+ // Arrange - Parallel function calls: only first has ThoughtSignature per Google docs
+ ChatHistory chatHistory = [];
+ var geminiParts = new[]
+ {
+ new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "function1" },
+ ThoughtSignature = "signature-for-first-only"
+ },
+ new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "function2" },
+ ThoughtSignature = null
+ },
+ new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart { FunctionName = "function3" },
+ ThoughtSignature = null
+ }
+ };
+ chatHistory.Add(new GeminiChatMessageContent(AuthorRole.Assistant, null, "model-id", geminiParts));
+ var executionSettings = new GeminiPromptExecutionSettings();
+
+ // Act
+ var request = GeminiRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings);
+
+ // Assert
+ Assert.Single(request.Contents);
+ var parts = request.Contents[0].Parts;
+ Assert.NotNull(parts);
+ Assert.Equal(3, parts.Count);
+ Assert.Equal("signature-for-first-only", parts[0].ThoughtSignature);
+ Assert.Null(parts[1].ThoughtSignature);
+ Assert.Null(parts[2].ThoughtSignature);
+ }
+
+ [Fact]
+ public void FromChatHistoryTextResponseWithThoughtSignatureIncludesSignatureInRequest()
+ {
+ // Arrange - Text response with ThoughtSignature in Metadata
+ ChatHistory chatHistory = [];
+ var metadata = new GeminiMetadata { ThoughtSignature = "text-response-signature" };
+ chatHistory.Add(new GeminiChatMessageContent(
+ AuthorRole.Assistant,
+ "This is a text response",
+ "model-id",
+ calledToolResults: null,
+ metadata));
+ var executionSettings = new GeminiPromptExecutionSettings();
+
+ // Act
+ var request = GeminiRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings);
+
+ // Assert
+ Assert.Single(request.Contents);
+ var parts = request.Contents[0].Parts;
+ Assert.NotNull(parts);
+ var part = Assert.Single(parts);
+ Assert.Equal("This is a text response", part.Text);
+ Assert.Equal("text-response-signature", part.ThoughtSignature);
+ }
+
+ [Fact]
+ public void FromChatHistoryTextResponseWithoutThoughtSignatureDoesNotIncludeSignature()
+ {
+ // Arrange - Text response without ThoughtSignature (thinking disabled)
+ ChatHistory chatHistory = [];
+ chatHistory.AddAssistantMessage("This is a text response");
+ var executionSettings = new GeminiPromptExecutionSettings();
+
+ // Act
+ var request = GeminiRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings);
+
+ // Assert
+ Assert.Single(request.Contents);
+ var parts = request.Contents[0].Parts;
+ Assert.NotNull(parts);
+ var part = Assert.Single(parts);
+ Assert.Null(part.ThoughtSignature);
+ }
+
+ [Fact]
+ public void FromChatHistoryMultiTurnWithThoughtSignaturesPreservesAllSignatures()
+ {
+ // Arrange - Multi-turn conversation with different ThoughtSignatures
+ ChatHistory chatHistory = [];
+ chatHistory.AddUserMessage("Question 1");
+
+ var metadata1 = new GeminiMetadata { ThoughtSignature = "signature-turn-1" };
+ chatHistory.Add(new GeminiChatMessageContent(
+ AuthorRole.Assistant,
+ "Answer 1",
+ "model-id",
+ calledToolResults: null,
+ metadata1));
+
+ chatHistory.AddUserMessage("Question 2");
+
+ var metadata2 = new GeminiMetadata { ThoughtSignature = "signature-turn-2" };
+ chatHistory.Add(new GeminiChatMessageContent(
+ AuthorRole.Assistant,
+ "Answer 2",
+ "model-id",
+ calledToolResults: null,
+ metadata2));
+
+ var executionSettings = new GeminiPromptExecutionSettings();
+
+ // Act
+ var request = GeminiRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings);
+
+ // Assert
+ Assert.Equal(4, request.Contents.Count);
+ Assert.Null(request.Contents[0].Parts![0].ThoughtSignature); // User message
+ Assert.Equal("signature-turn-1", request.Contents[1].Parts![0].ThoughtSignature); // Assistant 1
+ Assert.Null(request.Contents[2].Parts![0].ThoughtSignature); // User message
+ Assert.Equal("signature-turn-2", request.Contents[3].Parts![0].ThoughtSignature); // Assistant 2
+ }
+
+ [Fact]
+ public void FromChatHistoryThoughtSignatureFromDictionaryMetadataFallback()
+ {
+ // Arrange - Simulate deserialized chat history where Metadata is a dictionary
+ ChatHistory chatHistory = [];
+ var metadata = new Dictionary { ["ThoughtSignature"] = "fallback-signature" };
+ chatHistory.Add(new ChatMessageContent(AuthorRole.Assistant, "Text response", "model-id", metadata));
+ var executionSettings = new GeminiPromptExecutionSettings();
+
+ // Act
+ var request = GeminiRequest.FromChatHistoryAndExecutionSettings(chatHistory, executionSettings);
+
+ // Assert - Should NOT include signature because it's not a GeminiChatMessageContent
+ // The fallback only works for GeminiChatMessageContent with dictionary metadata
+ Assert.Single(request.Contents);
+ Assert.Null(request.Contents[0].Parts![0].ThoughtSignature);
+ }
+
private sealed class DummyContent(object? innerContent, string? modelId = null, IReadOnlyDictionary? metadata = null) :
KernelContent(innerContent, modelId, metadata);
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/TestData/chat_function_with_thought_signature_response.json b/dotnet/src/Connectors/Connectors.Google.UnitTests/TestData/chat_function_with_thought_signature_response.json
new file mode 100644
index 000000000000..820e5744b040
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/TestData/chat_function_with_thought_signature_response.json
@@ -0,0 +1,66 @@
+{
+ "candidates": [
+ {
+ "content": {
+ "parts": [
+ {
+ "functionCall": {
+ "name": "TimePlugin%nameSeparator%Now",
+ "args": {
+ "param1": "hello"
+ }
+ },
+ "thoughtSignature": "test-thought-signature-abc123"
+ }
+ ],
+ "role": "model"
+ },
+ "finishReason": "STOP",
+ "index": 0,
+ "safetyRatings": [
+ {
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_HATE_SPEECH",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_HARASSMENT",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
+ "probability": "NEGLIGIBLE"
+ }
+ ]
+ }
+ ],
+ "promptFeedback": {
+ "safetyRatings": [
+ {
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_HATE_SPEECH",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_HARASSMENT",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
+ "probability": "NEGLIGIBLE"
+ }
+ ]
+ },
+ "usageMetadata": {
+ "promptTokenCount": 9,
+ "candidatesTokenCount": 27,
+ "totalTokenCount": 36
+ }
+}
+
diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/TestData/chat_text_with_thought_signature_response.json b/dotnet/src/Connectors/Connectors.Google.UnitTests/TestData/chat_text_with_thought_signature_response.json
new file mode 100644
index 000000000000..f3e82de0af6d
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/TestData/chat_text_with_thought_signature_response.json
@@ -0,0 +1,61 @@
+{
+ "candidates": [
+ {
+ "content": {
+ "parts": [
+ {
+ "text": "This is a text response with thinking enabled.",
+ "thoughtSignature": "text-response-thought-signature-xyz789"
+ }
+ ],
+ "role": "model"
+ },
+ "finishReason": "STOP",
+ "index": 0,
+ "safetyRatings": [
+ {
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_HATE_SPEECH",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_HARASSMENT",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
+ "probability": "NEGLIGIBLE"
+ }
+ ]
+ }
+ ],
+ "promptFeedback": {
+ "safetyRatings": [
+ {
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_HATE_SPEECH",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_HARASSMENT",
+ "probability": "NEGLIGIBLE"
+ },
+ {
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
+ "probability": "NEGLIGIBLE"
+ }
+ ]
+ },
+ "usageMetadata": {
+ "promptTokenCount": 9,
+ "candidatesTokenCount": 27,
+ "totalTokenCount": 36
+ }
+}
+
diff --git a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs
index 2d093c2207cd..3c3501622b74 100644
--- a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs
@@ -7,6 +7,7 @@
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
+using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -236,6 +237,9 @@ public async IAsyncEnumerable StreamGenerateChatMes
for (state.Iteration = 1; ; state.Iteration++)
{
+ // Reset LastMessage at the start of each iteration to detect if tool calls were found
+ state.LastMessage = null;
+
using (var activity = ModelDiagnostics.StartCompletionActivity(
this._chatGenerationEndpoint, this._modelId, ModelProvider, chatHistory, state.ExecutionSettings))
{
@@ -288,7 +292,8 @@ public async IAsyncEnumerable StreamGenerateChatMes
}
}
- if (!state.AutoInvoke)
+ // If auto-invoke is disabled or no tool calls were found, we're done
+ if (!state.AutoInvoke || state.LastMessage is null)
{
yield break;
}
@@ -350,39 +355,126 @@ private async IAsyncEnumerable GetStreamingChatMess
{
var chatResponsesEnumerable = this.ProcessChatResponseStreamAsync(responseStream, ct: ct);
IAsyncEnumerator chatResponsesEnumerator = null!;
+
+ // Track content and items from chunks before tool calls (lazy-init, only used when AutoInvoke is enabled)
+ StringBuilder? preToolCallContent = null;
+ List? preToolCallItems = null;
+
try
{
chatResponsesEnumerator = chatResponsesEnumerable.GetAsyncEnumerator(ct);
while (await chatResponsesEnumerator.MoveNextAsync().ConfigureAwait(false))
{
var messageContent = chatResponsesEnumerator.Current;
- if (state.AutoInvoke && messageContent.ToolCalls is not null)
+ if (state.AutoInvoke && messageContent.ToolCalls is { Count: > 0 })
{
- if (await chatResponsesEnumerator.MoveNextAsync().ConfigureAwait(false))
+ // Accumulate all tool calls from streaming chunks (needed for Gemini 3 with thought signatures)
+ // where multiple chunks may each contain function calls
+ var allToolCalls = new List(messageContent.ToolCalls);
+ var combinedContent = new StringBuilder();
+ var allItems = new List();
+ GeminiMetadata? lastMetadata = messageContent.Metadata as GeminiMetadata;
+
+ // Include any content and items accumulated before we saw tool calls
+ if (preToolCallContent is { Length: > 0 })
{
- // We disable auto-invoke because we have more than one message in the stream.
- // This scenario should not happen but I leave it as a precaution
- state.AutoInvoke = false;
- // We return the first message
- yield return this.GetStreamingChatContentFromChatContent(messageContent);
- // We return the second message
- messageContent = chatResponsesEnumerator.Current;
- yield return this.GetStreamingChatContentFromChatContent(messageContent);
- continue;
+ combinedContent.Append(preToolCallContent);
+ }
+ if (preToolCallItems is { Count: > 0 })
+ {
+ allItems.AddRange(preToolCallItems);
}
- // If function call was returned there is no more data in stream
- state.LastMessage = messageContent;
-
- // Yield the message also if it contains text
+ // Accumulate content and items from first tool-call chunk
if (!string.IsNullOrWhiteSpace(messageContent.Content))
{
- yield return this.GetStreamingChatContentFromChatContent(messageContent);
+ combinedContent.Append(messageContent.Content);
+ }
+ if (messageContent.Items is { Count: > 0 })
+ {
+ allItems.AddRange(messageContent.Items);
}
+ // Yield the first chunk
+ yield return this.GetStreamingChatContentFromChatContent(messageContent);
+
+ // Consume the entire stream - accumulate tool calls, content, and items from all chunks
+ while (await chatResponsesEnumerator.MoveNextAsync().ConfigureAwait(false))
+ {
+ var nextMessage = chatResponsesEnumerator.Current;
+
+ // Always update metadata to capture the final chunk's usage stats and finish reason
+ if (nextMessage.Metadata is GeminiMetadata metadata)
+ {
+ lastMetadata = metadata;
+ }
+
+ // Accumulate tool calls if present
+ if (nextMessage.ToolCalls is { Count: > 0 })
+ {
+ allToolCalls.AddRange(nextMessage.ToolCalls);
+ }
+
+ // Accumulate content if present
+ if (!string.IsNullOrWhiteSpace(nextMessage.Content))
+ {
+ combinedContent.Append(nextMessage.Content);
+ }
+
+ // Accumulate items (ReasoningContent) if present
+ if (nextMessage.Items is { Count: > 0 })
+ {
+ allItems.AddRange(nextMessage.Items);
+ }
+
+ // Always yield the chunk to the caller for streaming output
+ yield return this.GetStreamingChatContentFromChatContent(nextMessage);
+ }
+
+ // Create a combined message with all accumulated tool calls for auto-invoke processing
+ // Note: We must preserve thought signatures from each tool call
+ var combinedMessage = new GeminiChatMessageContent(
+ role: messageContent.Role,
+ content: combinedContent.Length > 0 ? combinedContent.ToString() : null,
+ modelId: messageContent.ModelId ?? this._modelId,
+ partsWithFunctionCalls: allToolCalls.Select(tc => new GeminiPart
+ {
+ FunctionCall = new GeminiPart.FunctionCallPart
+ {
+ FunctionName = tc.FullyQualifiedName,
+ Arguments = tc.Arguments != null ? JsonSerializer.SerializeToNode(tc.Arguments) : null
+ },
+ ThoughtSignature = tc.ThoughtSignature
+ }).ToArray(),
+ metadata: lastMetadata);
+
+ // Add accumulated items (ReasoningContent) to the combined message
+ // These are needed for chat history to preserve the model's reasoning trace
+ foreach (var item in allItems)
+ {
+ combinedMessage.Items.Add(item);
+ }
+
+ state.LastMessage = combinedMessage;
yield break;
}
+ // Track content and items before we see tool calls (only if auto-invoke enabled)
+ // This ensures pre-tool-call content is included in state.LastMessage for chat history
+ if (state.AutoInvoke)
+ {
+ if (!string.IsNullOrWhiteSpace(messageContent.Content))
+ {
+ preToolCallContent ??= new StringBuilder();
+ preToolCallContent.Append(messageContent.Content);
+ }
+ if (messageContent.Items is { Count: > 0 })
+ {
+ preToolCallItems ??= [];
+ preToolCallItems.AddRange(messageContent.Items);
+ }
+ }
+
// If we don't want to attempt to invoke any functions, just return the result.
yield return this.GetStreamingChatContentFromChatContent(messageContent);
}
@@ -776,17 +868,23 @@ private GeminiChatMessageContent GetChatMessageContentFromCandidate(GeminiRespon
var regularText = string.Concat(regularTextParts);
// Gemini sometimes returns function calls with text parts, so collect them
- var toolCalls = candidate.Content?.Parts?
- .Select(part => part.FunctionCall!)
- .Where(toolCall => toolCall is not null).ToArray();
+ // Use full GeminiPart[] to preserve ThoughtSignature for function calls with thinking enabled
+ var partsWithFunctionCalls = candidate.Content?.Parts?
+ .Where(part => part.FunctionCall is not null).ToArray();
+
+ // For text responses (no function calls), capture ThoughtSignature from last part for metadata
+ // Per Google docs: "The final content part returned by the model may contain a thought_signature"
+ var lastPart = candidate.Content?.Parts?.LastOrDefault();
+ var hasFunctionCalls = partsWithFunctionCalls is { Length: > 0 };
+ string? textThoughtSignature = (!hasFunctionCalls && lastPart?.FunctionCall is null) ? lastPart?.ThoughtSignature : null;
// Pass null if there's no regular (non-thinking) text to avoid creating an empty TextContent item
var chatMessage = new GeminiChatMessageContent(
role: candidate.Content?.Role ?? AuthorRole.Assistant,
content: string.IsNullOrEmpty(regularText) ? null : regularText,
modelId: this._modelId,
- functionsToolCalls: toolCalls,
- metadata: GetResponseMetadata(geminiResponse, candidate));
+ partsWithFunctionCalls: partsWithFunctionCalls,
+ metadata: GetResponseMetadata(geminiResponse, candidate, textThoughtSignature));
// Add items to the message
foreach (var item in items)
@@ -874,7 +972,8 @@ private static void ValidateAutoInvoke(bool autoInvoke, int resultsPerPrompt)
private static GeminiMetadata GetResponseMetadata(
GeminiResponse geminiResponse,
- GeminiResponseCandidate candidate) => new()
+ GeminiResponseCandidate candidate,
+ string? thoughtSignature = null) => new()
{
FinishReason = candidate.FinishReason,
Index = candidate.Index,
@@ -887,6 +986,7 @@ private static GeminiMetadata GetResponseMetadata(
PromptFeedbackBlockReason = geminiResponse.PromptFeedback?.BlockReason,
PromptFeedbackSafetyRatings = geminiResponse.PromptFeedback?.SafetyRatings.ToList(),
ResponseSafetyRatings = candidate.SafetyRatings?.ToList(),
+ ThoughtSignature = thoughtSignature,
};
private sealed class ChatCompletionState
diff --git a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiPart.cs b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiPart.cs
index 725e6ae54fb3..d49c196da744 100644
--- a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiPart.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiPart.cs
@@ -54,6 +54,19 @@ internal sealed class GeminiPart : IJsonOnDeserialized
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? Thought { get; set; }
+ ///
+ /// Gets or sets the thought signature for maintaining reasoning context across turns.
+ ///
+ ///
+ /// When thinking is enabled, Gemini returns an opaque signature that must be included
+ /// in subsequent requests to maintain reasoning context. For function calls, this is
+ /// mandatory (HTTP 400 without it). For text responses, it is recommended for optimal
+ /// reasoning quality.
+ ///
+ [JsonPropertyName("thoughtSignature")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ThoughtSignature { get; set; }
+
///
/// Checks whether only one property of the GeminiPart instance is not null.
/// Returns true if only one property among Text, InlineData, FileData, FunctionCall, and FunctionResponse is not null,
diff --git a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs
index 49a24f05f041..cd15974a886c 100644
--- a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs
@@ -211,7 +211,8 @@ private static List CreateGeminiParts(ChatMessageContent content)
{
FunctionName = toolCall.FullyQualifiedName,
Arguments = JsonSerializer.SerializeToNode(toolCall.Arguments),
- }
+ },
+ ThoughtSignature = toolCall.ThoughtSignature
}));
break;
default:
@@ -224,6 +225,35 @@ private static List CreateGeminiParts(ChatMessageContent content)
parts.Add(new GeminiPart { Text = content.Content ?? string.Empty });
}
+ // Restore ThoughtSignature for text responses (non-ToolCall messages)
+ // Per Google docs: "The final content part returned by the model may contain a thought_signature"
+ if (content is GeminiChatMessageContent geminiContent
+ && (geminiContent.ToolCalls is null || geminiContent.ToolCalls.Count == 0))
+ {
+ string? signature = null;
+
+ // Try typed GeminiMetadata first, then dictionary fallback for deserialized history
+ if (geminiContent.Metadata is GeminiMetadata meta)
+ {
+ signature = meta.ThoughtSignature;
+ }
+ else if (content.Metadata?.TryGetValue("ThoughtSignature", out var sigObj) == true
+ && sigObj is string sigStr)
+ {
+ signature = sigStr;
+ }
+
+ if (signature is not null)
+ {
+ // Apply signature to last text part
+ var lastTextPart = parts.LastOrDefault(p => p.Text is not null);
+ if (lastTextPart is not null)
+ {
+ lastTextPart.ThoughtSignature = signature;
+ }
+ }
+ }
+
return parts;
}
diff --git a/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiChatMessageContent.cs b/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiChatMessageContent.cs
index 723d7f99dcaf..558d27548cf4 100644
--- a/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiChatMessageContent.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiChatMessageContent.cs
@@ -133,6 +133,38 @@ internal GeminiChatMessageContent(
this.ToolCalls = functionsToolCalls?.Select(tool => new GeminiFunctionToolCall(tool)).ToList();
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Role of the author of the message
+ /// Content of the message
+ /// The model ID used to generate the content
+ /// Parts containing function calls (preserves ThoughtSignature)
+ /// Additional metadata
+ ///
+ /// This constructor preserves from the parent
+ /// , which is required for function calling when thinking is enabled.
+ ///
+ internal GeminiChatMessageContent(
+ AuthorRole role,
+ string? content,
+ string modelId,
+ IEnumerable? partsWithFunctionCalls,
+ GeminiMetadata? metadata = null)
+ : base(
+ role: role,
+ content: content,
+ modelId: modelId,
+ innerContent: content,
+ encoding: Encoding.UTF8,
+ metadata: metadata)
+ {
+ this.ToolCalls = partsWithFunctionCalls?
+ .Where(p => p.FunctionCall is not null)
+ .Select(part => new GeminiFunctionToolCall(part))
+ .ToList();
+ }
+
///
/// A list of the tools returned by the model with arguments.
///
diff --git a/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiFunctionToolCall.cs
index 79fb416eddd6..1c75314ae50e 100644
--- a/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiFunctionToolCall.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiFunctionToolCall.cs
@@ -9,7 +9,7 @@
namespace Microsoft.SemanticKernel.Connectors.Google;
///
-/// Represents an Gemini function tool call with deserialized function name and arguments.
+/// Represents a Gemini function tool call with deserialized function name and arguments.
///
public sealed class GeminiFunctionToolCall
{
@@ -41,6 +41,39 @@ internal GeminiFunctionToolCall(GeminiPart.FunctionCallPart functionToolCall)
}
}
+ /// Initialize the from a containing a function call.
+ ///
+ /// This constructor preserves the from the parent ,
+ /// which is required for function calling when thinking is enabled.
+ ///
+ internal GeminiFunctionToolCall(GeminiPart part)
+ {
+ Verify.NotNull(part);
+ Verify.NotNull(part.FunctionCall);
+ Verify.NotNull(part.FunctionCall.FunctionName);
+
+ string fullyQualifiedFunctionName = part.FunctionCall.FunctionName;
+ string functionName = fullyQualifiedFunctionName;
+ string? pluginName = null;
+
+ int separatorPos = fullyQualifiedFunctionName.IndexOf(GeminiFunction.NameSeparator, StringComparison.Ordinal);
+ if (separatorPos >= 0)
+ {
+ pluginName = fullyQualifiedFunctionName.AsSpan(0, separatorPos).Trim().ToString();
+ functionName = fullyQualifiedFunctionName.AsSpan(separatorPos + GeminiFunction.NameSeparator.Length).Trim().ToString();
+ }
+
+ this._fullyQualifiedFunctionName = fullyQualifiedFunctionName;
+ this.PluginName = pluginName;
+ this.FunctionName = functionName;
+ if (part.FunctionCall.Arguments is not null)
+ {
+ this.Arguments = part.FunctionCall.Arguments.Deserialize>();
+ }
+
+ this.ThoughtSignature = part.ThoughtSignature;
+ }
+
/// Gets the name of the plugin with which this function is associated, if any.
public string? PluginName { get; }
@@ -50,6 +83,14 @@ internal GeminiFunctionToolCall(GeminiPart.FunctionCallPart functionToolCall)
/// Gets a name/value collection of the arguments to the function, if any.
public IReadOnlyDictionary? Arguments { get; }
+ /// Gets the thought signature for this function call, if any.
+ ///
+ /// When thinking is enabled, Gemini returns an opaque signature that must be included
+ /// in subsequent requests when sending function results. This is mandatory for
+ /// Gemini 2.5/3 Pro with thinking enabled.
+ ///
+ public string? ThoughtSignature { get; }
+
/// Gets the fully-qualified name of the function.
///
/// This is the concatenation of the and the ,
diff --git a/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiMetadata.cs b/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiMetadata.cs
index 1765cebb6ce5..0493867a3408 100644
--- a/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiMetadata.cs
+++ b/dotnet/src/Connectors/Connectors.Google/Models/Gemini/GeminiMetadata.cs
@@ -115,6 +115,20 @@ public IReadOnlyList? ResponseSafetyRatings
internal init => this.SetValueInDictionary(value, nameof(this.ResponseSafetyRatings));
}
+ ///
+ /// The thought signature for text responses with thinking enabled.
+ ///
+ ///
+ /// When thinking is enabled, Gemini may return a signature on the last text part that should
+ /// be included in subsequent requests to maintain optimal reasoning quality. Unlike function
+ /// call signatures, text response signatures are recommended but not strictly validated.
+ ///
+ public string? ThoughtSignature
+ {
+ get => this.GetValueFromDictionary(nameof(this.ThoughtSignature)) as string;
+ internal init => this.SetValueInDictionary(value, nameof(this.ThoughtSignature));
+ }
+
///
/// Converts a dictionary to a object.
///