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
44 changes: 43 additions & 1 deletion src/AI.Tests/OpenAITests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public async Task GPT5_ThinkingTime(ReasoningEffort effort)
Assert.StartsWith("gpt-5", response.ModelId);
Assert.DoesNotContain("nano", response.ModelId);

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

[SecretsTheory("OPENAI_API_KEY")]
[InlineData(Verbosity.Low)]
[InlineData(Verbosity.Medium)]
[InlineData(Verbosity.High)]
public async Task GPT5_Verbosity(Verbosity verbosity)
{
var messages = new Chat()
{
{ "system", "You are an intelligent AI assistant that's an expert on everything." },
{ "user", "What's the answer to the universe and everything?" },
};

var requests = new List<JsonNode>();

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

var options = new ChatOptions
{
ModelId = "gpt-5-mini",
Verbosity = verbosity
};

var watch = System.Diagnostics.Stopwatch.StartNew();
var response = await chat.GetResponseAsync(messages, options);
watch.Stop();

var text = response.Text;
output.WriteLine(text);

Assert.StartsWith("gpt-5", response.ModelId);
Assert.DoesNotContain("nano", response.ModelId);

// Verbosity should have been set to the expected value
Assert.All(requests, x =>
{
var text = Assert.IsType<JsonObject>(x["text"]);
Assert.Equal(verbosity.ToString().ToLowerInvariant(), text["verbosity"]?.GetValue<string>());
});

output.WriteLine($"Verbosity: {verbosity}, Time: {watch.ElapsedMilliseconds}ms, Tokens: {response.Usage?.TotalTokenCount}");
}

[SecretsFact("OPENAI_API_KEY")]
public async Task WebSearchCountryHighContext()
Expand Down
20 changes: 20 additions & 0 deletions src/AI/ChatExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,25 @@ public ReasoningEffort? ReasoningEffort
}
}
}

/// <summary>
/// Sets the <see cref="Verbosity"/> level for a GPT-5 model when generating responses, if supported
/// </summary>
public Verbosity? Verbosity
{
get => options.AdditionalProperties?.TryGetValue("verbosity", out var value) == true && value is Verbosity verbosity ? verbosity : null;
set
{
if (value is not null)
{
options.AdditionalProperties ??= [];
options.AdditionalProperties["verbosity"] = value;
}
else
{
options.AdditionalProperties?.Remove("verbosity");
}
}
}
}
}
34 changes: 20 additions & 14 deletions src/AI/OpenAI/OpenAIChatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,18 @@ IChatClient GetChatClient(string modelId) => clients.GetOrAdd(modelId, model
if (options is null)
return null;

if (options.ReasoningEffort is ReasoningEffort effort)
if (options.ReasoningEffort.HasValue || options.Verbosity.HasValue)
{
options.RawRepresentationFactory = _ => new ResponseCreationOptions
options.RawRepresentationFactory = _ =>
{
ReasoningOptions = new ReasoningEffortOptions(effort)
//ReasoningOptions = new ResponseReasoningOptions()
//{
// ReasoningEffortLevel = effort switch
// {
// ReasoningEffort.High => ResponseReasoningEffortLevel.High,
// ReasoningEffort.Medium => ResponseReasoningEffortLevel.Medium,
// // TODO: not exposed yet in the OpenAI package
// // ReasoningEffort.Minimal => ResponseReasoningEffortLevel.Minimal,
// _ => ResponseReasoningEffortLevel.Low
// },
//}
var creation = new ResponseCreationOptions();
if (options.ReasoningEffort.HasValue)
creation.ReasoningOptions = new ReasoningEffortOptions(options.ReasoningEffort!.Value);

if (options.Verbosity.HasValue)
creation.TextOptions = new VerbosityOptions(options.Verbosity!.Value);

return creation;
};
}

Expand All @@ -85,4 +81,14 @@ protected override void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWri
base.JsonModelWriteCore(writer, options);
}
}

class VerbosityOptions(Verbosity verbosity) : ResponseTextOptions
{
protected override void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options)
{
writer.WritePropertyName("verbosity"u8);
writer.WriteStringValue(verbosity.ToString().ToLowerInvariant());
base.JsonModelWriteCore(writer, options);
}
}
}
13 changes: 13 additions & 0 deletions src/AI/Verbosity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Devlooped.Extensions.AI;

/// <summary>
/// Verbosity determines how many output tokens are generated for models that support it, such as GPT-5.
/// Lowering the number of tokens reduces overall latency.
/// </summary>
/// <see href="https://platform.openai.com/docs/guides/latest-model?utm_source=chatgpt.com#verbosity"/>
public enum Verbosity
{
Low,
Medium,
High
}
Loading