Skip to content

Reasoning models

A reasoning student writes out its reasoning in a thinking block, then gives the answer. For Qwen3 models the block is <think>…</think> at the start of the reply. The student learns to do this from reasoning written by the teacher during synthetic data generation, or from reasoning you put in your own training data.

Use a reasoning student when getting to the answer takes several steps: a calculation, a rule applied to the facts, a choice between close options. The cost is more output tokens on every request, so replies are slower than from the same student without thinking. Only the answer is scored, so train a normal student first and compare.

One setting controls it:

base:
  task: question-answering
  student_model_name: Qwen3-1.7B
  teacher_model_name: openai.gpt-oss-120b
  enable_thinking: true

base.enable_thinking defaults to false. When it is true, synthetic data generation writes reasoning for every training example, and the student is trained, evaluated and served with its chat template’s thinking mode on. It works with every task type.

Set it on the seed dataset’s config, before you generate training data. The training dataset and the model inherit it from there.

Model Value
Qwen3 0.6B Qwen3-0.6B
Qwen3 1.7B Qwen3-1.7B
Qwen3 8B Qwen3-8B
Qwen3.5 0.8B Qwen3.5-0.8B
Qwen3.5 2B Qwen3.5-2B
Qwen3.5 4B Qwen3.5-4B
Qwen3.5 9B Qwen3.5-9B
Qwen3.6 35B A3B Qwen3.6-35B-A3B
Nemotron 3.5 Lightning 30B A3B Nemotron-3.5-Lightning-30B-A3B

Any other student fails config validation. This includes Qwen3-4B-Instruct-2507, which has no thinking mode:

base.enable_thinking=true is not available for student model 'Qwen3-4B-Instruct-2507'. Its chat
template either has no thinking mode, or its response template cannot parse the thinking block
back out of the generated text. Supported: Qwen3-0.6B, Qwen3-1.7B, ...

The teacher does not need to be a reasoning model. -thinking teachers are a separate setting, covered on Supported models.

You don’t have to write reasoning. With base.enable_thinking: true, synthetic data generation produces it in two places:

  1. During generation. The teacher writes the reasoning next to each assistant message it generates.
  2. Backfill. A final pass goes over every training example, your seed rows included. When the final assistant message has no reasoning, the teacher writes it. The backfill changes only the reasoning, never the answer or the tool calls. Rows where the teacher still writes none after a retry are dropped.

The teacher is told to write the reasoning in the first person, to work the answer out rather than explain it, to rule out the closest wrong option, and to use only facts that are in the conversation. Put instructions on the style and length of the reasoning in synthetic_data_generation_instructions in the job description, for example “Keep the reasoning under 250 tokens: one short line per check.”

The backfill costs one teacher call per example that has no reasoning. Check the train row count after generation: the dropped rows are not replaced.

To supply reasoning yourself, put it in reasoning_content on the assistant message:

{"messages": [
  {"role": "user", "content": "A pack holds 12 pens. How many packs for 50 pens?"},
  {"role": "assistant", "content": "5", "reasoning_content": "50 / 12 is 4.17. Four packs hold 48, which is 2 short, so it takes 5."}
]}

For tool calls, the reasoning goes on the assistant message that carries tool_calls:

{"messages": [
  {"role": "user", "content": "What's the weather in Berlin?"},
  {"role": "assistant", "content": "", "reasoning_content": "The user asks about current weather in one city, so get_weather with city Berlin.", "tool_calls": [
    {"type": "function", "function": {"name": "get_weather", "arguments": {"city": "Berlin"}}}
  ]}
]}

These rules apply:

  • The key is reasoning_content, on the assistant message. A reasoning key, or a key at the row’s top level, is ignored without an error.
  • Only the final assistant message keeps its reasoning. Multi-turn conversations are split into one example per assistant turn, and reasoning on earlier turns is removed. A deployed model never sees the reasoning of earlier turns, so it does not train on it either.
  • Reasoning is optional per row. The backfill writes it for rows that have none.
  • With enable_thinking: false, reasoning is removed when the data is read. The same data works for both kinds of student.
  • The legacy format uses a top-level reasoning field next to question and answer. Legacy multi-turn tool calling has no reasoning field.

test.jsonl needs no reasoning. Evaluation scores only the answer.

Trace processing does not accept enable_thinking: true. Production traces have no reasoning, and this stage writes none. Process the traces with thinking off, then turn it on for generation:

# 1. Process the traces with base.enable_thinking: false (the default)
distil seed-dataset create-from-traces <traces-id>

# 2. Read back the config the seed dataset carries
distil seed-dataset download-metadata -d ./reasoning <seed-dataset-id>
# in ./reasoning/config.yaml, under base:
#   enable_thinking: true

# 3. Generate the training data with thinking on
distil training-dataset create-from-seed-dataset --output json \
  --config ./reasoning/config.yaml <seed-dataset-id> | jq -r .id

The override replaces config.yaml whole, so edit the file you read back and send all of it. See How the platform works.

To keep your training set as it is and only add reasoning, set synthgen.generation_target: 0. Generation then makes no new examples, but the backfill still runs over your seed rows. This does not work for question-answering-closed-book, which computes its own target.

Don’t turn on enable_thinking for a training run on a training dataset that was generated without it. Its rows have no reasoning, nothing adds it at training time, and the student learns to write an empty thinking block. Generate a new training dataset with thinking on instead.

The reasoning makes each example longer, and two settings limit the length:

  • tuning.max_completion_length (default 2048 tokens) is the most the student can generate during evaluation and RLVR, reasoning included. If the longest training completion is longer, training logs a warning, and completions that are cut off score near zero. Raise it when the reasoning is long.
  • synthgen.validation_max_total_length (default 30000 characters) applies to the whole example. Examples that the backfill makes longer than this are dropped.

The metrics score only the answer: the content and the tool calls. The reasoning is not scored.

The student is evaluated with its thinking mode on, at the sampling settings its vendor recommends for thinking. For Qwen3 that is temperature 0.6, top_p 0.95 and top_k 20. For Qwen3.5 and Qwen3.6 it is temperature 1.0, top_p 0.95 and top_k 20.

The generated model_client.py sends chat_template_kwargs: {"enable_thinking": true} for a reasoning student, with temperature=0. Use it rather than a hand-built request, as for any other model. Hosted inference supports reasoning students, so a model trained with enable_thinking: true needs no extra setup there.

When you serve the model yourself, start vLLM with the reasoning parser for the student’s family, so the reasoning is returned separately from the answer:

Student vLLM flags
Qwen3 --enable-auto-tool-choice --tool-call-parser hermes --reasoning-parser qwen3
Qwen3.5, Qwen3.6 --enable-auto-tool-choice --tool-call-parser qwen3_xml --reasoning-parser qwen3
Nemotron --enable-auto-tool-choice --tool-call-parser qwen3_xml --reasoning-parser nemotron_v3
vllm serve model --reasoning-parser qwen3 --api-key EMPTY

With the parser, the returned message holds the reasoning in a separate field (reasoning_content, or reasoning in newer vLLM versions) and only the answer in content. Without it, the thinking block stays at the start of content. See Local deployment.

Every other parameter: Config file. The generation stage that writes the reasoning: Synthetic data generation.