Data Formats
Soup supports 20 data formats (raft added for Retrieval-Augmented Fine-Tuning in v0.62, asr added for Whisper fine-tuning in v0.71.32, cross_encoder added for paired-text classification in v0.76.0). Files can be JSONL, JSON, CSV, Parquet, or TXT — local paths or remote URIs (s3 / gs / gcs / az / abfs / abfss / oci). Format is auto-detected from up to the first 100 rows for the twelve formats with a distinctive set of keys; the seven at the end of this page cannot be detected from their keys and must be named in data.format.
Note on the count: 20 is the number of values data.format accepts, and one of them is auto, which asks Soup to detect the shape rather than naming one. Nineteen name a row shape, and those nineteen plus auto are the values soup data validate --format accepts.
Alpaca
{"instruction": "Explain gravity", "input": "", "output": "Gravity is..."}ShareGPT
{"conversations": [{"from": "human", "value": "Hi"}, {"from": "gpt", "value": "Hello!"}]}ChatML
{"messages": [{"role": "user", "content": "Hi"}, {"role": "assistant", "content": "Hello!"}]}DPO / ORPO / SimPO / IPO (preference pairs)
{"prompt": "Explain gravity", "chosen": "Gravity is a force...", "rejected": "I don't know"}KTO (unpaired preferences)
{"prompt": "Explain gravity", "completion": "Gravity is a force...", "label": true}LLaVA (vision)
{"image": "photo.jpg", "conversations": [{"from": "human", "value": "<image>\nDescribe this."}, {"from": "gpt", "value": "A cat."}]}ShareGPT4V (vision)
{"image": "chart.png", "conversations": [{"from": "human", "value": "<image>\nExplain this chart."}, {"from": "gpt", "value": "Revenue growth."}]}Plaintext (pre-training)
{"text": "Raw text document for continued pre-training..."}Or use .txt files directly (one document per line).
Embedding
{"anchor": "What is Python?", "positive": "Python is a programming language."}
{"anchor": "What is Python?", "positive": "A programming language.", "negative": "A type of snake."}With embedding_loss: contrastive (the default), each row's negatives are the other rows' positives in the same batch, so a batch needs at least two rows. batch_size: 1 is refused at config load, batch_size: auto resolves to at least 2, and the last partial batch of each epoch is dropped. The same applies when triplet falls back to contrastive because the rows have no negative. Use triplet with a negative on every row, or cosine, to train at batch size 1 (v0.76.0).
Audio
{"audio": "recording.wav", "messages": [{"role": "user", "content": "Transcribe."}, {"role": "assistant", "content": "Hello world."}]}ASR (new in v0.71.32)
{"audio": "clip.wav", "text": "the transcript"}For Whisper fine-tuning (task: asr): the clip becomes 16 kHz log-mel input features, the text becomes the decoder labels. Paths resolve against data.audio_dir (containment-checked).
Tool-calling (new in v0.25.0)
{"messages": [{"role": "user", "content": "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\"}"}}]}Tool definitions are embedded in a leading system message during normalization. The converter reads calls where agent data puts them: an assistant turn keeps its own tool_calls and a tool turn its tool_call_id, in source order (the OpenAI fine-tuning shape, and what soup agent synth writes). A top-level tool_calls list, as in the row above, still works as the single-call form: it becomes one assistant turn appended after messages, and only when no assistant turn already carries calls. tools is optional for conversion, but format: auto recognises a row by messages plus tools. A call's arguments must be a JSON string that parses. See the Tool-calling guide for the full pipeline.
{"messages": [{"role": "user", "content": "Weather in Tokyo?"}, {"role": "assistant", "content": "", "tool_calls": [{"function": {"name": "get_weather", "arguments": "{\"city\": \"Tokyo\"}"}}]}, {"role": "tool", "tool_call_id": "call_1", "content": "18C, clear"}, {"role": "assistant", "content": "It is 18C and clear in Tokyo."}], "tools": [{"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}}}]}Through v0.75.2 the detector wanted a top-level tool_calls as well, so every row soup agent synth wrote was dropped. A dry run of soup train then still said it was ready to train, on zero rows (#1217; see "Rows that do not convert" below).
The seven the page above does not show a row for
Twenty values are declared for data.format (auto is one of them), and the twelve above are the ones you will meet first. These are the rest, one row each.
// prm — process reward, stepwise supervision
{"prompt": "Solve 2+2", "completions": ["First, add", "Result is 4"], "labels": [true, true]}
// pre_tokenized — skip the tokenize stage entirely
{"input_ids": [1, 2, 3], "labels": [-100, 2, 3], "attention_mask": [1, 1, 1]}
// input_output — template-free, per-segment loss control
{"segments": [{"text": "Q: hi", "label": false}, {"text": "A: hello", "label": true}]}
// video — the path string is kept as written; data.video_dir is not applied
{"video": "clip.mp4", "messages": [{"role": "user", "content": "Describe this clip."}]}
// multimodal — typed content parts, text / image / audio / video in one message
{"messages": [{"role": "user", "content": [{"type": "text", "text": "What's in this?"}, {"type": "image", "url": "x.png"}]}]}
// raft — Retrieval-Augmented Fine-Tuning, v0.62
{"query": "...", "golden_doc": "...", "distractor_docs": ["...", "..."], "answer": "..."}
// cross_encoder — paired text classification, v0.76.0
{"text_a": "how do I reset my password", "text_b": "Open Settings, then Security.", "label": 1}pre_tokenized pairs with data.tokenized_path after soup data preprocess; see Data pipeline pro for the cache key, the labels column and what is refused. For raft, query, golden_doc and answer are required and distractor_docs is optional; a row with other key names is dropped.
cross_encoder rows are two strings and a label. {"question": "...", "answer": "...", "label": 1} is accepted in place of text_a and text_b, and label is kept when present. The format is valid only with task: cross_encoder: data.format: cross_encoder on any other task is refused at config load, and with that task format: auto resolves to it, since {question, answer} is also the shape of plain Q&A data. It is not detected from the keys of an arbitrary file; soup data validate on a text_a/text_b file needs --format cross_encoder.
And auto, the default, is the twentieth value. It reads up to the first 100 rows, not only the first, and only for the twelve formats with a section of their own higher up this page. The seven in the block above must be named in data.format. prm, pre_tokenized, input_output and raft rows fail auto-detection with "Cannot detect format". video and multimodal rows carry messages, so auto-detection reads them as chatml: for video that drops the video path, and for multimodal it skips the content-part validation. One more pair to know: a sharegpt4v row has exactly the keys of a llava row, so auto-detection reads it as llava; name sharegpt4v if you mean it.
How a mixed file is read (v0.76.0). Each of the sampled rows resolves to the richest format its keys fit. If every row agrees, that format is used. A file that mixes a richer and a poorer shape of the same family (llava with sharegpt, tool-calling with chatml, audio with chatml, asr with plaintext) loads as the richer one only when that converter accepts the poorer rows and yields the same messages for them. Any other mix is refused naming both shapes and the row where each first appears, for example Cannot detect format: this dataset mixes unrelated shapes 'chatml' (row 0) and 'llava' (row 1). ... Set data.format explicitly. Set data.format to choose. Rows after the first 100 are not inspected, and soup data validate lists every row the chosen format drops. Before v0.76.0 only the first row decided, so a first row that lacked an optional key (a text-only LLaVA row, say) picked the poorer converter for the whole file and silently dropped that key from every row that had it. Re-run a training that auto-detected a file like that.
Rows that do not convert
A row the converter rejects is still dropped, so one bad line cannot abort a load. Since v0.76.0 it is no longer silent. soup train, --dry-run included, prints Warning: N of M rows dropped: they do not convert as '<format>'. First: row <i>: <reason>. and, for a local file, the soup data validate command that lists them all. If the load ends with zero training rows, soup train exits 1 before loading the model, naming the format and the first drop reason; a dry run used to print "Ready to train!" on zero rows.
Nested lists and dicts in a Parquet file (a messages column, tool calls) load as Python values. Before v0.76.0 they were turned into NumPy arrays and the rows were dropped by validation, so check a Parquet dataset that trained on fewer rows than it held.
Image and audio paths
A relative path in a llava, sharegpt4v, audio or asr row resolves against data.image_dir or data.audio_dir when it is set, and otherwise against the directory of the local file the row came from. Since v0.76.0 that holds on every load path: a local file, a local or streaming interleave, a remote URI, a Hub dataset and the data.replay file. A row whose file lies outside that directory is skipped with a warning. A remote URI or a Hub dataset has no local directory, so when its rows hold file paths data.image_dir or data.audio_dir is required, and the load stops naming it; data.image_dir: . resolves them under the working directory. A path that names a network share, a device or an extended-length path (\\host\share\..., //host/share/..., \\?\..., \\.\...), or that holds a NUL byte, is skipped without being looked up. Write such rows relative to the media directory instead. Through v0.75.2 the streaming, remote and Hub paths kept a relative path as written, so data.image_dir did nothing there. See Vision and audio for the training side.
All data normalizes internally to {"messages": [...]} structure (+ "image" for vision, "audio" for audio, + "tools" for tool-calling).
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.