Skip to content

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.

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.

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

distil signup and distil auth are the same command with a different destination page. Each one:

  1. Starts an HTTP server on 127.0.0.1, on a random free port, carrying a one-time state value.
  2. Opens https://app.distillabs.ai/sign-up (or /sign-in) with that port as the return address.
  3. Waits for the browser to post a refresh token back, checks the state matches, 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.

--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.

--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.

--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.

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.

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
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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

distil --version          # installed version
distil update             # replace the binary in place
distil docs               # open these docs
distil credits-balance    # metered routes and remaining calls

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.