By Sagar Shankaran, Founder of CallSphere
A complete walkthrough of fine-tuning models through the OpenAI API, covering data preparation in JSONL format, file upload, training job creation, evaluation, and deploying your custom model.
Key takeaways
The OpenAI fine-tuning API lets you train a customized version of GPT-4o-mini, GPT-4o, or other supported models on your own data. The result is a model that behaves the way you want without requiring a long system prompt on every request. OpenAI manages the training infrastructure, GPU allocation, and model hosting. You provide the training data and configuration.
Fine-tuning through the API is particularly valuable when you need consistent output formatting, domain-specific terminology, or a particular reasoning style that few-shot prompting cannot reliably achieve.
Training data must be in JSONL format where each line is a JSON object containing a messages array. Each message has a role (system, user, or assistant) and content field.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
DATA[("Curated dataset<br/>instruction or chat")]
CLEAN["Clean and dedupe<br/>PII filter"]
TOK["Tokenize and pack"]
METHOD{"Method"}
LORA["LoRA or QLoRA<br/>adapters only"]
SFT["Full SFT<br/>all params"]
DPO["DPO or RLHF<br/>preference learning"]
EVAL["Held out eval<br/>plus regression suite"]
DEPLOY[("Adapter or<br/>merged model")]
DATA --> CLEAN --> TOK --> METHOD
METHOD --> LORA --> EVAL
METHOD --> SFT --> EVAL
METHOD --> DPO --> EVAL
EVAL --> DEPLOY
style METHOD fill:#4f46e5,stroke:#4338ca,color:#fff
style EVAL fill:#f59e0b,stroke:#d97706,color:#1f2937
style DEPLOY fill:#059669,stroke:#047857,color:#fff
import json
training_examples = [
{
"messages": [
{
"role": "system",
"content": "You are a medical coding assistant. Output ICD-10 codes in the format CODE: DESCRIPTION."
},
{
"role": "user",
"content": "Patient presents with acute bronchitis."
},
{
"role": "assistant",
"content": "J20.9: Acute bronchitis, unspecified"
}
]
},
{
"messages": [
{
"role": "system",
"content": "You are a medical coding assistant. Output ICD-10 codes in the format CODE: DESCRIPTION."
},
{
"role": "user",
"content": "Diagnosis: Type 2 diabetes with diabetic chronic kidney disease, stage 3."
},
{
"role": "assistant",
"content": "E11.22: Type 2 diabetes mellitus with diabetic chronic kidney disease\nN18.3: Chronic kidney disease, stage 3 (moderate)"
}
]
},
]
# Write to JSONL
with open("training_data.jsonl", "w") as f:
for example in training_examples:
f.write(json.dumps(example) + "\n")
Before uploading, validate that every line parses correctly and follows the expected schema. OpenAI provides a data preparation utility, but you can also validate manually.
import json
def validate_training_file(filepath: str) -> dict:
errors = []
valid_count = 0
with open(filepath, "r") as f:
for line_num, line in enumerate(f, 1):
try:
data = json.loads(line)
except json.JSONDecodeError:
errors.append(f"Line {line_num}: Invalid JSON")
continue
if "messages" not in data:
errors.append(f"Line {line_num}: Missing 'messages' key")
continue
messages = data["messages"]
roles = [m.get("role") for m in messages]
if "assistant" not in roles:
errors.append(f"Line {line_num}: No assistant message")
continue
for msg in messages:
if "content" not in msg or not msg["content"].strip():
errors.append(f"Line {line_num}: Empty content in {msg.get('role')}")
continue
valid_count += 1
return {
"total_lines": line_num,
"valid": valid_count,
"errors": errors[:20],
}
result = validate_training_file("training_data.jsonl")
print(f"Valid examples: {result['valid']}/{result['total_lines']}")
from openai import OpenAI
client = OpenAI()
# Upload training file
training_file = client.files.create(
file=open("training_data.jsonl", "rb"),
purpose="fine-tune",
)
print(f"File ID: {training_file.id}")
# Output: File ID: file-abc123...
# Optionally upload a validation file
validation_file = client.files.create(
file=open("validation_data.jsonl", "rb"),
purpose="fine-tune",
)
job = client.fine_tuning.jobs.create(
training_file=training_file.id,
validation_file=validation_file.id,
model="gpt-4o-mini-2024-07-18",
hyperparameters={
"n_epochs": 3,
"batch_size": "auto",
"learning_rate_multiplier": "auto",
},
suffix="medical-coder", # Custom name suffix
)
print(f"Job ID: {job.id}")
print(f"Status: {job.status}")
The suffix parameter adds a custom label to your model name, making it easy to identify: ft:gpt-4o-mini-2024-07-18:your-org:medical-coder:abc123.
import time
def monitor_job(client, job_id: str, poll_interval: int = 30):
while True:
job = client.fine_tuning.jobs.retrieve(job_id)
print(f"Status: {job.status}")
if job.status == "succeeded":
print(f"Fine-tuned model: {job.fine_tuned_model}")
return job.fine_tuned_model
if job.status == "failed":
print(f"Error: {job.error}")
return None
# List recent events
events = client.fine_tuning.jobs.list_events(
fine_tuning_job_id=job_id, limit=5
)
for event in events.data:
print(f" [{event.created_at}] {event.message}")
time.sleep(poll_interval)
model_name = monitor_job(client, job.id)
Once training succeeds, use the fine-tuned model exactly like any other OpenAI model.
response = client.chat.completions.create(
model=model_name, # ft:gpt-4o-mini-2024-07-18:your-org:medical-coder:abc123
messages=[
{
"role": "system",
"content": "You are a medical coding assistant. Output ICD-10 codes in the format CODE: DESCRIPTION."
},
{
"role": "user",
"content": "Patient diagnosed with essential hypertension and hyperlipidemia."
},
],
temperature=0.0,
)
print(response.choices[0].message.content)
# I10: Essential (primary) hypertension
# E78.5: Hyperlipidemia, unspecified
Always compare your fine-tuned model against the base model on a held-out test set.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
import json
def evaluate_model(client, model: str, test_file: str) -> dict:
correct = 0
total = 0
with open(test_file, "r") as f:
for line in f:
example = json.loads(line)
messages = example["messages"]
expected = messages[-1]["content"]
prompt = messages[:-1]
response = client.chat.completions.create(
model=model,
messages=prompt,
temperature=0.0,
)
predicted = response.choices[0].message.content.strip()
if predicted == expected:
correct += 1
total += 1
return {"model": model, "accuracy": correct / total, "total": total}
base_results = evaluate_model(client, "gpt-4o-mini", "test_data.jsonl")
ft_results = evaluate_model(client, model_name, "test_data.jsonl")
print(f"Base model accuracy: {base_results['accuracy']:.1%}")
print(f"Fine-tuned accuracy: {ft_results['accuracy']:.1%}")
Training costs depend on the model and the number of tokens in your training data. For GPT-4o-mini, training costs approximately $3.00 per million tokens. A dataset of 500 examples at 500 tokens each totals about 250K tokens per epoch — roughly $0.75 per epoch. With 3 epochs, that is about $2.25 total for training. Inference on fine-tuned models costs the same as the base model.
Most fine-tuning jobs complete in 15 minutes to 2 hours, depending on dataset size and the number of epochs. Smaller datasets with 3 epochs typically finish in under 30 minutes. The OpenAI platform queues jobs, so there may be additional wait time during peak demand.
Yes. You can use a previously fine-tuned model as the base for a new fine-tuning job. This is useful for iterative improvement — train on your initial dataset, evaluate, then fine-tune again on a curated set of examples where the model performed poorly. Just reference the fine-tuned model ID as the model parameter.
#OpenAI #FineTuning #CustomModels #API #GPT #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Using multiple chat AIs at once is a real 2026 workflow. Here is when it makes sense, how to set it up, and how CallSphere handles multi-model routing.
OpenAI's Frontier platform makes model-native orchestration the default. What that means for agent builders, voice/chat buyers, and the build-vs-buy decision.
The 2026 desktop AI agent landscape — ServiceNow Project Arc, Anthropic Claude offerings, OpenAI agents, and Google Mariner. A buyer's map.
May 2026's biggest agent-architecture shift: planning, tool selection, and self-correction move inside the model. Framework code shrinks. Here is what changes.
A three-way comparison of Gemini Enterprise, Anthropic managed agents and OpenAI Frontier Platform after Cloud Next 2026 — strengths, gaps, buyer fit.
Anthropic's May 2026 push positions Claude as a vertical platform for financial services. The strategic positioning versus OpenAI and Google.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI