From 5a9e3fe2643ac6a7b44821862d0b32409f8fb2a8 Mon Sep 17 00:00:00 2001 From: Adit Sheth Date: Fri, 18 Apr 2025 14:28:29 -0700 Subject: [PATCH 01/10] Introduce Gemini Thinking Budget Configuration. --- .../Core/Gemini/GeminiRequestTests.cs | 104 ++++++++++++++++++ .../Core/Gemini/Models/GeminiRequest.cs | 7 +- .../GeminiPromptExecutionSettings.cs | 28 +++++ .../GeminiThinkingConfig .cs | 55 +++++++++ 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs 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 caa91e39860a..da97ec85a3a0 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs @@ -638,6 +638,110 @@ public void ResponseSchemaAddsTypeToEnumProperties() Assert.Equal(2, roleProperty.GetProperty("enum").GetArrayLength()); } + [Fact] + public void WithThinkingConfigReturnsInGenerationConfig() + { + // Arrange + var prompt = "prompt-example"; + var executionSettings = new GeminiPromptExecutionSettings + { + ModelId = "gemini-2.5-flash-preview-04-17", + ThinkingConfig = new GeminiThinkingConfig { ThinkingBudget = 1024 } + }; + + // Act + var request = GeminiRequest.FromPromptAndExecutionSettings(prompt, executionSettings); + + // Assert + Assert.Equal(executionSettings.ThinkingConfig.ThinkingBudget, request.ThinkingConfig?.ThinkingBudget); + } + + [Fact] + public void WithoutThinkingConfigDoesNotIncludeThinkingConfigInGenerationConfig() + { + // Arrange + var prompt = "prompt-example"; + var executionSettings = new GeminiPromptExecutionSettings + { + ModelId = "gemini-pro" // Not a Gemini 2.5 model + }; + + // Act + var request = GeminiRequest.FromPromptAndExecutionSettings(prompt, executionSettings); + + // Assert + Assert.Null(request.ThinkingConfig); + } + + [Fact] + public void WithNullThinkingConfigDoesNotIncludeThinkingConfigInGenerationConfig() + { + // Arrange + var prompt = "prompt-example"; + var executionSettings = new GeminiPromptExecutionSettings + { + ModelId = "gemini-2.5-flash-preview-04-17", + ThinkingConfig = null + }; + + // Act + var request = GeminiRequest.FromPromptAndExecutionSettings(prompt, executionSettings); + + // Assert + Assert.Null(request.ThinkingConfig); + } + + [Fact] + public void ThinkingConfigSetValueOnNonGemini25ModelThrowsInvalidOperationException() + { + // Arrange + var executionSettings = new GeminiPromptExecutionSettings { ModelId = "gemini-pro" }; + var thinkingConfig = new GeminiThinkingConfig { ThinkingBudget = 1024 }; + + // Act & Assert + Assert.Throws(() => executionSettings.ThinkingConfig = thinkingConfig); + } + + [Fact] + public void ThinkingConfigSetValueNullOnNonGemini25ModelDoesNotThrowException() + { + // Arrange + var executionSettings = new GeminiPromptExecutionSettings { ModelId = "gemini-pro" }; + + // Act + executionSettings.ThinkingConfig = null; + + // Assert + Assert.Null(executionSettings.ThinkingConfig); + } + + [Fact] + public void ThinkingConfigSetValueOnGemini25ModelDoesNotThrowException() + { + // Arrange + var executionSettings = new GeminiPromptExecutionSettings { ModelId = "gemini-2.5-flash-preview-04-17" }; + var thinkingConfig = new GeminiThinkingConfig { ThinkingBudget = 1024 }; + + // Act + executionSettings.ThinkingConfig = thinkingConfig; + + // Assert + Assert.NotNull(executionSettings.ThinkingConfig); + Assert.Equal(1024, executionSettings.ThinkingConfig.ThinkingBudget); + } + + [Theory] + [InlineData(-1)] + [InlineData(24577)] + public void ThinkingBudgetSetValueOutOfRangeThrowsArgumentOutOfRangeException(int invalidBudget) + { + // Arrange + var thinkingConfig = new GeminiThinkingConfig(); + + // Act & Assert + Assert.Throws(() => thinkingConfig.ThinkingBudget = invalidBudget); + } + private sealed class DummyContent(object? innerContent, string? modelId = null, IReadOnlyDictionary? metadata = null) : KernelContent(innerContent, modelId, metadata); 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 034e03b4c8e4..313cdcd53f2f 100644 --- a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs +++ b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs @@ -46,6 +46,10 @@ internal sealed class GeminiRequest [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? CachedContent { get; set; } + [JsonPropertyName("thinkingConfig")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public GeminiThinkingConfig? ThinkingConfig { get; set; } + public void AddFunction(GeminiFunction function) { // NOTE: Currently Gemini only supports one tool i.e. function calling. @@ -305,7 +309,7 @@ private static void AddConfiguration(GeminiPromptExecutionSettings executionSett CandidateCount = executionSettings.CandidateCount, AudioTimestamp = executionSettings.AudioTimestamp, ResponseMimeType = executionSettings.ResponseMimeType, - ResponseSchema = GetResponseSchemaConfig(executionSettings.ResponseSchema) + ResponseSchema = GetResponseSchemaConfig(executionSettings.ResponseSchema), }; } @@ -430,6 +434,7 @@ private static void AddSafetySettings(GeminiPromptExecutionSettings executionSet private static void AddAdditionalBodyFields(GeminiPromptExecutionSettings executionSettings, GeminiRequest request) { request.CachedContent = executionSettings.CachedContent; + request.ThinkingConfig = executionSettings.ThinkingConfig; } internal sealed class ConfigurationElement diff --git a/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs index bcbbbd4f9e8c..1461a97e62d5 100644 --- a/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs @@ -30,6 +30,7 @@ public sealed class GeminiPromptExecutionSettings : PromptExecutionSettings private string? _cachedContent; private IList? _safetySettings; private GeminiToolCallBehavior? _toolCallBehavior; + private GeminiThinkingConfig? _thinkingConfig; /// /// Default max tokens for a text generation. @@ -262,6 +263,32 @@ public string? CachedContent } } + /// + /// Configuration for the thinking budget in Gemini 2.5. + /// **This property is specific to Gemini 2.5 and similar experimental models.** + /// + [JsonPropertyName("thinking_config")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public GeminiThinkingConfig? ThinkingConfig + { + get => this._thinkingConfig; + set + { + this.ThrowIfFrozen(); + bool isGemini25 = false; + if (this.ModelId != null) + { + isGemini25 = this.ModelId?.StartsWith("gemini-2.5", StringComparison.OrdinalIgnoreCase) ?? false; + } + + if (!isGemini25 && value != null) + { + throw new InvalidOperationException("ThinkingConfig is only applicable to Gemini-2.5 models."); + } + this._thinkingConfig = value; + } + } + /// public override void Freeze() { @@ -301,6 +328,7 @@ public override PromptExecutionSettings Clone() AudioTimestamp = this.AudioTimestamp, ResponseMimeType = this.ResponseMimeType, ResponseSchema = this.ResponseSchema, + ThinkingConfig = this.ThinkingConfig?.Clone() }; } diff --git a/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs b/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs new file mode 100644 index 000000000000..62f53e1132b3 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Google; + +/// +/// GeminiThinkingConfig class +/// +public class GeminiThinkingConfig +{ + private int? _thinkingBudget; + + /// + /// The thinking budget parameter gives the model guidance on how many thinking tokens it can use for its thinking process. + /// A greater number of tokens is typically associated with more detailed thinking, which is needed for solving more complex tasks. + /// thinkingBudget must be an integer in the range 0 to 24576. Setting the thinking budget to 0 disables thinking. + /// Budgets from 1 to 1024 tokens will be set to 1024. + /// **This parameter is specific to Gemini 2.5 and similar experimental models.** + /// + [JsonPropertyName("thinkingBudget")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? ThinkingBudget + { + get => this._thinkingBudget; + set + { + if (value is < 0 or > 24576) + { + throw new ArgumentOutOfRangeException(nameof(value), "ThinkingBudget must be an integer in the range 0 to 24576."); + } + this._thinkingBudget = value == 0 ? 0 : (value < 1024 ? 1024 : value); + } + } + + /// + /// Initializes a new instance of the class. + /// For Gemini 2.5, if no ThinkingBudget is explicitly set, the API default (likely 0) will be used. + /// You can explicitly set a value here if needed. + /// + public GeminiThinkingConfig() + { + this._thinkingBudget = 0; + } + + /// + /// Clones this instance. + /// + /// + public GeminiThinkingConfig Clone() + { + return (GeminiThinkingConfig)this.MemberwiseClone(); + } +} From 66e0ca600cdeaa07eff5589c6e46c2717214cc8b Mon Sep 17 00:00:00 2001 From: Adit Sheth Date: Tue, 22 Apr 2025 08:29:36 -0700 Subject: [PATCH 02/10] Update dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../src/Connectors/Connectors.Google/GeminiThinkingConfig .cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs b/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs index 62f53e1132b3..d75aa411c2b1 100644 --- a/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs +++ b/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs @@ -17,8 +17,8 @@ public class GeminiThinkingConfig /// A greater number of tokens is typically associated with more detailed thinking, which is needed for solving more complex tasks. /// thinkingBudget must be an integer in the range 0 to 24576. Setting the thinking budget to 0 disables thinking. /// Budgets from 1 to 1024 tokens will be set to 1024. - /// **This parameter is specific to Gemini 2.5 and similar experimental models.** /// + /// This parameter is specific to Gemini 2.5 and similar experimental models. [JsonPropertyName("thinkingBudget")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? ThinkingBudget From 38b4221d2ea638679608cffb861a292ee48ba4c0 Mon Sep 17 00:00:00 2001 From: Adit Sheth Date: Tue, 22 Apr 2025 08:30:17 -0700 Subject: [PATCH 03/10] Update dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../Connectors.Google/GeminiPromptExecutionSettings.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs index 1461a97e62d5..3b4024896f83 100644 --- a/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs @@ -275,16 +275,6 @@ public GeminiThinkingConfig? ThinkingConfig set { this.ThrowIfFrozen(); - bool isGemini25 = false; - if (this.ModelId != null) - { - isGemini25 = this.ModelId?.StartsWith("gemini-2.5", StringComparison.OrdinalIgnoreCase) ?? false; - } - - if (!isGemini25 && value != null) - { - throw new InvalidOperationException("ThinkingConfig is only applicable to Gemini-2.5 models."); - } this._thinkingConfig = value; } } From 760bd9452768201996fc942e61130b413bb90111 Mon Sep 17 00:00:00 2001 From: Adit Sheth Date: Tue, 22 Apr 2025 08:30:27 -0700 Subject: [PATCH 04/10] Update dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../Connectors.Google/GeminiPromptExecutionSettings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs index 3b4024896f83..6fd93d3c1e98 100644 --- a/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs @@ -265,7 +265,8 @@ public string? CachedContent /// /// Configuration for the thinking budget in Gemini 2.5. - /// **This property is specific to Gemini 2.5 and similar experimental models.** + /// + /// This property is specific to Gemini 2.5 and similar experimental models. /// [JsonPropertyName("thinking_config")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] From dfca49fbd237b32aad3cfa31e6baad00806d4d7a Mon Sep 17 00:00:00 2001 From: Adit Sheth Date: Tue, 22 Apr 2025 09:44:14 -0700 Subject: [PATCH 05/10] Comments Fixes. --- .../Core/Gemini/GeminiRequestTests.cs | 86 ------------------- ...oogleAIGeminiChatCompletionServiceTests.cs | 39 +++++++++ .../GeminiPromptExecutionSettings.cs | 5 +- .../GeminiThinkingConfig .cs | 4 - 4 files changed, 42 insertions(+), 92 deletions(-) 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 da97ec85a3a0..99857d9de502 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs @@ -656,92 +656,6 @@ public void WithThinkingConfigReturnsInGenerationConfig() Assert.Equal(executionSettings.ThinkingConfig.ThinkingBudget, request.ThinkingConfig?.ThinkingBudget); } - [Fact] - public void WithoutThinkingConfigDoesNotIncludeThinkingConfigInGenerationConfig() - { - // Arrange - var prompt = "prompt-example"; - var executionSettings = new GeminiPromptExecutionSettings - { - ModelId = "gemini-pro" // Not a Gemini 2.5 model - }; - - // Act - var request = GeminiRequest.FromPromptAndExecutionSettings(prompt, executionSettings); - - // Assert - Assert.Null(request.ThinkingConfig); - } - - [Fact] - public void WithNullThinkingConfigDoesNotIncludeThinkingConfigInGenerationConfig() - { - // Arrange - var prompt = "prompt-example"; - var executionSettings = new GeminiPromptExecutionSettings - { - ModelId = "gemini-2.5-flash-preview-04-17", - ThinkingConfig = null - }; - - // Act - var request = GeminiRequest.FromPromptAndExecutionSettings(prompt, executionSettings); - - // Assert - Assert.Null(request.ThinkingConfig); - } - - [Fact] - public void ThinkingConfigSetValueOnNonGemini25ModelThrowsInvalidOperationException() - { - // Arrange - var executionSettings = new GeminiPromptExecutionSettings { ModelId = "gemini-pro" }; - var thinkingConfig = new GeminiThinkingConfig { ThinkingBudget = 1024 }; - - // Act & Assert - Assert.Throws(() => executionSettings.ThinkingConfig = thinkingConfig); - } - - [Fact] - public void ThinkingConfigSetValueNullOnNonGemini25ModelDoesNotThrowException() - { - // Arrange - var executionSettings = new GeminiPromptExecutionSettings { ModelId = "gemini-pro" }; - - // Act - executionSettings.ThinkingConfig = null; - - // Assert - Assert.Null(executionSettings.ThinkingConfig); - } - - [Fact] - public void ThinkingConfigSetValueOnGemini25ModelDoesNotThrowException() - { - // Arrange - var executionSettings = new GeminiPromptExecutionSettings { ModelId = "gemini-2.5-flash-preview-04-17" }; - var thinkingConfig = new GeminiThinkingConfig { ThinkingBudget = 1024 }; - - // Act - executionSettings.ThinkingConfig = thinkingConfig; - - // Assert - Assert.NotNull(executionSettings.ThinkingConfig); - Assert.Equal(1024, executionSettings.ThinkingConfig.ThinkingBudget); - } - - [Theory] - [InlineData(-1)] - [InlineData(24577)] - public void ThinkingBudgetSetValueOutOfRangeThrowsArgumentOutOfRangeException(int invalidBudget) - { - // Arrange - var thinkingConfig = new GeminiThinkingConfig(); - - // Act & Assert - Assert.Throws(() => thinkingConfig.ThinkingBudget = invalidBudget); - } - 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/Services/GoogleAIGeminiChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs index 0d986d21ca5a..c6065511b783 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs @@ -69,6 +69,45 @@ public async Task RequestCachedContentWorksCorrectlyAsync(string? cachedContent) } } + [Theory] + [InlineData(null, false)] + [InlineData(0, true)] + [InlineData(500, true)] + [InlineData(2048, true)] + public async Task RequestBodyIncludesThinkingConfigWhenSetAsync(int? thinkingBudget, bool shouldContain) + { + // Arrange + string model = "gemini-2.5-pro"; + var sut = new GoogleAIGeminiChatCompletionService(model, "key", httpClient: this._httpClient); + + var executionSettings = new GeminiPromptExecutionSettings + { + ThinkingConfig = thinkingBudget.HasValue + ? new GeminiThinkingConfig { ThinkingBudget = thinkingBudget.Value } + : null + }; + + // Act + var result = await sut.GetChatMessageContentAsync("my prompt", executionSettings); + + // Assert + Assert.NotNull(result); + Assert.NotNull(this._messageHandlerStub.RequestContent); + + var requestBody = UTF8Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent); + + if (shouldContain) + { + Assert.Contains("thinkingConfig", requestBody); + Assert.Contains("thinkingBudget", requestBody); + } + else + { + Assert.DoesNotContain("thinkingConfig", requestBody); + } + } + + public void Dispose() { this._httpClient.Dispose(); diff --git a/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs index 6fd93d3c1e98..b3473db1fe63 100644 --- a/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.Google/GeminiPromptExecutionSettings.cs @@ -266,8 +266,9 @@ public string? CachedContent /// /// Configuration for the thinking budget in Gemini 2.5. /// - /// This property is specific to Gemini 2.5 and similar experimental models. - /// + /// + /// This property is specific to Gemini 2.5 and similar experimental models. + /// [JsonPropertyName("thinking_config")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public GeminiThinkingConfig? ThinkingConfig diff --git a/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs b/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs index d75aa411c2b1..5c1d89d38f1a 100644 --- a/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs +++ b/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs @@ -26,10 +26,6 @@ public int? ThinkingBudget get => this._thinkingBudget; set { - if (value is < 0 or > 24576) - { - throw new ArgumentOutOfRangeException(nameof(value), "ThinkingBudget must be an integer in the range 0 to 24576."); - } this._thinkingBudget = value == 0 ? 0 : (value < 1024 ? 1024 : value); } } From 617735f50dd927916186d92a19dd9f16abb111e4 Mon Sep 17 00:00:00 2001 From: Adit Sheth Date: Wed, 23 Apr 2025 09:06:25 -0700 Subject: [PATCH 06/10] Fixed build error. --- .../Services/GoogleAIGeminiChatCompletionServiceTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs index c6065511b783..caba5f91f081 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs @@ -107,7 +107,6 @@ public async Task RequestBodyIncludesThinkingConfigWhenSetAsync(int? thinkingBud } } - public void Dispose() { this._httpClient.Dispose(); From 9431d963f7cf5f0b6b1ec8198c9fbdc3a98b5f68 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:27:04 +0100 Subject: [PATCH 07/10] Conclude the PR, added tests and checks --- .../Core/Gemini/GeminiRequestTests.cs | 2 +- .../GeminiPromptExecutionSettingsTests.cs | 21 ++++++-- ...oogleAIGeminiChatCompletionServiceTests.cs | 3 +- .../Core/Gemini/Models/GeminiRequest.cs | 14 ++++- .../GeminiThinkingConfig .cs | 51 ------------------- .../Connectors.Google/GeminiThinkingConfig.cs | 33 ++++++++++++ 6 files changed, 66 insertions(+), 58 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs create mode 100644 dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig.cs 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 99857d9de502..c4387414f976 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs @@ -639,7 +639,7 @@ public void ResponseSchemaAddsTypeToEnumProperties() } [Fact] - public void WithThinkingConfigReturnsInGenerationConfig() + public void FromPromptAndExecutionSettingsWithThinkingConfigReturnsInGenerationConfig() { // Arrange var prompt = "prompt-example"; diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiPromptExecutionSettingsTests.cs index 0d2955f18d7f..a0b456e5d2f5 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiPromptExecutionSettingsTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.Json; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.Google; @@ -109,7 +110,10 @@ public void ItCreatesGeminiExecutionSettingsFromJsonSnakeCase() "category": "{{category.Label}}", "threshold": "{{threshold.Label}}" } - ] + ], + "thinking_config": { + "thinking_budget": 1000 + } } """; var actualSettings = JsonSerializer.Deserialize(json); @@ -129,6 +133,8 @@ public void ItCreatesGeminiExecutionSettingsFromJsonSnakeCase() Assert.Single(executionSettings.SafetySettings!, settings => settings.Category.Equals(category) && settings.Threshold.Equals(threshold)); + + Assert.Equal(1000, executionSettings.ThinkingConfig?.ThinkingBudget); } [Fact] @@ -152,7 +158,10 @@ public void PromptExecutionSettingsCloneWorksAsExpected() "category": "{{category.Label}}", "threshold": "{{threshold.Label}}" } - ] + ], + "thinking_config": { + "thinking_budget": 1000 + } } """; var executionSettings = JsonSerializer.Deserialize(json); @@ -168,6 +177,7 @@ public void PromptExecutionSettingsCloneWorksAsExpected() Assert.Equivalent(executionSettings.StopSequences, clone.StopSequences); Assert.Equivalent(executionSettings.SafetySettings, clone.SafetySettings); Assert.Equal(executionSettings.AudioTimestamp, clone.AudioTimestamp); + Assert.Equivalent(executionSettings.ThinkingConfig, clone.ThinkingConfig); } [Fact] @@ -191,7 +201,10 @@ public void PromptExecutionSettingsFreezeWorksAsExpected() "category": "{{category.Label}}", "threshold": "{{threshold.Label}}" } - ] + ], + "thinking_config": { + "thinking_budget": 1000 + } } """; var executionSettings = JsonSerializer.Deserialize(json); @@ -206,5 +219,7 @@ public void PromptExecutionSettingsFreezeWorksAsExpected() Assert.Throws(() => executionSettings.Temperature = 0.5); Assert.Throws(() => executionSettings.AudioTimestamp = false); Assert.Throws(() => executionSettings.StopSequences!.Add("baz")); + Assert.Throws(() => executionSettings.SafetySettings!.Add(new GeminiSafetySetting(GeminiSafetyCategory.Toxicity, GeminiSafetyThreshold.Unspecified))); + Assert.Throws(() => executionSettings.ThinkingConfig = new GeminiThinkingConfig { ThinkingBudget = 1 }); } } diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs index caba5f91f081..c8d12af36fd5 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs @@ -4,6 +4,7 @@ using System.IO; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Google; @@ -99,7 +100,7 @@ public async Task RequestBodyIncludesThinkingConfigWhenSetAsync(int? thinkingBud if (shouldContain) { Assert.Contains("thinkingConfig", requestBody); - Assert.Contains("thinkingBudget", requestBody); + Assert.Contains($"\"thinkingBudget\":{thinkingBudget}", requestBody); } else { 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 313cdcd53f2f..bfc483b0f819 100644 --- a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs +++ b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs @@ -48,7 +48,7 @@ internal sealed class GeminiRequest [JsonPropertyName("thinkingConfig")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public GeminiThinkingConfig? ThinkingConfig { get; set; } + public GeminiRequestThinkingConfig? ThinkingConfig { get; set; } public void AddFunction(GeminiFunction function) { @@ -434,7 +434,10 @@ private static void AddSafetySettings(GeminiPromptExecutionSettings executionSet private static void AddAdditionalBodyFields(GeminiPromptExecutionSettings executionSettings, GeminiRequest request) { request.CachedContent = executionSettings.CachedContent; - request.ThinkingConfig = executionSettings.ThinkingConfig; + if (executionSettings.ThinkingConfig is not null) + { + request.ThinkingConfig = new GeminiRequestThinkingConfig { ThinkingBudget = executionSettings.ThinkingConfig.ThinkingBudget }; + } } internal sealed class ConfigurationElement @@ -475,4 +478,11 @@ internal sealed class ConfigurationElement [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public JsonElement? ResponseSchema { get; set; } } + + internal sealed class GeminiRequestThinkingConfig + { + [JsonPropertyName("thinkingBudget")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? ThinkingBudget { get; set; } + } } diff --git a/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs b/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs deleted file mode 100644 index 5c1d89d38f1a..000000000000 --- a/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig .cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.Google; - -/// -/// GeminiThinkingConfig class -/// -public class GeminiThinkingConfig -{ - private int? _thinkingBudget; - - /// - /// The thinking budget parameter gives the model guidance on how many thinking tokens it can use for its thinking process. - /// A greater number of tokens is typically associated with more detailed thinking, which is needed for solving more complex tasks. - /// thinkingBudget must be an integer in the range 0 to 24576. Setting the thinking budget to 0 disables thinking. - /// Budgets from 1 to 1024 tokens will be set to 1024. - /// - /// This parameter is specific to Gemini 2.5 and similar experimental models. - [JsonPropertyName("thinkingBudget")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public int? ThinkingBudget - { - get => this._thinkingBudget; - set - { - this._thinkingBudget = value == 0 ? 0 : (value < 1024 ? 1024 : value); - } - } - - /// - /// Initializes a new instance of the class. - /// For Gemini 2.5, if no ThinkingBudget is explicitly set, the API default (likely 0) will be used. - /// You can explicitly set a value here if needed. - /// - public GeminiThinkingConfig() - { - this._thinkingBudget = 0; - } - - /// - /// Clones this instance. - /// - /// - public GeminiThinkingConfig Clone() - { - return (GeminiThinkingConfig)this.MemberwiseClone(); - } -} diff --git a/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig.cs b/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig.cs new file mode 100644 index 000000000000..ac4699736844 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Google/GeminiThinkingConfig.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Connectors.Google; + +/// +/// GeminiThinkingConfig class +/// +public class GeminiThinkingConfig +{ + /// The thinking budget parameter gives the model guidance on how many thinking tokens it can use for its thinking process. + /// + /// A greater number of tokens is typically associated with more detailed thinking, which is needed for solving more complex tasks. + /// thinkingBudget must be an integer in the range 0 to 24576. Setting the thinking budget to 0 disables thinking. + /// Budgets from 1 to 1024 tokens will be set to 1024. + /// + /// This parameter is specific to Gemini 2.5 and similar experimental models. + /// If no ThinkingBudget is explicitly set, the API default (likely 0) will be used + /// + [JsonPropertyName("thinking_budget")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? ThinkingBudget { get; set; } + + /// + /// Clones this instance. + /// + /// + public GeminiThinkingConfig Clone() + { + return (GeminiThinkingConfig)this.MemberwiseClone(); + } +} From b0ed002c070ee508d097f164b608711e6541a99f Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:28:14 +0100 Subject: [PATCH 08/10] Fix warnings --- .../GeminiPromptExecutionSettingsTests.cs | 1 - .../Services/GoogleAIGeminiChatCompletionServiceTests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiPromptExecutionSettingsTests.cs index a0b456e5d2f5..5ba6895da18b 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/GeminiPromptExecutionSettingsTests.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text.Json; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.Google; diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs index c8d12af36fd5..46cfc91f8946 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/GoogleAIGeminiChatCompletionServiceTests.cs @@ -4,7 +4,6 @@ using System.IO; using System.Net.Http; using System.Text; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Google; From 6f83d52d2b3a9632a67dc23c5dc7637647c3aaf3 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:59:42 +0100 Subject: [PATCH 09/10] Add missing concept example + fix request bug --- ..._GeminiChatCompletionWithThinkingBudget.cs | 157 ++++++++++++++++++ ...ertexAIGeminiChatCompletionServiceTests.cs | 38 +++++ .../Core/Gemini/Models/GeminiRequest.cs | 11 +- 3 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionWithThinkingBudget.cs diff --git a/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionWithThinkingBudget.cs b/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionWithThinkingBudget.cs new file mode 100644 index 000000000000..85e510ffac6f --- /dev/null +++ b/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionWithThinkingBudget.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Google.Apis.Auth.OAuth2; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.Google; + +namespace ChatCompletion; + +/// +/// These examples demonstrate different ways of using chat completion with Google VertexAI and GoogleAI APIs. +/// +/// Currently thinking budget is only supported in Google AI Gemini 2.5+ models +/// See: https://developers.googleblog.com/en/start-building-with-gemini-25-flash/#:~:text=thinking%20budgets +/// +/// +public sealed class Google_GeminiChatCompletionWithThinkingBudget(ITestOutputHelper output) : BaseTest(output) +{ + [Fact] + public async Task GoogleAIChatCompletionUsingThinkingBudget() + { + Console.WriteLine("============= Google AI - Gemini 2.5 Chat Completion using Thinking Budget ============="); + + Assert.NotNull(TestConfiguration.GoogleAI.ApiKey); + string geminiModelId = "gemini-2.5-pro-exp-03-25"; + + Kernel kernel = Kernel.CreateBuilder() + .AddGoogleAIGeminiChatCompletion( + modelId: geminiModelId, + apiKey: TestConfiguration.GoogleAI.ApiKey) + .Build(); + + try + { + await this.ProcessChatAsync(kernel); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + Console.WriteLine("Gemini 2.5 model is not available in your region."); + } + } + + [Fact] + public async Task VertexAIUsingChatCompletion() + { + Console.WriteLine("============= Vertex AI - Gemini Chat Completion ============="); + + string? bearerToken = null; + Assert.NotNull(TestConfiguration.VertexAI.ClientId); + Assert.NotNull(TestConfiguration.VertexAI.ClientSecret); + Assert.NotNull(TestConfiguration.VertexAI.Location); + Assert.NotNull(TestConfiguration.VertexAI.ProjectId); + + // Currently thinking budget is only supported in Google AI Gemini 2.5+ models + // See: https://developers.googleblog.com/en/start-building-with-gemini-25-flash/#:~:text=thinking%20budgets + string geminiModelId = "gemini-2.5-pro-preview-03-25"; + + Kernel kernel = Kernel.CreateBuilder() + .AddVertexAIGeminiChatCompletion( + modelId: geminiModelId, + bearerTokenProvider: GetBearerToken, + location: TestConfiguration.VertexAI.Location, + projectId: TestConfiguration.VertexAI.ProjectId) + .Build(); + + // To generate bearer key, you need installed google sdk or use google web console with command: + // + // gcloud auth print-access-token + // + // Above code pass bearer key as string, it is not recommended way in production code, + // especially if IChatCompletionService will be long lived, tokens generated by google sdk lives for 1 hour. + // You should use bearer key provider, which will be used to generate token on demand: + // + // Example: + // + // Kernel kernel = Kernel.CreateBuilder() + // .AddVertexAIGeminiChatCompletion( + // modelId: TestConfiguration.VertexAI.Gemini.ModelId, + // bearerKeyProvider: () => + // { + // // This is just example, in production we recommend using Google SDK to generate your BearerKey token. + // // This delegate will be called on every request, + // // when providing the token consider using caching strategy and refresh token logic when it is expired or close to expiration. + // return GetBearerToken(); + // }, + // location: TestConfiguration.VertexAI.Location, + // projectId: TestConfiguration.VertexAI.ProjectId); + + async ValueTask GetBearerToken() + { + if (!string.IsNullOrEmpty(bearerToken)) + { + return bearerToken; + } + + var credential = GoogleWebAuthorizationBroker.AuthorizeAsync( + new ClientSecrets + { + ClientId = TestConfiguration.VertexAI.ClientId, + ClientSecret = TestConfiguration.VertexAI.ClientSecret + }, + ["https://www.googleapis.com/auth/cloud-platform"], + "user", + CancellationToken.None); + + var userCredential = await credential.WaitAsync(CancellationToken.None); + bearerToken = userCredential.Token.AccessToken; + + return bearerToken; + } + + await this.ProcessChatAsync(kernel); + } + + private async Task ProcessChatAsync(Kernel kernel) + { + var chatHistory = new ChatHistory("You are an expert in the tool shop."); + var chat = kernel.GetRequiredService(); + var executionSettings = new GeminiPromptExecutionSettings + { + // This parameter gives the model how much tokens it can use during the thinking process. + ThinkingConfig = new() { ThinkingBudget = 2000 } + }; + + // First user message + chatHistory.AddUserMessage("Hi, I'm looking for new power tools, any suggestion?"); + await MessageOutputAsync(chatHistory); + + // First assistant message + var reply = await chat.GetChatMessageContentAsync(chatHistory, executionSettings); + chatHistory.Add(reply); + await MessageOutputAsync(chatHistory); + + // Second user message + chatHistory.AddUserMessage("I'm looking for a drill, a screwdriver and a hammer."); + await MessageOutputAsync(chatHistory); + + // Second assistant message + reply = await chat.GetChatMessageContentAsync(chatHistory, executionSettings); + chatHistory.Add(reply); + await MessageOutputAsync(chatHistory); + } + + /// + /// Outputs the last message of the chat history + /// + private Task MessageOutputAsync(ChatHistory chatHistory) + { + var message = chatHistory.Last(); + + Console.WriteLine($"{message.Role}: {message.Content}"); + Console.WriteLine("------------------------"); + + return Task.CompletedTask; + } +} diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/VertexAIGeminiChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/VertexAIGeminiChatCompletionServiceTests.cs index 0376924c0e91..e4cba485563e 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/VertexAIGeminiChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Services/VertexAIGeminiChatCompletionServiceTests.cs @@ -80,6 +80,44 @@ public async Task RequestCachedContentWorksCorrectlyAsync(string? cachedContent) } } + [Theory] + [InlineData(null, false)] + [InlineData(0, true)] + [InlineData(500, true)] + [InlineData(2048, true)] + public async Task RequestBodyIncludesThinkingConfigWhenSetAsync(int? thinkingBudget, bool shouldContain) + { + // Arrange + string model = "gemini-2.5-pro"; + var sut = new VertexAIGeminiChatCompletionService(model, () => new ValueTask("key"), "location", "project", httpClient: this._httpClient); + + var executionSettings = new GeminiPromptExecutionSettings + { + ThinkingConfig = thinkingBudget.HasValue + ? new GeminiThinkingConfig { ThinkingBudget = thinkingBudget.Value } + : null + }; + + // Act + var result = await sut.GetChatMessageContentAsync("my prompt", executionSettings); + + // Assert + Assert.NotNull(result); + Assert.NotNull(this._messageHandlerStub.RequestContent); + + var requestBody = UTF8Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent); + + if (shouldContain) + { + Assert.Contains("thinkingConfig", requestBody); + Assert.Contains($"\"thinkingBudget\":{thinkingBudget}", requestBody); + } + else + { + Assert.DoesNotContain("thinkingConfig", requestBody); + } + } + public void Dispose() { this._httpClient.Dispose(); 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 bfc483b0f819..a3dca0b03d87 100644 --- a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs +++ b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Models/GeminiRequest.cs @@ -46,10 +46,6 @@ internal sealed class GeminiRequest [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? CachedContent { get; set; } - [JsonPropertyName("thinkingConfig")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public GeminiRequestThinkingConfig? ThinkingConfig { get; set; } - public void AddFunction(GeminiFunction function) { // NOTE: Currently Gemini only supports one tool i.e. function calling. @@ -436,7 +432,8 @@ private static void AddAdditionalBodyFields(GeminiPromptExecutionSettings execut request.CachedContent = executionSettings.CachedContent; if (executionSettings.ThinkingConfig is not null) { - request.ThinkingConfig = new GeminiRequestThinkingConfig { ThinkingBudget = executionSettings.ThinkingConfig.ThinkingBudget }; + request.Configuration ??= new ConfigurationElement(); + request.Configuration.ThinkingConfig = new GeminiRequestThinkingConfig { ThinkingBudget = executionSettings.ThinkingConfig.ThinkingBudget }; } } @@ -477,6 +474,10 @@ internal sealed class ConfigurationElement [JsonPropertyName("responseSchema")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public JsonElement? ResponseSchema { get; set; } + + [JsonPropertyName("thinkingConfig")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public GeminiRequestThinkingConfig? ThinkingConfig { get; set; } } internal sealed class GeminiRequestThinkingConfig From 3940c92cf3a6d61ee8c12e25e8347ba758bd7449 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Thu, 24 Apr 2025 17:27:05 +0100 Subject: [PATCH 10/10] Added missing UT + removed vertex examples/tests --- ..._GeminiChatCompletionWithThinkingBudget.cs | 97 +------------------ dotnet/samples/Concepts/README.md | 1 + .../Core/Gemini/GeminiRequestTests.cs | 2 +- .../Gemini/GeminiChatCompletionTests.cs | 22 +++++ .../Connectors/Google/TestsBase.cs | 6 +- 5 files changed, 28 insertions(+), 100 deletions(-) diff --git a/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionWithThinkingBudget.cs b/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionWithThinkingBudget.cs index 85e510ffac6f..dbe095bfd67c 100644 --- a/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionWithThinkingBudget.cs +++ b/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionWithThinkingBudget.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using Google.Apis.Auth.OAuth2; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.Google; @@ -8,7 +7,7 @@ namespace ChatCompletion; /// -/// These examples demonstrate different ways of using chat completion with Google VertexAI and GoogleAI APIs. +/// These examples demonstrate different ways of using chat completion with Google AI API. /// /// Currently thinking budget is only supported in Google AI Gemini 2.5+ models /// See: https://developers.googleblog.com/en/start-building-with-gemini-25-flash/#:~:text=thinking%20budgets @@ -30,91 +29,6 @@ public async Task GoogleAIChatCompletionUsingThinkingBudget() apiKey: TestConfiguration.GoogleAI.ApiKey) .Build(); - try - { - await this.ProcessChatAsync(kernel); - } - catch (Exception ex) - { - Console.WriteLine($"Error: {ex.Message}"); - Console.WriteLine("Gemini 2.5 model is not available in your region."); - } - } - - [Fact] - public async Task VertexAIUsingChatCompletion() - { - Console.WriteLine("============= Vertex AI - Gemini Chat Completion ============="); - - string? bearerToken = null; - Assert.NotNull(TestConfiguration.VertexAI.ClientId); - Assert.NotNull(TestConfiguration.VertexAI.ClientSecret); - Assert.NotNull(TestConfiguration.VertexAI.Location); - Assert.NotNull(TestConfiguration.VertexAI.ProjectId); - - // Currently thinking budget is only supported in Google AI Gemini 2.5+ models - // See: https://developers.googleblog.com/en/start-building-with-gemini-25-flash/#:~:text=thinking%20budgets - string geminiModelId = "gemini-2.5-pro-preview-03-25"; - - Kernel kernel = Kernel.CreateBuilder() - .AddVertexAIGeminiChatCompletion( - modelId: geminiModelId, - bearerTokenProvider: GetBearerToken, - location: TestConfiguration.VertexAI.Location, - projectId: TestConfiguration.VertexAI.ProjectId) - .Build(); - - // To generate bearer key, you need installed google sdk or use google web console with command: - // - // gcloud auth print-access-token - // - // Above code pass bearer key as string, it is not recommended way in production code, - // especially if IChatCompletionService will be long lived, tokens generated by google sdk lives for 1 hour. - // You should use bearer key provider, which will be used to generate token on demand: - // - // Example: - // - // Kernel kernel = Kernel.CreateBuilder() - // .AddVertexAIGeminiChatCompletion( - // modelId: TestConfiguration.VertexAI.Gemini.ModelId, - // bearerKeyProvider: () => - // { - // // This is just example, in production we recommend using Google SDK to generate your BearerKey token. - // // This delegate will be called on every request, - // // when providing the token consider using caching strategy and refresh token logic when it is expired or close to expiration. - // return GetBearerToken(); - // }, - // location: TestConfiguration.VertexAI.Location, - // projectId: TestConfiguration.VertexAI.ProjectId); - - async ValueTask GetBearerToken() - { - if (!string.IsNullOrEmpty(bearerToken)) - { - return bearerToken; - } - - var credential = GoogleWebAuthorizationBroker.AuthorizeAsync( - new ClientSecrets - { - ClientId = TestConfiguration.VertexAI.ClientId, - ClientSecret = TestConfiguration.VertexAI.ClientSecret - }, - ["https://www.googleapis.com/auth/cloud-platform"], - "user", - CancellationToken.None); - - var userCredential = await credential.WaitAsync(CancellationToken.None); - bearerToken = userCredential.Token.AccessToken; - - return bearerToken; - } - - await this.ProcessChatAsync(kernel); - } - - private async Task ProcessChatAsync(Kernel kernel) - { var chatHistory = new ChatHistory("You are an expert in the tool shop."); var chat = kernel.GetRequiredService(); var executionSettings = new GeminiPromptExecutionSettings @@ -131,15 +45,6 @@ private async Task ProcessChatAsync(Kernel kernel) var reply = await chat.GetChatMessageContentAsync(chatHistory, executionSettings); chatHistory.Add(reply); await MessageOutputAsync(chatHistory); - - // Second user message - chatHistory.AddUserMessage("I'm looking for a drill, a screwdriver and a hammer."); - await MessageOutputAsync(chatHistory); - - // Second assistant message - reply = await chat.GetChatMessageContentAsync(chatHistory, executionSettings); - chatHistory.Add(reply); - await MessageOutputAsync(chatHistory); } /// diff --git a/dotnet/samples/Concepts/README.md b/dotnet/samples/Concepts/README.md index 814e6341a644..53a63c441f0b 100644 --- a/dotnet/samples/Concepts/README.md +++ b/dotnet/samples/Concepts/README.md @@ -58,6 +58,7 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom - [Connectors_WithMultipleLLMs](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Connectors_WithMultipleLLMs.cs) - [Google_GeminiChatCompletion](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletion.cs) - [Google_GeminiChatCompletionStreaming](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionStreaming.cs) +- [Google_GeminiChatCompletionWithThinkingBudget](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiChatCompletionWithThinkingBudget.cs) - [Google_GeminiGetModelResult](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiGetModelResult.cs) - [Google_GeminiStructuredOutputs](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiStructuredOutputs.cs) - [Google_GeminiVision](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/ChatCompletion/Google_GeminiVision.cs) 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 c4387414f976..58185a691ab5 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs @@ -653,7 +653,7 @@ public void FromPromptAndExecutionSettingsWithThinkingConfigReturnsInGenerationC var request = GeminiRequest.FromPromptAndExecutionSettings(prompt, executionSettings); // Assert - Assert.Equal(executionSettings.ThinkingConfig.ThinkingBudget, request.ThinkingConfig?.ThinkingBudget); + Assert.Equal(executionSettings.ThinkingConfig.ThinkingBudget, request.Configuration?.ThinkingConfig?.ThinkingBudget); } private sealed class DummyContent(object? innerContent, string? modelId = null, IReadOnlyDictionary? metadata = null) : diff --git a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs index 6249b85c7a9c..90984d8edb07 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/Gemini/GeminiChatCompletionTests.cs @@ -578,4 +578,26 @@ public async Task ChatStreamingReturnsResponseSafetyRatingsAsync(ServiceType ser this.Output.WriteLine($"ResponseSafetyRatings: {JsonSerializer.Serialize(geminiMetadata.ResponseSafetyRatings)}"); Assert.NotNull(geminiMetadata.ResponseSafetyRatings); } + + [RetryFact(Skip = "This test is for manual verification.")] + public async Task GoogleAIChatReturnsResponseWorksWithThinkingBudgetAsync() + { + // Arrange + var modelId = "gemini-2.5-pro-exp-03-25"; + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello, I'm Brandon, how are you?"); + chatHistory.AddAssistantMessage("I'm doing well, thanks for asking."); + chatHistory.AddUserMessage("Call me by my name and expand this abbreviation: LLM"); + + var sut = this.GetChatService(ServiceType.GoogleAI, isBeta: true, overrideModelId: modelId); + var settings = new GeminiPromptExecutionSettings { ThinkingConfig = new() { ThinkingBudget = 2000 } }; + + // Act + var streamResponses = await sut.GetStreamingChatMessageContentsAsync(chatHistory, settings).ToListAsync(); + var responses = await sut.GetChatMessageContentsAsync(chatHistory, settings); + + // Assert + Assert.NotNull(streamResponses[0].Content); + Assert.NotNull(responses[0].Content); + } } diff --git a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs b/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs index da0d98838f77..b656f70421b9 100644 --- a/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs +++ b/dotnet/src/IntegrationTests/Connectors/Google/TestsBase.cs @@ -36,14 +36,14 @@ protected TestsBase(ITestOutputHelper output) this._configuration.GetSection("VertexAI").Bind(this._vertexAI); } - protected IChatCompletionService GetChatService(ServiceType serviceType, bool isBeta = false) => serviceType switch + protected IChatCompletionService GetChatService(ServiceType serviceType, bool isBeta = false, string? overrideModelId = null) => serviceType switch { ServiceType.GoogleAI => new GoogleAIGeminiChatCompletionService( - this.GoogleAI.Gemini.ModelId, + overrideModelId ?? this.GoogleAI.Gemini.ModelId, this.GoogleAI.ApiKey, isBeta ? GoogleAIVersion.V1_Beta : GoogleAIVersion.V1), ServiceType.VertexAI => new VertexAIGeminiChatCompletionService( - modelId: this.VertexAI.Gemini.ModelId, + modelId: overrideModelId ?? this.VertexAI.Gemini.ModelId, bearerKey: this.VertexAI.BearerKey, location: this.VertexAI.Location, projectId: this.VertexAI.ProjectId,