Skip to content

Trace inputs

If you already run an LLM in production, its logs are training data. This page covers what a trace processing job takes as input. Trace processing covers running the job.

Pick your task type before you convert anything, since it determines the shape everything else takes. See Task selection.

traces-input/
├── traces.jsonl          # your production logs
├── job_description.json  # what the task is, in words
├── config.yaml           # task type, models, and a trace_processing section
└── test.jsonl            # optional curated test set

You submit the whole directory:

distil traces upload --data ./traces-input

Supplying test.jsonl replaces the generated test split and makes num_traces_as_testing_base inert. Leave it out and the platform builds a test set for you from the traces.

Your logs, one observation per line. Set trace_processing.observation_format in config.yaml to match their shape.

openai_messages is the default. Each line carries a messages array with roles system, user, assistant and tool. Assistant turns can carry tool_calls.

{"messages": [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}

openai_messages_with_images is the same, except user content can be a list of parts:

[
  {"type": "text", "text": "What is in this image?"},
  {"type": "image_url", "image_url": {"url": "..."}}
]

unstructured_with_openai_messages gives each line a context field whose string wraps a messages array. Use it when your traces double as unstructured context.

  • Constant prompt text belongs in the job description, not in the traces. Variable input becomes the user message and model output becomes the assistant message. remove_system_prompt_from_traces (default true) strips leading system messages anyway.
  • Every trace is a conversation, rewritten whole. A simple one-shot exchange is just a two-turn conversation.
  • Watch the length ceiling. synthgen.validation_max_total_length (default 30,000 characters) applies to processed examples too, so raise it if your inputs embed documents or schemas.
  • Emit clean JSONL. Strip control characters and unicode line separators (U+2028 and U+2029), which are a common cause of parse failures in exported logs.

What your task is, in words. The teacher reads it when it relabels your traces, and again when it generates training data later.

For a trace-derived build you usually want three fields:

{
  "task_description": "...",
  "llm_as_a_judge_instructions": "...",
  "trace_processing_instructions": "..."
}
  • task_description describes the task itself. Derive it from the system prompt your production system runs, with the same care: the output format with an example, the include and exclude rules, the edge cases.

  • llm_as_a_judge_instructions tells the judge what to accept when it scores an answer. Optional, and not valid for classification, which is judged on label accuracy instead.

  • trace_processing_instructions is the field specific to this path. It’s appended to the rewrite and fix instructions, so use it when the edits have to respect something unusual about your traces:

    This is a live phone call; preserve the caller’s interruptions and any cut-off utterances verbatim.

    Leave it out when no special handling is needed.

Classification tasks also need classes_description, and tool-calling tasks need tools. The full field reference, including which task types accept which fields, is Job description.

The task type and models, plus a trace_processing section:

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

trace_processing:
  observation_format: openai_messages

Every other trace processing field has a default, and Trace processing covers the ones worth setting deliberately.

Any task type can train from traces. For RAG-style traces where the retrieved context is already embedded in the user message, use question-answering and the context stays inside the question.

Process the traces into a seed dataset.