Skip to content

Commit c2d3dc0

Browse files
committed
1 parent 7dd7fc1 commit c2d3dc0

File tree

4 files changed

+96
-15
lines changed

4 files changed

+96
-15
lines changed

src/AI.Tests/OpenAITests.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public async Task GPT5_ThinkingTime(ReasoningEffort effort)
156156
Assert.StartsWith("gpt-5", response.ModelId);
157157
Assert.DoesNotContain("nano", response.ModelId);
158158

159-
// Reasoning should have been set to medium
159+
// Reasoning should have been set to expected value
160160
Assert.All(requests, x =>
161161
{
162162
var search = Assert.IsType<JsonObject>(x["reasoning"]);
@@ -166,6 +166,48 @@ public async Task GPT5_ThinkingTime(ReasoningEffort effort)
166166
output.WriteLine($"Effort: {effort}, Time: {watch.ElapsedMilliseconds}ms, Tokens: {response.Usage?.TotalTokenCount}");
167167
}
168168

169+
[SecretsTheory("OPENAI_API_KEY")]
170+
[InlineData(Verbosity.Low)]
171+
[InlineData(Verbosity.Medium)]
172+
[InlineData(Verbosity.High)]
173+
public async Task GPT5_Verbosity(Verbosity verbosity)
174+
{
175+
var messages = new Chat()
176+
{
177+
{ "system", "You are an intelligent AI assistant that's an expert on everything." },
178+
{ "user", "What's the answer to the universe and everything?" },
179+
};
180+
181+
var requests = new List<JsonNode>();
182+
183+
var chat = new OpenAIChatClient(Configuration["OPENAI_API_KEY"]!, "gpt-5-nano",
184+
OpenAIClientOptions.Observable(requests.Add).WriteTo(output));
185+
186+
var options = new ChatOptions
187+
{
188+
ModelId = "gpt-5-mini",
189+
Verbosity = verbosity
190+
};
191+
192+
var watch = System.Diagnostics.Stopwatch.StartNew();
193+
var response = await chat.GetResponseAsync(messages, options);
194+
watch.Stop();
195+
196+
var text = response.Text;
197+
output.WriteLine(text);
198+
199+
Assert.StartsWith("gpt-5", response.ModelId);
200+
Assert.DoesNotContain("nano", response.ModelId);
201+
202+
// Verbosity should have been set to the expected value
203+
Assert.All(requests, x =>
204+
{
205+
var text = Assert.IsType<JsonObject>(x["text"]);
206+
Assert.Equal(verbosity.ToString().ToLowerInvariant(), text["verbosity"]?.GetValue<string>());
207+
});
208+
209+
output.WriteLine($"Verbosity: {verbosity}, Time: {watch.ElapsedMilliseconds}ms, Tokens: {response.Usage?.TotalTokenCount}");
210+
}
169211

170212
[SecretsFact("OPENAI_API_KEY")]
171213
public async Task WebSearchCountryHighContext()

src/AI/ChatExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,25 @@ public ReasoningEffort? ReasoningEffort
4343
}
4444
}
4545
}
46+
47+
/// <summary>
48+
/// Sets the <see cref="Verbosity"/> level for a GPT-5 model when generating responses, if supported
49+
/// </summary>
50+
public Verbosity? Verbosity
51+
{
52+
get => options.AdditionalProperties?.TryGetValue("verbosity", out var value) == true && value is Verbosity verbosity ? verbosity : null;
53+
set
54+
{
55+
if (value is not null)
56+
{
57+
options.AdditionalProperties ??= [];
58+
options.AdditionalProperties["verbosity"] = value;
59+
}
60+
else
61+
{
62+
options.AdditionalProperties?.Remove("verbosity");
63+
}
64+
}
65+
}
4666
}
4767
}

src/AI/OpenAI/OpenAIChatClient.cs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,18 @@ IChatClient GetChatClient(string modelId) => clients.GetOrAdd(modelId, model
4747
if (options is null)
4848
return null;
4949

50-
if (options.ReasoningEffort is ReasoningEffort effort)
50+
if (options.ReasoningEffort.HasValue || options.Verbosity.HasValue)
5151
{
52-
options.RawRepresentationFactory = _ => new ResponseCreationOptions
52+
options.RawRepresentationFactory = _ =>
5353
{
54-
ReasoningOptions = new ReasoningEffortOptions(effort)
55-
//ReasoningOptions = new ResponseReasoningOptions()
56-
//{
57-
// ReasoningEffortLevel = effort switch
58-
// {
59-
// ReasoningEffort.High => ResponseReasoningEffortLevel.High,
60-
// ReasoningEffort.Medium => ResponseReasoningEffortLevel.Medium,
61-
// // TODO: not exposed yet in the OpenAI package
62-
// // ReasoningEffort.Minimal => ResponseReasoningEffortLevel.Minimal,
63-
// _ => ResponseReasoningEffortLevel.Low
64-
// },
65-
//}
54+
var creation = new ResponseCreationOptions();
55+
if (options.ReasoningEffort.HasValue)
56+
creation.ReasoningOptions = new ReasoningEffortOptions(options.ReasoningEffort!.Value);
57+
58+
if (options.Verbosity.HasValue)
59+
creation.TextOptions = new VerbosityOptions(options.Verbosity!.Value);
60+
61+
return creation;
6662
};
6763
}
6864

@@ -85,4 +81,14 @@ protected override void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWri
8581
base.JsonModelWriteCore(writer, options);
8682
}
8783
}
84+
85+
class VerbosityOptions(Verbosity verbosity) : ResponseTextOptions
86+
{
87+
protected override void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options)
88+
{
89+
writer.WritePropertyName("verbosity"u8);
90+
writer.WriteStringValue(verbosity.ToString().ToLowerInvariant());
91+
base.JsonModelWriteCore(writer, options);
92+
}
93+
}
8894
}

src/AI/Verbosity.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Devlooped.Extensions.AI;
2+
3+
/// <summary>
4+
/// Verbosity determines how many output tokens are generated for models that support it, such as GPT-5.
5+
/// Lowering the number of tokens reduces overall latency.
6+
/// </summary>
7+
/// <see cref="https://platform.openai.com/docs/guides/latest-model?utm_source=chatgpt.com#verbosity"/>
8+
public enum Verbosity
9+
{
10+
Low,
11+
Medium,
12+
High
13+
}

0 commit comments

Comments
 (0)