Know which call starts compute
POST /distill/recipes/propose analyzes a ready version and returns an editable plan. It requires distill:read and starts no compute. A run create requiresdistill:write. A dry run executes the workflow with stub artifacts and no GPU; a live run can spend the submitted budget immediately.
Your client owns approval
A live write token sends the mutation directly to the run service.
No implicit Pilot pausePilot prepares a proposal
The person reviews the exact live action before Pilot executes it.
Approval is part of the Pilot workflowEvery run is forced dry
Setting dry_run false with a test token still produces a test-mode dry run.
Test artifacts cannot authorize live workNew integrations should always prove live work with validated_dry_run andplan_hash. The service retains a compatibility path for older clients that omit those fields, but that path does not give an operator evidence that the submitted plan was rehearsed.
Propose a version-scoped recipe
Wait until the dataset version is ready and has rows. Supply exactly one immutable version, one objective from cost, quality, orlatency, and the reward IDs you intend to optimize. Choosedeployable_student only when the run should include the publish stage.
curl --fail-with-body -sS -X POST "https://lizzy.albinilabs.com/v1/distill/recipes/propose" \
-H "Authorization: Bearer $LIZZY_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"dataset_version": "dsv_...",
"objective": "quality",
"max_cost_usd": 25,
"rewards": ["rwd_..."],
"outcome": "deployable_student"
}'
{
"object": "distill_recipe",
"case": "single_call",
"student_model": "qwen3-8b",
"stages": [
{ "kind": "sft", "config": { "epochs": 2, "lora_rank": 32, "learning_rate": 0.0001, "max_seq_len": 8192 } },
{ "kind": "eval", "config": { "rewards": ["rwd_..."], "judge_model": "claude", "sample": 500 } },
{ "kind": "publish", "config": {} }
],
"rewards": ["rwd_..."],
"estimate": { "gpu_seconds": 2400, "cost_usd": 18.40, "wall_clock_minutes": 47 },
"dataset_version": "dsv_...",
"outcome": "deployable_student",
"plan_hash": "<proposal-hash>"
}
For a stable executable payload, retain only student_model,stages, and rewards from the proposal. Review every stage. The canonical stage order is materialize, sft, grpo,eval, then publish; materialize is implicit.
const proposal = await response.json();
const recipe = {
student_model: proposal.student_model,
stages: proposal.stages,
rewards: proposal.rewards,
};
// Preserve this exact object for dry and live requests.
const budget = { max_cost_usd: 25 };
const compute = { profile: "serverless" };
Validate the exact plan in live mode
Submit the intended dataset version, recipe, budget, and compute withdry_run: true. Use the same live credential intended for training. A test-mode dry run lives in a separate resource partition and cannot be referenced by a live run.
DRY_BODY='{
"dataset_version": "dsv_...",
"student_model_name": "support-student-v1",
"name": "Support student rehearsal",
"recipe": {
"student_model": "qwen3-8b",
"stages": [
{ "kind": "sft", "config": { "epochs": 2, "lora_rank": 32, "learning_rate": 0.0001, "max_seq_len": 8192 } },
{ "kind": "eval", "config": { "rewards": ["rwd_..."], "judge_model": "claude", "sample": 500 } },
{ "kind": "publish", "config": {} }
],
"rewards": ["rwd_..."]
},
"budget": { "max_cost_usd": 25 },
"compute": { "profile": "serverless" },
"dry_run": true
}'
curl --fail-with-body -sS -X POST "https://lizzy.albinilabs.com/v1/distill/runs" \
-H "Authorization: Bearer $LIZZY_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: support-plan-dry-v1" \
--data "$DRY_BODY"
{
"id": "run_dry...",
"object": "distill_run",
"livemode": true,
"status": "queued",
"dry_run": true,
"dataset_version": "dsv_...",
"plan_hash": "9b4d...<64 hex characters>",
"validated_dry_run": null,
"compute": { "profile": "serverless", "machine": null, "spot": false, "gpu": null },
"totals": { "gpu_seconds": 0, "judge_tokens": 0, "cost_usd": 0.0 },
"budget": { "max_cost_usd": 25 },
"report": null,
"student_model": null,
"current_stage": { "seq": 1, "kind": "materialize", "status": "pending" },
"stages": [
{ "seq": 1, "kind": "materialize", "status": "pending", "attempt": 0 },
{ "seq": 2, "kind": "sft", "status": "pending", "attempt": 0 },
{ "seq": 3, "kind": "grpo", "status": "skipped", "attempt": 0 },
{ "seq": 4, "kind": "eval", "status": "pending", "attempt": 0 },
{ "seq": 5, "kind": "publish", "status": "pending", "attempt": 0 }
]
}
The real response includes each stage's config, timestamps, metrics, error, and log availability. A dry run walks the same five-stage timeline, but it uses stubs, spends no GPU, skips student publication, and never creates a deployable student. Wait forsucceeded, then persist its ID and returned plan_hash.
Start only the approved live plan
- 1
Review the rehearsal
Check its version, rewards, stages, compute profile, estimate, and hard budget.
- 2
Record the human decision
Store the approver, timestamp, dry-run ID, plan hash, and exact serialized plan.
- 3
Send the matching plan
Change dry_run to false, attach the proof fields, and use a new idempotency key.
LIVE_BODY='{
"dataset_version": "dsv_...",
"student_model_name": "support-student-v1",
"name": "Support student live v1",
"recipe": {
"student_model": "qwen3-8b",
"stages": [
{ "kind": "sft", "config": { "epochs": 2, "lora_rank": 32, "learning_rate": 0.0001, "max_seq_len": 8192 } },
{ "kind": "eval", "config": { "rewards": ["rwd_..."], "judge_model": "claude", "sample": 500 } },
{ "kind": "publish", "config": {} }
],
"rewards": ["rwd_..."]
},
"budget": { "max_cost_usd": 25 },
"compute": { "profile": "serverless" },
"dry_run": false,
"validated_dry_run": "run_dry...",
"plan_hash": "9b4d...<64 hex characters>"
}'
curl --fail-with-body -sS -X POST "https://lizzy.albinilabs.com/v1/distill/runs" \
-H "Authorization: Bearer $LIZZY_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: support-plan-live-v1" \
--data "$LIVE_BODY"
Keep the idempotency key and byte-identical LIVE_BODY together. After a timeout, resend that pair. idempotency_key_in_flight means wait and retry the same operation; idempotency_key_reused means that key was paired with a different body and must not be treated as a replay.
Monitor the run by ID
- 1queued
The run and all five stage rows have been created.
- 2validating
The worker validates and materializes frozen inputs.
- 3running
An SFT, GRPO, or publish stage is active.
- 4evaluating
The holdout evaluation stage is active.
- 5terminal
Succeeded, failed, or canceled. Read the final resource before deciding next steps.
/v1/distill/runs/{run_id}Source of truth for status, current stage, all stage attempts, totals, report, student, and structured errors.
/v1/distill/runs/{run_id}/metricsMetric series for active or completed training stages. Responses cap the number of points.
/v1/distill/runs/{run_id}/logs?stage=2&after={cursor}&limit=100Sanitized log windows. Keep the returned cursor instead of replaying an unbounded stream.
const terminal = new Set(["succeeded", "failed", "canceled"]);
export async function waitForRun(runId: string, attempts = 24) {
for (let attempt = 0; attempt < attempts; attempt += 1) {
const response = await fetch(
"https://lizzy.albinilabs.com/v1/distill/runs/" + encodeURIComponent(runId),
{ headers: { Authorization: "Bearer " + process.env.LIZZY_API_TOKEN } },
);
if (!response.ok) throw await response.json();
const run = await response.json();
if (terminal.has(run.status)) return run;
await new Promise((resolve) =>
setTimeout(resolve, Math.min(1_000 * 2 ** attempt, 15_000)),
);
}
throw new Error("Watch window ended. Resume with the same run ID: " + runId);
}
Webhooks such as distill.run.succeeded anddistill.run.failed can wake your worker, but the subsequent GET remains the source of truth. Subscribe to distill.report.ready for the evaluation artifact, or use distill.* when one endpoint handles the complete workflow.
Diagnose before retrying
from_stage and may spend again, so require another approval./v1/distill/runs/{run_id}/cancelCancel a non-terminal run to stop future work. Re-read it to observe the final canceled state.
/v1/distill/runs/{run_id}/retryRetry a failed or canceled run with an optional integer stage sequence and replacement budget.
When the run finishes, continue to the results guide to resolve its report and student model.