Skip to main content

Training jobs

A training job fine-tunes a base model on examples you select, and produces a model you can host, call and download. Text-generation and embedding projects train through the same API: the project's type decides which request body POST .../training-jobs accepts, and every other route (list, detail, attempts, logs, pause, resume, finish, cancel, retry, download) is shared and returns the same job shape.

Training runs in the background. You start a job, follow it while it runs, and use its model once model_available is true. Only measured training time is billed; a run that never begins training is free.

GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs
POST /api/v1/projects/{owner_slug}/{project_slug}/training-jobs
GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/estimate-messages text
GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/profiles embedding
POST /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/preview embedding
POST /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/estimate embedding
GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}
POST /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/pause
POST /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/resume
POST /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/finish
POST /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/cancel
POST /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/retry
GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/retry-estimate
GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/attempts
GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/attempts/{attempt_id}/logs
GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/dataset
GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/download

Every field of every request and response is in the API reference. POST .../resume and POST .../retry require an Idempotency-Key header: repeating a request with the same key returns the first result instead of starting a second run. Creation has no idempotency guarantee; after an uncertain response, check the job list before submitting again.

The list always returns {data, next_cursor, total_count} with an optional limit (25 by default, at most 100). Pass cursor=<next_cursor> unchanged for the next page. No pagination mode is required. A malformed cursor or one from another project answers 400.

Start a text-generation run​

Select chats by tag and choose a base model:

{
"dataset_tags": ["support-clean"],
"model_name": "<catalog-model-id>",
"max_seq_length": 2048,
"num_train_epochs": 3
}

dataset_tags must name existing tags exactly. Several tags select the union of their chats, duplicates are ignored, and an empty list selects every chat in the project. An unknown or blank tag answers training_dataset_selection_invalid; an empty selection training_dataset_empty; a selection with no trainable text training_dataset_unusable. These are 422 on create and 409 on retry, and nothing is created or charged.

num_train_epochs is how many complete passes the run makes over your selected examples, and it accepts fractions (0.5 is half a pass). The server converts it into training steps from the selection size and the batch shape, and freezes the result on the job as planned_steps:

effective_batch_size = per_device_train_batch_size × gradient_accumulation_steps
steps_per_epoch = ceil(train_examples / effective_batch_size)
planned_steps = max(1, ceil(steps_per_epoch × num_train_epochs))

For example, 152 training examples at the default effective batch of 8 take ceil(152 / 8) = 19 steps per epoch, or 57 planned steps for 3 epochs. A final partial batch still counts as one step. max_steps (1 to 100) is the alternative: a fixed number of steps. Send one or the other, never both.

Other settings you may send: system_prompt, learning_rate, lora_rank, lora_alpha, per_device_train_batch_size, gradient_accumulation_steps, loss_mask (assistant_only or full_sequence), base_load_precision, warmup_steps, early_stopping_patience and the evaluation settings below. Every setting you omit is filled in from the base model's training profile and frozen with the job, so a job read back later reports exactly what it ran with. Read the allowed models and each setting's min, max, default and whether it accepts fractions (integer) from GET /api/v1/base-models rather than copying them. A refused value answers with a code and the field it is about.

system_prompt is one instruction for the whole job, added as the first system message of every example. It never affects inference: a hosted model answers exactly the messages you send it. If a selected example already has its own system message, creating the job with a system_prompt answers 422 and says how many conflict, rather than merging two sets of instructions.

Estimate before you start​

GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/estimate-messages?tags=support-clean&model_name=<catalog-model-id>&num_train_epochs=3

tags is a comma-separated selection, and the estimate accepts the same workload choices as create: model_name, num_train_epochs or max_steps, max_seq_length, per_device_train_batch_size, gradient_accumulation_steps, base_load_precision, warmup_steps (including 0; omit it for automatic warmup), and evaluation_cadence_kind with its matching evaluation_events or evaluation_interval_steps.

It returns the selection counts (selected_chats, selected_messages, trainable_chats, nonempty_text_messages, and is_trainable: false for an empty or unusable selection), the exact train/evaluation split, the planned steps and evaluation checks, the estimated duration and credits, and two previews:

{
"configuration_preview": {
"base_load_precision": "4bit",
"warmup_steps": 2,
"warmup_source": "automatic"
},
"evaluation_preview": {
"holdout_percent": 5,
"minimum_validation_examples": 8,
"selection_examples_required": 160
}
}

warmup_source is custom when you sent warmup_steps and automatic otherwise. The estimate uses the same arithmetic as create, but it is advisory: create still performs its own final check.

Start an embedding run​

An embedding run trains on the anchor, positive and optional negative examples of one embedding dataset. Four calls, in order:

GET .../training-jobs/profiles base models and the bounds of every setting
POST .../training-jobs/preview the exact selection, split and steps
POST .../training-jobs/estimate the credits needed to start
POST .../training-jobs start the run

Choose the dataset, a base model from the profiles, optional tag IDs to narrow the selection, and a whole-epoch max_steps. The preview returns a selection_sha256, the exact train/validation split, explicit-negative counts, the pairs and triplets scheduled per epoch, and planned_steps_per_epoch. Create requires that selection_sha256, so a run always trains on the selection you previewed. A one-step preview is a way to discover the batch count; estimate and create require a whole number of epochs.

A pair (an example with no negative) learns from the other passages in its batch, and two examples of the same query never share a batch, since each positive is a correct answer for the other. When a query has several positives and no negatives, as a search where every result was relevant produces, some of its pairs would have no batch partner. Each of those pairs instead receives, as its negative, a positive from another query in the same training split (never a passage known to answer its own query) and trains with the triplets. The preview's borrowed_negative_count reports how many training pairs were converted; the explicit-negative counts still describe your saved examples. Your data is unchanged. Validation pairs use the same fallback within the validation split. If a stranded pair has no safe passage to borrow in its own split, the selection is still refused with embedding_batches_infeasible.

The estimate is provisional: estimated_credits is null, and admission_ceiling_credits is the amount that must be available to start the run, not its expected charge. The job's cost_credits reports what was actually charged.

Preview, estimate and create also accept optional execution_limit_seconds, an integer of at least 5 seconds that cannot exceed the platform ceiling. The selected training service must support that limit; an incompatible service is refused rather than extending it. Omitting it keeps the platform ceiling. Send the same limit with all three requests. The estimate returns the chosen admission_ceiling_seconds and its credit authorization; create freezes that ceiling for the job. Retry and resume keep it, including after the platform limit is raised. A platform limit lowered below the frozen value refuses retry/resume rather than changing the job. The shared GET .../retry-estimate confirmation is available for embedding jobs and quotes authorization from that frozen ceiling at the current rate, rather than a text-training workload estimate. It is not a predicted charge. The deadline applies separately to each attempt, including model and data preparation; time in the queue is separate. It is not an aggregate spending limit across retries/resumes, and only measured training time is billed. A short limit can expire before training starts or before the requested checkpoint is reached. This API option is separate from settings; the app currently uses the default.

Embedding settings​

Preview, estimate and create accept an optional settings object; GET .../profiles publishes each setting's range and default as setting_bounds, and the current choices for each base model. A refused value answers 422 with detail.field.

SettingWhat it does
learning_rate, warmup_stepsThe optimizer's rate and its warmup.
per_device_train_batch_sizeExamples per batch. Every other example in a batch serves as a negative, so larger batches give harder training signal.
gradient_accumulation_stepsAverages gradients over several batches. It adds no negatives.
loss_scaleThe temperature of the contrastive loss.
per_device_eval_batch_size0 picks automatically from the training batch and the profile's memory_tokens.
evaluation_checksHow many validation checks the run makes (1 to 10).
early_stopping_patienceStop after this many checks without improvement, keeping the best checkpoint.
max_examples_per_queryThe most examples one query may contribute.
lora_rank, lora_alpha, lora_dropoutThe adapter's shape. A higher rank makes a larger adapter; it is applied at alpha ÷ rank; dropout applies during training only.
query_instructionOne line (up to 1,024 characters) put before every query in training, validation, the API and Search. Documents never get it.
output_dimensionsThe vector size the model is trained at and returns.
max_seq_lengthTokens per training example. A model trained above 512 accepts inputs up to its training length; one trained at 512 or less accepts 512.
training_modeHow the base model is held while the adapter trains: qlora_4bit uses the least memory, lora_16bit trains against the same weights the hosted model uses.
normalize_embeddingstrue by default. false returns raw vectors whose length carries meaning, compared by dot product; it requires loss_scale 1.

With accumulation, max_steps counts optimizer steps; the preview's planned_steps_per_epoch gives the number per epoch.

Training settings​

Embedding projects use the same New training workflow: select a dataset and base, choose epochs, inspect the split, and adjust the available Advanced settings. Optimizer steps are calculated from the planned batches and chosen epochs. Reusable training presets are planned; there is no customer recipe selector.

Validation and the kept checkpoint​

About 10% of the selection is held out for validation, always as whole groups of examples that share any text, so nothing held out is also trained on. Large groups stay in training when smaller groups can provide validation. The actual fraction can differ from 10% because groups are indivisible; check its counts in the preview. Existing frozen versions keep their original split. The run checks the validation loss up to ten times (the last at its final step): the same contrastive loss it trains on, computed on the held-out examples at the vector size and similarity the model is served with (cosine for normalized vectors, dot product for raw ones). It keeps the checkpoint with the lowest loss, the earliest on a tie. With at least four checks, two checks in a row without a new best halve the learning rate; with fewer, the rate decays linearly. Runs started before this rule keep their own: they kept the checkpoint with the best validation nDCG@10, and their job page still shows it.

That loss only chooses a checkpoint: it is measured during training, which can hold the base model at a lower precision than the hosted model uses, so it is not a measure of retrieval quality. Separate evaluation jobs for text and embeddings are planned for a later release; this release has no Retrieval quality or Retrieval evaluation panel. A successful model can be hosted without them.

Reading an embedding run. The job page draws the validation loss at each check (lower is better) and marks the checkpoint the run kept.

  • Still falling at the last check: more epochs may help.
  • Falling, then flat: more epochs will not help; the run already kept its best check.
  • Lowest, then rising: the model is fitting the training pairs more than the held-out examples. The kept checkpoint is the lowest point; next time try fewer epochs, a lower learning rate, or early stopping.
  • Flat from the first check: try a higher learning rate or more data.

Evaluation and early stopping​

For a text run, a small share of a large enough selection is held out for evaluation and never trained on. Which examples are held out is a property of your dataset: every run over an unchanged dataset uses the same ones, which makes runs comparable. The policy assigns 5% to evaluation, needs at least 8 evaluation examples, and caps the set at 512:

candidate_eval = floor(usable_examples × 0.05)
eval_examples = 0 if candidate_eval < 8 else min(candidate_eval, 512)
train_examples = usable_examples - eval_examples

So evaluation starts at 160 usable examples; a smaller selection trains on all of its examples and reports no evaluation loss.

You can choose when evaluation runs:

{"evaluation_cadence": {"kind": "events", "events": 10}}

or {"evaluation_cadence": {"kind": "fixed_interval", "interval_steps": 300}}. An event budget is 1 to 100, and a fixed interval may resolve to at most 100 checks; both include the final step exactly once. Without a cadence the run schedules about ten checks per epoch, at least two and at most fifty, and the job shows it as Managed.

The served model is the checkpoint with the lowest validation loss across the run, not necessarily the last one; on a tie the earlier checkpoint is kept. Validation loss measures next-token prediction on held-out rows that are also reused to select the checkpoint, so it is a selection signal, not task accuracy. Keep an untouched test set outside the training selection and compare the tuned model with the base model on it before relying on it.

Early stopping is off unless you ask for it. Send early_stopping_patience (1 or more) to stop once evaluation loss has not improved for that many checks. A run that stops early still ends succeeded, before planned_steps. The estimate returns early_stopping_patience_max, two fewer than the scheduled checks (the first check sets the baseline and the final one cannot shorten the run), so a schedule with fewer than three checks allows only 0.

Three settings control how an evaluation pass runs. They change neither which rows are evaluated nor when, so they cannot change what the run learns; they exist because evaluation is where a long run is most likely to run out of memory.

SettingDefaultWhat it does
per_device_eval_batch_size0Evaluation batch size; 0 means the training batch size. Lowering it is the first fix for an evaluation out-of-memory failure.
fp16_full_evalfalseHalf-precision evaluation uses less memory but changes the numerics of the loss that selects the model.
eval_accumulation_steps1Evaluation output buffering; see below.

Evaluation output buffering​

eval_accumulation_steps is the Evaluation output buffering control. The value 1 means evaluation output is collected at the end of each check, shown in the app as End of check (default); it is not a one-step interval. Custom intervals run from 2 to 32. Smaller intervals move output more often and can reduce buffer memory, at some cost in speed. Buffering does not split evaluation batches: for an evaluation out-of-memory failure, lower the evaluation batch size first, then consider half-precision evaluation, and only then change buffering.

Reading the loss charts​

  • Falling training loss means the model fits the training examples more closely; it does not by itself show that answers improved.
  • Validation loss measures prediction on examples that did not update the model; lower is better when comparing checkpoints of one run.
  • Training loss falling while validation loss rises suggests overfitting: try fewer epochs or a lower learning rate and compare on the same dataset.
  • The learning-rate and gradient-norm charts are diagnostics, not scores: read their shape within a run rather than any absolute value.

A finished job that evaluated has an evaluation block: the lowest loss and its step, the last loss and step, how many checks ran, and which checkpoint is served. promoted_before_last_eval: true says the served checkpoint is earlier than the last check. A job that never evaluated reports evaluation: null.

Following a run​

The job detail reports status, progress, the frozen configuration, metrics, cost_credits (the actual charge; 0 when nothing was charged) and model_available. Each job has a number, counted from 1 within its project and shown in the app, and an id used by the API. To read a job by its number:

GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{number}?identifier=number

Jobs are never deleted, so numbers never shift; hosted-model and billing responses show the same value as training_job_number.

Waiting for compute. An embedding job can be accepted while compute is busy with another run. It waits for available capacity, and queue time is not charged. Each account still has one active training run.

Preparation. A new job first copies its selected examples, exactly as they are, into a frozen record: editing or deleting them later cannot change what the job trains on, and a retry trains on the same data. preparation moves through queued, snapshotting, then verified, failed or cancelled; training starts only after verified. A preparation failure is an input failure (failure_category: "input") and is not charged. A job created before this step existed reports preparation: null.

Timing. timing breaks the run into queued_seconds, preparing_seconds, training_seconds, post_processing_seconds and finalizing_seconds, with total_seconds (active time), elapsed_seconds (wall clock) and idle_seconds (the difference, such as the time before a retry). A phase without reliable timestamps is omitted rather than guessed. These are not the billed interval; see Billing.

Why it ended. A finished job carries outcome.stop_reason: completed, max_steps, early_stopping, cancelled or execution_deadline. outcome.downgrades lists settings the run could not apply as sent, as setting:action:cause tokens (for example early_stopping:disabled:no_eval_set when early stopping was asked for without an evaluation set). outcome: null means the run reported nothing, as for jobs that never reached training.

Environment. The Run card records the hardware the run used (GPU model and count, memory, driver and framework versions) and the frozen per-minute rate. These explain why similar runs can differ in price or behavior; they are not quality scores.

The data it trained on. A job's detail includes the first 50 frozen examples; the rest are paged, up to 200 at a time:

GET .../training-jobs/{job_id}/dataset?offset=0&limit=25
GET .../training-jobs/{job_id}/dataset?offset=0&limit=25&search=invoice&split=eval
GET .../training-jobs/{job_id}/dataset/{chat_id}

Each item says whether it was train or eval input. search matches frozen text, and a searched page returns total: null. These read the frozen record, not your current data.

Attempts and logs​

A job runs as one or more attempts (a retry or a resume adds one). The attempts route returns each attempt's status, timestamps, failure code and log metadata; the logs route streams the attempt's log as newline-delimited JSON, with offset and limit in bytes.

Attempt statusMeaning
leasedCapacity was reserved for the attempt.
acceptedThe run was accepted for execution.
startedThe run is executing.
finalizingTraining ended; the model is being verified and published.
succeededThe attempt completed.
failedThe attempt ended with a failure code.
cancelledThe attempt was cancelled before training began.
expiredContact with the run was lost and the attempt was closed.
submission_uncertainWhether the run was accepted could not be confirmed; no duplicate run is started.

log_complete: true (and the header X-Log-Complete: true) marks a complete log. After an abrupt stop the log is the last saved copy: integrity-checked but possibly missing the final output, marked log_complete: false and log_recovered: true.

Job states​

StatusMeaning
pendingAccepted and waiting to start.
runningPreparing, training, or publishing the model.
cancel_requestedCancelled before training began; the run is being stopped.
pause_requestedPausing: the run trains on to its next checkpoint, and those steps are billed.
pausedStopped at a checkpoint with everything kept. Resume it or finish it. A paused job does not hold your active-training slot.
succeededThe model was verified and published.
failedThe run ended without a model.
cancelledCancelled before training began: no model, no charge.

Use model_available, not the status, to decide whether a model can be hosted or downloaded: some older stopped jobs are cancelled and still have a model.

Pause, resume, finish and cancel​

Which control applies depends only on whether training has begun:

PhaseControlResult
Queued or preparingPOST .../cancelThe job settles cancelled. Nothing was learned, so there is no model and no charge.
TrainingPOST .../pause (Stop in the app)The run trains on to its next checkpoint and settles paused.
PublishingneitherTraining is over and the model is moments away.

The server enforces the boundary, because a client only sees progress after the fact: cancel answers 409 training_job_already_training once training has begun, and pause answers 409 training_job_not_training_yet before it.

A pause is not immediate, and the wait is billed. It is acknowledged at once, but the run continues to its next checkpoint, the only point it can later resume from. pause.steps_until_pause and pause.next_boundary_step on the job say how far that is, and the app shows them before you confirm. Checkpoints are at most 25 steps apart, so a pause trains at most 25 more steps. Pausing before the first checkpoint still works and can take longer. When no checkpoint remains both values are null (the pause is then effectively the end of the run); a job without a pause block cannot be paused. A pause that arrives after the last checkpoint lets the run finish normally, and it settles succeeded with its model.

A paused job offers two operations:

OperationResult
POST .../resumeContinues from the checkpoint; charts and the selected checkpoint carry on where they stopped. Takes no body and requires an Idempotency-Key.
POST .../finishPublishes the model the run reached and settles succeeded. Free: the training time was charged when the pause settled.

Embedding runs have the same three controls. An embedding run writes a checkpoint at every validation check, so pause.next_boundary_step is the next check before the final step, and the distance is the run's evaluation cadence rather than the 25-step bound above. Finish publishes the checkpoint with the best held-out score so far, the one the run would have kept had it ended there, which can be earlier than the checkpoint the pause stopped at. Both Qwen3-Embedding-0.6B and Qwen3-Embedding-4B support stopping at a checkpoint and resuming from it. Resume preserves the selected checkpoint even when it was written by an earlier attempt; missing or unverifiable checkpoint files are refused. A failed run can also be retried from the start.

Embedding resumes keep the job's frozen learning-rate schedule. Older profiles that did not record a schedule retain linear decay. The checkpoint must still verify, and the frozen base must remain eligible for training. When early stopping is enabled, the count of consecutive evaluations without improvement also continues from the checkpoint. An incomplete evaluation history or a checkpoint that already reached early stopping cannot restart training. If an embedding run reaches early stopping at the requested pause checkpoint, it finishes with early_stopping rather than entering paused.

There is no cancel for a paused job (409): it has already trained and been charged, so discarding its model would help no one. Finish publishes the best verified checkpoint, or, for a run with no evaluation set, the last checkpoint: the same model the run would have produced by ending there. If publishing cannot verify the model, the job settles failed with no further charge and can be retried. Resume and finish refuse each other with training_job_finalizing.

When a run reaches the platform's execution limit it stops itself, keeps the checkpoint it would have selected, settles succeeded and reports outcome.stop_reason: "execution_deadline". It is charged for the time it trained and can be resumed for the remaining steps.

Retry​

A failed or cancelled job can be retried. A failed job never retries itself: the platform keeps the evidence and any verified checkpoint, and you decide whether to run again.

curl -X POST "https://reopenly.com/api/v1/projects/<owner>/<project>/training-jobs/42/retry" \
-H "Authorization: Bearer $REOPENLY_API_KEY" \
-H "Idempotency-Key: 8f8d8ac4-8de5-4c63-a8f4-f0a39db1af4d" \
-H "Content-Type: application/json" \
-d '{"resume_from_checkpoint": false}'

A retry reuses the job's frozen settings and frozen data; it never re-reads the dataset, and a job whose frozen data no longer exists cannot be retried. resume_from_checkpoint: true continues from a verified checkpoint of the same frozen input when one exists. GET .../retry-estimate says whether a retry is possible and what it would cost; it starts nothing.

Billing and limits​

Only the training loop is billed: for each attempt, from its first training step to the moment training ends. Queueing, preparation (loading the model, reading and tokenizing your data) and publishing the model are not charged, and a run that never begins training is free. Failed attempts are not charged. cost_credits is the actual charge, from the credit ledger.

The estimate is calibrated on measured runs at an effective batch of 8 (per_device_train_batch_size 2 × gradient_accumulation_steps 4) and 8-bit loading. Other shapes are priced with a safety margin, and a batch below 8 is priced as 8. To enlarge the effective batch, raise gradient_accumulation_steps rather than per_device_train_batch_size: a larger per-device batch needs more memory per step and, near the limit of the training hardware, can slow every step far more than its size suggests. Sending num_train_epochs rather than max_steps keeps the length of a run tied to your data when you change the batch.

A run's maximum exposure must fit the platform's execution limit, six hours by default. Read the current value from duration_components.execution_limit_seconds on POST /api/v1/pricing/estimate rather than hard-coding it. A run that does not fit is refused before it starts with training_duration_exceeds_limit (422 on create, 409 on retry): reduce num_train_epochs, max_seq_length or the selection.

Failure codes​

Failure codes are stable, and none carries raw system output.

CodeMeaning
training_exceeds_provider_limitThe run is longer than the platform can execute in one go. Nothing started; reduce epochs, sequence length or the selection.
worker_disk_exhaustedThe run ran out of storage space.
provider_failedThe run failed while executing.
provider_execution_timeoutThe run exceeded its execution deadline.
provider_ttl_expiredThe run waited or ran longer than allowed and was closed.
provider_callback_missingThe run stopped reporting progress and was closed.
provider_terminal_callback_missingThe run ended without delivering a verified model.
provider_result_unavailableThe run's result could not be retrieved.
provider_status_unavailableThe run's state could not be confirmed after repeated checks.
provider_cancelledThe run was stopped by the platform without a request from you.

Download​

When model_available is true, download the trained adapter:

GET /api/v1/projects/{owner_slug}/{project_slug}/training-jobs/{job_id}/download?format=lora

Embedding adapters include manifest.json with SHA256 checksums and byte sizes for the files in the archive, including the adapter config's public metadata. You can use it to verify the downloaded files.

It streams a gzip-compressed archive. GET .../training-jobs/{job_id}/download-metadata lists its files and total size first, without downloading. To call the model instead, give it a name under Hosted models.

Text training estimates include training_plan, the schedule used to calculate the quote. It describes training and evaluation work for the selected settings.