Skip to content

Commit 4667fec

Browse files
committed
Add support for ReasoningEffort.None for latest GPT
This is the default mode for gpt-5.2 as documented at https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort, but can also be set for other models.
1 parent 4ce4cc0 commit 4667fec

File tree

5 files changed

+66
-7
lines changed

5 files changed

+66
-7
lines changed

src/Extensions/OpenAI/AzureOpenAIChatClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ public AzureOpenAIChatClient(Uri endpoint, ApiKeyCredential credential, string m
4545

4646
/// <inheritdoc/>
4747
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellation = default)
48-
=> GetChatClient(options?.ModelId ?? modelId).GetResponseAsync(messages, options.SetResponseOptions(), cancellation);
48+
=> GetChatClient(options?.ModelId ?? modelId).GetResponseAsync(messages, options.ApplyExtensions(), cancellation);
4949

5050
/// <inheritdoc/>
5151
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellation = default)
52-
=> GetChatClient(options?.ModelId ?? modelId).GetStreamingResponseAsync(messages, options.SetResponseOptions(), cancellation);
52+
=> GetChatClient(options?.ModelId ?? modelId).GetStreamingResponseAsync(messages, options.ApplyExtensions(), cancellation);
5353

5454
IChatClient GetChatClient(string modelId) => clients.GetOrAdd(modelId, model
5555
=> new PipelineClient(pipeline, endpoint, options).GetOpenAIResponseClient(modelId).AsIChatClient());

src/Extensions/OpenAI/OpenAIChatClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ public OpenAIChatClient(string apiKey, string modelId, OpenAIClientOptions? opti
3737

3838
/// <inheritdoc/>
3939
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellation = default)
40-
=> GetChatClient(options?.ModelId ?? modelId).GetResponseAsync(messages, options.SetResponseOptions(), cancellation);
40+
=> GetChatClient(options?.ModelId ?? modelId).GetResponseAsync(messages, options.ApplyExtensions(), cancellation);
4141

4242
/// <inheritdoc/>
4343
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellation = default)
44-
=> GetChatClient(options?.ModelId ?? modelId).GetStreamingResponseAsync(messages, options.SetResponseOptions(), cancellation);
44+
=> GetChatClient(options?.ModelId ?? modelId).GetStreamingResponseAsync(messages, options.ApplyExtensions(), cancellation);
4545

4646
IChatClient GetChatClient(string modelId) => clients.GetOrAdd(modelId, model
4747
=> new PipelineClient(pipeline, options).GetOpenAIResponseClient(modelId).AsIChatClient());

src/Extensions/OpenAI/OpenAIExtensions.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,23 @@
55

66
namespace Devlooped.Extensions.AI.OpenAI;
77

8-
static class OpenAIExtensions
8+
/// <summary>
9+
/// Allows applying extension properties to the <see cref="ChatOptions"/> when using
10+
/// them with an OpenAI client.
11+
/// </summary>
12+
public static class OpenAIExtensions
913
{
10-
public static ChatOptions? SetResponseOptions(this ChatOptions? options)
14+
/// <summary>
15+
/// Applies the extension properties to the <paramref name="options"/> so that
16+
/// the underlying OpenAI client can properly forward them to the endpoint.
17+
/// </summary>
18+
/// <remarks>
19+
/// Only use this if you are not using <see cref="OpenAIChatClient"/>, which already applies
20+
/// extensions before sending requests.
21+
/// </remarks>
22+
/// <returns>An options with the right <see cref="ChatOptions.RawRepresentationFactory"/> replaced
23+
/// so it can forward extensions to the underlying OpenAI API.</returns>
24+
public static ChatOptions? ApplyExtensions(this ChatOptions? options)
1125
{
1226
if (options is null)
1327
return null;

src/Extensions/ReasoningEffort.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
/// </summary>
66
public enum ReasoningEffort
77
{
8+
/// <summary>
9+
/// Lowest latency by indicating no reasoning tokens should be spent at all. Support depends on the model.
10+
/// </summary>
11+
/// <seealso href="https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort"/>
12+
None,
813
/// <summary>
914
/// Minimal reasoning effort, which may result in faster responses. Support depends on the model.
1015
/// </summary>

src/Tests/OpenAITests.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public async Task GPT5_ThinksFast()
121121
}
122122

123123
[SecretsTheory("OPENAI_API_KEY")]
124+
[InlineData(ReasoningEffort.None)]
124125
[InlineData(ReasoningEffort.Minimal)]
125126
[InlineData(ReasoningEffort.Low)]
126127
[InlineData(ReasoningEffort.Medium)]
@@ -135,7 +136,7 @@ public async Task GPT5_ThinkingTime(ReasoningEffort effort)
135136

136137
var requests = new List<JsonNode>();
137138

138-
var chat = new OpenAIChatClient(Configuration["OPENAI_API_KEY"]!, "gpt-5-nano",
139+
var chat = new OpenAIChatClient(Configuration["OPENAI_API_KEY"]!, "gpt-5.2",
139140
OpenAIClientOptions.Observable(requests.Add).WriteTo(output));
140141

141142
var options = new ChatOptions
@@ -166,6 +167,45 @@ public async Task GPT5_ThinkingTime(ReasoningEffort effort)
166167
output.WriteLine($"Effort: {effort}, Time: {watch.ElapsedMilliseconds}ms, Tokens: {response.Usage?.TotalTokenCount}");
167168
}
168169

170+
[SecretsFact("OPENAI_API_KEY")]
171+
public async Task GPT5_NoReasoningTokens()
172+
{
173+
var requests = new List<JsonNode>();
174+
175+
//var chat = new OpenAIChatClient(Configuration["OPENAI_API_KEY"]!, "gpt-4o",
176+
// OpenAIClientOptions.Observable(requests.Add).WriteTo(output));
177+
178+
var chat = new OpenAIClient(new ApiKeyCredential(Configuration["OPENAI_API_KEY"]!),
179+
OpenAIClientOptions.Observable(requests.Add).WriteTo(output))
180+
.GetOpenAIResponseClient("gpt-4o")
181+
.AsIChatClient();
182+
183+
var reasoned = await chat.GetResponseAsync(
184+
"How much gold would it take to coat the Statue of Liberty in a 1mm layer?",
185+
new ChatOptions
186+
{
187+
ModelId = "gpt-5.1",
188+
ReasoningEffort = ReasoningEffort.Low
189+
}.ApplyExtensions());
190+
191+
Assert.StartsWith("gpt-5.1", reasoned.ModelId);
192+
Assert.NotNull(reasoned.Usage?.AdditionalCounts);
193+
Assert.True(reasoned.Usage.AdditionalCounts.ContainsKey("OutputTokenDetails.ReasoningTokenCount"));
194+
Assert.True(reasoned.Usage.AdditionalCounts["OutputTokenDetails.ReasoningTokenCount"] > 0);
195+
196+
var nonreasoned = await chat.GetResponseAsync(
197+
"How much gold would it take to coat the Statue of Liberty in a 1mm layer?",
198+
new ChatOptions
199+
{
200+
ModelId = "gpt-5.1",
201+
ReasoningEffort = ReasoningEffort.None
202+
}.ApplyExtensions());
203+
204+
Assert.NotNull(nonreasoned.Usage?.AdditionalCounts);
205+
Assert.True(nonreasoned.Usage.AdditionalCounts.ContainsKey("OutputTokenDetails.ReasoningTokenCount"));
206+
Assert.True(nonreasoned.Usage.AdditionalCounts["OutputTokenDetails.ReasoningTokenCount"] == 0);
207+
}
208+
169209
[SecretsTheory("OPENAI_API_KEY")]
170210
[InlineData(Verbosity.Low)]
171211
[InlineData(Verbosity.Medium)]

0 commit comments

Comments
 (0)