Tool-Calling Fine-Tuning
Soup v0.25.0 adds an end-to-end pipeline for training models that call functions.
Data format
A new tool-calling format was added to soup_cli/data/formats.py:
{
"messages": [{"role": "user", "content": "What's the weather in Tokyo?"}],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
}
}
],
"tool_calls": [
{"function": {"name": "get_weather", "arguments": "{\"city\": \"Tokyo\"}"}}
]
}The example above is the single-call form: one top-level tool_calls list that becomes one assistant turn after messages. Tool definitions are embedded in a leading system message during normalization.
Since v0.76.0 (#1217) the converter also reads calls where agent data puts them, which is the OpenAI fine-tuning shape and what soup agent synth writes:
{
"messages": [
{"role": "user", "content": "What's the weather in Tokyo?"},
{"role": "assistant", "content": "", "tool_calls": [{"function": {"name": "get_weather", "arguments": "{\"city\": \"Tokyo\"}"}}]},
{"role": "tool", "content": "21 C, clear", "tool_call_id": "call_1"},
{"role": "assistant", "content": "It is 21 C and clear in Tokyo."}
],
"tools": [{"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}}}]
}An assistant turn keeps its own tool_calls and a tool turn keeps its tool_call_id, in source order, so a multi-turn trajectory is not rewritten as "answer first, call afterwards". A top-level tool_calls list is appended as one assistant turn only when no assistant turn already carries calls. tools is optional for the conversion, and format: auto detects a row with messages and tools as tool-calling; a top-level tool_calls is no longer required.
soup train now exits 1, on --dry-run and on a real run, when the data loads zero training rows, and the message names the format and the first dropped row's reason.
Auto-detection reads up to the first 100 rows rather than the first row alone. A file that mixes chatml and tool-calling rows loads as tool-calling when the tool-calling converter accepts the chatml rows and produces the same messages; any other mix is refused with a message naming both shapes and the row where each first appears, and you then set data.format yourself. See Data formats.
Generate tool-call training data
soup data generate \
--prompt "Function-calling examples: a user request, then one tool call with arguments" \
--provider openai \
--format chatml \
--count 1000 \
--output tool_calls.jsonl--prompt is required, and --template is an allowlist of exactly five: code, conversation, qa, preference, reasoning. There is no tool-calling template on the CLI. The bundled builder in soup_cli/data/templates/tool_calling.py, configurable across API domains (weather, search, database, filesystem), is a library helper with no command-line selector, in the same way the Elo arena and judge calibration are library helpers. Describe the shape you want in --prompt instead.
Train
Use a ready-made recipe:
soup recipes use qwen3-8b-tools
soup trainOr start from a recipe clone and edit:
base: Qwen/Qwen3-8B
task: sft
data:
train: ./tool_calls.jsonl
format: tool-calling
training:
epochs: 3
lr: 2e-4
lora: { r: 16, alpha: 32 }Evaluation
soup eval custom ships three tool-call scoring functions:
tool_call_match— exact function name + argumentstool_call_name_match— function name onlytool_call_args_subset— partial credit for matching a subset of arguments
Recipes
qwen3-8b-tools— Qwen 3 8B, 4bit, LoRA r=16llama4-scout-tools— Llama 4 Scout (17B active parameters, 109B total), 4bit, LoRA r=16
Re-run this
| If you trained | What was wrong | What to do |
|---|---|---|
format: tool-calling on soup agent synth output, or on any file whose tool calls sit inside assistant turns and which has no top-level tool_calls | Through v0.75.2 the detector needed messages, tools and a top-level tool_calls, so every such row was dropped. A dry run still printed "Ready to train!" on zero rows (#1217) | Re-run if some of your rows had a top-level tool_calls: only those rows were trained on. If none did, the dry run said "Ready to train!" and the real run loaded the model and then failed on an empty dataset |
format: auto on a file whose first row lacked an optional key such as tools | Detection read only the first row, so the poorer converter ran for the whole file and dropped the key from every row (#1424) | Re-run. A file like this now loads as the richer format or stops at load |
See also
- Data formats
- Data cleaning —
soup data clean --repair-jsonreads calls inside assistant turns as well as a top-leveltool_calls - Autopilot —
--goal tool-callingpicks this format automatically
Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.