Calling Azure OpenAI from C#

Calling Azure OpenAI from C#

GenAI-Powered .NET Β· double-click any text to add a note Β· hover dotted terms for definitions
Tutorial 11 of 27 Intermediate C#

1 Overview: From Portal to Program

In the previous tutorial you provisioned an Azure OpenAI resource and created a model deployment. This tutorial makes it real: you will call that deployment from C# code. We cover the two operations you will use most — the chat completion API for generating text and the embeddings API for turning text into a vector — and the practical engineering around them: tuning temperature and max tokens, handling errors and retries safely, and keeping your endpoint and API key out of source code.

By the end you will have built a complete .NET console app that reads a prompt from the user, sends it to Azure OpenAI, and displays the response. Everything here is foundation for the rest of the course: function calling, RAG, and Semantic Kernel all sit on top of the same client, the same parameters, and the same error-handling discipline you learn now.

This tutorial is for developers comfortable with C# and the .NET CLI. You need an Azure OpenAI resource with at least one chat model deployment (for example gpt-4o-mini) and ideally one embedding model deployment.

2 Learning Objectives

  • Call the chat completion API from C# with system and user messages and read the generated reply.
  • Call the embeddings API and understand what the returned vector represents and what it is used for.
  • Explain what temperature and max tokens do, and choose sensible values for different kinds of tasks.
  • Recognize transient faults such as HTTP 429 rate limit errors and implement retries with exponential backoff.
  • Build and run a .NET console app that sends a prompt and displays the response end to end.
  • Manage configuration and API keys safely using environment variables, user secrets, and Managed Identity — never hardcoded strings.

3 Prerequisites

  • C# fundamentals: classes, async/await, exception handling, and using the .NET CLI (dotnet new, dotnet run).
  • .NET 8 SDK installed on your machine.
  • An Azure OpenAI resource with a chat model deployment (note its deployment name) and the resource's endpoint URL and API key from the Azure portal.
  • Concepts from earlier tutorials: what a token is, what a prompt is, and what an Azure OpenAI deployment is.
If you do not have an embedding model deployed yet, deploy text-embedding-3-small alongside your chat model — the embeddings section of this tutorial uses it, and later RAG tutorials depend on it.

4 Key Concepts: The Client, the Deployment, and Two APIs

All access goes through a client object from the official SDK, installed from NuGet as the Azure.AI.OpenAI package. You construct the client once with two pieces of information: the endpoint of your resource and a credential (an API key or an Azure identity). From that client you ask for an operation-specific sub-client bound to a deployment name — a chat client for text generation, an embedding client for vectors.

The distinction between model and deployment matters in code: you never pass 'gpt-4o' to the client. You pass the name you chose when you created the deployment (which might also be 'gpt-4o', but is your label, not the model id). This indirection is what lets operations teams swap model versions without changing application code.

API Input Output Typical use
Chat completion A list of role-tagged messages (system, user, assistant) Generated text (the assistant's next message) Q&A, summarization, drafting, code help, chatbots
Embeddings One or more strings of text A vector of floating-point numbers per string Semantic search, similarity, clustering, RAG retrieval

Both APIs are metered in tokens and both are governed by the rate limit assigned to your deployment. That is why parameters like max tokens and habits like exponential backoff retries are not optional extras — they are how you keep a real application within quota, on budget, and resilient.

5 Deep Dive 1: The Chat Completion API

A chat completion request is a conversation snapshot: an ordered list of messages, each tagged with a role. The system message sets the assistant's behavior and constraints ('You are a concise assistant for .NET developers. Answer in C#.'). User messages carry what the human said. Assistant messages carry what the model said earlier — you include them when you want the model to see conversation history, because the API itself is stateless: it remembers nothing between calls.

🎬 Anatomy of a chat completion call
Follow one request from your C# code to the generated reply.
Your C# app builds messages
➜
Request messages + options
➜
Deployment your model instance
➜
Model generates tokens
➜
Response assistant message

The response contains more than text. The finish reason tells you why generation stopped: 'stop' means the model completed naturally, 'length' means it was truncated by the max tokens limit, and a content-filter reason means Azure's Responsible AI filters intervened. Usage counts report prompt tokens and completion tokens — log them, because they are exactly what you are billed for and what your rate limit consumes.

Because the API is stateless, a multi-turn chatbot must resend relevant history with every call. That history counts as prompt tokens each time — conversation memory is a cost decision, not just a UX decision.

6 Deep Dive 2: The Embeddings API

The embeddings API does not generate text at all. You send it a string and it returns an embedding — a vector of floating-point numbers (1,536 dimensions for text-embedding-3-small) that encodes the meaning of the text. Two texts that mean similar things produce vectors that point in similar directions, which you can measure with cosine similarity. That single property powers semantic search: instead of matching keywords, you match meaning.

🎬 How embeddings enable semantic search
Watch a user question find the right document by meaning, not keywords.
Documents your content
➜
Embeddings API text to vector
➜
Vector store saved vectors
➜
User question embedded too
➜
Best match cosine similarity

In C#, the flow is symmetrical with chat: get an embedding client bound to your embedding deployment, call it with text, and read back the vector as a ReadOnlyMemory of float. Embeddings are cheap and fast compared with chat completions, but they still consume tokens and rate limit, and one rule is absolute: vectors are only comparable when produced by the same embedding model. If you switch models, re-embed everything.

Chat generates, embeddings measure. When a task is 'find the most relevant X' rather than 'write me a Y', reach for the embeddings API — it is orders of magnitude cheaper than asking a chat model to compare texts.

7 Deep Dive 3: Key Parameters — Temperature and Max Tokens

Temperature controls how the model picks each next token. At low temperature (near 0) it almost always picks the most probable token, giving focused, consistent, near-deterministic output. At higher temperature (0.8 and up) it samples more freely, giving varied and creative output — and a higher chance of drifting off course. Temperature does not make the model smarter or dumber; it changes how adventurous its choices are.

Task Suggested temperature Why
Code generation, data extraction, classification 0.0 – 0.2 You want the most probable, repeatable answer
Q&A, summarization, documentation 0.2 – 0.5 Mostly factual with a little natural variation
Brainstorming, marketing copy, story ideas 0.7 – 1.0 Variety is the point; repeated calls should differ

Max tokens caps the length of the generated response — it limits output tokens only, not your prompt. It is a hard stop: when the cap is reached the reply is truncated mid-thought and the finish reason comes back as 'length'. Set it deliberately: high enough for a complete answer, low enough to bound cost and latency. A one-line classifier might need 20 tokens; a code explanation might need 800. Remember that in .NET SDK code the property is named MaxOutputTokenCount, but the concept everyone says out loud is max tokens.

If users report answers that stop mid-sentence, check the finish reason before blaming the model — a 'length' finish means your max tokens value is too small for the task. Detect it in code and either raise the limit or tell the user the answer was truncated.

These two parameters interact with cost directly: temperature costs nothing extra, but sloppy max tokens settings do. A safe pattern is to set temperature per task type in configuration, set max tokens to about double your typical expected answer length, and log actual usage so you can tighten both with data.

8 Deep Dive 4: Errors, Retries, and Protecting Your API Key

Calls to Azure OpenAI fail in two fundamentally different ways, and your code must treat them differently. A transient fault — HTTP 429 when you exceed your rate limit, or an occasional 5xx service hiccup — will likely succeed if retried after a short wait. A permanent error — 401 (bad API key), 404 (wrong endpoint or deployment name), 400 (malformed request or prompt blocked by content filtering) — will fail identically every time, and retrying it is pure waste. Retry the first kind; fix the second.

🎬 Retrying a rate-limited request with exponential backoff
See how a 429 becomes a success instead of a crash.
API call attempt n
➜
429 returned rate limit hit
➜
Backoff wait 1s, 2s, 4s...
➜
Retry call same request
➜
200 OK response returned

The second discipline of this section is configuration. Your endpoint and API key must never appear in source code or appsettings.json committed to a repository. In local development, use an environment variable or the .NET user secrets store (dotnet user-secrets), both of which live outside the project folder. In production, prefer Managed Identity: your app authenticates as an Azure identity with a role assignment on the Azure OpenAI resource, and there is simply no key to leak, rotate, or steal. When a key is unavoidable, keep it in Azure Key Vault and rotate it on a schedule.

A leaked API key is spendable money: anyone holding it can consume your quota at your expense until you regenerate it. Treat keys like passwords — never in code, never in screenshots, never in logs.

9 Ecosystem and Tools

Tool / package Role in this tutorial
Azure.AI.OpenAI (NuGet) The official .NET client SDK; provides AzureOpenAIClient, chat and embedding sub-clients
Azure.Identity (NuGet) Provides DefaultAzureCredential and Managed Identity support for keyless authentication
dotnet user-secrets CLI tool that stores development secrets in your user profile, outside source control
Azure Key Vault Production-grade secret storage with access policies and rotation
Azure AI Foundry portal Where you create deployments, find your endpoint and API key, and try prompts in the playground
Polly (NuGet) A resilience library for .NET offering retry, backoff, and circuit-breaker policies when you outgrow hand-rolled loops

The Azure.AI.OpenAI package builds on the general OpenAI .NET library, so chat message types like SystemChatMessage and UserChatMessage come from the OpenAI.Chat namespace while the Azure-specific entry point, AzureOpenAIClient, handles the endpoint and credential. Install both Azure.AI.OpenAI and Azure.Identity via NuGet and you have everything the code in this tutorial needs.

The SDK has built-in retry behavior for some transient failures, and libraries like Polly can express sophisticated policies in a few lines. Learn the manual backoff loop first — in this tutorial — so you understand exactly what those layers are doing for you.

10 Use Cases

  • Internal help desk assistant: a console or web tool where staff ask policy questions and a chat completion call answers from a grounded prompt.
  • Document summarizer: a batch job that feeds long reports through the chat completion API with low temperature and a generous max tokens budget.
  • Semantic FAQ search: embed every FAQ entry once, embed each incoming user question, and use cosine similarity to surface the right answer without any keyword matching.
  • Support ticket router: an embedding of each new ticket compared against category examples to auto-assign teams — cheaper and faster than asking a chat model to classify.
  • Code review helper: a CLI tool that sends a diff to your deployment and prints suggested improvements, with retries so CI does not fail on a momentary rate limit.
  • Duplicate detection: embeddings of bug reports compared pairwise to flag likely duplicates before a human triages them.

Notice the pattern across all of these: chat completions where text must be produced, embeddings where texts must be compared, tuned temperature and max tokens per task, and retry logic anywhere the call sits in an automated pipeline.

11 Code Examples

Three complete examples follow. They assume the environment variables AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_KEY are set, and a chat deployment named gpt-4o-mini plus an embedding deployment named text-embedding-3-small exist on your resource. Install packages first: dotnet add package Azure.AI.OpenAI and dotnet add package Azure.Identity.

Example 1 — Minimal chat completion
using Azure;
using Azure.AI.OpenAI;
using OpenAI.Chat;

// Read the endpoint and API key from environment variables — never hardcode them.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
    ?? throw new InvalidOperationException("Set AZURE_OPENAI_ENDPOINT first.");
string apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")
    ?? throw new InvalidOperationException("Set AZURE_OPENAI_KEY first.");

// One client for the resource; a chat sub-client bound to a deployment name.
AzureOpenAIClient azureClient = new(new Uri(endpoint), new AzureKeyCredential(apiKey));
ChatClient chat = azureClient.GetChatClient("gpt-4o-mini");

ChatCompletion completion = await chat.CompleteChatAsync(
    new SystemChatMessage("You are a concise assistant for .NET developers."),
    new UserChatMessage("Explain async/await in two sentences."));

Console.WriteLine(completion.Content[0].Text);
Console.WriteLine($"Finish reason: {completion.FinishReason}");
Example 2 — Setting temperature and max tokens
using OpenAI.Chat;

// Low temperature for a factual task; a tight output cap to bound cost.
ChatCompletionOptions options = new()
{
    Temperature = 0.2f,
    MaxOutputTokenCount = 300   // the SDK name for the max tokens limit
};

ChatCompletion completion = await chat.CompleteChatAsync(
    new ChatMessage[]
    {
        new SystemChatMessage("Answer with a short, accurate definition."),
        new UserChatMessage("What is dependency injection?")
    },
    options);

if (completion.FinishReason == ChatFinishReason.Length)
{
    Console.WriteLine("[Warning] Reply was truncated by the max tokens limit.");
}
Console.WriteLine(completion.Content[0].Text);
Example 3 — Embeddings with cosine similarity
using OpenAI.Embeddings;

EmbeddingClient embedder = azureClient.GetEmbeddingClient("text-embedding-3-small");

OpenAIEmbedding a = await embedder.GenerateEmbeddingAsync("How do I read a file in C#?");
OpenAIEmbedding b = await embedder.GenerateEmbeddingAsync("Reading file contents with .NET");

float[] va = a.ToFloats().ToArray();
float[] vb = b.ToFloats().ToArray();

// Cosine similarity: dot product over the product of magnitudes.
double dot = 0, magA = 0, magB = 0;
for (int i = 0; i < va.Length; i++)
{
    dot += va[i] * vb[i];
    magA += va[i] * va[i];
    magB += vb[i] * vb[i];
}
double similarity = dot / (Math.Sqrt(magA) * Math.Sqrt(magB));

Console.WriteLine($"Vector dimensions: {va.Length}");
Console.WriteLine($"Similarity: {similarity:F4}");  // similar meanings score close to 1
Example 4 — Retry loop with exponential backoff
using System.ClientModel;
using OpenAI.Chat;

async Task<ChatCompletion> CompleteWithRetryAsync(
    ChatClient chat, ChatMessage[] messages, ChatCompletionOptions options,
    int maxAttempts = 4)
{
    var random = new Random();
    for (int attempt = 1; ; attempt++)
    {
        try
        {
            return await chat.CompleteChatAsync(messages, options);
        }
        catch (ClientResultException ex)
            when ((ex.Status == 429 || ex.Status >= 500) && attempt < maxAttempts)
        {
            // Transient fault: wait 1s, 2s, 4s... plus jitter, then retry.
            int delayMs = (int)(Math.Pow(2, attempt - 1) * 1000) + random.Next(0, 250);
            Console.WriteLine($"Attempt {attempt} got {ex.Status}; retrying in {delayMs} ms...");
            await Task.Delay(delayMs);
        }
        // 401/404/400 fall through and throw immediately — retrying cannot fix them.
    }
}

12 Step by Step: A Console App That Sends a Prompt and Displays the Response

This walkthrough builds the tutorial's deliverable from an empty folder: a console app that asks the user for a prompt, calls your chat deployment, and prints the reply — with safe configuration and a retry loop.

  1. Create the project: run 'dotnet new console -n AskAzureOpenAI' and 'cd AskAzureOpenAI'.
  2. Add packages from NuGet: 'dotnet add package Azure.AI.OpenAI' and 'dotnet add package Azure.Identity'.
  3. Store your secrets outside the code. Enable the store with 'dotnet user-secrets init', then run 'dotnet user-secrets set AzureOpenAI:Endpoint https://<your-resource>.openai.azure.com' and 'dotnet user-secrets set AzureOpenAI:Key <your-api-key>'. (Alternatively, set the two environment variables from Example 1.)
  4. Load configuration in Program.cs using Microsoft.Extensions.Configuration with AddUserSecrets and AddEnvironmentVariables so either source works.
  5. Create the client: new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(key)), then GetChatClient with your deployment name.
  6. Read the user's prompt with Console.ReadLine(), build a SystemChatMessage plus a UserChatMessage, and set ChatCompletionOptions with Temperature = 0.3f and MaxOutputTokenCount = 400.
  7. Send the request through the CompleteWithRetryAsync helper from Example 4 so a 429 rate limit becomes a short wait instead of a crash.
  8. Display the response text, and if the finish reason is Length, warn that the answer hit the max tokens cap.
  9. Run it with 'dotnet run', type a question such as 'What is a C# record type?', and confirm the model's answer prints.
  10. Verify safety: check that 'git status' shows no file containing your key, and that deleting the environment variables plus user secrets makes the app fail fast with a clear error message.
Program.cs — the complete console app
using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.Configuration;
using OpenAI.Chat;

var config = new ConfigurationBuilder()
    .AddUserSecrets<Program>()          // dev-time secrets, outside the repo
    .AddEnvironmentVariables()          // or environment variables in CI / prod
    .Build();

string endpoint = config["AzureOpenAI:Endpoint"] ?? config["AZURE_OPENAI_ENDPOINT"]
    ?? throw new InvalidOperationException("No endpoint configured.");
string key = config["AzureOpenAI:Key"] ?? config["AZURE_OPENAI_KEY"]
    ?? throw new InvalidOperationException("No API key configured.");

var azureClient = new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(key));
ChatClient chat = azureClient.GetChatClient("gpt-4o-mini");

Console.Write("Ask me anything: ");
string question = Console.ReadLine() ?? string.Empty;

var options = new ChatCompletionOptions { Temperature = 0.3f, MaxOutputTokenCount = 400 };
var messages = new ChatMessage[]
{
    new SystemChatMessage("You are a helpful assistant for .NET developers."),
    new UserChatMessage(question)
};

ChatCompletion reply = await CompleteWithRetryAsync(chat, messages, options);

Console.WriteLine();
Console.WriteLine(reply.Content[0].Text);
if (reply.FinishReason == ChatFinishReason.Length)
    Console.WriteLine("[Note] Answer truncated — raise MaxOutputTokenCount for longer replies.");
For a keyless variant, replace AzureKeyCredential with new DefaultAzureCredential() from Azure.Identity and grant your identity the 'Cognitive Services OpenAI User' role on the resource — the code change is one line, and there is no key to manage.

13 Limitations and Caveats

  • SDK surface changes between versions. The examples target the Azure.AI.OpenAI 2.x line (AzureOpenAIClient, GetChatClient, CompleteChatAsync, MaxOutputTokenCount, ClientResultException). Exact type, method, and property names differ in the older 1.x SDK and may evolve — treat the examples as correct in spirit and check the package's current documentation if a name does not compile.
  • The chat completion API is stateless: no conversation memory exists unless you resend history, and resent history is billed as prompt tokens on every call.
  • Max tokens limits output only; it does not protect you from oversized prompts. The model's total context window bounds prompt plus response together.
  • Embedding vectors from different models (or different versions of a model) are not comparable — switching embedding deployments means re-embedding your whole corpus.
  • Retries multiply cost and latency: a request that consumed tokens before failing may still have counted against your quota. Cap attempts (3–5) and never retry 400/401/404.
  • Content filtering can block a prompt or a completion; your code must handle a filtered result as a normal outcome, not an exception path only.
  • Environment variables and user secrets are development conveniences, not production security. Production apps should use Managed Identity or Key Vault.
  • Deployment names in this tutorial (gpt-4o-mini, text-embedding-3-small) are examples — yours are whatever you named them in the portal.

14 Best Practices

  • Create one client per application (it is thread-safe) rather than one per request — register it as a singleton in dependency injection.
  • Always send a system message; an unanchored model drifts. Keep it short, specific, and version it with your code.
  • Set temperature and max tokens explicitly for every call path — defaults tuned for chat demos are rarely right for production tasks.
  • Check the finish reason on every completion and handle 'length' and content-filter outcomes deliberately.
  • Log token usage (prompt and completion counts) from day one; it is your cost dashboard and your early warning for rate limit pressure.
  • Retry only transient faults (429, 5xx) with exponential backoff plus jitter and a bounded attempt count; fail fast on everything else.
  • Keep credentials in the environment, user secrets, Key Vault, or best of all eliminate them with Managed Identity — and rotate any long-lived API key on a schedule.
  • Validate configuration at startup and fail with a clear message, rather than surfacing a confusing 401 deep inside a user request.
Common mistake Do this instead
Hardcoding the API key 'just for testing' Set an environment variable or user secret from minute one — test code gets committed
Retrying every exception in a blanket catch Match on status: retry 429/5xx, throw immediately on 400/401/404
Setting max tokens to the maximum 'to be safe' Budget output per task; oversized caps hide runaway prompts and inflate worst-case cost
Comparing embeddings from two different models Pin one embedding deployment per index and re-embed if it ever changes

20 Summary

  • One client (AzureOpenAIClient from the Azure.AI.OpenAI NuGet package), built from an endpoint plus a credential, hands out chat and embedding sub-clients bound to deployment names.
  • The chat completion API generates text from role-tagged messages and is stateless — your app owns conversation memory and pays for resent history in prompt tokens.
  • The embeddings API turns text into a vector; cosine similarity between vectors powers semantic search, and vectors are only comparable within one embedding model.
  • Temperature tunes randomness (low for code and facts, high for creativity); max tokens caps output length and truncates with finish reason 'length' when hit.
  • Retry only transient faults — 429 rate limit and 5xx — using exponential backoff with jitter and a bounded attempt count; fail fast on 400/401/404.
  • Keep the endpoint and API key in an environment variable or user secrets during development, and prefer Managed Identity in production so no key exists at all.

You now have the complete request path from C# code to a model deployment and back, including the two parameters you will tune most and the failure modes you will meet first. The console app you built — configuration, client, prompt, retry, display — is the skeleton every later project in this course grows from. Keep the discipline you practiced here: explicit parameters, checked finish reasons, logged token usage, and secrets that never touch the repository.

21 Next Steps

Next tutorial: Function Calling and Tools (function-calling-and-tools). So far the model can only answer with text; function calling lets it request that your C# code run a method — check an order, query a database, call an API — and then use the result in its reply. That turns the model from a text generator into the reasoning core of an application, and it is the doorway to agents later in the course.

  • Practice: extend the console app into a multi-turn chat loop that resends history, then add a '/reset' command that clears it — watch prompt token usage grow and shrink in your logs.
  • Practice: embed ten of your own FAQ entries, then embed three test questions and rank the FAQs by cosine similarity — verify the best match is the right one.
  • Practice: temporarily set MaxOutputTokenCount to 30, observe the 'length' finish reason, and make the app print a clear truncation warning.
  • Practice: switch the console app from AzureKeyCredential to DefaultAzureCredential and authenticate with your developer identity instead of a key.
  • Read: the official 'Azure OpenAI Service documentation' quickstarts for C#, the 'Azure SDK for .NET' guidance on retries, and the 'Microsoft identity platform' pages on Managed Identity.
Keep your console app in a repository — tutorials 12 (function calling) and 15 (RAG) both build directly on it, and you will extend rather than restart.

15 Quiz: Calling Azure OpenAI from C#

Pick an answer for each question, then press Check answer. (Notes are disabled in this tab.)

1. What does a chat completion request consist of?

βœ… Correct!
❌ Not quite β€” the correct answer is .
The chat completion API takes a conversation snapshot: messages tagged system, user, or assistant, in order, along with options like temperature and max tokens. The model returns the next assistant message.

2. Which message role is used to set the assistant's overall behavior and constraints?

βœ… Correct!
❌ Not quite β€” the correct answer is .
The system message establishes persona, tone, and rules before any user input. User messages carry the human's input and assistant messages carry prior model replies.

3. What does the embeddings API return for an input string?

βœ… Correct!
❌ Not quite β€” the correct answer is .
An embedding is a numeric vector (for example 1,536 dimensions for text-embedding-3-small). Texts with similar meaning produce vectors that are close together, which enables semantic search.

4. Which task is the embeddings API better suited for than the chat completion API?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Embeddings measure similarity between texts via cosine similarity, which is far cheaper and faster than asking a generative model to compare documents. Generation tasks belong to chat completions.

5. What effect does lowering temperature toward 0 have?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Temperature controls sampling randomness. Near 0 the model almost always picks the most probable next token, which is what you want for code generation, extraction, and classification.

6. What exactly does the max tokens setting limit?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Max tokens (MaxOutputTokenCount in the .NET SDK) caps output only. The prompt is bounded separately by the model's context window, and calls per minute are governed by the rate limit.

7. A completion comes back with finish reason 'length'. What happened?

βœ… Correct!
❌ Not quite β€” the correct answer is .
'length' means generation stopped at the output cap, so the reply is likely cut off mid-thought. 'stop' is the natural finish; a content-filter finish indicates Responsible AI intervention.

8. Your app receives HTTP 429 from Azure OpenAI. What does it mean and what should you do?

βœ… Correct!
❌ Not quite β€” the correct answer is .
429 is a transient fault: your deployment's requests-per-minute or tokens-per-minute quota was exceeded. Waiting with exponential backoff and retrying usually succeeds; 401, 404, and 400 are the permanent errors in the other options.

9. Which retry strategy is recommended for transient Azure OpenAI failures?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Doubling the delay between attempts (1s, 2s, 4s) gives the service room to recover, jitter prevents synchronized retries from parallel clients, and a cap (3–5 attempts) bounds latency. Permanent errors like 401/404 should never be retried.

10. Which of these is a safe place for your Azure OpenAI API key during local development?

βœ… Correct!
❌ Not quite β€” the correct answer is .
User secrets live in your user profile outside the project folder, and environment variables never enter source control. Anything committed to the repo — code, appsettings.json, or docs — leaks the key to everyone with repo access.

11. What is the main security advantage of Managed Identity over an API key?

βœ… Correct!
❌ Not quite β€” the correct answer is .
With Managed Identity, Azure issues and rotates credentials for your app's identity automatically; your code uses DefaultAzureCredential and holds no key. It changes authentication only — performance, quotas, and encryption are unaffected.

12. Which NuGet package provides the official Azure OpenAI client for .NET?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Azure.AI.OpenAI supplies AzureOpenAIClient plus the chat and embedding sub-clients, building on the OpenAI .NET library. Azure.Identity adds keyless authentication; the other packages are unrelated.

13. What value do you pass to GetChatClient(...) when creating a chat client?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Client code targets your deployment name — your label for a model instance in the resource. It may happen to match the model name, but it is the deployment that routing, quota, and version pinning attach to.

14. Why must a multi-turn chatbot resend earlier messages with each chat completion call?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Each call stands alone: the model only sees what is in that request's message list. Conversation memory is your application's job — and resent history is billed as prompt tokens each time.

15. Two embedding vectors have a cosine similarity of 0.97. What can you conclude?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Cosine similarity near 1 means the vectors point in nearly the same direction, which for embeddings means very similar meaning. It says nothing about length, wording, or which text derived from which.

16 Exam: Written Questions

Try answering each question yourself before expanding the model answer.

1. Describe the anatomy of a chat completion request and response, naming the three message roles and three useful fields on the response.
A request contains an ordered list of messages, each tagged with a role: system (sets behavior and constraints), user (the human's input), and assistant (the model's earlier replies, resent for context). It also carries options such as temperature and max tokens. The response contains the generated assistant message content; a finish reason ('stop' for natural completion, 'length' for truncation at the output cap, or a content-filter reason); and usage counts for prompt and completion tokens, which drive billing and rate-limit consumption.
2. Explain why the chat completion API being stateless matters for both application design and cost.
Stateless means the service retains nothing between calls — each request must contain every message the model should consider. For design, the application owns conversation memory: it must store history and decide how much to resend (all of it, a window, or a summary). For cost, every resent message is billed as prompt tokens on every call, so a long conversation grows linearly more expensive per turn. Techniques like windowing or summarizing old turns trade fidelity for cost.
3. What is an embedding, and what property of embeddings makes semantic search possible?
An embedding is a fixed-length vector of floating-point numbers produced by a model from a piece of text — 1,536 dimensions for text-embedding-3-small. The key property is that semantically similar texts map to vectors that are close together (high cosine similarity), regardless of shared keywords. Semantic search embeds all documents ahead of time, embeds the query at search time with the same model, and returns the documents whose vectors are closest to the query vector.
4. Why can you not compare embedding vectors produced by two different embedding models, and what is the operational consequence?
Each model defines its own vector space — the dimensions have no meaning across models, and even the vector lengths may differ. A distance between a vector from model A and one from model B is meaningless. The operational consequence: an index or vector store is pinned to the embedding model that produced it. If you upgrade or switch embedding deployments, you must re-embed the entire corpus with the new model before queries give sensible results.
5. Compare temperature 0.1 and temperature 0.9 for the task of generating C# code, and justify which you would choose.
At 0.1 the model nearly always picks its most probable token, producing consistent, conventional code — the same request yields nearly the same result, which aids testing and review. At 0.9 sampling is much freer: more varied idioms and creative structures, but a higher chance of subtle mistakes and inconsistency between runs. For code generation, choose low temperature (0.0–0.2): correctness and reproducibility matter more than variety. High temperature suits brainstorming, naming ideas, or creative text.
6. A user reports that answers from your app sometimes stop mid-sentence. Walk through how you would diagnose and fix this.
First check the finish reason on the affected completions. If it is 'length', the max tokens cap (MaxOutputTokenCount) is too small for the task — the model was cut off, not confused. Fix by raising the cap to fit a complete answer (roughly double the typical expected length), or by detecting the 'length' finish in code and either warning the user or issuing a follow-up request. If the finish reason is a content-filter value instead, the reply was blocked by Responsible AI filtering and needs different handling. Log finish reasons and token usage so this diagnosis takes minutes, not guesswork.
7. Classify these errors as transient or permanent, and state the correct handling for each: HTTP 429, HTTP 401, HTTP 503, HTTP 404.
429 (rate limit exceeded) is transient: wait using exponential backoff and retry a bounded number of times. 503 (service temporarily unavailable) is transient: same treatment. 401 (unauthorized) is permanent: the API key or identity is wrong — fail fast, fix configuration, never retry. 404 (not found) is permanent: the endpoint URL or deployment name is wrong — fail fast and fix. The dividing line: retry only when the identical request could plausibly succeed in a few seconds.
8. Design a retry policy for production Azure OpenAI calls. Specify what you retry, the delay schedule, why jitter is included, and when you give up.
Retry only transient faults: 429 and 5xx status codes (and transport-level timeouts). Use exponential backoff — for example 1s, 2s, 4s, 8s — so a saturated deployment gets recovery room. Add random jitter (e.g. 0–250 ms) to each delay so many parallel clients that failed together do not retry in synchronized waves that re-trigger the rate limit. Cap attempts at 3–5; on final failure, surface a clear, friendly error and log the details. Never retry 400/401/404, and remember failed attempts may still have consumed quota, so a low cap also protects cost.
9. List the layers of configuration/secret management for an Azure OpenAI app from development to production, and state when each is appropriate.
Development: environment variables (simple, works everywhere) or the .NET user secrets store (dotnet user-secrets — per-user, outside the project folder, cannot be committed accidentally). CI/CD: pipeline secret variables injected as environment variables. Production with a key: Azure Key Vault, giving access control, audit, and rotation. Production, preferred: Managed Identity with DefaultAzureCredential — the app authenticates as an Azure identity holding the 'Cognitive Services OpenAI User' role, so no key exists to leak or rotate. The constant rule at every layer: the key never appears in source code or committed config files.
10. Explain the difference between a model and a deployment in Azure OpenAI, and why client code uses the deployment name.
A model is the published artifact (gpt-4o, text-embedding-3-small) with a version. A deployment is your named instance of a chosen model version inside your resource, with its own quota and rate limit. Client code calls GetChatClient or GetEmbeddingClient with the deployment name because that indirection decouples code from model versions: operations can create a new deployment on a newer model version and update configuration, and application code — which only knows the deployment name — does not change.
11. Outline the steps to build a console app that sends a prompt to Azure OpenAI and displays the response, including package installation and secret setup.
1) dotnet new console and cd into it. 2) dotnet add package Azure.AI.OpenAI (plus Azure.Identity for keyless auth). 3) dotnet user-secrets init, then set AzureOpenAI:Endpoint and AzureOpenAI:Key with dotnet user-secrets set (or export environment variables). 4) In Program.cs, build configuration from user secrets and environment variables; fail fast if values are missing. 5) Create AzureOpenAIClient with the endpoint and credential, then GetChatClient with the deployment name. 6) Read Console.ReadLine into a UserChatMessage under a SystemChatMessage; set Temperature and MaxOutputTokenCount explicitly. 7) Call CompleteChatAsync inside a retry helper. 8) Print the content text and warn if the finish reason is Length. 9) dotnet run and verify; confirm no secret is visible to git.
12. Why should the Azure OpenAI client be created once and shared rather than instantiated per request?
The client is thread-safe and manages underlying HTTP connections. Creating one per request wastes sockets and handshake time, can exhaust connection pools under load, and defeats connection reuse. The correct pattern in ASP.NET Core is registering AzureOpenAIClient (or the sub-clients) as a singleton in dependency injection; a console app simply creates it once at startup. Per-call objects should be limited to messages and options.
13. What does token usage reporting on each response give you, and name three decisions it should inform.
Each completion reports prompt tokens and completion tokens consumed. Logged over time, this is (1) your cost model — billing is per token, so usage logs are a per-feature cost dashboard; (2) your rate-limit early warning — tokens per minute against quota shows how close you run to 429s and whether to request more quota or add backoff; (3) your tuning feedback — if completions regularly use far less than max tokens you can tighten the cap, and if prompts balloon you know conversation history or context stuffing needs a windowing strategy.
14. How should an application handle a response blocked by content filtering, and why is treating it as a crash wrong?
Content filtering is a normal, expected outcome in Azure OpenAI: a prompt or completion that trips the Responsible AI filters returns a filtered result or a specific error rather than generated text. The app should detect this case, show the user a clear message ('this request couldn't be completed'), log it for review, and continue running. Treating it as an unhandled exception crashes or confuses the app for something that is working as designed — filtering is a feature protecting the service and users, and legitimate inputs occasionally trigger it, so graceful handling is mandatory.
15. Your team wants semantic FAQ search. Describe the full pipeline using the embeddings API, including what happens at index time and at query time.
Index time: for each FAQ entry, call the embeddings API with the entry text (question plus answer, or question alone) and store the returned vector next to the original text — in a database, a search index, or memory for small sets. Record which embedding deployment produced the vectors. Query time: embed the incoming user question with the same deployment, compute cosine similarity between the query vector and every stored vector (or use a vector index for scale), and return the top matches above a similarity threshold. Optionally pass the best match plus the question to a chat completion call to phrase a natural answer — which is the essence of RAG, covered later in this course.

17 Flashcards

Click a card to reveal the back.

Chat completion API
Takes an ordered list of role-tagged messages (system/user/assistant) and returns the model's next reply. Stateless — resend history for multi-turn context.
System message
The behavior-setting message at the start of a chat request: persona, tone, rules. Keep it short, specific, and versioned with your code.
Embeddings API
Converts text into a vector of floats that encodes meaning. Similar texts → similar vectors. Used for semantic search, clustering, RAG retrieval.
Cosine similarity
Similarity measure based on the angle between two vectors; near 1 = very similar meaning. The standard way to compare embeddings.
Temperature
Sampling randomness control. ~0 = focused and repeatable (code, extraction); ~0.8+ = varied and creative (brainstorming). Does not change model knowledge.
Max tokens
Cap on generated output tokens (SDK: MaxOutputTokenCount). Hitting it truncates the reply with finish reason 'length'. Limits output only, not the prompt.
Finish reason
Why generation stopped: 'stop' = natural end, 'length' = hit max tokens, content-filter = blocked by Responsible AI. Check it on every completion.
HTTP 429
Rate limit exceeded — a transient fault. Correct response: exponential backoff with jitter, bounded retries. Not an authentication or request error.
Exponential backoff
Retry delays that double each attempt (1s, 2s, 4s…), plus random jitter to desynchronize parallel clients. Cap at 3–5 attempts.
Transient vs permanent errors
Transient (429, 5xx): retry with backoff. Permanent (400, 401, 404): fix the request, key, or names — retrying is waste.
Deployment name
Your label for a model instance in your Azure OpenAI resource. What you pass to GetChatClient/GetEmbeddingClient — decouples code from model versions.
AzureOpenAIClient
Entry-point class from the Azure.AI.OpenAI NuGet package. Built once with endpoint + credential; hands out chat and embedding sub-clients. Thread-safe singleton.
.NET user secrets
dotnet user-secrets — dev-time store in your user profile, outside the repo. Right place for endpoint/key during development; not a production mechanism.
Managed Identity
Azure-issued identity for your app; authenticate with DefaultAzureCredential and a role assignment. No API key exists to leak or rotate — preferred in production.
Token usage (prompt + completion)
Reported on every response. Log it: it is your billing meter, rate-limit early warning, and evidence for tuning max tokens and history windowing.

18 Interview Questions and Answers

1. Walk me through what happens when your C# code calls the chat completion API.
The app builds an ordered message list — a system message defining behavior and a user message with the input — plus options like temperature and max tokens. The SDK sends this over HTTPS to the resource endpoint, where Azure routes it to the named deployment. The model generates the reply token by token until it finishes or hits the output cap. The response returns the assistant message content, a finish reason, and token usage counts. The API is stateless, so any conversation history the model should see must be resent in the message list.
2. When would you use embeddings instead of chat completions?
Whenever the task is comparison or retrieval rather than generation: semantic search, finding duplicates, clustering feedback, routing tickets to categories, or fetching relevant context for RAG. Embeddings turn text into vectors so similarity becomes cheap math — cosine similarity — instead of an expensive generative call. My rule of thumb: 'write me X' is chat; 'find the most relevant X' is embeddings.
3. How do you choose a temperature value for a feature?
By how much variability the task tolerates. Deterministic tasks — code generation, extraction, classification — get 0.0 to 0.2 so results are consistent and testable. Explanatory tasks like summarization sit around 0.3 to 0.5. Creative tasks where repeated calls should differ — brainstorming, copywriting — get 0.7 and up. I set it explicitly per call path in configuration rather than trusting defaults, and I keep it low anywhere output feeds automated processing.
4. What does max tokens actually control, and what goes wrong when it is misconfigured?
It caps output tokens only — the reply the model generates — not the prompt. Too low, and answers truncate mid-sentence with finish reason 'length', which users experience as a broken product. Too high, and worst-case cost and latency balloon, and runaway generations go unnoticed. I size it to roughly double the typical complete answer for the task, check the finish reason in code, and adjust from logged usage data.
5. You get a spike of HTTP 429 responses in production. What is your response, immediately and long-term?
Immediately: confirm backoff-and-retry is absorbing the spike — 429 is transient, so exponential backoff with jitter and a 3–5 attempt cap turns most of them into slightly slower successes. Short-term: check whether a deploy or a traffic surge raised token consumption; usage logs show which. Long-term: request a higher quota for the deployment, reduce prompt sizes (trim resent history), split load across deployments, or add client-side rate limiting so we stay under tokens-per-minute rather than bouncing off it.
6. Which errors from Azure OpenAI should never be retried, and why?
400 (malformed request or filtered input), 401 (invalid key or identity), and 404 (wrong endpoint or deployment name). They are deterministic: the identical request fails identically every time, so retrying adds latency and load without any chance of success. Retries belong exclusively to transient faults — 429 and 5xx — where the same request can plausibly succeed seconds later. A blanket catch-and-retry is a design smell that hides real configuration bugs.
7. How do you keep the API key out of source control across dev, CI, and production?
Dev: dotnet user-secrets or environment variables — both live outside the project folder so they cannot be committed. CI: the pipeline's secret variables injected into the environment at run time. Production: preferably no key at all — Managed Identity with DefaultAzureCredential and a role assignment on the resource; where a key is unavoidable, Azure Key Vault with rotation. And configuration loading is written so a missing secret fails fast at startup with a clear message instead of a mysterious 401 later.
8. What is the advantage of Managed Identity over storing a key in Key Vault?
Key Vault protects a secret that still exists — it can still be mishandled after retrieval, must be rotated, and access to the vault becomes the new secret to guard. Managed Identity removes the secret entirely: Azure issues and rotates credentials for the app's identity automatically, the code path is just DefaultAzureCredential, and access control is a role assignment you can audit. Less to leak, less to rotate, less to get wrong — which is why it is the production default where supported.
9. Why does the SDK have you call GetChatClient with a deployment name instead of a model name?
Because the deployment is the operational unit in Azure OpenAI: it pins a model version and carries the quota and rate limit. Code that targets a deployment name is decoupled from model churn — ops can stand up a new deployment on a newer model version, flip a configuration value, and the application never changes. It also allows different deployments of the same model with different quotas for different workloads.
10. How would you add conversation memory to a chatbot built on this API?
The API is stateless, so the app stores history — in session, a database, or a cache keyed by conversation id — and replays it in the message list on each call. Because replayed history bills as prompt tokens every turn, I bound it: keep the last N turns, or summarize older turns into a single system-side note and resend the summary plus recent messages. Which strategy depends on how much earlier context genuinely changes answers; usage logs tell you what the memory is costing.
11. What would you log for every Azure OpenAI call in production, and why?
Timestamp, deployment name, prompt and completion token counts, latency, finish reason, status code, and retry count — plus a correlation id linking to the user request. Token counts are the cost meter and quota early-warning; finish reasons reveal truncation and content filtering trends; retry counts expose rate-limit pressure; latency feeds SLOs. I do not log prompt or response bodies by default — they may contain user data — or at most log them redacted under a debug flag.
12. A teammate compares embeddings generated by text-embedding-3-small against vectors made last year by an older model. What do you tell them?
The comparison is meaningless: every embedding model defines its own vector space, so distances across models are noise even when dimensions happen to match. The index must be pinned to one embedding deployment, and mixing generations silently degrades search quality — often without obvious errors. The fix is to re-embed the entire corpus with the current model before comparing, and to record the embedding model name alongside the index so this cannot happen unnoticed.
13. How do you make an app resilient to Azure OpenAI content filtering?
Treat a filtered outcome as a normal branch, not an exception path: detect the content-filter finish reason or error, show the user a clear non-technical message, log the event for review, and continue serving. Legitimate inputs occasionally trigger filters, so the UX must not look like a crash. On the input side, basic validation and prompt design reduce accidental triggers; on the process side, recurring false positives are worth reviewing against the filter configuration options available on the resource.
14. Describe how you would structure Azure OpenAI access in an ASP.NET Core application rather than a console app.
Register AzureOpenAIClient as a singleton in dependency injection — it is thread-safe and should share connections. Wrap it in a small service interface (e.g. IChatService) so controllers depend on an abstraction that is easy to mock in tests. Bind endpoint and deployment names from configuration via the options pattern; the credential comes from DefaultAzureCredential in production. Retry policy lives in the service (or a Polly pipeline), and token usage logging is middleware-adjacent so every call is metered uniformly.
15. What is jitter in a retry policy and why does it matter at scale?
Jitter is a small random addition to each backoff delay. Without it, many clients that failed at the same moment — say when a deployment hit its rate limit — all retry at exactly 1s, 2s, 4s, arriving in synchronized waves that re-saturate the service and fail together again (the thundering herd). Randomizing each client's delay spreads retries over time so capacity recovers. It costs one line of code and materially improves recovery behavior for any multi-instance deployment.

19 Glossary

Chat completion API
The Azure OpenAI operation that accepts a list of role-tagged messages and generates the assistant's next reply. Stateless between calls.
Embeddings API
The operation that converts text into a numeric vector representing its meaning, enabling similarity comparison and semantic search.
System message
The first message in a chat request, role 'system', defining the assistant's behavior, tone, and constraints.
Embedding
A fixed-length vector of floating-point numbers encoding the meaning of a text; similar texts produce nearby vectors.
Vector
An ordered array of numbers. Embedding vectors typically have hundreds to thousands of dimensions.
Cosine similarity
A measure of the angle between two vectors; values near 1 indicate very similar meaning. The standard metric for comparing embeddings.
Temperature
Sampling parameter controlling randomness of token selection: low = focused and repeatable, high = varied and creative.
Max tokens
The cap on tokens the model may generate in one response (MaxOutputTokenCount in the .NET SDK). Exceeding it truncates the reply.
Token
A small unit of text the model processes; both billing and rate limits are counted in tokens.
Finish reason
Response field explaining why generation ended: natural stop, max tokens truncation ('length'), or content filtering.
Deployment
A named instance of a model version inside an Azure OpenAI resource, carrying its own quota and rate limit; what client code targets.
Endpoint
The base URL of an Azure OpenAI resource to which all API requests for that resource are addressed.
API key
A secret string authenticating requests to a resource. Must be kept out of source control and rotated; replaceable by Managed Identity.
Rate limit
The requests-per-minute and tokens-per-minute caps on a deployment. Exceeding them returns HTTP 429.
Exponential backoff
Retry strategy where the wait doubles after each failed attempt, usually with random jitter and a bounded attempt count.
Transient fault
A temporary failure (429, 5xx) likely to succeed on retry, as opposed to permanent errors (400, 401, 404) that must be fixed.
User secrets
The .NET development-time secret store (dotnet user-secrets) keeping configuration values in the user profile, outside the repository.
Managed Identity
An Azure-managed identity an app uses to authenticate to services via role assignments — eliminating stored keys entirely.
Console app
A terminal-hosted .NET application (dotnet new console); in this tutorial, the host for sending a prompt to Azure OpenAI and displaying the response.

πŸ—’ My Notes