Skip to content

Commit 008d40a

Browse files
.Net: feat(ollama): add Think property to OllamaPromptExecutionSettings (#14122)
## Summary - Adds `Think` (`bool?`) property to `OllamaPromptExecutionSettings` to control thinking for Ollama reasoning models (deepseek-r1, qwen3, phi4-reasoning) - Maps `Think` to `GenerateRequest.Think` in `OllamaTextGenerationService.CreateRequest()` - Bumps OllamaSharp from 5.4.12 to 5.4.25 which introduces `GenerateRequest.Think` ## Motivation Fixes #14078. When a reasoning model has thinking enabled by default (e.g. qwen3, phi4-reasoning), the model output lands in a separate thinking stream rather than the standard response field. This causes `GetTextContentsAsync` to return empty content. The only way to get a usable response is to pass `think=false` to suppress thinking, but there was no way to set that from `OllamaPromptExecutionSettings`. ## Changes ### `OllamaPromptExecutionSettings.cs` - New `Think` property with `bool?` type, JSON name `think`, `WhenWritingNull` ignore condition, `ThrowIfFrozen()` guard, and `Clone()` support ### `OllamaTextGenerationService.cs` - `CreateRequest()` maps `settings.Think` to `GenerateRequest.Think` using `OllamaSharp.Models.Chat.ThinkValue` ### `Directory.Packages.props` - OllamaSharp bumped from 5.4.12 to 5.4.25 (adds `GenerateRequest.Think` and `ThinkValue`) ### Tests - `OllamaPromptExecutionSettingsTests`: serialization round-trip, Clone, Freeze guard for `Think` - `OllamaTextGenerationTests`: verifies `Think` is present/absent in the serialized request payload for both `GetTextContentsAsync` and `GetStreamingTextContentsAsync` ## Test plan - [ ] All 115 existing unit tests pass (1 pre-existing skip) - [ ] `ThinkPropertyRoundTripsViaSerialization` - `true`/`false` round-trips via JSON - [ ] `ThinkPropertyIsPreservedByClone` - Clone copies the value - [ ] `ThinkPropertyThrowsWhenFrozen` - setter throws after Freeze - [ ] `GetTextContentsShouldSendThinkSettingAsync` - request payload contains correct `think` field - [ ] `GetTextContentsShouldNotSendThinkWhenNotSetAsync` - request payload omits `think` when null - [ ] `GetStreamingTextContentsShouldSendThinkSettingAsync` - streaming path also sends `think` --------- Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
1 parent 35ba23e commit 008d40a

5 files changed

Lines changed: 120 additions & 2 deletions

File tree

‎dotnet/Directory.Packages.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
9494
<PackageVersion Include="Npgsql" Version="10.0.2" />
9595
<PackageVersion Include="OData2Linq" Version="2.2.0" />
96-
<PackageVersion Include="OllamaSharp" Version="5.4.12" />
96+
<PackageVersion Include="OllamaSharp" Version="5.4.25" />
9797
<PackageVersion Include="OpenAI" Version="2.10.0" />
9898
<PackageVersion Include="OpenTelemetry" Version="1.15.3" />
9999
<PackageVersion Include="OpenTelemetry.Api" Version="1.15.3" />

‎dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaTextGenerationTests.cs‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,57 @@ public async Task GetTextContentsExecutionSettingsMustBeSentAsync()
188188
Assert.Equal(ollamaExecutionSettings.TopK, requestPayload.Options.TopK);
189189
}
190190

191+
[Theory]
192+
[InlineData(true)]
193+
[InlineData(false)]
194+
public async Task GetTextContentsShouldSendThinkSettingAsync(bool thinkValue)
195+
{
196+
// Arrange
197+
var sut = new OllamaTextGenerationService("fake-model", httpClient: this._httpClient);
198+
var settings = new OllamaPromptExecutionSettings { Think = thinkValue };
199+
200+
// Act
201+
await sut.GetTextContentsAsync("Any prompt", settings);
202+
203+
// Assert
204+
var requestPayload = JsonSerializer.Deserialize<GenerateRequest>(this._messageHandlerStub.RequestContent);
205+
Assert.NotNull(requestPayload);
206+
Assert.Equal(thinkValue, (bool?)requestPayload.Think);
207+
}
208+
209+
[Fact]
210+
public async Task GetTextContentsShouldNotSendThinkWhenNotSetAsync()
211+
{
212+
// Arrange
213+
var sut = new OllamaTextGenerationService("fake-model", httpClient: this._httpClient);
214+
215+
// Act
216+
await sut.GetTextContentsAsync("Any prompt");
217+
218+
// Assert
219+
var requestPayload = JsonSerializer.Deserialize<GenerateRequest>(this._messageHandlerStub.RequestContent);
220+
Assert.NotNull(requestPayload);
221+
Assert.Null(requestPayload.Think);
222+
}
223+
224+
[Theory]
225+
[InlineData(true)]
226+
[InlineData(false)]
227+
public async Task GetStreamingTextContentsShouldSendThinkSettingAsync(bool thinkValue)
228+
{
229+
// Arrange
230+
var sut = new OllamaTextGenerationService("fake-model", httpClient: this._httpClient);
231+
var settings = new OllamaPromptExecutionSettings { Think = thinkValue };
232+
233+
// Act
234+
await sut.GetStreamingTextContentsAsync("Any prompt", settings).GetAsyncEnumerator().MoveNextAsync();
235+
236+
// Assert
237+
var requestPayload = JsonSerializer.Deserialize<GenerateRequest>(this._messageHandlerStub.RequestContent);
238+
Assert.NotNull(requestPayload);
239+
Assert.Equal(thinkValue, (bool?)requestPayload.Think);
240+
}
241+
191242
/// <summary>
192243
/// Disposes resources used by this class.
193244
/// </summary>

‎dotnet/src/Connectors/Connectors.Ollama.UnitTests/Settings/OllamaPromptExecutionSettingsTests.cs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public void FromExecutionSettingsWhenNullShouldReturnDefault()
4141
Assert.Null(ollamaExecutionSettings.Temperature);
4242
Assert.Null(ollamaExecutionSettings.TopP);
4343
Assert.Null(ollamaExecutionSettings.TopK);
44+
Assert.Null(ollamaExecutionSettings.Think);
4445
}
4546

4647
[Fact]
@@ -187,6 +188,45 @@ public void ClonePreservesServiceId()
187188
Assert.Equal(testSettings.Temperature, cloned.Temperature);
188189
}
189190

191+
[Theory]
192+
[InlineData(true)]
193+
[InlineData(false)]
194+
public void ThinkPropertyRoundTripsViaSerialization(bool thinkValue)
195+
{
196+
// Arrange
197+
string jsonSettings = $$"""{ "think": {{thinkValue.ToString().ToLowerInvariant()}} }""";
198+
199+
// Act
200+
var executionSettings = JsonSerializer.Deserialize<OllamaPromptExecutionSettings>(jsonSettings);
201+
202+
// Assert
203+
Assert.Equal(thinkValue, executionSettings!.Think);
204+
}
205+
206+
[Fact]
207+
public void ThinkPropertyIsPreservedByClone()
208+
{
209+
// Arrange
210+
var settings = new OllamaPromptExecutionSettings { Think = false };
211+
212+
// Act
213+
var clone = (OllamaPromptExecutionSettings)settings.Clone();
214+
215+
// Assert
216+
Assert.Equal(false, clone.Think);
217+
}
218+
219+
[Fact]
220+
public void ThinkPropertyThrowsWhenFrozen()
221+
{
222+
// Arrange
223+
var settings = new OllamaPromptExecutionSettings();
224+
settings.Freeze();
225+
226+
// Act & Assert
227+
Assert.Throws<InvalidOperationException>(() => settings.Think = true);
228+
}
229+
190230
[Fact]
191231
public void PromptExecutionSettingsFreezeWorksAsExpected()
192232
{

‎dotnet/src/Connectors/Connectors.Ollama/Services/OllamaTextGenerationService.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ private static GenerateRequest CreateRequest(OllamaPromptExecutionSettings setti
148148
NumPredict = settings.NumPredict
149149
},
150150
Model = selectedModel,
151-
Stream = true
151+
Stream = true,
152+
Think = settings.Think.HasValue ? (OllamaSharp.Models.Chat.ThinkValue?)settings.Think.Value : null
152153
};
153154

154155
return request;

‎dotnet/src/Connectors/Connectors.Ollama/Settings/OllamaPromptExecutionSettings.cs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,30 @@ public int? NumPredict
131131
}
132132
}
133133

134+
/// <summary>
135+
/// Enables or disables thinking for reasoning models such as deepseek-r1, qwen3, and phi4-reasoning.
136+
/// Set to <c>false</c> to disable thinking and receive a standard response when using a model that
137+
/// enables thinking by default. Set to <c>true</c> to explicitly enable thinking.
138+
/// When <c>null</c> (the default), the model's own default behavior is used.
139+
/// </summary>
140+
/// <remarks>
141+
/// When thinking is active, the model's reasoning output lands in a separate thinking stream
142+
/// rather than in the main response content. Setting this to <c>false</c> suppresses thinking
143+
/// so that all output appears in the standard response field.
144+
/// </remarks>
145+
[JsonPropertyName("think")]
146+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
147+
public bool? Think
148+
{
149+
get => this._think;
150+
151+
set
152+
{
153+
this.ThrowIfFrozen();
154+
this._think = value;
155+
}
156+
}
157+
134158
/// <inheritdoc/>
135159
public override void Freeze()
136160
{
@@ -161,6 +185,7 @@ public override PromptExecutionSettings Clone()
161185
NumPredict = this.NumPredict,
162186
Stop = this.Stop is not null ? new List<string>(this.Stop) : null,
163187
FunctionChoiceBehavior = this.FunctionChoiceBehavior,
188+
Think = this.Think,
164189
};
165190
}
166191

@@ -171,6 +196,7 @@ public override PromptExecutionSettings Clone()
171196
private float? _topP;
172197
private int? _topK;
173198
private int? _numPredict;
199+
private bool? _think;
174200

175201
#endregion
176202
}

0 commit comments

Comments
 (0)