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:

NamespaceClient classPurpose
OpenAI.ChatChatClientChat completions, tool calls, structured outputs, audio in/out
OpenAI.ResponsesResponsesClientThe newer, stateful Responses API with reasoning and built-in tools
OpenAI.EmbeddingsEmbeddingClientText embeddings for search and RAG
OpenAI.ImagesImageClientImage generation and edits (DALL-E 3, GPT image models)
OpenAI.AudioAudioClientTranscription, translation and text-to-speech
OpenAI.RealtimeRealtimeClientLow-latency streaming voice and text over a persistent session
OpenAI.VectorStoresVectorStoreClientVector stores used by the Responses API's file search tool
OpenAI.AssistantsAssistantClientThe 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:

Bash
dotnet add package OpenAI

A minimal chat completion needs only a model name and a prompt:

C#
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.

Chat Completions: Messages, Streaming and History#

Multi-turn conversations use a List<ChatMessage> built from SystemChatMessage, UserChatMessage, AssistantChatMessage and ToolChatMessage. Watch the namespace: this ChatMessage lives in OpenAI.Chat, and Microsoft.Extensions.AI defines its own ChatMessage in a different namespace, so importing both in the same file causes an ambiguous-reference error you must resolve with a using alias.

Streaming avoids making users wait for a full response before seeing anything. CompleteChatStreamingAsync returns an AsyncCollectionResult<StreamingChatCompletionUpdate> you consume with await foreach:

C#
List<ChatMessage> history =
[
    new SystemChatMessage("You are a terse .NET performance advisor."),
    new UserChatMessage("Why does string concatenation in a loop hurt performance?"),
];

await foreach (StreamingChatCompletionUpdate update in client.CompleteChatStreamingAsync(history))
{
    if (update.ContentUpdate.Count > 0)
    {
        Console.Write(update.ContentUpdate[0].Text);
    }
}

To continue the conversation, append the assistant's reply as a new AssistantChatMessage built from the completed ChatCompletion, then add the user's next message and call the client again. The library does not track history for you between calls; you own the list.

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:

C#
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:

C#
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.

C#
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.

Embeddings, Images and Audio#

EmbeddingClient generates vectors for semantic search; GenerateEmbedding handles one input and GenerateEmbeddings batches many in a single request. Both accept an optional Dimensions setting for models that support shortened vectors:

C#
using OpenAI.Embeddings;

EmbeddingClient embeddingClient = new("text-embedding-3-small", apiKey);
EmbeddingGenerationOptions embedOptions = new() { Dimensions = 512 };

OpenAIEmbeddingCollection batch = await embeddingClient.GenerateEmbeddingsAsync(
    ["reset a forgotten password", "rotate an API key"], embedOptions);

foreach (OpenAIEmbedding e in batch)
{
    ReadOnlyMemory<float> vector = e.ToFloats();
}

See Embeddings and Vector Databases in .NET for storing and querying these vectors, and how they feed a RAG pipeline.

ImageClient.GenerateImage creates images from a prompt. Newer GPT image models add options such as Background and ModerationLevel alongside the classic DALL-E 3 Quality, Size and Style settings, all on the same ImageGenerationOptions type; GenerateImageEdit edits an existing image, optionally with a mask:

C#
using OpenAI.Images;

ImageClient imageClient = new("gpt-image-1", apiKey);
ImageGenerationOptions imageOptions = new()
{
    Quality = GeneratedImageQuality.High,
    Background = GeneratedImageBackground.Transparent,
    ResponseFormat = GeneratedImageFormat.Bytes,
};

GeneratedImage logo = await imageClient.GenerateImageAsync(
    "A minimalist line-art icon of a package being delivered, on a transparent background.",
    imageOptions);

AudioClient covers speech in both directions. TranscribeAudio runs Whisper-family speech-to-text (with a diarized variant, TranscribeAudioDiarized, that labels who spoke each segment), TranslateAudio transcribes non-English audio directly into English text, and GenerateSpeech turns text into an audio file with a chosen voice. Multimodal AI in .NET: Vision, Audio and Speech goes deeper on when to reach for each one.

C#
using OpenAI.Audio;

AudioClient audioClient = new("gpt-4o-transcribe", apiKey);
AudioTranscription transcript = await audioClient.TranscribeAudioAsync("support-call.mp3");
Console.WriteLine(transcript.Text);

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:

C#
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:

C#
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:

C#
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 OpenAIClient when 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-secrets in 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 IChatClient at 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 ChatMessage types. OpenAI.Chat.ChatMessage and Microsoft.Extensions.AI's ChatMessage are different types with the same name; mixing both using directives in one file causes ambiguous-reference errors.
  • Ignoring FinishReason values other than Stop and ToolCalls. Length means the output was truncated and ContentFilter means content was withheld; silently printing Content[0].Text in either case shows an incomplete or empty answer.
  • Resending full history to the Responses API. Once you have a response Id, pass it as PreviousResponseId instead of resending every prior turn, or you pay for the same input tokens repeatedly.
  • Treating Chat Completions and Responses API items as interchangeable. ChatMessage and ResponseItem are 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#

CriterionOpenAI library (OpenAI)Microsoft.Extensions.AIAzure.AI.OpenAI
Primary purposeFull, typed access to the OpenAI REST APIProvider-neutral chat and embedding abstractionsAzure-specific client built on top of the OpenAI library
Newest OpenAI featuresAvailable immediatelyUsually available, or via raw optionsDepends on the underlying OpenAI library version
Works with other providersNoYes, by designNo
Azure OpenAI supportYes, via the /openai/v1/ endpointYes, through this library's AsIChatClientYes, natively
Middleware, caching, DI helpersManualBuilt in (ChatClientBuilder)Manual
Best forOpenAI-specific features: Responses, images, audio, realtimeApplication code that should stay portableTeams 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 OpenAI NuGet package is the official, fully typed .NET client for the OpenAI REST API, organized into one client class per feature area.
  • ChatClient covers chat completions, streaming, tool calls, structured outputs and audio in/out; ResponsesClient adds server-side state, reasoning controls and built-in file and web search tools.
  • EmbeddingClient, ImageClient and AudioClient round out embeddings, image generation and edits, transcription, translation and text-to-speech.
  • Built-in retries cover transient failures; catch ClientResultException for the rest, and always pass a CancellationToken.
  • Bridge to IChatClient with AsIChatClient() when code needs to stay provider-neutral, and call Azure OpenAI with the same library through the /openai/v1/ endpoint.

Further Reading#