Training
Submit, list, stream, and cancel fine-tuning jobs against the configured TrainingBackend (Unsloth for the personal tier; Axolotl or peers for SME / enterprise). Successful runs auto-register the produced AdapterArtifact in the workspace's AdapterRegistry, where it becomes visible at /admin/adapters and activatable via /admin/models/{model_id}/adapters.
The router lives at packages/training/admin_routes.py. Wire schemas: CreateJobRequest, CreateJobResponse, TrainingJobRecord.
Endpoints
| Method | Path | Auth | Summary |
|---|---|---|---|
| POST | /admin/training/jobs | Bearer | Submit a training job; returns 202 with {job_id}. |
| GET | /admin/training/jobs | Bearer | List jobs submitted by the workspace in this process. |
| GET | /admin/training/jobs/{job_id} | Bearer | Current TrainingJobRecord. |
| GET | /admin/training/jobs/{job_id}/stream | Bearer | SSE stream of status transitions + heartbeats. |
| POST | /admin/training/jobs/{job_id}:cancel | Bearer | Request cancellation; returns updated record. |
POST /admin/training/jobs
Body CreateJobRequest:
{
"base_model": "meta-llama/Llama-3.1-8B",
"dataset_id": "ds_01HF…",
"recipe": "sft",
"hparams": {"lr": 2e-4, "epochs": 3},
"eval_split": 0.1
}recipe is a TrainingRecipe enum value (sft, dpo, …); hparams is a free-form mapping validated by the concrete backend; eval_split is in [0.0, 1.0].
Returns 202 with {"job_id": "..."}. Per PRD 04 NFR-1 the call must return within 500 ms p95 — the actual training runs asynchronously (subprocess for Unsloth; Arq worker when BIGWAVE_RUNNER=arq).
curl -X POST -H "Authorization: Bearer $BIGWAVE_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"base_model":"meta-llama/Llama-3.1-8B","dataset_id":"ds_01HF...","recipe":"sft"}' \
http://localhost:8000/admin/training/jobsGET /admin/training/jobs
Returns a JSON array of TrainingJobRecord for jobs submitted by the workspace in this API process. The tracker is per-process; restarts lose visibility into previous submissions for the direct backend. The Arq path persists job metadata in Redis so it survives restarts.
GET /admin/training/jobs/{job_id}
Returns the current TrainingJobRecord:
{
"handle": {
"job_id": "trj_01HF…",
"backend_name": "unsloth"
},
"status": "succeeded",
"artifact": {
"id": "adp_01HF…",
"base_model": "meta-llama/Llama-3.1-8B",
"artifact_uri": "file:///var/lib/bigwave/adapters/adp_01HF.bin",
"dataset_sha": "sha256:…"
},
"error": null
}On observed succeeded, the route registers the artifact in the workspace AdapterRegistry (idempotent; safe to call from polling loops). Returns 404 when the job was not submitted by this workspace.
GET /admin/training/jobs/{job_id}/stream
Server-Sent Events. Frame layout:
event: status— one frame per status transition (queued → running → succeededorfailed/cancelled). Data is the fullTrainingJobRecordJSON.: pingheartbeats every 15 s keep idle proxies (nginx default 60 s) from severing the connection.- A terminal
event: statuscloses the stream. Onsucceeded, the artifact is registered inAdapterRegistrybefore the stream closes — clients observe the adapter at/admin/adaptersonce the stream ends.
Total polls are capped at one hour (_MAX_STREAM_POLLS = 7200 at 0.5 s cadence); a runaway connection emits a synthetic event: error and closes.
POST /admin/training/jobs/{job_id}:cancel
Idempotent — calling it on an already-terminal job is a no-op. Returns the updated TrainingJobRecord. When the backend cannot interrupt mid-step the route returns 409 Conflict. Returns 404 when the job was not submitted by this workspace.
curl -X POST -H "Authorization: Bearer $BIGWAVE_ADMIN_TOKEN" \
http://localhost:8000/admin/training/jobs/trj_01HF...:cancelOperator notes
- Workspace fence. Every route resolves the workspace from the bearer principal; the in-memory job tracker is keyed by
(workspace, job_id)so a workspace cannot see another's jobs. See workspace fence. - Direct vs Arq path. When
app.state.job_runneris wired (setBIGWAVE_RUNNER=arq) the route enqueues an Arq functionrun_training_job; otherwise the route callsbackend.submitdirectly. The wire shape and route contract are identical — only the durability of the tracker differs. - Artifact registration. On
succeeded, the producedAdapterArtifactis persisted inAdapterRegistry(idempotent on(workspace, adapter_id)). The GET and SSE paths both trigger this so non-streaming consumers still observe the new adapter at/admin/adapters. - Cancel semantics.
409 Conflictfrom cancel means the backend received the signal but cannot interrupt mid-step. The job continues to terminal state on its own. 503causes.app.state.training_backendandapp.state.adapter_registryare wired in lifespan; routes return503with the missing-binding detail when either is absent.- Audit.
submitandcancelwritetraining.submit/training.cancelaudit rows. See Audit. - Related guide. See 05 — Train an adapter for the operator- facing tour and feedback-loop.md for the end-to-end runbook.