A team is moving from text only SFT to OpenAI's hosted fine-tuning API for a tool calling model (e.g. gpt-5.5 class). They have working multi-turn JSONL but the API rejects half their rows once they add tool calls. Specify the extra fields a tool aware row needs, both on the assistant turn that issues the call and on the tool response that follows, and explain which field links them. What breaks if the linkage is wrong or missing?
The assistant turn carries a tool_calls array with unique id, type, name, and JSON-string arguments. The next turn uses role tool with a matching tool_call_id, the linkage that survives parallel calls.
Picture a busy kitchen where the head chef shouts orders to the prep station. Every order gets its own ticket number stuck to it: order forty-one, two onions diced; order forty-two, one carrot grated. When the prep returns, they staple the chopped food to a matching ticket so the head chef knows what came back from where. If you forget the ticket, or two tickets share a number, the chef ends up putting onions where carrots should go. The hosted fine-tuning API works the same way. Each tool call carries its own ticket, the response carries the same ticket back, and the system uses tickets to keep everything paired even when several orders fly at once.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Moving from text-only SFT to OpenAI's hosted fine-tuning for a tool-calling model is one of those migrations that looks small on paper and breaks half your rows in practice. The reason is that the chat-completions schema changes shape in three places once tool calls appear, and the schema is strict. Rows that worked fine for plain conversational SFT get rejected the moment they include a function call, because two new fields become mandatory, one existing field changes type, and a brand-new role appears in the conversation.
The API is strict for good reasons. The shape it enforces at training time is the same shape the model emits and consumes at inference time. Any mismatch between training and inference shape would create a distribution gap the model would feel, so the API refuses to let you train on rows that diverge from the runtime contract.
This deep dive walks every required change in the row shape, explains what each field does, and covers the failure modes you will hit if you skip any of them. There are two distinct failure classes worth distinguishing: hard failures that the API catches at upload time and you fix in minutes, and soft failures that train cleanly but corrupt the model's grounding behaviour at inference, where you discover them only after production starts mismatching tool results.
The assistant turn shape changes
In text-only SFT the assistant turn carries a single content field with the free-text reply. In tool-calling SFT the assistant turn that issues a call abandons free-text content entirely and instead carries a tool_calls array.
Each entry in tool_calls is an object with three required keys. The id is a unique string for this call, conventionally call_ followed by a UUID. The type is the literal string function (the API reserves the type field for future extensibility, but function is the only supported value today). The function object holds two fields: name, the function being called, and arguments, the serialised parameters.
The single detail that catches most teams on the first migration is that arguments is a JSON string, not a nested JSON object. The API is strict here. A row that passes arguments as a JSON object, even if the object is well-formed, will be rejected at upload time. The string requirement exists because the OpenAI inference API emits arguments as a string for the client SDK to parse, and the training and inference shapes have to match.
The call turn should carry an empty (or null) content alongside the tool_calls array. The convention is that a call turn does one thing, it commits to a tool invocation. The natural-language reply belongs on a separate assistant turn that comes after the tool result. Some validators reject rows that mix tool_calls with non-empty content; all OpenAI internal tooling expects the split.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
Real products, models, and research that use this idea.
- OpenAI's hosted fine-tuning for gpt-5.5 family enforces this shape exactly and rejects rows at upload when arguments is nested or tool_call_id is missing.
- OpenAI's Python SDK ChatCompletion examples emit ids as call_<uuid> and pass arguments as a JSON string, the same convention training data must follow.
What an interviewer would ask next. Try answering before peeking at the approach.
QWhy does the OpenAI spec keep arguments as a JSON string rather than a nested object?
Talk about training and inference symmetry. At inference the model emits arguments as a string for the SDK to parse. Encoding the same shape during training removes a distribution gap between fine-tune data and runtime output, which would otherwise show up as malformed argument structures.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Passing arguments as a nested JSON object instead of a JSON string. The OpenAI spec is strict here, arguments must be a string, and rows that nest the object are rejected at upload time.
60 second bullets to scan on the way to the call.
Three required keys on each tool_calls entry and why each is mandatory
Why arguments is a JSON string and not a nested object
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.