CLI reference
The distil CLI drives the whole platform. Install it from
Install the CLI.
This page describes 0.27.0. Check your version with distil --version before trusting a flag,
and distil update to move to the current one.
The entity model
Section titled “The entity model”Every stage is an entity created by one command, and every entity is created either from local files or by running a job over the entity before it.
| Stage | Entity | Create | Read |
|---|---|---|---|
| Trace processing | PreparedTraces → SeedDataset | distil traces upload --data <dir>, then distil seed-dataset create-from-traces <traces-id> |
distil seed-dataset {status,logs,metrics,download,download-metadata,download-traces-predictions} |
| Job input | SeedDataset | distil seed-dataset create --data <dir> |
distil seed-dataset {status,download,download-metadata} |
| Teacher evaluation | TeacherEvaluation | distil teacher-evaluation create-from-seed-dataset <seed-dataset-id> |
distil teacher-evaluation {status,logs,metrics,download-metadata,download-predictions} |
| Synthetic data generation | TrainingDataset | distil training-dataset create-from-seed-dataset <seed-dataset-id> |
distil training-dataset {status,logs,metrics,sample,download,download-metadata} |
| Model training | SLM | distil slm create-from-training-dataset <training-dataset-id> |
distil slm {status,logs,metrics,download,download-metadata,download-predictions} |
| Deployment | Deployment | distil deployment create-from-slm <slm-id> |
distil deployment {status,endpoint,logs}, distil deployment delete |
upload and uploads are aliases of seed-dataset, and create-from-upload of
create-from-seed-dataset. Write the seed-dataset spelling.
Each group also answers to its plural (slms, deployments, teacher-evaluations,
training-datasets), and list answers to ls.
Authentication
Section titled “Authentication”You can create an account and sign in without leaving the terminal.
distil signup # create an account; aliases: register, join
distil auth # sign in; alias: login
distil auth --email <email> --password <password> # sign in without a browser
distil whoami # the current user
distil logout
How the browser handoff works
Section titled “How the browser handoff works”distil signup and distil auth are the same command with a different destination page. Each one:
- Starts an HTTP server on
127.0.0.1, on a random free port, carrying a one-timestatevalue. - Opens
https://app.distillabs.ai/sign-up(or/sign-in) with that port as the return address. - Waits for the browser to post a refresh token back, checks the
statematches, writes the token to the session file and stops the server.
Both finish by printing Logged in as <email>, so distil signup leaves you signed in and needs
no distil auth after it. If the browser doesn’t open, the CLI prints the URL for you to paste.
The wait times out after 20 minutes. The first line is always Timed out waiting for browser authorization; the lines after it name the likely cause and the two ways forward.
Signing in without a browser
Section titled “Signing in without a browser”--email and --password authenticate directly, with no browser and no local server. This is the
form for CI and remote shells.
distil auth --email you@example.com --password "$DISTIL_PASSWORD"
There is no headless signup. Accounts are only created through the browser flow, so on a machine
without one, sign up elsewhere and then authenticate here with --email and --password.
The CLI holds its own session in ~/.config/distillabs/token (under $XDG_CONFIG_HOME when set)
and refreshes it, so a run spanning hours needs no second login.
Which commands speak JSON
Section titled “Which commands speak JSON”--output json is registered on the read commands (list, show, status, logs, metrics,
sample, endpoint, plus whoami and credits-balance) and on the job creates that name a
parent id: teacher-evaluation create-from-seed-dataset,
training-dataset create-from-seed-dataset, slm create-from-training-dataset and
deployment create-from-slm. inference-endpoint create and api-keys create take it too,
though they create from flags rather than from a parent id.
It isn’t registered on the commands that read local files (traces upload,
seed-dataset create, training-dataset create, slm create), on
seed-dataset create-from-traces, or on any download command. Passing it there fails with
No flag registered for --output and creates nothing.
Without --output json a read prints a panel for a human reader and suggests the next command
underneath. Parse the JSON form in scripts.
Supplying files
Section titled “Supplying files”--data <dir> names a directory and the CLI reads the files out of it by name.
| Command | Required in --data <dir> |
Optional |
|---|---|---|
distil traces upload |
traces.jsonl, config.yaml, job_description.json |
test.jsonl |
distil seed-dataset create |
train.jsonl, test.jsonl, config.yaml, job_description.json |
unstructured.jsonl |
distil training-dataset create |
train.jsonl, test.jsonl, config.yaml, job_description.json |
- |
distil slm create |
model.tar, config.yaml |
- |
Optional here means optional to the CLI, which won’t stop you before the upload. Validation is
what enforces the per-task rules: question-answering-open-book and
question-answering-closed-book both need unstructured.jsonl, and leaving it out fails the job
rather than the command.
config.yml is accepted for config.yaml. The per-file flags (--traces, --train, --test,
--unstructured, --config, --job-description, --model) replace one path each, and
combining them with --data is refused rather than merged. A file missing from the directory is
named before anything uploads.
The download commands write these same names, so a downloaded directory feeds straight back into
the matching create --data. That round trip is how a change to the data is made, since no
override can reach it.
Credits
Section titled “Credits”distil credits-balance
# prepared_traces_post 100
# seed_datasets_post 100
# teacher_evaluations_post 20
# training_datasets_from_seed_datasets_post 5
# slms_from_training_datasets_post 2
The read is free and answers at zero balance. --output json returns {"balances": {…}} under
the same keys.
Only metered routes appear. A route the platform never charges for is absent rather than reported
as unlimited, so a missing key isn’t a zero, and an account with nothing metered reads
No metered endpoints. How metering works:
How the platform works.
Overrides
Section titled “Overrides”The four job creates (seed-dataset create-from-traces,
teacher-evaluation create-from-seed-dataset, training-dataset create-from-seed-dataset and
slm create-from-training-dataset) each accept --config <file> (-c) and
--job-description <file>. Both are optional and independent, and omitting one means the job
inherits the parent’s.
Each file replaces the parent’s whole. The CLI doesn’t merge, so whatever the file omits takes a
library default rather than the parent’s value. A config carrying a complete base but no
synthgen or tuning section is accepted, and those sections silently revert. An override is
therefore always read-edit-resend, never hand-written:
distil <group> download-metadata -d <dir> <id> → edit one field → pass to --config
The full rule, with a worked example of what reverts: How the platform works.
download-metadata writes the entity’s config.yaml and job_description.json and costs no
credits. What it writes is the fully expanded config, with every default materialised, keys
sorted and comments dropped, which is exactly why editing it and sending it back preserves
everything you didn’t touch.
distil seed-dataset download-metadata -d ./iter-2 <seed-dataset-id>
# edit ./iter-2/config.yaml
distil training-dataset create-from-seed-dataset --output json \
--config ./iter-2/config.yaml <seed-dataset-id> | jq -r .id
Uploaded traces are the exception: they echo the config they were staged with, comments intact and nothing expanded.
Because the read-back config is complete, checking what a submission actually ran with is a diff rather than an audit:
distil training-dataset download-metadata -d ./check <training-dataset-id>
diff ./iter-2/config.yaml ./check/config.yaml
Monitoring jobs
Section titled “Monitoring jobs”distil <group> status --output json <id> | jq -r .status
distil <group> logs --output json <id> | jq -r .logs
Poll every 20 seconds or so. Status values: How the platform works.
Read the status field, not the exit code. A status command that reaches the platform exits 0
whatever the job did. Deployments answer {"deployment_status": …, "endpoint_status": …} and have
no status field at all.
Typical timeouts to plan for:
| Stage | Command group | Typical timeout |
|---|---|---|
| Trace processing | seed-dataset |
45 min |
| Teacher evaluation | teacher-evaluation |
30 min |
| Synthetic data generation | training-dataset |
90 min |
| Model training | slm |
90 min |
| Deployment | deployment |
40 min |
logs returns the whole job log as one string and fills while the job runs, so it’s also how a
long job is watched. When a job fails, search the log for the first error rather than reading the
last one, since a crash often unwinds into a second, unrelated error.
Recovering a lost id
Section titled “Recovering a lost id”distil <group> list --output json
Returns one object per entity, newest first, carrying id, created_at and the parent’s id
under its own key (seed_dataset_id, training_dataset_id, slm_id, and so on). It’s how a
lost id is recovered and how a child is traced back to its parent. It carries no status, so checking state costs one status call
per id.
Fetching metrics
Section titled “Fetching metrics”distil teacher-evaluation metrics --output json <id> | jq .teacher_performance
# {"rouge": 1, "binary": 0.82, "llm-as-a-judge": 1, "llm-as-a-judge-reference-free": 1}
distil slm metrics --output json <id> \
| jq '{base: .base_model_performance, tuned: .tuned_model_performance}'
distil teacher-evaluation download-predictions <id>
distil slm download-predictions <id>
What the numbers mean: Metrics.
metrics --output json also carries the *_download_url the download command uses. It’s
presigned and expires after an hour, so take it directly only to hand the data to another
program.
Reading a training dataset
Section titled “Reading a training dataset”distil training-dataset sample --output json <id> > sample.json # free
distil training-dataset download -d ./data <id> # costs a credit
distil training-dataset metrics --output json <id> | jq .train_data_size_bytes
sample returns at most 128 train rows, drawn deterministically from the first 384, and never
test rows. metrics reports bytes rather than rows, so for a full run divide the byte count by
the mean row size in the sample and treat the result as an estimate.
Downloads
Section titled “Downloads”Every command that writes a directory takes --destination <dir> (-d) and otherwise names one
after the entity: <id>-traces, <id>-data, <id>-slm, <id>-metadata. The *-predictions
commands write a single file and take --file-name instead, and so does
inference-endpoint download-traces. Downloads overwrite what’s
already there.
distil slm download --destination ./model <slm-id> # model.tar + config.yaml
distil slm download-metadata --destination ./model <slm-id> # config, job description, client
slm download writes a tarball of several gigabytes, about 1.2 GB for a Qwen3-0.6B run, and
checks free disk space before it starts. slm download-metadata writes the model’s inference
client alongside its config, a few kilobytes instead.
Metrics and data downloads answer only once that entity’s own job reaches JOB_SUCCESS. Until
then a metrics field is null, and a download names the reason and exits 1 rather than writing an
empty file: SLM is still training, SLM training failed, The model is not available for this SLM. A download that writes nothing always fails loudly, so the exit code is worth trusting.
Deployment
Section titled “Deployment”distil deployment create-from-slm --output json <slm-id> | jq -r .id
distil deployment status --output json <id> | jq -r .deployment_status
distil deployment endpoint --output json <id> # {"url": ..., "api_key": ...}
distil deployment delete <id> # alias: shutdown
The deployment serves the model with vLLM, and the job doesn’t return until vLLM answers, so
JOB_SUCCESS means serving rather than merely scheduled. Before that, endpoint answers
{"url": null, "api_key": null} and exits 0 either way, so poll the status rather than probing
the endpoint.
A deployment is a session, not a permanent endpoint. It stops after six hours, or after one hour
with no traffic, and it bills until that idle timeout, so delete it when you’re finished. It
can’t be restarted, and a new deployment carries a new URL and a new key. Replacing a stopped one
spends another deployments_from_slms_post credit, so have your test inputs ready before you
create it. After the delete,
deployment_status stays JOB_SUCCESS, and endpoint_status going to stopped is what
confirms it’s down.
The API key protects the endpoint. The tunnel has no authentication of its own and the URL is open to all.
See distil labs inference for querying it.
Inference endpoints
Section titled “Inference endpoints”An inference endpoint is an OpenAI-compatible gateway that sits in front of the model you already run in production. You point your application at it and name the model you use today as its fallback, and it serves every request while recording traces. Once enough have accumulated they become the training data for the smaller distil labs model that takes the traffic over, and a second endpoint then serves that model with the first one as its fallback. The walkthrough is Collect traces with an endpoint and Serving behind the endpoint; this section is the reference.
distil inference-endpoint create --name <prefix> --fallback-model <owner/model> \
[--trace-sample-rate <0-1>] [--primary-url <url> --primary-api-key <key>]
distil inference-endpoint list # alias: ls
distil inference-endpoint show <unique-endpoint-name>
distil inference-endpoint create --name support --fallback-model "openai/gpt-4.1-mini"
Inference endpoint created.
ID: 1ca04deb-50d0-4e0f-bd99-aa16c40d13b1
Endpoint Name: support-yeOdAS
Endpoint: https://inference.distillabs.ai/v1/chat/completions
Created At: 2026-09-15 19:06
Primary URL: none
Fallback Model: openai/gpt-4.1-mini
Trace Sampling Rate: 1.0
API Keys: none
--name is a prefix, not the final name. The platform appends a suffix and returns the
unique_endpoint_name: support becomes support-yeOdAS. That unique name is what every other
command takes, and what the model field of a request carries. There’s no lookup by prefix, no
rename and no delete, so record it. list is how a lost one is recovered.
--fallback-model takes an OpenRouter model slug in owner/model
form; the model directory lists every slug it accepts. Name the
model your application calls today and the endpoint answers exactly as it does now. These are the
ones we often run - a reasonable starting point if you’re choosing fresh:
| Slug | Notes |
|---|---|
openai/gpt-4.1-mini |
Small and cheap |
openai/gpt-5.4 |
Frontier OpenAI |
google/gemini-3.1-flash-lite |
Cheap Gemini model |
--trace-sample-rate is the fraction of calls the endpoint records, between 0 and 1, default 1.
Endpoints created with a CLI older than 0.27.0 carry a rate of 0.01, and show reports it as
trace_sampling_rate.
--primary-url and --primary-api-key name a model of your own that the endpoint calls before
the fallback: the base URL of an OpenAI-compatible server, without /v1 since the endpoint
appends /v1/chat/completions itself, and the key that authenticates it. They come together or
not at all. The endpoint forwards the request to the primary with model rewritten to the name
the server expects, and falls back whenever the primary fails, a timeout included. A hosted
deployment is the usual primary; see
Serving behind the endpoint. --readiness-gate-timeout-ms
belongs to the primary and is for internal use.
create, list and show take --output json. link-api-key, unlink-api-key and
download-traces don’t.
API keys
Section titled “API keys”A caller authenticates with an inference API key. It’s a separate credential from your CLI session: it reaches the endpoint and nothing else, and the endpoint accepts nothing else. Keys are created on their own and then linked.
distil api-keys create <key-name> # prints the secret once, writes <key-name>.json
distil api-keys create <key-name> --no-file # print it and write nothing
distil api-keys list
distil api-keys delete <key-name>
distil inference-endpoint link-api-key <unique-endpoint-name> <key-name>
distil inference-endpoint unlink-api-key <unique-endpoint-name> <key-name>
The secret is shown at creation and never again, which is why api-keys create also writes it to
<key-name>.json in the current directory. Use --no-file where the key should go straight into
a secret store instead. api-keys list returns names and creation dates, never secrets. An
account holds at most five keys.
A new key authenticates nothing until it’s linked to an endpoint. The link is many-to-many, so a
rotation runs: create the new key, link it, move your traffic, then unlink and delete the old
one. Deleting a key revokes it everywhere it was linked, and show lists the key names an
endpoint currently accepts.
Key changes take up to a minute to propagate. A freshly linked key can be rejected by the endpoint, so wait and retry rather than reading either as a failure. Give a new key that minute before you move traffic onto it, and before you unlink or delete the key it replaces.
Calling it
Section titled “Calling it”curl https://inference.distillabs.ai/v1/chat/completions \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "support-yeOdAS",
"messages": [{"role": "user", "content": "Say hi in three words."}]
}'
The model field carries the unique endpoint name rather than a model name, because the endpoint
decides what answers. Everything else is an ordinary chat completions request, so an existing
OpenAI client moves across by changing three strings:
from openai import OpenAI
client = OpenAI(
base_url="https://inference.distillabs.ai/v1",
api_key="<api-key>",
)
client.chat.completions.create(
model="support-yeOdAS",
messages=[{"role": "user", "content": "…"}],
)
The endpoint’s page in the dashboard prints this call with your own name already filled in.
Downloading the traces
Section titled “Downloading the traces”distil inference-endpoint download-traces <unique-endpoint-name>
distil inference-endpoint download-traces --count 5000 <unique-endpoint-name> # -c
distil inference-endpoint download-traces --all <unique-endpoint-name>
distil inference-endpoint download-traces --file-name support.jsonl <unique-endpoint-name>
One record per line, written to <unique-endpoint-name>-traces.jsonl unless --file-name says
otherwise. The default is the newest 1000. The platform pages the transfer itself, so --count
caps what is kept, --all walks every page, and the search window is roughly the last 90 days.
An endpoint with nothing recorded writes no file.
Each record carries the request and the response as JSON strings in input and output, and a
metadata object with the HTTP status and source, which names whether the fallback or the
primary answered. That isn’t a trace processing input as it stands, since
distil traces upload wants one {"messages": [...]} object per line.
Endpoint records to trace inputs has the record shape and the
conversion.
Other commands
Section titled “Other commands”distil --version # installed version
distil update # replace the binary in place
distil docs # open these docs
distil credits-balance # metered routes and remaining calls
Deprecated: the model command group
Section titled “Deprecated: the model command group”distil model … is deprecated and hidden. Use the entity chain at the top of this page instead.
Every distil model subcommand prints a pointer to its equivalent.