.NET: Add Reasoning to ChatClientAgent ChatOptions merging (#5463)

* Add reasoning option to request chat options in ChatClientAgent

* Add tests for ChatOptions reasoning merging in ChatClientAgent

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
This commit is contained in:
MaciejWarchalowski
2026-06-09 06:25:31 -05:00
committed by GitHub
Unverified
parent caa75f7cdd
commit 9486c76ef8
2 changed files with 110 additions and 0 deletions
@@ -564,6 +564,7 @@ public sealed partial class ChatClientAgent : AIAgent
requestChatOptions.ModelId ??= this._agentOptions.ChatOptions.ModelId;
requestChatOptions.PresencePenalty ??= this._agentOptions.ChatOptions.PresencePenalty;
requestChatOptions.ResponseFormat ??= this._agentOptions.ChatOptions.ResponseFormat;
requestChatOptions.Reasoning ??= this._agentOptions.ChatOptions.Reasoning;
requestChatOptions.Seed ??= this._agentOptions.ChatOptions.Seed;
requestChatOptions.Temperature ??= this._agentOptions.ChatOptions.Temperature;
requestChatOptions.TopP ??= this._agentOptions.ChatOptions.TopP;
@@ -347,6 +347,115 @@ public class ChatClientAgent_ChatOptionsMergingTests
Assert.Equal(expectedSetting, capturedChatOptions.RawRepresentationFactory(null!));
}
/// <summary>
/// Verify that <see cref="ChatOptions.Reasoning"/> from the request takes priority over the agent's.
/// </summary>
[Fact]
public async Task ChatOptionsMergingUsesRequestReasoningOverAgentReasoningAsync()
{
// Arrange
var agentReasoning = new ReasoningOptions { Effort = ReasoningEffort.Low, Output = ReasoningOutput.Full };
var requestReasoning = new ReasoningOptions { Effort = ReasoningEffort.High, Output = ReasoningOutput.Full };
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new ChatOptions { Reasoning = agentReasoning }
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(new ChatOptions { Reasoning = requestReasoning }));
// Assert
Assert.NotNull(capturedChatOptions);
Assert.NotNull(capturedChatOptions.Reasoning);
Assert.Equal(requestReasoning.Effort, capturedChatOptions.Reasoning.Effort);
Assert.Equal(requestReasoning.Output, capturedChatOptions.Reasoning.Output);
}
/// <summary>
/// Verify that <see cref="ChatOptions.Reasoning"/> falls back to the agent's when the request has none.
/// </summary>
[Fact]
public async Task ChatOptionsMergingFallsBackToAgentReasoningWhenRequestHasNoneAsync()
{
// Arrange
var agentReasoning = new ReasoningOptions { Effort = ReasoningEffort.Low, Output = ReasoningOutput.Full };
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new ChatOptions { Reasoning = agentReasoning }
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(new ChatOptions()));
// Assert
Assert.NotNull(capturedChatOptions);
Assert.NotNull(capturedChatOptions.Reasoning);
Assert.Equal(agentReasoning.Effort, capturedChatOptions.Reasoning.Effort);
Assert.Equal(agentReasoning.Output, capturedChatOptions.Reasoning.Output);
}
/// <summary>
/// Verify that <see cref="ChatOptions.Reasoning"/> from the request is used when the agent has none.
/// </summary>
[Fact]
public async Task ChatOptionsMergingUsesRequestReasoningWhenAgentHasNoneAsync()
{
// Arrange
var requestReasoning = new ReasoningOptions { Effort = ReasoningEffort.High, Output = ReasoningOutput.Full };
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new ChatOptions()
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(new ChatOptions { Reasoning = requestReasoning }));
// Assert
Assert.NotNull(capturedChatOptions);
Assert.NotNull(capturedChatOptions.Reasoning);
Assert.Equal(requestReasoning.Effort, capturedChatOptions.Reasoning.Effort);
Assert.Equal(requestReasoning.Output, capturedChatOptions.Reasoning.Output);
}
/// <summary>
/// Verify that ChatOptions merging handles all scalar properties correctly.
/// </summary>