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:

json
{
  "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:

json
{
  "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

bash
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:

bash
soup recipes use qwen3-8b-tools
soup train

Or start from a recipe clone and edit:

yaml
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 + arguments
  • tool_call_name_match — function name only
  • tool_call_args_subset — partial credit for matching a subset of arguments

Recipes

  • qwen3-8b-tools — Qwen 3 8B, 4bit, LoRA r=16
  • llama4-scout-tools — Llama 4 Scout (17B active parameters, 109B total), 4bit, LoRA r=16

Re-run this

If you trainedWhat was wrongWhat 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_callsThrough 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 toolsDetection 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-json reads calls inside assistant turns as well as a top-level tool_calls
  • Autopilot — --goal tool-calling picks 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.