Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.morphllm.com/llms.txt

Use this file to discover all available pages before exploring further.

Send labeled examples, get back a text classifier. Create a job, poll until it finishes, then classify text against it. A small Reflex trains in about 30 seconds. See the Reflexes overview for what a Reflex is. Jobs use the OpenAI fine-tuning API, so the official SDKs work unchanged, with two differences:
  • Inline training data. Pass training_data in the body. No Files API, no training_file.
  • Fully managed. No hyperparameters. One base model, morph-reflex-v1.
Base modelmorph-reflex-v1 (default, optional on create)
Minimums2 distinct labels, 5 examples per label
Concurrency3 jobs run at once per key; the rest queue

Quick Start

Four steps: get a key, create a job, wait for it, classify text.
training_data is a Morph extension. The OpenAI Python SDK rejects unknown arguments, so pass it through extra_body=.
1

Get an API key

Grab one from the dashboard.
2

Create a training job

Send labeled examples. No data? Use generate or label_data instead.
from openai import OpenAI

client = OpenAI(base_url="https://api.morphllm.com/v1", api_key="YOUR_API_KEY")

job = client.fine_tuning.jobs.create(
    model="morph-reflex-v1",
    suffix="support-classifier",
    extra_body={
        "training_data": [
            {"text": "I need a refund for my order", "label": "billing"},
            {"text": "Charged me twice this month", "label": "billing"},
            {"text": "Cancel my subscription", "label": "billing"},
            {"text": "Update my card on file", "label": "billing"},
            {"text": "The invoice amount is wrong", "label": "billing"},
            {"text": "The app crashed on launch", "label": "bug"},
            {"text": "Submit button does nothing", "label": "bug"},
            {"text": "Page never finishes loading", "label": "bug"},
            {"text": "Getting a 500 error on save", "label": "bug"},
            {"text": "Login fails every time", "label": "bug"},
        ]
    },
)
print(job.id)  # ftjob-...
3

Wait for training

Poll until status is succeeded. A small Reflex takes about 30 seconds.
import time

while job.status in ("queued", "running"):
    time.sleep(3)
    job = client.fine_tuning.jobs.retrieve(job.id)
if job.status != "succeeded":
    raise RuntimeError(f"{job.status}: {job.error}")
4

Classify text

Predict against fine_tuned_model (your suffix, or the job id if you gave none).
import requests

res = requests.post(
    "https://api.morphllm.com/v1/reflex/predict",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={"model": job.fine_tuned_model, "text": "I was billed twice this month"},
)
print(res.json())  # {"label": "billing", "confidence": 0.97, ...}

Create a Job

POST /v1/fine_tuning/jobs
Starts a training job. Provide exactly one input: training_data, generate, or label_data (see Input modes).
FieldTypeRequiredDescription
modelstringNomorph-reflex-v1 (default and only value).
suffixstringNoNames the served model. Becomes fine_tuned_model on success.
labelsarray*The classes. 2+ required for generate and label_data; inferred from training_data if omitted.
// → 200
{
  "id": "ftjob-a1b2c3d4-...",
  "object": "fine_tuning.job",
  "model": "morph-reflex-v1",
  "status": "queued",
  "labels": ["billing", "bug"],
  "trained_examples": 10,
  "fine_tuned_model": null,
  "result": null,
  "suffix": "support-classifier"
}

Input modes

Pick one. The training set, however it is produced, must have 2+ labels and 5+ examples per label, else the job fails.
generate and label_data synthesize or label data through the OpenAI Batch API, so the job spends a few minutes on data before training. status stays running the whole time. Poll as usual.
1. training_data: labeled rows you supply.
{ "training_data": [ { "text": "I was charged twice", "label": "billing" } ] }
FieldTypeRequiredDescription
training_dataarrayYes{ "text": string, "label": string } rows.
2. generate: no data; synthesize it from a description.
{
  "labels": ["billing", "bug", "feature"],
  "generate": { "description": "classify support tickets by topic", "examples_per_label": 25 }
}
FieldTypeRequiredDescription
generate.descriptionstringYesWhat the classifier is for.
generate.examples_per_labelintegerNoExamples to synthesize per label. Default 25, max 100.
labelsarrayYesThe classes to generate for. 2+.
3. label_data: your unlabeled text, sorted into your classes.
{
  "labels": ["billing", "bug", "feature"],
  "label_data": { "texts": ["I was charged twice", "the app crashes on login"], "description": "support tickets" }
}
FieldTypeRequiredDescription
label_data.textsarrayYes10 to 20,000 unlabeled strings.
label_data.descriptionstringNoContext for more accurate labeling.
labelsarrayYesThe classes to sort into. 2+.

Retrieve a Job

GET /v1/fine_tuning/jobs/{job_id}
Poll until status is succeeded, failed, or cancelled.
// → 200 (succeeded)
{
  "id": "ftjob-a1b2c3d4-...",
  "object": "fine_tuning.job",
  "status": "succeeded",
  "fine_tuned_model": "support-classifier",
  "result": { "accuracy": 0.95, "f1_score": 0.94 },
  "finished_at": 1780107148
}

List Jobs

GET /v1/fine_tuning/jobs?limit=&after=
Returns the key’s jobs, newest first.
Query paramTypeDescription
limitintegerJobs per page. Default 20, max 100.
afterstringJob id cursor. Returns jobs created before it.
// → 200
{
  "object": "list",
  "data": [ { "id": "ftjob-a1b2c3d4-...", "object": "fine_tuning.job", "status": "succeeded" } ],
  "has_more": false
}

Cancel a Job

POST /v1/fine_tuning/jobs/{job_id}/cancel
Stops a queued or running job. status becomes cancelled.

Training Events

GET /v1/fine_tuning/jobs/{job_id}/events
Returns the loss curve as per-step events, plus one terminal event. Add ?stream=true for a live Server-Sent Events stream.
// → 200
{
  "object": "list",
  "data": [
    {
      "id": "ftevent-...",
      "object": "fine_tuning.job.event",
      "level": "info",
      "message": "Step 5: train_loss 0.42",
      "type": "metrics",
      "data": { "epoch": 1, "step": 5, "train_loss": 0.42 }
    }
  ],
  "has_more": false
}

Predict

POST /v1/reflex/predict
Classifies text against a trained model. A Morph endpoint, not an OpenAI method, so call it with a plain POST. The model must be ready, else 409 (model_not_ready).
FieldTypeRequiredDescription
modelstringYesA fine_tuned_model name or job id.
textstringYesThe text to classify.
// → 200
{
  "model": "support-classifier",
  "label": "billing",
  "confidence": 0.97,
  "all_scores": [0.97, 0.03],
  "inference_time_ms": 8
}
all_scores is the per-class probabilities, ordered by the model’s labels.

Classify against multiple models

predict takes one model per request. To run several of your classifiers over the same text, make one call per model and fire them in parallel.
import requests, concurrent.futures

def predict(model, text):
    return requests.post(
        "https://api.morphllm.com/v1/reflex/predict",
        headers={"Authorization": "Bearer YOUR_API_KEY"},
        json={"model": model, "text": text},
    ).json()

text = "I was billed twice this month"
models = ["billing-classifier", "urgency-classifier", "topic-classifier"]
with concurrent.futures.ThreadPoolExecutor() as ex:
    results = list(ex.map(lambda m: predict(m, text), models))
Each call is a separate request, independently subject to your per-key concurrency limit. There is no batched multi-model endpoint today.

Delete a Job

DELETE /v1/fine_tuning/jobs/{job_id}
Deletes the job and its trained model.
// → 200
{ "id": "ftjob-a1b2c3d4-...", "object": "fine_tuning.job.deleted", "deleted": true }

Delete a Model

DELETE /v1/models/{model}
Deletes a model by name (the fine_tuned_model value or job id). Same effect as deleting the job; OpenAI Models-API parity.
// → 200
{ "id": "support-classifier", "object": "model", "deleted": true }

Reference

FieldTypeDescription
idstringJob id, prefixed ftjob-.
objectstringAlways fine_tuning.job.
modelstringAlways morph-reflex-v1.
created_atintegerUnix timestamp (seconds) at creation.
finished_atinteger / nullUnix timestamp at terminal state, else null.
fine_tuned_modelstring / nullServed model name once succeeded. The suffix, or the job id if none.
statusstringqueued, running, succeeded, failed, or cancelled.
labelsarrayThe label set used for training.
trained_examplesintegerNumber of training examples.
resultobject / null{ "accuracy", "f1_score" } when succeeded, else null. Each value may be null.
errorobject / null{ "message" } when failed, else null.
suffixstring / nullThe suffix supplied at creation, or null.
FieldTypeDescription
idstringEvent id, prefixed ftevent-.
objectstringAlways fine_tuning.job.event.
created_atintegerUnix timestamp (seconds).
levelstringinfo, warn, or error.
messagestringHuman-readable message.
typestringmetrics for per-step loss, message for the terminal event.
dataobject{ "epoch", "step", "train_loss" }.
OpenAI-shaped: { "error": { "message", "type", "param", "code" } }.
StatustypeWhen
401authentication_errorInvalid or missing API key.
400invalid_request_errorValidation failed. param names the offending field.
404invalid_request_errorNot found. code is job_not_found or model_not_found.
409invalid_request_errorModel not ready. code is model_not_ready.