From 5e20bb1c7a1c6fd8d9f49accd056871d5f700ab0 Mon Sep 17 00:00:00 2001 From: Arash Date: Sun, 28 Jun 2026 13:27:23 +0200 Subject: [PATCH 1/2] feat(ollama): add Think property to OllamaPromptExecutionSettings Adds a Think property (bool?) to OllamaPromptExecutionSettings that controls thinking behavior for Ollama reasoning models (deepseek-r1, qwen3, phi4-reasoning). When a reasoning model has thinking enabled by default, its output lands in a separate thinking stream rather than the standard response field, causing GetTextContentsAsync to return empty content. Setting Think = false suppresses thinking so all output appears in the standard response field. Changes: - OllamaPromptExecutionSettings: adds Think property with backing field, JSON serialization (think), ThrowIfFrozen guard, and Clone support - OllamaTextGenerationService: maps Think to GenerateRequest.Think in CreateRequest() - Bumps OllamaSharp from 5.4.12 to 5.4.25 (adds GenerateRequest.Think) - Tests: serialization round-trip, Clone, Freeze, and request payload verification for both GetTextContentsAsync and GetStreamingTextContentsAsync Fixes #14078 --- dotnet/Directory.Packages.props | 2 +- .../Services/OllamaTextGenerationTests.cs | 51 +++++++++++++++++++ .../OllamaPromptExecutionSettingsTests.cs | 40 +++++++++++++++ .../Services/OllamaTextGenerationService.cs | 3 +- .../Settings/OllamaPromptExecutionSettings.cs | 26 ++++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 4dcc213ea1f1..c07a450931f0 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -93,7 +93,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaTextGenerationTests.cs b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaTextGenerationTests.cs index c765bf1d678d..c66dbf71ea07 100644 --- a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaTextGenerationTests.cs +++ b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaTextGenerationTests.cs @@ -188,6 +188,57 @@ public async Task GetTextContentsExecutionSettingsMustBeSentAsync() Assert.Equal(ollamaExecutionSettings.TopK, requestPayload.Options.TopK); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetTextContentsShouldSendThinkSettingAsync(bool thinkValue) + { + // Arrange + var sut = new OllamaTextGenerationService("fake-model", httpClient: this._httpClient); + var settings = new OllamaPromptExecutionSettings { Think = thinkValue }; + + // Act + await sut.GetTextContentsAsync("Any prompt", settings); + + // Assert + var requestPayload = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(requestPayload); + Assert.Equal(thinkValue, (bool?)requestPayload.Think); + } + + [Fact] + public async Task GetTextContentsShouldNotSendThinkWhenNotSetAsync() + { + // Arrange + var sut = new OllamaTextGenerationService("fake-model", httpClient: this._httpClient); + + // Act + await sut.GetTextContentsAsync("Any prompt"); + + // Assert + var requestPayload = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(requestPayload); + Assert.Null(requestPayload.Think); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetStreamingTextContentsShouldSendThinkSettingAsync(bool thinkValue) + { + // Arrange + var sut = new OllamaTextGenerationService("fake-model", httpClient: this._httpClient); + var settings = new OllamaPromptExecutionSettings { Think = thinkValue }; + + // Act + await sut.GetStreamingTextContentsAsync("Any prompt", settings).GetAsyncEnumerator().MoveNextAsync(); + + // Assert + var requestPayload = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent); + Assert.NotNull(requestPayload); + Assert.Equal(thinkValue, (bool?)requestPayload.Think); + } + /// /// Disposes resources used by this class. /// diff --git a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs index fb41f2f991cc..7f387fde66d3 100644 --- a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs @@ -41,6 +41,7 @@ public void FromExecutionSettingsWhenNullShouldReturnDefault() Assert.Null(ollamaExecutionSettings.Temperature); Assert.Null(ollamaExecutionSettings.TopP); Assert.Null(ollamaExecutionSettings.TopK); + Assert.Null(ollamaExecutionSettings.Think); } [Fact] @@ -187,6 +188,45 @@ public void ClonePreservesServiceId() Assert.Equal(testSettings.Temperature, cloned.Temperature); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ThinkPropertyRoundTripsViaSerialization(bool thinkValue) + { + // Arrange + string jsonSettings = $$"""{ "think": {{thinkValue.ToString().ToLower()}} }"""; + + // Act + var executionSettings = JsonSerializer.Deserialize(jsonSettings); + + // Assert + Assert.Equal(thinkValue, executionSettings!.Think); + } + + [Fact] + public void ThinkPropertyIsPreservedByClone() + { + // Arrange + var settings = new OllamaPromptExecutionSettings { Think = false }; + + // Act + var clone = (OllamaPromptExecutionSettings)settings.Clone(); + + // Assert + Assert.Equal(false, clone.Think); + } + + [Fact] + public void ThinkPropertyThrowsWhenFrozen() + { + // Arrange + var settings = new OllamaPromptExecutionSettings(); + settings.Freeze(); + + // Act & Assert + Assert.Throws(() => settings.Think = true); + } + [Fact] public void PromptExecutionSettingsFreezeWorksAsExpected() { diff --git a/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs b/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs index d149d7a1b3fa..70ecfcf52cf1 100644 --- a/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs @@ -148,7 +148,8 @@ private static GenerateRequest CreateRequest(OllamaPromptExecutionSettings setti NumPredict = settings.NumPredict }, Model = selectedModel, - Stream = true + Stream = true, + Think = settings.Think.HasValue ? (OllamaSharp.Models.Chat.ThinkValue?)new OllamaSharp.Models.Chat.ThinkValue(settings.Think) : null }; return request; diff --git a/dotnet/src/Connectors/Connectors.Ollama/Settings/OllamaPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.Ollama/Settings/OllamaPromptExecutionSettings.cs index 1b49aa99d97d..898dfa6a5ab4 100644 --- a/dotnet/src/Connectors/Connectors.Ollama/Settings/OllamaPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.Ollama/Settings/OllamaPromptExecutionSettings.cs @@ -131,6 +131,30 @@ public int? NumPredict } } + /// + /// Enables or disables thinking for reasoning models such as deepseek-r1, qwen3, and phi4-reasoning. + /// Set to false to disable thinking and receive a standard response when using a model that + /// enables thinking by default. Set to true to explicitly enable thinking. + /// When null (the default), the model's own default behavior is used. + /// + /// + /// When thinking is active, the model's reasoning output lands in a separate thinking stream + /// rather than in the main response content. Setting this to false suppresses thinking + /// so that all output appears in the standard response field. + /// + [JsonPropertyName("think")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Think + { + get => this._think; + + set + { + this.ThrowIfFrozen(); + this._think = value; + } + } + /// public override void Freeze() { @@ -161,6 +185,7 @@ public override PromptExecutionSettings Clone() NumPredict = this.NumPredict, Stop = this.Stop is not null ? new List(this.Stop) : null, FunctionChoiceBehavior = this.FunctionChoiceBehavior, + Think = this.Think, }; } @@ -171,6 +196,7 @@ public override PromptExecutionSettings Clone() private float? _topP; private int? _topK; private int? _numPredict; + private bool? _think; #endregion } From 6575acf83d688c4554e2fc5e9c331791f1353b38 Mon Sep 17 00:00:00 2001 From: Arash Date: Sun, 28 Jun 2026 14:12:37 +0200 Subject: [PATCH 2/2] refactor: address Copilot review comments - Simplify ThinkValue mapping: use settings.Think.Value (non-null bool) instead of passing nullable bool to constructor - Use ToLowerInvariant() instead of ToLower() in test JSON builder to avoid culture-sensitive casing --- .../Settings/OllamaPromptExecutionSettingsTests.cs | 2 +- .../Connectors.Ollama/Services/OllamaTextGenerationService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs index 7f387fde66d3..fff0aef5921c 100644 --- a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs @@ -194,7 +194,7 @@ public void ClonePreservesServiceId() public void ThinkPropertyRoundTripsViaSerialization(bool thinkValue) { // Arrange - string jsonSettings = $$"""{ "think": {{thinkValue.ToString().ToLower()}} }"""; + string jsonSettings = $$"""{ "think": {{thinkValue.ToString().ToLowerInvariant()}} }"""; // Act var executionSettings = JsonSerializer.Deserialize(jsonSettings); diff --git a/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs b/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs index 70ecfcf52cf1..b965c90b690c 100644 --- a/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs @@ -149,7 +149,7 @@ private static GenerateRequest CreateRequest(OllamaPromptExecutionSettings setti }, Model = selectedModel, Stream = true, - Think = settings.Think.HasValue ? (OllamaSharp.Models.Chat.ThinkValue?)new OllamaSharp.Models.Chat.ThinkValue(settings.Think) : null + Think = settings.Think.HasValue ? (OllamaSharp.Models.Chat.ThinkValue?)settings.Think.Value : null }; return request;