Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,11 @@ 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>
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,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Extensions.AI;

/// <summary>
/// Specifies the level of reasoning effort that should be applied when generating chat responses.
/// </summary>
/// <remarks>
/// This value suggests 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 or even between models from the same provider.
/// </remarks>
public enum ReasoningEffort
{
/// <summary>
/// No reasoning effort.
/// </summary>
None,

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

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

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

/// <summary>
/// Extra high reasoning effort. Maximum reasoning for the most demanding tasks.
/// </summary>
ExtraHigh,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

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 or model doesn't support reasoning or doesn't support the requested configuration of reasoning, these options may be ignored.
/// </para>
/// </remarks>
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>
internal ReasoningOptions Clone() => new()
{
Effort = Effort,
Output = Output,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

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>
public enum ReasoningOutput
{
/// <summary>
/// No reasoning output. Do not include reasoning content in the response.
/// </summary>
None,

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

/// <summary>
/// Full reasoning output. Include all reasoning content in the response.
/// </summary>
Full,
}
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,10 @@
"Member": "System.Func<Microsoft.Extensions.AI.IChatClient, object?>? Microsoft.Extensions.AI.ChatOptions.RawRepresentationFactory { get; set; }",
"Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.ReasoningOptions? Microsoft.Extensions.AI.ChatOptions.Reasoning { get; set; }",
"Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.ChatResponseFormat? Microsoft.Extensions.AI.ChatOptions.ResponseFormat { get; set; }",
"Stage": "Stable"
Expand Down Expand Up @@ -2111,6 +2115,90 @@
}
]
},
{
"Type": "enum Microsoft.Extensions.AI.ReasoningEffort",
"Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.ReasoningEffort.ReasoningEffort();",
"Stage": "Stable"
}
],
"Fields": [
{
"Member": "const Microsoft.Extensions.AI.ReasoningEffort Microsoft.Extensions.AI.ReasoningEffort.None",
"Stage": "Stable",
"Value": "0"
},
{
"Member": "const Microsoft.Extensions.AI.ReasoningEffort Microsoft.Extensions.AI.ReasoningEffort.Low",
"Stage": "Stable",
"Value": "1"
},
{
"Member": "const Microsoft.Extensions.AI.ReasoningEffort Microsoft.Extensions.AI.ReasoningEffort.Medium",
"Stage": "Stable",
"Value": "2"
},
{
"Member": "const Microsoft.Extensions.AI.ReasoningEffort Microsoft.Extensions.AI.ReasoningEffort.High",
"Stage": "Stable",
"Value": "3"
},
{
"Member": "const Microsoft.Extensions.AI.ReasoningEffort Microsoft.Extensions.AI.ReasoningEffort.ExtraHigh",
"Stage": "Stable",
"Value": "4"
}
]
},
{
"Type": "sealed class Microsoft.Extensions.AI.ReasoningOptions",
"Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.ReasoningOptions.ReasoningOptions();",
"Stage": "Stable"
}
],
"Properties": [
{
"Member": "Microsoft.Extensions.AI.ReasoningEffort? Microsoft.Extensions.AI.ReasoningOptions.Effort { get; set; }",
"Stage": "Stable"
},
{
"Member": "Microsoft.Extensions.AI.ReasoningOutput? Microsoft.Extensions.AI.ReasoningOptions.Output { get; set; }",
"Stage": "Stable"
}
]
},
{
"Type": "enum Microsoft.Extensions.AI.ReasoningOutput",
"Stage": "Stable",
"Methods": [
{
"Member": "Microsoft.Extensions.AI.ReasoningOutput.ReasoningOutput();",
"Stage": "Stable"
}
],
"Fields": [
{
"Member": "const Microsoft.Extensions.AI.ReasoningOutput Microsoft.Extensions.AI.ReasoningOutput.None",
"Stage": "Stable",
"Value": "0"
},
{
"Member": "const Microsoft.Extensions.AI.ReasoningOutput Microsoft.Extensions.AI.ReasoningOutput.Summary",
"Stage": "Stable",
"Value": "1"
},
{
"Member": "const Microsoft.Extensions.AI.ReasoningOutput Microsoft.Extensions.AI.ReasoningOutput.Full",
"Stage": "Stable",
"Value": "2"
}
]
},
{
"Type": "sealed class Microsoft.Extensions.AI.RequiredChatToolMode : Microsoft.Extensions.AI.ChatToolMode",
"Stage": "Stable",
Expand Down
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,16 @@ ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransf
_ => null
};

private static ChatReasoningEffortLevel? ToOpenAIChatReasoningEffortLevel(ReasoningEffort? effort) =>
effort switch
{
ReasoningEffort.Low => ChatReasoningEffortLevel.Low,
ReasoningEffort.Medium => ChatReasoningEffortLevel.Medium,
ReasoningEffort.High => ChatReasoningEffortLevel.High,
ReasoningEffort.ExtraHigh => ChatReasoningEffortLevel.High,
_ => (ChatReasoningEffortLevel?)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
_ => (ResponseReasoningEffortLevel?)null, // None or null - let OpenAI use its default
};

ResponseReasoningSummaryVerbosity? summary = reasoning.Output switch
{
ReasoningOutput.Summary => ResponseReasoningSummaryVerbosity.Concise,
ReasoningOutput.Full => ResponseReasoningSummaryVerbosity.Detailed,
_ => (ResponseReasoningSummaryVerbosity?)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
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.Full };
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.Full, deserialized.Reasoning.Output);
Assert.IsType<ChatResponseFormatJson>(deserialized.ResponseFormat);
Assert.Equal("modelId", deserialized.ModelId);
Assert.NotSame(stopSequences, deserialized.StopSequences);
Expand Down
Loading
Loading