Endpoint records to traces
The endpoint’s records become a traces object, the input of Test set from traces. There are two routes:
| Directly from the endpoint | Download, convert, upload | |
|---|---|---|
| How | the platform reads the endpoint’s records itself | you download the records, convert them to conversations, and upload the file |
| Delay | a call becomes available about 24 hours after it was made | none: a call can be downloaded right after it was made |
| Size | no limit: every record the endpoint holds | a few thousand traces at most |
| Filtering | every record goes in; trace processing skips failed calls | you choose: by status, by which model answered, by time window |
Use the direct route for the full trace set. Use the download route when you need the traces now, when there are only a few thousand, or when the records must be filtered first, for example to keep only the fallback’s answers once a trained model serves the endpoint.
Both routes read a config.yaml and a job_description.json from a directory, so write them
first (Trace inputs covers both files).
Directly from the endpoint
Section titled “Directly from the endpoint”Create the traces object from the endpoint support-yeOdAS and the files in ./traces-input:
distil traces create-from-inference-endpoint --data ./traces-input support-yeOdAS
The command prints Prepared traces created. ID: <traces-id>. traces-input/ holds config.yaml, job_description.json and optionally a curated
test.jsonl. It must not hold a traces.jsonl: the traces come from the endpoint, and the
command refuses a directory that has one. Set the observation format of the endpoint’s records in
the config:
trace_processing:
observation_format: langfuse
--config, --job-description and --test replace --data with one path each.
Download, convert, upload
Section titled “Download, convert, upload”Download the records
Section titled “Download the records”With no flags, the command downloads the newest 1,000 records from the last day:
distil inference-endpoint download-traces support-yeOdAS
It writes one record per line to support-yeOdAS-traces.jsonl. --from-start-time and
--to-start-time set the window, as an ISO 8601 date or date-time. Within the window,
--count (-c) caps the records (default 1000) and --all takes every one. --file-name names
the output file. With no records in the window, no file is written. To take every record since
September 1:
distil inference-endpoint download-traces --from-start-time 2026-09-01 --all support-yeOdAS
What a record holds
Section titled “What a record holds”| Field | What it holds |
|---|---|
input |
The request body, as a JSON string: model, messages, and tools when the caller sent any |
output |
The chat completions response, as a JSON string. The reply is choices[0].message |
metadata.status |
The HTTP status the caller received |
metadata.source |
Which model answered: fallback, or primary when a trained model serves the endpoint |
start_time, latency |
When the call started, and how long it took in seconds |
The rest is identifiers. Read one record before converting.
Convert to conversations
Section titled “Convert to conversations”Per record: parse input and output, append the reply as the assistant turn to the request’s
messages, carry tools across when present, and write one {"messages": [...]} object. Skip the
records that should not become training data:
- Failed calls. Keep
metadata.statusof 200 only. - Empty replies. Skip a response with neither
contentnortool_calls, so no conversation ends on the user’s turn. - The wrong model’s answers, once a trained model serves the endpoint. Filter on
metadata.sourcefor the answers the next iteration should learn from.
This script applies the first two filters and writes the conversations from
support-yeOdAS-traces.jsonl to traces-input/traces.jsonl:
import json
def convert(record):
if record["metadata"].get("status") != 200:
return None
request = json.loads(record["input"])
reply = json.loads(record["output"])["choices"][0]["message"]
if not reply.get("content") and not reply.get("tool_calls"):
return None
assistant = {"role": "assistant", "content": reply.get("content") or ""}
if reply.get("tool_calls"):
assistant["tool_calls"] = reply["tool_calls"]
converted = {"messages": [*request["messages"], assistant]}
if request.get("tools"):
converted["tools"] = request["tools"]
return converted
with open("support-yeOdAS-traces.jsonl") as src, open("traces-input/traces.jsonl", "w") as dst:
for line in src:
converted = convert(json.loads(line))
if converted is not None:
dst.write(json.dumps(converted, ensure_ascii=False) + "\n")
The reply can carry a reasoning field next to content when the fallback is a reasoning model.
The conversion keeps content, which is what your application used. Check the kept count
against the download and read the first few lines.
Upload
Section titled “Upload”The result is a traces.jsonl in the default openai_messages format. Put it next to
config.yaml and job_description.json and upload the directory:
distil traces upload --data ./traces-input
The command prints Prepared traces created. ID: <traces-id>.