
Learn how to fine-tune AI models like GPT and call them from C# with the OpenAI .NET SDK. Step-by-step code, best practices — start building today.
Fine-tuning AI models is one of the most searched-for skills in modern software development — and for good reason. A fine-tuned model can outperform a much larger general-purpose model on your specific task, respond faster, and cost less per request. In this full guide, you'll learn what fine-tuning actually does, when you should (and shouldn't) use it, how to prepare training data, how to run a fine-tuning job on the OpenAI platform, and — most importantly for us — how to call your custom model from a C# application using the official OpenAI .NET SDK.
Everything here is practical and runnable with .NET 8 or later. Whether you're a beginner searching "how to fine-tune GPT in C#" or a senior engineer evaluating fine-tuning versus RAG for a production system, this guide covers the full workflow end to end.
What Is Fine-Tuning, and Why Should C# Developers Care?
Fine-tuning takes a pre-trained foundation model (like gpt-4o-mini) and continues training it on your own examples. The base model already understands language, code, and reasoning — fine-tuning teaches it your specific behavior: your tone of voice, your output format, your domain terminology, your classification labels.
The "why" matters more than the "how" here. Developers often reach for fine-tuning when they actually need something else, so let's be precise about what fine-tuning is good at:
- Consistent style and format — e.g., always returning a specific JSON shape, writing in your brand's voice, or following a strict output template without a 2,000-token prompt.
- Classification and extraction at scale — a fine-tuned small model can match a frontier model on a narrow task at a fraction of the cost and latency.
- Reliability on edge cases — behaviors that are hard to describe in a prompt but easy to demonstrate with 50–100 examples.
- Shorter prompts — instructions get "baked in," cutting token costs on every single request.
And what fine-tuning is not good at: teaching the model new facts. If you need the model to answer questions about your product docs or database, use retrieval-augmented generation (RAG) instead — fetch the relevant data and put it in the prompt. Fine-tuning changes behavior; RAG supplies knowledge. Many production systems use both.
Step 1: Prepare Your Training Data (JSONL Format)
Fine-tuning on the OpenAI platform requires a JSONL file — one JSON object per line, each containing a complete example conversation. This is the single most important step: the quality of your training data determines the quality of your custom AI model. Fifty excellent examples beat five thousand mediocre ones.
Each line looks like this (shown formatted for readability — in the real file, each example is one line):
{"messages": [
{"role": "system", "content": "You classify customer support tickets."},
{"role": "user", "content": "My invoice from March shows a double charge."},
{"role": "assistant", "content": "{\"category\":\"billing\",\"priority\":\"high\"}"}
]}
Rather than hand-writing JSONL (and fighting escaping bugs), generate it from C#. Here's a small console utility that serializes training examples correctly:
using System.Text.Json;
var options = new JsonSerializerOptions
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
var examples = new List<TrainingExample>
{
new(
System: "You classify customer support tickets into JSON with 'category' and 'priority'.",
User: "My invoice from March shows a double charge.",
Assistant: """{"category":"billing","priority":"high"}"""),
new(
System: "You classify customer support tickets into JSON with 'category' and 'priority'.",
User: "How do I change my profile picture?",
Assistant: """{"category":"account","priority":"low"}""")
// Add at least 10 examples (the platform minimum); aim for 50-100+
};
await using var writer = new StreamWriter("training-data.jsonl");
foreach (var ex in examples)
{
var line = JsonSerializer.Serialize(new
{
messages = new object[]
{
new { role = "system", content = ex.System },
new { role = "user", content = ex.User },
new { role = "assistant", content = ex.Assistant }
}
}, options);
await writer.WriteLineAsync(line);
}
Console.WriteLine($"Wrote {examples.Count} examples to training-data.jsonl");
record TrainingExample(string System, string User, string Assistant);
Why this matters: the system message you train with should be the same one you use at inference time. If they differ, you're training the model for a conversation shape it will never see in production, and quality drops noticeably. Also hold back 10–20% of your examples as a validation file — you'll pass it to the fine-tuning job to detect overfitting.
Step 2: Fine-Tuning AI Models from C# Code
You can run fine-tuning jobs from the OpenAI dashboard, but doing it in code makes the process repeatable — essential once you retrain regularly. Install the official SDK:
dotnet add package OpenAI
The fine-tuning workflow has three phases: upload the file, create the job, and poll until it finishes. Here's the complete flow:
using OpenAI;
using OpenAI.Files;
using OpenAI.FineTuning;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
?? throw new InvalidOperationException("Set the OPENAI_API_KEY environment variable.");
var client = new OpenAIClient(apiKey);
// Phase 1: Upload the training file
var fileClient = client.GetOpenAIFileClient();
var uploadedFile = await fileClient.UploadFileAsync(
"training-data.jsonl",
FileUploadPurpose.FineTune);
Console.WriteLine($"Uploaded file: {uploadedFile.Value.Id}");
// Phase 2: Create the fine-tuning job
var ftClient = client.GetFineTuningClient();
var operation = await ftClient.FineTuneAsync(
baseModel: "gpt-4o-mini-2024-07-18",
trainingFileId: uploadedFile.Value.Id,
waitUntilCompleted: false);
Console.WriteLine($"Job started: {operation.JobId}");
// Phase 3: Poll until the job completes (typically 10-60 minutes)
while (!operation.HasCompleted)
{
await Task.Delay(TimeSpan.FromSeconds(30));
await operation.UpdateStatusAsync();
Console.WriteLine($"Status: {operation.Status}");
}
Console.WriteLine($"Fine-tuned model ID: {operation.Value.FineTunedModel}");
// Example: ft:gpt-4o-mini-2024-07-18:my-org:ticket-classifier:AbCd1234
The resulting model ID (starting with ft:) is what you'll use for every future request. Store it in configuration, not in code — you'll retrain and get new IDs over time.
Why choose a small base model? Fine-tuning shines when it lets a cheap, fast model do a job that previously needed a large one. A fine-tuned gpt-4o-mini on a narrow classification task frequently matches a frontier model's accuracy while costing an order of magnitude less per call — that's the economic argument that justifies the effort.
Step 3: Call Your Fine-Tuned Model from a C# Application
Calling a fine-tuned model is identical to calling a base model — you just pass your custom model ID. Here's a clean, production-shaped service class:
using OpenAI.Chat;
using System.Text.Json;
public class TicketClassifier
{
private readonly ChatClient _chat;
// Same system prompt you trained with — consistency matters
private const string SystemPrompt =
"You classify customer support tickets into JSON with 'category' and 'priority'.";
public TicketClassifier(string apiKey, string fineTunedModelId)
{
_chat = new ChatClient(fineTunedModelId, apiKey);
}
public async Task<TicketResult> ClassifyAsync(
string ticketText, CancellationToken ct = default)
{
var messages = new ChatMessage[]
{
new SystemChatMessage(SystemPrompt),
new UserChatMessage(ticketText)
};
var options = new ChatCompletionOptions
{
Temperature = 0f, // deterministic output for classification
ResponseFormat = ChatResponseFormat.CreateJsonObjectFormat()
};
ChatCompletion completion =
await _chat.CompleteChatAsync(messages, options, ct);
var json = completion.Content[0].Text;
return JsonSerializer.Deserialize<TicketResult>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? throw new InvalidOperationException("Model returned invalid JSON.");
}
}
public record TicketResult(string Category, string Priority);
And wiring it into an ASP.NET Core minimal API with dependency injection:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(sp => new TicketClassifier(
apiKey: builder.Configuration["OpenAI:ApiKey"]!,
fineTunedModelId: builder.Configuration["OpenAI:FineTunedModel"]!));
var app = builder.Build();
app.MapPost("/classify", async (TicketRequest req, TicketClassifier classifier) =>
{
var result = await classifier.ClassifyAsync(req.Text);
return Results.Ok(result);
});
app.Run();
record TicketRequest(string Text);
Notice two deliberate choices. First, Temperature = 0: for classification and extraction you want repeatable answers, not creativity. Second, JSON response format plus deserialization into a typed record — in C# we get compile-time safety on the model's output shape, which is exactly where strongly typed languages earn their keep in AI applications.
Using Azure OpenAI Instead
Many enterprise .NET teams deploy through Azure OpenAI for compliance and regional data residency. The same code works — swap the client construction for the Azure.AI.OpenAI package and point it at your deployment name:
using Azure.AI.OpenAI;
using System.ClientModel;
var azureClient = new AzureOpenAIClient(
new Uri("https://your-resource.openai.azure.com/"),
new ApiKeyCredential(azureKey));
// The deployment name of your fine-tuned model in Azure
var chat = azureClient.GetChatClient("ticket-classifier-deployment");
Fine-tuned Azure deployments carry an hourly hosting fee on top of token costs, so they make sense at sustained volume rather than for experiments — that's a common budgeting surprise worth knowing before you commit.
Best Practices for Fine-Tuning AI Models in Production
- Establish a baseline first. Before fine-tuning, measure how far you can get with prompt engineering and few-shot examples on the base model. If a good prompt gets you to 95% accuracy, fine-tuning may not be worth the operational overhead.
- Build an evaluation set before training. Keep 50+ held-out examples with known correct answers and score every model version against them in a unit-test-style harness. Without evals, you cannot tell whether a retrain improved or regressed.
- Version everything. Training data, base model version, and resulting model ID belong together in source control (the data) and configuration (the IDs), so you can reproduce or roll back any deployment.
- Handle transient failures. Wrap API calls with retry logic (Polly is the idiomatic .NET choice) for 429 rate limits and transient 5xx errors, with exponential backoff.
- Keep secrets out of code. Use environment variables, user secrets in development, and a vault (Azure Key Vault, AWS Secrets Manager) in production. Never commit an API key.
Common Pitfalls to Avoid
- Fine-tuning to inject knowledge. The most common mistake. The model will confidently hallucinate facts it was never reliably taught. Use RAG for knowledge; fine-tune for behavior.
- Inconsistent system prompts between training and inference, which silently degrades quality.
- Too few or too-noisy examples. Ten examples is the platform minimum, not a recommendation. Contradictory labels in your training data teach the model to be inconsistent.
- Trusting model output blindly. Even at temperature 0, always validate deserialized output (e.g., check the category is one of your known labels) before acting on it.
- Forgetting base-model deprecations. Fine-tuned models are pinned to a base snapshot. When that snapshot retires, you must retrain — another reason your data pipeline should be a runnable program, not a one-off script.
Conclusion: Key Takeaways
Fine-tuning AI models and calling them from C# is a workflow every .NET developer can master with the official OpenAI .NET SDK: prepare high-quality JSONL training data, run the fine-tuning job in code so it's repeatable, then call the resulting ft: model exactly like any other chat model — with temperature 0 and typed JSON output for reliability.
- Fine-tune for behavior (format, style, classification); use RAG for knowledge.
- Data quality beats data quantity — 50 excellent examples outperform thousands of noisy ones.
- Keep training and inference system prompts identical.
- A fine-tuned small model often matches a large model on narrow tasks at a fraction of the cost.
- Build evals before you train, and version your data and model IDs like any other release artifact.
Start small: pick one narrow, high-volume task in your application, collect 50 real examples, and run your first fine-tuning job this week. Once you see a cheap custom model matching your best-prompted large model, the economics of fine-tuning AI models in C# speak for themselves.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment