Extensions for Microsoft.Extensions.AI
This package adds configuration-driven client registration, provider resolution, tool helpers, OpenAI-specific extensions, and observability helpers for Microsoft.Extensions.AI.
It is split into two packages:
| Package | Purpose |
|---|---|
Devlooped.Extensions.AI |
Configuration, providers, chat helpers, tools, OpenAI extras, and pipeline observability |
Devlooped.Extensions.AI.Console |
Rich JSON console logging for chat and HTTP pipeline messages |
Register clients from configuration with AddAIClients:
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.AddAIClients();
var app = builder.Build();
var chat = app.Services.GetChatClient("AI:Clients:Grok");The default configuration prefix is ai:clients. A minimal configuration section looks like this:
{
"AI": {
"Clients": {
"Grok": {
"provider": "xai",
"apikey": "xai-...",
"modelid": "grok-4-fast"
}
}
}
}Useful rules:
| Config shape | Registration |
|---|---|
section has apikey |
keyed IClientFactory |
section has modelid |
keyed IChatClient |
section has id |
adds an extra lookup key |
section has lifetime |
controls the chat client lifetime |
IChatClient and IClientFactory registrations are keyed by their full configuration section path. An optional id adds a secondary lookup key without replacing the section-path key. IChatClient registrations are reloadable. IClientFactory registrations stay valid across configuration changes and create fresh clients each time you call CreateChatClient, CreateSpeechToTextClient, or CreateTextToSpeechClient.
Built-in provider support:
| Provider | Name | Chat | Speech-to-text | Text-to-speech | Match |
|---|---|---|---|---|---|
| OpenAI | openai |
yes | yes | yes | explicit provider, or https://api.openai.com/ |
| Azure OpenAI | azure.openai |
yes | yes | yes | explicit provider, or *.openai.azure.com |
| Azure AI Inference | azure.inference |
yes | no | no | explicit provider, or https://ai.azure.com/ |
| xAI / Grok | xai |
yes | yes | yes | explicit provider, or https://api.x.ai/ |
When provider is omitted, endpoint-based matching is used. If no endpoint is provided at all, OpenAI is the default provider.
You can also register your own provider:
builder.Services.AddAIClientProvider<MyClientProvider>();
// or
builder.Services.AddAIClientProvider(sp => new MyClientProvider(sp));Use useDefaultProviders: false if you want only your own providers:
builder.AddAIClients(useDefaultProviders: false);Section-bound clients expose the provider options they were created with. Most callers can request the provider options type directly, for example:
var options = chat.GetService<OpenAIClientOptions>();For keyed lookup, use GetChatClient, GetSpeechToTextClient, and GetTextToSpeechClient.
Use the Configure*ClientDefaults methods to apply shared pipelines without touching each registration site.
builder
.ConfigureChatClientDefaults(b => b.UseLogging())
.ConfigureChatClientDefaults("AI:Clients:Grok", b => b.UseLogging())
.ConfigureSpeechToTextClientDefaults(b => b.UseLogging())
.ConfigureTextToSpeechClientDefaults(b => b.UseLogging())
.AddAIClients();Behavior:
| Rule | Meaning |
|---|---|
| Global defaults | apply to every client of that modality |
| Section-specific defaults | match the exact configuration section path, case-insensitively |
| Section paths | use : separators, not . |
| Order | registrations run in the order they were added |
Chat defaults survive reloads because they are applied outside the reloadable chat wrapper. Factory-created speech/chat clients get defaults applied on each Create* call.
Chat is a convenient IList<ChatMessage> implementation with factory helpers:
var messages = new Chat
{
Chat.System("You are a helpful assistant."),
Chat.User("What is 101 * 3?")
};
var options = new ChatOptions
{
EndUserId = "user-123"
};Chat also supports collection initializer syntax with string roles:
var chat = new Chat
{
{ "system", "You are concise." },
{ "user", "Say hello." }
};Chat.Developer(...) is also available for developer-role messages.
For source-generated serialization, use ChatJsonContext.DefaultOptions.
ToolFactory.Create turns a delegate into an AIFunction with safe, snake_case tool names:
static MyResult RunTool(string name, string description, string content) => new(name, description, content);
AIFunction tool = ToolFactory.Create(RunTool);ToolExtensions.FindCalls locates tool invocations and their results in ChatResponse or message histories:
var response = await client.GetResponseAsync(messages, options);
var call = response.FindCalls<MyResult>(tool).FirstOrDefault();
if (call is not null)
{
Console.WriteLine(call.Result);
}If you only need the raw call/result pair, use the untyped FindCalls overload and inspect Outcome.Exception.
ToolJsonOptions.Default provides the serializer settings used by the tool helpers.
The Devlooped.Extensions.AI.OpenAI namespace adds OpenAI-specific helpers on top of ChatOptions.
Verbosity is available as an extension property on ChatOptions:
using Devlooped.Extensions.AI.OpenAI;
var options = new ChatOptions
{
Verbosity = Verbosity.Low
};Verbosity is supported by GPT-5+ models. Setting it automatically configures the raw response factory, so do not set a custom RawRepresentationFactory yourself when using it.
If you want a bindable options type, use OpenAIChatOptions.
WebSearchTool wraps the OpenAI Responses API web search tool with typed location and domain controls:
var options = new ChatOptions
{
Tools =
[
new WebSearchTool("AR")
{
Region = "Bariloche",
TimeZone = "America/Argentina/Buenos_Aires",
AllowedDomains = ["catedralaltapatagonia.com"]
}
]
};Supported properties:
| Property | Meaning |
|---|---|
Country |
ISO alpha-2 country code |
Region |
Free-text region |
City |
Free-text city |
TimeZone |
IANA time zone |
AllowedDomains |
Domain allow-list for search results |
ClientPipelineExtensions adds low-level request/response observation for any ClientPipelineOptions-derived type:
var requests = new List<JsonNode>();
var responses = new List<JsonNode>();
var options = OpenAIClientOptions.Observable(requests.Add, responses.Add);Observe adds the pipeline policy to an existing options instance; Observable creates a configured instance in one call. Non-JSON payloads are ignored.
Install Devlooped.Extensions.AI.Console to get rich JSON console logging.
using Devlooped.Extensions.AI;
using Microsoft.Extensions.AI;
var chat = someChatClient
.AsBuilder()
.UseJsonConsoleLogging(new JsonConsoleOptions
{
InteractiveOnly = false,
TruncateLength = 200
})
.Build();var client = new OpenAIClient(
apiKey,
new OpenAIClientOptions().UseJsonConsoleLogging());JsonConsoleOptions lets you control:
| Option | Meaning |
|---|---|
Border / BorderStyle |
panel appearance |
IncludeAdditionalProperties |
include extra message/response data |
InteractiveConfirm |
ask before enabling logging in interactive consoles |
InteractiveOnly |
suppress output when the console is not interactive |
TruncateLength |
trim long text |
WrapLength |
wrap long text |
The default settings favor interactive development sessions and keep non-interactive output quiet.
To ensure the long-term sustainability of this project, users of this package who generate revenue must pay an Open Source Maintenance Fee. While the source code is freely available under the terms of the License, this package and other aspects of the project require adherence to the Maintenance Fee.
To pay the Maintenance Fee, become a Sponsor at the proper OSMF tier. A single fee covers all of Devlooped packages.