OpenAI SDK in .NET — Chat, Streaming, and Function Calling
Use the official OpenAI .NET SDK to build AI features: chat completions, streaming responses, structured outputs with function calling, and token management.
OpenAI SDK in .NET
The official OpenAI NuGet package gives you typed access to the entire OpenAI API: chat completions, streaming, function calling, embeddings, and more.
Setup
dotnet add package OpenAI// Register in DI (Program.cs)
builder.Services.AddOpenAIClient(options =>
options.Credential = new ApiKeyCredential(
builder.Configuration["OpenAI:ApiKey"]!));Or construct directly:
var client = new OpenAIClient(new ApiKeyCredential(Environment.GetEnvironmentVariable("OPENAI_API_KEY")!));
var chat = client.GetChatClient("gpt-4o");Basic Chat Completion
public class ChatService(OpenAIClient openai)
{
private readonly ChatClient _chat = openai.GetChatClient("gpt-4o");
public async Task<string> AskAsync(string userMessage)
{
var messages = new List<ChatMessage>
{
new SystemChatMessage("You are a helpful assistant."),
new UserChatMessage(userMessage)
};
ChatCompletion completion = await _chat.CompleteChatAsync(messages);
return completion.Content[0].Text;
}
}// Usage
var answer = await chat.AskAsync("What is the capital of Norway?");
Console.WriteLine(answer); // OsloMulti-Turn Conversation
public class ConversationService(OpenAIClient openai)
{
private readonly ChatClient _chat = openai.GetChatClient("gpt-4o");
private readonly List<ChatMessage> _history =
[
new SystemChatMessage("You are a concise technical assistant.")
];
public async Task<string> SendAsync(string userMessage)
{
_history.Add(new UserChatMessage(userMessage));
var completion = await _chat.CompleteChatAsync(_history);
var reply = completion.Content[0].Text;
_history.Add(new AssistantChatMessage(reply));
return reply;
}
}Streaming Responses
public async Task StreamAsync(string prompt)
{
var messages = new List<ChatMessage>
{
new SystemChatMessage("You are helpful."),
new UserChatMessage(prompt)
};
await foreach (StreamingChatCompletionUpdate update in
_chat.CompleteChatStreamingAsync(messages))
{
foreach (var part in update.ContentUpdate)
{
Console.Write(part.Text); // print tokens as they arrive
}
}
Console.WriteLine();
}Streaming is essential for good UX — users see the response as it's generated rather than waiting for the full reply.
Function Calling (Tool Use)
Function calling lets the model request data it doesn't have. You define tools, the model decides when to call them, you execute them and return results.
// Define the tool
var weatherTool = ChatTool.CreateFunctionTool(
functionName: "get_weather",
functionDescription: "Get current weather for a city",
functionParameters: BinaryData.FromString("""
{
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name" }
},
"required": ["city"]
}
""")
);
var options = new ChatCompletionOptions { Tools = { weatherTool } };
var messages = new List<ChatMessage>
{
new SystemChatMessage("You answer weather questions."),
new UserChatMessage("What's the weather in Oslo?")
};
// First call — model may request a tool
var response = await _chat.CompleteChatAsync(messages, options);
while (response.FinishReason == ChatFinishReason.ToolCalls)
{
messages.Add(new AssistantChatMessage(response));
// Execute each tool call
foreach (var toolCall in response.ToolCalls)
{
var args = JsonDocument.Parse(toolCall.FunctionArguments);
var city = args.RootElement.GetProperty("city").GetString()!;
var result = await GetWeatherAsync(city); // your real implementation
messages.Add(new ToolChatMessage(toolCall.Id, result));
}
// Continue the conversation with tool results
response = await _chat.CompleteChatAsync(messages, options);
}
Console.WriteLine(response.Content[0].Text);Structured Output (JSON Mode)
var options = new ChatCompletionOptions
{
ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat()
};
var messages = new List<ChatMessage>
{
new SystemChatMessage("You respond only with JSON. Extract product info."),
new UserChatMessage("Widget Pro 2000, priced at $49.99, in Electronics")
};
var completion = await _chat.CompleteChatAsync(messages, options);
var json = completion.Content[0].Text;
var product = JsonSerializer.Deserialize<ProductInfo>(json);Embeddings
var embeddingClient = openai.GetEmbeddingClient("text-embedding-3-small");
EmbeddingCollection embeddings = await embeddingClient.GenerateEmbeddingsAsync(
new[] { "Azure Functions are serverless", "I like pizza" }
);
float[] vec1 = embeddings[0].ToFloats().ToArray();
float[] vec2 = embeddings[1].ToFloats().ToArray();
// Cosine similarity — high = semantically similar
float similarity = CosineSimilarity(vec1, vec2);Token Management
// Check usage after each call
var completion = await _chat.CompleteChatAsync(messages);
Console.WriteLine($"Prompt tokens: {completion.Usage.InputTokens}");
Console.WriteLine($"Completion tokens: {completion.Usage.OutputTokens}");
Console.WriteLine($"Total: {completion.Usage.TotalTokens}");
// Limit response length
var options = new ChatCompletionOptions { MaxOutputTokenCount = 500 };gpt-4o pricing (approximate): $2.50 per 1M input tokens, $10 per 1M output tokens.
Key Takeaways
- Use DI registration for
OpenAIClient— injectChatClientinto services - Streaming (
CompleteChatStreamingAsync) is essential for responsive AI UX - Function calling lets the model request real data — implement it as a tool execution loop
- JSON mode forces structured output — use it when you need to parse model responses
- Track token usage on every call — unmanaged costs compound fast
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.