The OpenAI .NET library is the official, fully typed client for the OpenAI REST API, published as the OpenAI NuGet package and built by OpenAI in collaboration with Microsoft. It gives C# developers direct access to every OpenAI capability, including chat completions, the Responses API, embeddings, images, audio and realtime, without waiting for a higher-level abstraction to add support for a new parameter or model. This guide is for .NET developers who call OpenAI directly: how to stream responses, call tools, request structured JSON, use the Responses API's reasoning and built-in tools, generate embeddings, images and audio, handle errors and retries, and combine the library with Microsoft.Extensions.AI and Azure OpenAI.
What Is the OpenAI .NET Library?#
The OpenAI package is generated from OpenAI's own OpenAPI specification, so it tracks the REST API closely and usually gains new endpoints within days of their announcement. As of this writing the stable release is 2.14.0, and the library targets .NET Standard 2.0, so it runs on .NET 8, .NET 9, .NET 10 and even .NET Framework, though newer language features appear in the samples throughout this guide.
It is organized into namespaces by feature area, each with its own client class:
| Namespace | Client class | Purpose |
|---|---|---|
OpenAI.Chat | ChatClient | Chat completions, tool calls, structured outputs, audio in/out |
OpenAI.Responses | ResponsesClient | The newer, stateful Responses API with reasoning and built-in tools |
OpenAI.Embeddings | EmbeddingClient | Text embeddings for search and RAG |
OpenAI.Images | ImageClient | Image generation and edits (DALL-E 3, GPT image models) |
OpenAI.Audio | AudioClient | Transcription, translation and text-to-speech |
OpenAI.Realtime | RealtimeClient | Low-latency streaming voice and text over a persistent session |
OpenAI.VectorStores | VectorStoreClient | Vector stores used by the Responses API's file search tool |
OpenAI.Assistants | AssistantClient | The older, stateful Assistants API (see the FAQ below) |
This library is deliberately different from two other packages you will see in .NET AI code. Microsoft.Extensions.AI, covered in Microsoft.Extensions.AI: Unified AI Abstractions for .NET, defines provider-neutral interfaces such as IChatClient that work with OpenAI, Azure OpenAI, Ollama and others behind one API. Azure.AI.OpenAI is Azure's own client, built as a thin extension of this very package for Azure-specific concerns. You will often use the OpenAI library directly for OpenAI-specific features, and wrap it in IChatClient for the parts of your application that should stay provider-agnostic.
How the Library Is Organized#
Every typed client, such as ChatClient or AudioClient, can be constructed directly with a model name and credential, or obtained from a shared OpenAIClient instance through methods such as GetChatClient(model) and GetAudioClient(model). Using OpenAIClient is preferable when a service needs several clients, because they then share the same HTTP pipeline, connection pool and options. All clients are thread-safe and designed to be registered as singletons.
Two further details shape how you use the library day to day. First, almost every call comes in a synchronous and an Async form (CompleteChat and CompleteChatAsync); always prefer the async form in server and UI code. Second, alongside the strongly typed "convenience" methods, every client also exposes protocol methods that accept BinaryContent and return BinaryData directly, bypassing the typed models entirely. Protocol methods are your escape hatch for a request or response field the typed model does not yet expose, without blocking on a new package version.
Under the hood, the library is built on System.ClientModel, the same low-level pipeline used by modern Azure SDKs. OpenAIClientOptions derives from that library's ClientPipelineOptions, which is why it can configure a custom Endpoint, a NetworkTimeout and the underlying ClientPipeline transport, topics covered in the error-handling section below.
Getting Started#
Install the package and set your API key as an environment variable rather than a literal string in source:
dotnet add package OpenAIA minimal chat completion needs only a model name and a prompt:
using OpenAI.Chat;
ChatClient client = new(
model: "gpt-5.1",
apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY"));
ChatCompletion completion = await client.CompleteChatAsync(
"Explain the difference between a struct and a class in one sentence.");
Console.WriteLine(completion.Content[0].Text);ChatCompletion.Content is a list because a response can mix content types, such as text and output audio, so production code should read Content[0].Text (or iterate the list) rather than assume a single plain-text part.
Tool Calls with ChatTool#
ChatTool.CreateFunctionTool describes a function with a name, description and JSON Schema for its parameters. You add it to ChatCompletionOptions.Tools, inspect ChatCompletion.FinishReason for ChatFinishReason.ToolCalls, run the matching local function yourself, and send the result back as a ToolChatMessage correlated by the call's Id:
ChatTool lookupStockTool = ChatTool.CreateFunctionTool(
functionName: "LookupStockLevel",
functionDescription: "Gets the current stock level for a warehouse SKU.",
functionParameters: BinaryData.FromBytes("""
{
"type": "object",
"properties": {
"sku": { "type": "string", "description": "The warehouse SKU, e.g. WH-4471." }
},
"required": ["sku"]
}
"""u8.ToArray()));
ChatCompletionOptions options = new() { Tools = { lookupStockTool } };
List<ChatMessage> messages = [new UserChatMessage("How many units of WH-4471 are left?")];
ChatCompletion completion = await client.CompleteChatAsync(messages, options);
if (completion.FinishReason == ChatFinishReason.ToolCalls)
{
messages.Add(new AssistantChatMessage(completion));
foreach (ChatToolCall call in completion.ToolCalls)
{
int units = LookupStockLevel(call.FunctionArguments); // parse JSON, then call your service
messages.Add(new ToolChatMessage(call.Id, units.ToString()));
}
completion = await client.CompleteChatAsync(messages, options);
}This is the raw, OpenAI-specific shape of tool calling. For most application code, prefer the portable version in Function Calling and Tool Use with LLMs in C#: AIFunctionFactory.Create generates the schema from an ordinary C# method, and FunctionInvokingChatClient runs the request loop for you. Reach for ChatTool directly when you need OpenAI-specific request fields the abstraction does not expose yet.
Structured Outputs with ChatResponseFormat#
To constrain a chat completion to a JSON Schema, set ChatCompletionOptions.ResponseFormat with ChatResponseFormat.CreateJsonSchemaFormat. With jsonSchemaIsStrict: true, the service rejects schema violations at generation time instead of leaving you to catch them after the fact:
ChatCompletionOptions options = new()
{
ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
jsonSchemaFormatName: "shipment_details",
jsonSchema: BinaryData.FromBytes("""
{
"type": "object",
"properties": {
"trackingNumber": { "type": "string" },
"carrier": { "type": "string", "enum": ["ups", "fedex", "usps", "dhl"] },
"estimatedDeliveryDate": { "type": "string" }
},
"required": ["trackingNumber", "carrier", "estimatedDeliveryDate"],
"additionalProperties": false
}
"""u8.ToArray()),
jsonSchemaIsStrict: true),
};
ChatCompletion completion = await client.CompleteChatAsync(
[new UserChatMessage($"Extract the shipment details from this email:\n{emailBody}")], options);
ShipmentDetails details = JsonSerializer.Deserialize<ShipmentDetails>(completion.Content[0].Text)!;This is the low-level mechanism. If you want a strongly typed result without hand-writing the schema, and a portable API across providers, see Structured Outputs: Reliable JSON from LLMs in C#, which covers Microsoft.Extensions.AI's GetResponseAsync<T>.
The Responses API: Reasoning, State and Built-in Tools#
The Responses API is OpenAI's newer, recommended entry point for new applications. ResponsesClient takes a list of ResponseItem inputs instead of ChatMessage, and it can hold conversation state on the server: set PreviousResponseId to the prior response's Id and send only the new turn, instead of resending the whole transcript on every call.
using OpenAI.Responses;
ResponsesClient responses = new(Environment.GetEnvironmentVariable("OPENAI_API_KEY"));
CreateResponseOptions options = new()
{
Model = "gpt-5.1",
Instructions = "You are a concise .NET architecture advisor.",
ReasoningOptions = new ResponseReasoningOptions
{
ReasoningEffortLevel = ResponseReasoningEffortLevel.Medium,
},
};
options.InputItems.Add(ResponseItem.CreateUserMessageItem(
"Should a new order-processing service use CQRS from day one?"));
ResponseResult first = await responses.CreateResponseAsync(options);
Console.WriteLine(first.GetOutputText());
// Continue the conversation without resending history.
CreateResponseOptions followUp = new() { Model = "gpt-5.1", PreviousResponseId = first.Id };
followUp.InputItems.Add(ResponseItem.CreateUserMessageItem("What if the team is only two engineers?"));
ResponseResult second = await responses.CreateResponseAsync(followUp);
Console.WriteLine(second.GetOutputText());Set StreamingEnabled = true and call CreateResponseStreamingAsync to get an IAsyncEnumerable<StreamingResponseUpdate>; watch for StreamingResponseOutputTextDeltaUpdate for incremental text and StreamingResponseOutputItemAddedUpdate to observe reasoning and tool items as they start. The Responses API also ships built-in, hosted tools you would otherwise build yourself: ResponseTool.CreateFileSearchTool(vectorStoreIds) searches a vector store you uploaded documents to, and ResponseTool.CreateWebSearchTool() lets the model search the live web, both without you writing retrieval code. For retrieval over your own systems with full control, see Retrieval-Augmented Generation (RAG) in .NET.
Error Handling, Retries and Timeouts#
By default, the library automatically retries 408, 429, 500, 502, 503 and 504 responses up to three additional times with exponential backoff, so most transient failures never reach your code. Everything else, including a 429 that exhausts its retries, surfaces as a ClientResultException, whose Status property carries the HTTP status code:
try
{
ChatCompletion completion = await client.CompleteChatAsync(messages, options, cancellationToken);
}
catch (ClientResultException ex) when (ex.Status == 429)
{
logger.LogWarning("Rate limited by OpenAI after retries; backing off further.");
throw;
}Every async method accepts a CancellationToken; always flow the token from the incoming HTTP request or UI action so an abandoned call stops consuming quota. To change the per-request timeout or replace the transport (for example, to point at a proxy), configure OpenAIClientOptions, which exposes NetworkTimeout and, because it derives from System.ClientModel's pipeline options, can also swap in a custom retry policy: new OpenAIClientOptions { NetworkTimeout = TimeSpan.FromSeconds(20) }, passed to the client constructor alongside your credential.
Using the Library with Microsoft.Extensions.AI#
Microsoft.Extensions.AI.OpenAI adds AsIChatClient() and AsIEmbeddingGenerator() extension methods on ChatClient and EmbeddingClient, bridging them into the provider-neutral IChatClient and IEmbeddingGenerator interfaces:
using Microsoft.Extensions.AI;
IChatClient chat = new ChatClient("gpt-5.1", apiKey).AsIChatClient();Do this whenever a component should stay swappable between OpenAI, Azure OpenAI or another provider, or whenever you want the middleware pipeline described in Microsoft.Extensions.AI: Unified AI Abstractions for .NET: caching, OpenTelemetry, automatic tool-call loops and dependency injection all come for free once you are behind IChatClient. ResponsesClient has an equivalent adapter, but it is marked experimental (diagnostic OPENAI001) while the Responses surface in this library itself is still stabilizing.
Calling Azure OpenAI with the Same Library#
You do not need a separate package to call Azure OpenAI in Microsoft Foundry. Point any typed client at your resource's unified /openai/v1/ endpoint, and authenticate with Microsoft Entra ID instead of an API key by passing a BearerTokenPolicy as the client's authentication policy:
using Azure.Identity;
using OpenAI.Chat;
var endpoint = new Uri($"{azureOpenAiEndpoint}/openai/v1/");
var authPolicy = new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default");
ChatClient azureClient = new(
model: "gpt-5-mini", // the Azure deployment name
authenticationPolicy: authPolicy,
options: new OpenAIClientOptions { Endpoint = endpoint });Everything else in this guide, streaming, tools, structured outputs and the Responses API, works unchanged. For the wider Azure picture, deployments, quotas, content filters and provisioned throughput, see Azure OpenAI and Microsoft Foundry for .NET Developers.
Best Practices#
- Register clients as singletons. They are thread-safe and reuse HTTP connections; use
OpenAIClientwhen a service needs several typed clients. - Always use the async overloads with a
CancellationToken. Long completions and streaming responses should stop when the caller disconnects. - Suppress
[Experimental]diagnostics narrowly, in the file that uses the feature, so you notice when an API graduates and the suppression becomes unnecessary. - Keep API keys out of source control. Use environment variables,
dotnet user-secretsin development, and a managed identity or Key Vault in production; prefer Entra ID entirely when calling Azure OpenAI. - Reach for protocol methods, not a library upgrade, for brand-new fields. They keep you moving without a hard dependency on a same-day package release.
- Wrap the library in
IChatClientat the composition root when a feature should not be hard-coded to OpenAI. - Validate tool arguments and structured output before acting on them. A model can still produce a well-formed value that is wrong.
Common Pitfalls#
- Colliding
ChatMessagetypes.OpenAI.Chat.ChatMessageand Microsoft.Extensions.AI'sChatMessageare different types with the same name; mixing bothusingdirectives in one file causes ambiguous-reference errors. - Ignoring
FinishReasonvalues other thanStopandToolCalls.Lengthmeans the output was truncated andContentFiltermeans content was withheld; silently printingContent[0].Textin either case shows an incomplete or empty answer. - Resending full history to the Responses API. Once you have a response
Id, pass it asPreviousResponseIdinstead of resending every prior turn, or you pay for the same input tokens repeatedly. - Treating Chat Completions and Responses API items as interchangeable.
ChatMessageandResponseItemare different type hierarchies; code that builds one does not compile against the other. - Assuming the built-in retry policy is enough for bulk jobs. Three retries help with occasional blips, but a large batch-embedding job still needs its own rate-limit-aware throttling.
- Building new features on the Assistants API. OpenAI has signaled that the Responses API, combined with hosted tools and server-side conversation state, is the long-term direction; see the FAQ below.
OpenAI .NET Library vs Microsoft.Extensions.AI vs Azure.AI.OpenAI#
| Criterion | OpenAI library (OpenAI) | Microsoft.Extensions.AI | Azure.AI.OpenAI |
|---|---|---|---|
| Primary purpose | Full, typed access to the OpenAI REST API | Provider-neutral chat and embedding abstractions | Azure-specific client built on top of the OpenAI library |
| Newest OpenAI features | Available immediately | Usually available, or via raw options | Depends on the underlying OpenAI library version |
| Works with other providers | No | Yes, by design | No |
| Azure OpenAI support | Yes, via the /openai/v1/ endpoint | Yes, through this library's AsIChatClient | Yes, natively |
| Middleware, caching, DI helpers | Manual | Built in (ChatClientBuilder) | Manual |
| Best for | OpenAI-specific features: Responses, images, audio, realtime | Application code that should stay portable | Teams standardized on Azure-native SDKs |
For the full ecosystem map, including Semantic Kernel and Microsoft Agent Framework, see AI in .NET: The Complete Landscape for Developers.
Frequently Asked Questions#
Is the OpenAI .NET library the same as Microsoft.Extensions.AI?#
No. The OpenAI package is a full, OpenAI-specific client generated from OpenAI's API specification. Microsoft.Extensions.AI is a thin, provider-neutral abstraction that this library plugs into through AsIChatClient(). Use the OpenAI library when you need an OpenAI-specific feature, and the abstraction when a component should work with any provider.
Can I use this library with Azure OpenAI?#
Yes. Point OpenAIClientOptions.Endpoint at your Azure resource's /openai/v1/ endpoint and authenticate with an Entra ID BearerTokenPolicy or an Azure API key. The rest of the API, chat, streaming, tools, structured outputs and the Responses API, behaves the same as it does against OpenAI directly.
Should new projects use Chat Completions or the Responses API?#
For new OpenAI-only projects, prefer the Responses API: it supports server-side conversation state through PreviousResponseId, built-in file and web search tools, and the latest reasoning controls. Chat Completions remains fully supported and is often the simpler choice when your code also needs to run unchanged against other Chat Completions-compatible endpoints.
Does the library retry failed requests automatically?#
Yes. 408, 429 and 5xx responses are retried up to three additional times with exponential backoff by default. You can still hit rate limits under sustained load, so high-volume jobs such as bulk embedding should add their own throttling on top of the built-in retries.
Is the Assistants API still worth building on?#
Treat it as a stable but legacy option. OpenAI has directed new development toward the Responses API, which folds in persistent, server-side conversations and the same hosted tools (file search, web search) that made Assistants useful. Existing Assistants-based code keeps working, but new features should start on the Responses API.
Summary#
- The
OpenAINuGet package is the official, fully typed .NET client for the OpenAI REST API, organized into one client class per feature area. ChatClientcovers chat completions, streaming, tool calls, structured outputs and audio in/out;ResponsesClientadds server-side state, reasoning controls and built-in file and web search tools.EmbeddingClient,ImageClientandAudioClientround out embeddings, image generation and edits, transcription, translation and text-to-speech.- Built-in retries cover transient failures; catch
ClientResultExceptionfor the rest, and always pass aCancellationToken. - Bridge to
IChatClientwithAsIChatClient()when code needs to stay provider-neutral, and call Azure OpenAI with the same library through the/openai/v1/endpoint.