Key takeaways
- Fine-tuning on Azure OpenAI means one of three methods, SFT, DPO, or RFT, and it runs on LoRA under the hood, so a training job is cheaper and faster than a full weight update.
- gpt-4.1, gpt-4.1-mini and gpt-4.1-nano take SFT and DPO. o4-mini takes RFT. Pick the method from the problem, not the model you happen to like.
- Training is billed per token and capped per job. The number that surprises people is hosting, which bills by the hour whether or not anyone calls the model.
- Start with about 50 clean examples on a mini model. Reach for DPO or RFT only after plain SFT stops improving.
A fine-tuned model you deployed and forgot about is not free while it sits idle. Azure bills the hosting of a fine-tune by the hour, from the moment the deployment exists, whether it serves a million requests that day or none. At the published rate of roughly 1.70 to 3.00 dollars an hour, one idle deployment runs about 1,200 to 2,200 dollars a month for answering nothing. [AUTHOR: add anecdote about an idle fine-tuned deployment that quietly ran up a bill]. So before any of the how-to: the training run is the cheap part, and the deployment is the meter you forget to read.
What fine-tuning changes, and when to skip it
Fine-tuning trains a base model on your own labeled examples so its weights adapt to your task. In contrast to few-shot prompting, where you show a handful of examples inside every request, fine-tuning learns from far more examples than could ever fit in a prompt. Once the weights hold your pattern, you send fewer instructions per call, which cuts tokens and often trims latency. Azure does this with LoRA, low-rank adaptation, which trains a small subset of parameters instead of every weight. That is why a job finishes in hours, not days, and costs a fraction of a full retrain.
Here is the part most teams get wrong. Fine-tuning does not teach the model new facts reliably. If the gap is knowledge, last quarter numbers, an internal policy, a product catalog, that belongs in retrieval, not weights. Feeding facts through training bakes them in stale and gives you no citation trail. Fine-tuning earns its keep when the gap is behavior: a fixed output shape, a house tone, a classification the base model keeps fumbling, a format that prompting nudges but never nails. If prompt engineering gets you 90 percent of the way, try that harder first. Reach for training when the last stubborn 10 percent is consistency, not knowledge.
SFT, DPO, and RFT, and how they differ
Azure OpenAI gives you three customization methods, and which ones you can use depends on the model you pick. They are not interchangeable, and choosing wrong wastes a training run.
Supervised fine-tuning (SFT) trains on labeled input and output pairs. You give the prompt and the exact answer you want back. This is the workhorse and where almost everyone should start. It handles task specialization, output format, and tone.
Direct preference optimization (DPO) aligns the model to preferred answers. Instead of one correct output, you supply a chosen response and a rejected one for the same prompt. It adjusts weights toward what people prefer without training a separate reward model, so it is lighter and faster than classic RLHF. Reach for DPO when quality is a matter of taste you can rank but cannot pin down as a single right answer.
Reinforcement fine-tuning (RFT) optimizes against reward signals from graders that score each output, so it fits complex or multi-step behavior you can measure programmatically. On Azure it runs on o4-mini. It is the most involved of the three and the one to save for when SFT and DPO have both fallen short.
| Method | What you provide | On which models | Reach for it when |
|---|---|---|---|
| SFT | Prompt and the exact target answer | gpt-4.1, 4.1-mini, 4.1-nano | Format, tone, task specialization. Start here. |
| DPO | Chosen vs rejected answer per prompt | gpt-4.1, 4.1-mini, 4.1-nano | Preference and style you can rank, not label exactly |
| RFT | Grader that scores each output | o4-mini | Complex behavior you can measure with a reward |
One more note on availability worth checking before you plan: Microsoft has said GPT-5 reinforcement fine-tuning reached general availability, but access is gated and invitation only, so do not build a roadmap around it until your subscription actually has it. [VERIFY: GPT-5 RFT invitation gating, confirm current status]
Which models you can fine-tune right now
The fine-tunable set is smaller than the full model catalog, and the region list matters because a model you can train in one region is simply absent in another. As of July 2026 the current generation looks like this.
| Model | Standard regions | Global | Developer | Methods | Modality |
|---|---|---|---|---|---|
| gpt-4.1 | North Central US, Sweden Central | Yes | Yes | SFT, DPO | Text and vision to text |
| gpt-4.1-mini | North Central US, Sweden Central | Yes | Yes | SFT, DPO | Text to text |
| gpt-4.1-nano | North Central US, Sweden Central | Yes | Yes | SFT, DPO | Text to text |
| o4-mini | East US2, Sweden Central | Yes | No | RFT | Text to text |
A few things that trip people up. The gpt-4.1 family trains only in North Central US and Sweden Central, so if your Foundry resource lives in East US you either create a resource in a supported region or use Global training, which runs the job on capacity outside your region. Older gpt-4o and gpt-4o-mini remain fine-tunable, and if you need open weight models like Llama, Phi, or Mistral, those train through Azure Machine Learning managed compute rather than the Azure OpenAI path, which is its own workflow I cover in Part 5. The AWS equivalent of this decision, custom models on Bedrock versus SageMaker, follows the same logic and I walk it in the Bedrock fine-tuning part.
How your training file has to look
Training and validation data go in as JSON Lines, one JSON object per line, formatted as a chat conversation exactly like the Chat Completions API expects. Each line carries a messages array with system, user, and assistant turns. Two format rules bite people who skip the docs: the file must be UTF-8 encoded with a byte-order mark (BOM), and each file must be under 512 MB. A validation pass runs on upload and rejects anything that fails those checks.
On volume, a job will not start with fewer than 10 examples, but 10 changes nothing you would notice. Start with about 50 well-crafted examples, and treat hundreds to low thousands as the real target for a model you plan to ship. Doubling a clean dataset tends to lift quality roughly linearly, but a pile of low-quality examples drags the model below the base you started from, so prune before you train. For multi-turn data you can mark specific assistant turns with a weight of 0 to exclude them from the loss and 1 to keep them, which is handy when only the final answer in a conversation is the one you want the model to imitate.
Here is a small SFT file and a script that validates it before you spend a token, then submits the job.
# train.jsonl (one object per line, chat format)
{"messages": [{"role": "system", "content": "You are a terse support triage bot."}, {"role": "user", "content": "my invoice is wrong"}, {"role": "assistant", "content": "category: billing | priority: P2"}]}
{"messages": [{"role": "system", "content": "You are a terse support triage bot."}, {"role": "user", "content": "the app keeps crashing on launch"}, {"role": "assistant", "content": "category: bug | priority: P1"}]}import json
from openai import AzureOpenAI
path = 'train.jsonl'
n = 0
# utf-8-sig reads through the BOM Azure needs on the file itself
with open(path, 'r', encoding='utf-8-sig') as f:
for line in f:
line = line.strip()
if not line:
continue
row = json.loads(line)
assert 'messages' in row, 'every line needs a messages key'
n += 1
assert n >= 10, 'Azure rejects jobs with fewer than 10 examples'
print(f'{n} examples look valid')
client = AzureOpenAI(api_version='2025-04-01-preview')
up = client.files.create(file=open(path, 'rb'), purpose='fine-tune')
job = client.fine_tuning.jobs.create(
training_file=up.id,
model='gpt-4.1-mini-2025-04-14',
method={'type': 'supervised'},
)
print(job.id, job.status)Expected output: 512 examples look valid then a job id and a status of pending or queued. Failure mode: if you export the JSONL from a tool that writes plain UTF-8 with no BOM, the local script still passes but the Azure upload validation fails the job. Re-save with a BOM. API version shown is one recent preview and moves often. [VERIFY: current fine-tuning API version]
Standard, Global, or Developer training
When you submit a job you choose a training type, and it is a genuine tradeoff between data residency, price, and queue time. This is separate from where you later deploy the model for inference.
Standard trains inside your Foundry resource region and guarantees data residency. Use it when data must stay in a specific region for compliance.
Global is cheaper because it uses capacity beyond your region. Your data and the resulting weights are copied to wherever the job runs, and queue times are usually shorter. Use it when residency is not a hard rule and you want the lower price.
Developer, in preview, is the cheapest and runs on idle capacity. There is no latency or SLA guarantee and no residency guarantee, and a job can be preempted and resumed later. Use it for experiments and price-sensitive runs where you do not care when it finishes.
Gotcha
The BOM requirement is the single most common reason a first job fails. Editors and export scripts default to plain UTF-8. On Windows PowerShell, Set-Content -Encoding utf8BOM writes it correctly. In Python, open the file for writing with encoding='utf-8-sig'. Check this before you blame your data.
Hyperparameters worth touching
On the first run, leave the hyperparameters on their defaults. Azure sets each of them to minus one, which means it picks a value from your data. You only reach in after you have seen a loss curve and have a reason. Three knobs are worth knowing.
n_epochs is how many full passes over the data the run makes. Set to minus one it is chosen for you. If the model overfits, fewer epochs is the first lever.
learning_rate_multiplier scales the training learning rate. Microsoft suggests experimenting between 0.02 and 0.2. A smaller value helps avoid overfitting on small datasets.
batch_size defaults to about 0.2 percent of your training set when left at minus one, up to a maximum of 256. Larger batches suit larger datasets and update weights less often but with less noise. There is also a seed you can pass so the same data and settings reproduce the same run, which is the difference between a controlled experiment and guessing.
What a fine-tuning run actually costs
Two meters run here, and they are very different sizes. Training is billed per token of training data, roughly 1.50 to 25 dollars per million tokens depending on the model. A single job is capped at 5,000 dollars: when a run hits that ceiling it pauses and hands you a deployable checkpoint rather than billing past it. Hosting is the other meter, and it bills a flat 1.70 to 3.00 dollars per hour for a deployed fine-tune regardless of traffic, which lands around 1,224 to 2,160 dollars a month. Inference tokens on top are priced the same as the base model. [VERIFY: per-model training rates and current hosting range on the Azure OpenAI pricing page]
Worked example
Say you train gpt-4.1-mini on 500 examples that average 800 tokens each, for 3 epochs. That is 500 × 800 × 3 = 1,200,000 training tokens, about 1.2 million. At an assumed 8 dollars per million that run costs near 10 dollars. Now deploy the result and leave it up for a month: 730 hours × 1.70 dollars = about 1,241 dollars. The training you agonized over cost ten dollars. The deployment you forgot about cost more than a hundred times that. Delete idle deployments.
Reading the metrics and picking a checkpoint
While a job runs you watch two numbers. full_valid_loss is the validation loss at the end of each epoch, and it should fall. full_valid_mean_token_accuracy is validation token accuracy, and it should rise. When training and validation curves separate, the training loss dropping while validation flattens or climbs, you are overfitting. A checkpoint is saved at the end of every epoch, and when a job finishes you can deploy any of the three most recent. The value of that is picking the checkpoint from just before the model started memorizing rather than the final one.
Deploying, then fine-tuning the fine-tune
When a job succeeds you deploy the model like any other, and you can turn on automatic deployment so a successful run deploys itself. Inference then bills at base-model token rates plus the hourly hosting fee. If results are close but not there, you do not start over. Continuous fine-tuning lets you pick your fine-tuned model as the base for a fresh job and train it further on new examples, which is how you fold in edge cases you only found in production. When you are done, delete the deployment and the model, and clean up the uploaded training files, so the hourly meter stops.
Start with SFT on 4.1-mini, reach for DPO later
Here is what I actually do. First run: SFT on gpt-4.1-mini, about 50 to a few hundred clean examples, Global training for the price, defaults on every hyperparameter, and a held-out validation split so the loss curve means something. That covers the large majority of real cases, format, tone, and narrow task behavior, at the lowest cost and the smallest model I can get away with. I only move up to gpt-4.1 when the mini genuinely cannot hold the task, and I only reach for DPO when the problem is preference I can rank rather than a target I can label. RFT on o4-mini is the last resort, for behavior I can score with a grader.
When not to do any of this: if the model is missing facts, that is retrieval, not training, and if you need one clever output shape, spend another hour on the prompt first. What to validate before you trust a fine-tune: run your own evaluation against the base model on data the training never saw, and price out a full month of hosting before you deploy, because that number, not the training run, is what shows up on the bill. Your next call is small: take 50 of your best examples, save them as JSONL with a BOM, and run one SFT job on 4.1-mini this week.
References
- Customize a model with fine-tuning, Microsoft Foundry docs
- Direct preference optimization, Microsoft Foundry docs
- Azure OpenAI Service pricing
- Announcing new fine-tuning models and techniques in Azure AI Foundry


DrJha