OpenAI Fine-Tuning JSONL Format
A reference for the JSONL file OpenAI's fine-tuning API expects — the record shape, every field that is allowed, the rules that get a file rejected, and how it differs from the batch format and from other providers' formats.
The record shape
One conversation per line. Each line is an object with a messages array, and nothing wraps
the lines — no enclosing array, no commas between records:
{"messages":[{"role":"system","content":"You are a terse assistant."},{"role":"user","content":"What is 2+2?"},{"role":"assistant","content":"4"}]}
{"messages":[{"role":"system","content":"You are a terse assistant."},{"role":"user","content":"Capital of France?"},{"role":"assistant","content":"Paris"}]}
That is the whole format for the common case. The pieces that matter:
messages— required, a non-empty array.role—system,user,assistant, andtoolfor tool results. Newer models also acceptdeveloperin place ofsystem.content— the text. Must not be empty on the assistant turn, which is what the model learns from.- The system prompt goes inside the array, as the first message. This is the difference from Anthropic's format that catches people most often.
- At least one assistant message per example, or there is nothing to train on.
Multi-turn conversations and the weight field
A conversation can have as many turns as fit the context window. By default every assistant message
contributes to the loss. The weight field controls that per message:
{"messages":[
{"role":"user","content":"Hi"},
{"role":"assistant","content":"Hello! How can I help?","weight":0},
{"role":"user","content":"Summarise this contract clause: ..."},
{"role":"assistant","content":"The clause limits liability to ...","weight":1}
]}
weight: 0 means "this turn is context, do not train on it";
weight: 1 is the default. Use it when earlier assistant turns exist only to set the scene —
training on a generic greeting teaches the model to produce generic greetings.
Tool calling
To teach a model when to call a function, include the tool definitions and an assistant turn containing
tool_calls rather than content:
{"messages":[
{"role":"user","content":"Weather in Kyiv?"},
{"role":"assistant","tool_calls":[{"id":"call_1","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"Kyiv\"}"}}]},
{"role":"tool","tool_call_id":"call_1","content":"{\"temp_c\":21}"},
{"role":"assistant","content":"It is 21 °C in Kyiv."}
],
"tools":[{"type":"function","function":{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}}}}}]}
Two details that trip people up: arguments is a JSON string, not an
object — so it is escaped inside the record — and every tool message needs a
tool_call_id matching the call it answers. Include the tools array in the
record so the model learns the signature alongside the usage.
Rules that get a file rejected
- Not valid JSON on some line. The most common cause, and usually a trailing comma or a pretty-printed record spread across lines. Validator, then Auto-fixer.
- A record with no assistant message. Nothing to learn from.
- Empty assistant
contentwith notool_calls. - An unrecognised role.
bot,human,gpt,model— all are other formats' conventions. The converter maps them. - An example over the model's context limit. Long examples are truncated rather than rejected, which is worse: the assistant turn gets cut and the model learns to stop mid-sentence. Check every example, not the average.
- Too few examples. There is a minimum (historically 10); the practical floor for useful results is higher.
Our OpenAI validator checks all of these locally, before you upload anything.
Not to be confused with the Batch API format
Both are JSONL and both go to OpenAI, and they are entirely different. Fine-tuning lines are training examples. Batch lines are API requests:
// Fine-tuning — a training example
{"messages":[{"role":"user","content":"hi"},{"role":"assistant","content":"hello"}]}
// Batch API — a request to run
{"custom_id":"req-1","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}}
Uploading one where the other is expected produces a confusing error. The tell is the first key:
{"messages" for training, {"custom_id" for batch. Validate them with the
fine-tune validator and the
batch validator respectively.
Differences from other providers
| Provider | System prompt | Assistant role | Content shape |
|---|---|---|---|
| OpenAI | First message in messages | assistant | String |
| Anthropic | Top-level system key | assistant | String or content blocks |
| Gemini | systemInstruction | model | parts array |
| ShareGPT | from: "system" entry | gpt | value string |
| Alpaca | Optional system | n/a — output | Single turn only |
The chat format converter moves between all of these, including the system-prompt relocation that is the most common manual mistake. Validate with the provider-specific validator afterwards — the shape being right does not mean the provider's extra rules are satisfied.
Frequently asked questions
How many examples do I need?
The API has a low minimum (historically 10) and the practical floor for a useful result is in the hundreds. Quality matters far more than count: a few hundred carefully-reviewed examples routinely beat thousands of scraped ones.
Should the system prompt be in every example?
If you will send one at inference, yes, and keep it identical. Inconsistency teaches the model the prompt is optional. The system-prompt deduper reports how many distinct system prompts a file contains.
Can I mix single-turn and multi-turn examples?
Yes. Both are valid and mixing them is common. Just make sure every example ends on an assistant turn with real content.
Does the file need a validation split?
It is optional — you can upload a separate validation file. If you do, check for overlap with the leakage detector first, or the reported validation loss will be optimistic.
Is the format stable across models?
The messages shape has been stable for a long time. Specifics — context limits, whether developer is accepted in place of system, tool-calling details — vary by model, so check the current docs for the model you are targeting.
Tools mentioned on this page
— S., [email protected]