Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ protected ChatOptions(ChatOptions? other)
ModelId = other.ModelId;
PresencePenalty = other.PresencePenalty;
RawRepresentationFactory = other.RawRepresentationFactory;
Reasoning = other.Reasoning?.Clone();
ResponseFormat = other.ResponseFormat;
Seed = other.Seed;
Temperature = other.Temperature;
Expand Down Expand Up @@ -108,6 +109,18 @@ protected ChatOptions(ChatOptions? other)
/// <summary>Gets or sets a seed value used by a service to control the reproducibility of results.</summary>
public long? Seed { get; set; }

/// <summary>
/// Gets or sets the reasoning options for the chat request.
/// </summary>
/// <remarks>
/// If <see langword="null"/>, no reasoning options are specified and the client will use its default.
/// Not all providers support reasoning options. Implementations should make a best-effort attempt
/// to map the requested options to the provider's capabilities. If a provider doesn't support reasoning,
/// these options may be ignored.
/// </remarks>
Comment thread
stephentoub marked this conversation as resolved.
Outdated
[Experimental(DiagnosticIds.Experiments.AIReasoning, UrlFormat = DiagnosticIds.UrlFormat)]
Comment thread
stephentoub marked this conversation as resolved.
Outdated
public ReasoningOptions? Reasoning { get; set; }

/// <summary>
/// Gets or sets the response format for the chat request.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Specifies the level of reasoning effort that should be applied when generating chat responses.
/// </summary>
/// <remarks>
/// This value controls how much computational effort the model should put into reasoning.
/// Higher values may result in more thoughtful responses but with increased latency and token usage.
/// The specific interpretation and support for each level may vary between providers.
Comment thread
stephentoub marked this conversation as resolved.
Outdated
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIReasoning, UrlFormat = DiagnosticIds.UrlFormat)]
public enum ReasoningEffort
{
/// <summary>
/// No reasoning effort. Disables reasoning for providers that support it.
Comment thread
stephentoub marked this conversation as resolved.
Outdated
/// </summary>
None = 0,
Comment thread
stephentoub marked this conversation as resolved.
Outdated

/// <summary>
/// Low reasoning effort. Minimal reasoning for faster responses.
/// </summary>
Low = 1,

/// <summary>
/// Medium reasoning effort. Balanced reasoning for most use cases.
/// </summary>
Medium = 2,

/// <summary>
/// High reasoning effort. Extensive reasoning for complex tasks.
/// </summary>
High = 3,

/// <summary>
/// Extra high reasoning effort. Maximum reasoning for the most demanding tasks.
/// </summary>
/// <remarks>
/// Not all providers support this level of reasoning effort. Some providers may map this to
Comment thread
stephentoub marked this conversation as resolved.
Outdated
/// their highest available level (such as <see cref="High"/>) if they don't have an equivalent.
/// </remarks>
ExtraHigh = 4,
Comment thread
stephentoub marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents options for configuring reasoning behavior in chat requests.
/// </summary>
/// <remarks>
/// <para>
/// Reasoning options allow control over how much computational effort the model
/// should put into reasoning about the response, and how that reasoning should
/// be exposed to the caller.
/// </para>
/// <para>
/// Not all providers support all reasoning options. Implementations should
/// make a best-effort attempt to map the requested options to the provider's
/// capabilities. If a provider doesn't support reasoning, these options may be ignored.
Comment thread
stephentoub marked this conversation as resolved.
Outdated
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIReasoning, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class ReasoningOptions
{
/// <summary>
/// Gets or sets the level of reasoning effort to apply.
/// </summary>
/// <value>
/// The reasoning effort level, or <see langword="null"/> to use the provider's default.
/// </value>
public ReasoningEffort? Effort { get; set; }

/// <summary>
/// Gets or sets how reasoning content should be included in the response.
/// </summary>
/// <value>
/// The reasoning output mode, or <see langword="null"/> to use the provider's default.
/// </value>
public ReasoningOutput? Output { get; set; }

/// <summary>Creates a shallow clone of this <see cref="ReasoningOptions"/> instance.</summary>
/// <returns>A shallow clone of this instance.</returns>
public ReasoningOptions Clone() => new()
Comment thread
stephentoub marked this conversation as resolved.
Outdated
{
Effort = Effort,
Output = Output,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Extensions.AI;

/// <summary>
/// Specifies how reasoning content should be included in the response.
/// </summary>
/// <remarks>
/// Some providers support including reasoning or thinking traces in the response.
/// This setting controls whether and how that reasoning content is exposed.
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIReasoning, UrlFormat = DiagnosticIds.UrlFormat)]
public enum ReasoningOutput
{
/// <summary>
/// No reasoning output. Do not include reasoning content in the response.
/// </summary>
None = 0,

/// <summary>
/// Summary reasoning output. Include a summary of the reasoning process.
/// </summary>
Summary = 1,

/// <summary>
/// Detailed reasoning output. Include detailed reasoning content in the response.
/// </summary>
Detailed = 2,
}
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
result.PresencePenalty ??= options.PresencePenalty;
result.Temperature ??= options.Temperature;
result.Seed ??= options.Seed;
result.ReasoningEffortLevel ??= ToOpenAIChatReasoningEffortLevel(options.Reasoning?.Effort);
OpenAIClientExtensions.PatchModelIfNotSet(ref result.Patch, options.ModelId);

if (options.StopSequences is { Count: > 0 } stopSequences)
Expand Down Expand Up @@ -637,6 +638,23 @@ ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransf
_ => null
};

private static ChatReasoningEffortLevel? ToOpenAIChatReasoningEffortLevel(ReasoningEffort? effort)
{
if (effort is null or ReasoningEffort.None)
Comment thread
stephentoub marked this conversation as resolved.
Outdated
{
return null;
}

return effort switch
{
ReasoningEffort.Low => ChatReasoningEffortLevel.Low,
ReasoningEffort.Medium => ChatReasoningEffortLevel.Medium,
ReasoningEffort.High => ChatReasoningEffortLevel.High,
ReasoningEffort.ExtraHigh => ChatReasoningEffortLevel.High, // Map to highest available
_ => null,
};
}

private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage)
{
var destination = new UsageDetails
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ private CreateResponseOptions AsCreateResponseOptions(ChatOptions? options, out
result.Model ??= options.ModelId ?? _responseClient.Model;
result.Temperature ??= options.Temperature;
result.TopP ??= options.TopP;
result.ReasoningOptions ??= ToOpenAIResponseReasoningOptions(options.Reasoning);

// If the CreateResponseOptions.PreviousResponseId is already set (likely rare), then we don't need to do
// anything with regards to Conversation, because they're mutually exclusive and we would want to ignore
Expand Down Expand Up @@ -814,6 +815,41 @@ ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransf
_ => null,
};

private static ResponseReasoningOptions? ToOpenAIResponseReasoningOptions(ReasoningOptions? reasoning)
{
if (reasoning is null)
{
return null;
}

ResponseReasoningEffortLevel? effortLevel = reasoning.Effort switch
{
ReasoningEffort.Low => ResponseReasoningEffortLevel.Low,
ReasoningEffort.Medium => ResponseReasoningEffortLevel.Medium,
ReasoningEffort.High => ResponseReasoningEffortLevel.High,
ReasoningEffort.ExtraHigh => ResponseReasoningEffortLevel.High, // Map to highest available
_ => null, // None or null - let OpenAI use its default
};

ResponseReasoningSummaryVerbosity? summary = reasoning.Output switch
{
ReasoningOutput.Summary => ResponseReasoningSummaryVerbosity.Concise,
ReasoningOutput.Detailed => ResponseReasoningSummaryVerbosity.Detailed,
_ => null, // None or null - let OpenAI use its default
};

if (effortLevel is null && summary is null)
{
return null;
}

return new ResponseReasoningOptions
{
ReasoningEffortLevel = effortLevel,
ReasoningSummaryVerbosity = summary,
};
}

/// <summary>Convert a sequence of <see cref="ChatMessage"/>s to <see cref="ResponseItem"/>s.</summary>
internal static IEnumerable<ResponseItem> ToOpenAIResponseItems(IEnumerable<ChatMessage> inputs, ChatOptions? options)
{
Expand Down
1 change: 1 addition & 0 deletions src/Shared/DiagnosticIds/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ internal static class Experiments
internal const string AIResponseContinuations = AIExperiments;
internal const string AICodeInterpreter = AIExperiments;
internal const string AIRealTime = AIExperiments;
internal const string AIReasoning = AIExperiments;
Comment thread
stephentoub marked this conversation as resolved.
Outdated

private const string AIExperiments = "MEAI001";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public void Constructor_Parameterless_PropsDefaulted()
Assert.Null(options.FrequencyPenalty);
Assert.Null(options.PresencePenalty);
Assert.Null(options.Seed);
Assert.Null(options.Reasoning);
Assert.Null(options.ResponseFormat);
Assert.Null(options.ModelId);
Assert.Null(options.StopSequences);
Expand All @@ -42,6 +43,7 @@ public void Constructor_Parameterless_PropsDefaulted()
Assert.Null(clone.FrequencyPenalty);
Assert.Null(clone.PresencePenalty);
Assert.Null(clone.Seed);
Assert.Null(clone.Reasoning);
Assert.Null(clone.ResponseFormat);
Assert.Null(clone.ModelId);
Assert.Null(clone.StopSequences);
Expand Down Expand Up @@ -89,6 +91,7 @@ public void Properties_Roundtrip()
options.FrequencyPenalty = 0.4f;
options.PresencePenalty = 0.5f;
options.Seed = 12345;
options.Reasoning = new ReasoningOptions { Effort = ReasoningEffort.Medium, Output = ReasoningOutput.Summary };
options.ResponseFormat = ChatResponseFormat.Json;
options.ModelId = "modelId";
options.StopSequences = stopSequences;
Expand All @@ -109,6 +112,9 @@ public void Properties_Roundtrip()
Assert.Equal(0.4f, options.FrequencyPenalty);
Assert.Equal(0.5f, options.PresencePenalty);
Assert.Equal(12345, options.Seed);
Assert.NotNull(options.Reasoning);
Assert.Equal(ReasoningEffort.Medium, options.Reasoning.Effort);
Assert.Equal(ReasoningOutput.Summary, options.Reasoning.Output);
Assert.Same(ChatResponseFormat.Json, options.ResponseFormat);
Assert.Equal("modelId", options.ModelId);
Assert.Same(stopSequences, options.StopSequences);
Expand All @@ -129,6 +135,10 @@ public void Properties_Roundtrip()
Assert.Equal(0.4f, clone.FrequencyPenalty);
Assert.Equal(0.5f, clone.PresencePenalty);
Assert.Equal(12345, clone.Seed);
Assert.NotNull(clone.Reasoning);
Assert.NotSame(options.Reasoning, clone.Reasoning); // Should be a shallow copy
Assert.Equal(ReasoningEffort.Medium, clone.Reasoning.Effort);
Assert.Equal(ReasoningOutput.Summary, clone.Reasoning.Output);
Assert.Same(ChatResponseFormat.Json, clone.ResponseFormat);
Assert.Equal("modelId", clone.ModelId);
Assert.Equal(stopSequences, clone.StopSequences);
Expand Down Expand Up @@ -168,6 +178,7 @@ public void JsonSerialization_Roundtrips()
options.FrequencyPenalty = 0.4f;
options.PresencePenalty = 0.5f;
options.Seed = 12345;
options.Reasoning = new ReasoningOptions { Effort = ReasoningEffort.High, Output = ReasoningOutput.Detailed };
options.ResponseFormat = ChatResponseFormat.Json;
options.ModelId = "modelId";
options.StopSequences = stopSequences;
Expand Down Expand Up @@ -197,6 +208,9 @@ public void JsonSerialization_Roundtrips()
Assert.Equal(0.4f, deserialized.FrequencyPenalty);
Assert.Equal(0.5f, deserialized.PresencePenalty);
Assert.Equal(12345, deserialized.Seed);
Assert.NotNull(deserialized.Reasoning);
Assert.Equal(ReasoningEffort.High, deserialized.Reasoning.Effort);
Assert.Equal(ReasoningOutput.Detailed, deserialized.Reasoning.Output);
Comment thread
stephentoub marked this conversation as resolved.
Outdated
Assert.IsType<ChatResponseFormatJson>(deserialized.ResponseFormat);
Assert.Equal("modelId", deserialized.ModelId);
Assert.NotSame(stopSequences, deserialized.StopSequences);
Expand Down
Loading