HTTP Server¶
The midigpt[http] extra adds a stateless REST API for generation. Every request carries the full score and generation parameters — the server holds no per-generation session state between calls. The persistent state is the loaded model(s) and device.
Setup¶
Local checkpoint¶
Pretrained from HuggingFace Hub¶
# Checkpoint filename prefix on the hub (downloads once, cached in ~/.cache/huggingface/)
midigpt-http --pretrained yellow_medium --port 8000
midigpt-http --pretrained prism_medium --port 8000
midigpt-http --pretrained expressive_medium --port 8000
# Custom repo
midigpt-http --pretrained my_model --hf-repo myorg/myrepo --port 8000
Device selection¶
midigpt-http --pretrained yellow_medium --device cuda # explicit GPU
midigpt-http --pretrained yellow_medium --device mps # Apple Silicon
midigpt-http --pretrained yellow_medium --device auto # auto-detect (default)
midigpt-http --pretrained yellow_medium --device cpu # force CPU
Multiple models in one server¶
Pass --config with a YAML file instead of --ckpt/--pretrained to serve several checkpoints from one process, selectable per-request via the model id:
# models.yaml
host: 0.0.0.0
port: 8000
idle_timeout: 0
max_queue: 64 # server-wide across every model
hf_repo: Metacreation/MIDI-GPT # default for pretrained entries
device: cpu # default device for every entry
max_parallel: 1 # default per-model concurrency
default_model: yellow_medium # defaults to the first entry's id if omitted
models:
- id: yellow_medium # the "model" id clients pass to /generate
pretrained: yellow_medium
- id: prism_medium
pretrained: prism_medium
device: cpu # per-entry override
max_parallel: 2 # per-entry override — benchmark before raising, see Concurrency below
- id: my_custom
ckpt: /path/to/custom-final.safetensors
hf_repo: myorg/myrepo # per-entry override (pretrained entries only)
host/port/idle_timeout/log_level/max_queue in the file override the equivalent CLI flags if both are given.
CLI reference¶
| Flag | Default | Description |
|---|---|---|
--config PATH |
— | YAML file describing one or more models (see above). Mutually exclusive with --ckpt/--pretrained |
--ckpt PATH |
— | Local .safetensors, .pt bundle, or checkpoint directory (mutually exclusive with --config/--pretrained) |
--pretrained NAME |
— | Checkpoint filename prefix on HuggingFace (yellow_medium, prism_medium, expressive_medium, ...) |
--hf-repo REPO |
Metacreation/MIDI-GPT |
HuggingFace repo ID to download --pretrained from |
--device DEVICE |
auto | cpu, cuda, mps, or auto |
--host HOST |
0.0.0.0 |
Bind address |
--port PORT |
8000 |
TCP port |
--idle-timeout SECONDS |
0 (off) |
Shut down automatically after this many seconds of inactivity |
--max-parallel N |
1 |
Max concurrent /generate calls against this model (single-model mode only — use per-entry max_parallel in --config for multi-model). Each is still a full sequential decode sharing CPU/GPU with the others, not a throughput multiplier — benchmark before raising above 1 |
--max-queue N |
64 |
Total requests admitted (queued + running), across all models, before returning 503 |
--log-level |
INFO |
DEBUG, INFO, WARNING, ERROR |
One of --config, --ckpt, or --pretrained is required.
Endpoints¶
Interactive docs are available at http://localhost:8000/docs once the server is running.
GET /health¶
Liveness probe (also resets the idle-shutdown timer).
inflight is the number of requests currently admitted (queued behind a model's concurrency limit or actively running) server-wide, across every model.
GET /info¶
Returns capabilities/attributes/resolution for one loaded model.
Omit model to get the server's default model.
{
"model": "yellow_medium",
"default_model": "yellow_medium",
"checkpoint": "Metacreation/MIDI-GPT/yellow_medium",
"capabilities": {
"tension": false,
"note_density": true,
"min_polyphony": true,
"max_polyphony": true,
"min_note_duration": true,
"max_note_duration": true,
"supports_token_mask": true,
"supports_attention_mask": true,
"supports_attention_approx": true,
"supports_attention_skip": true,
"supports_remove": true,
"supports_pitch_mask": true,
"pitch_mask_scale_presets": ["major", "minor", "..."],
"supports_rhythm_mask": true,
"rhythm_mask_grid_units": ["eighth", "eighth_triplet", "..."],
"supports_remix": true,
"supports_streaming": true
},
"attributes": {
"note_density": 10,
"min_polyphony": 10,
"max_polyphony": 10,
"min_note_duration": 10,
"max_note_duration": 10
},
"resolution": 480
}
supports_pitch_mask/supports_rhythm_mask/supports_remix gate whether this model's checkpoint can accept the pitch_mask, rhythm_mask, and remix controls documented in API Reference — TrackPrompt — always check per model rather than assuming any two checkpoints agree.
GET /models¶
Ids + labels of every model this server has loaded.
{
"default_model": "yellow_medium",
"models": [
{"id": "yellow_medium", "checkpoint": "Metacreation/MIDI-GPT/yellow_medium"},
{"id": "prism_medium", "checkpoint": "Metacreation/MIDI-GPT/prism_medium"}
]
}
POST /generate¶
Generate or infill music. Pass the full score and generation parameters; receive the result score back.
Request body
{
"score": { ... },
"request": { ... },
"request_id": "optional-caller-supplied-id",
"stream": false,
"model": "yellow_medium"
}
score— aScoreserialised withScore.to_dict()(see Inference API)request— aGenerationRequestdict (see below)request_id— optional idempotency/correlation id. If omitted, the server generates one (uuid4 hex) and returns it in the response body and theX-Request-Idheader. Needed to target this specific generation withPOST /generate/{request_id}/cancelstream—truefor atext/event-streamof newly-completed notes as they're generated (see Streaming below). Not yet supported together withconfig.num_candidates > 1model— which loaded model to run this request against (an id fromGET /models). Omit to use the server's default model; single-model deployments never need to set this
Response (config.num_candidates == 1, the default)
{
"request_id": "a1b2c3d4...",
"model": "yellow_medium",
"status": "completed",
"score": { ... },
"seed": 12345,
"timing": {
"model_forward_s": 0.42,
"encode_s": 0.01,
"decode_s": 0.01,
"gen_count": 4
},
"tokens": {
"context_tokens": 512,
"generated_tokens": 128,
"max_context_tokens": 1024,
"context_utilization": 0.5,
"tokens_per_second": 304.76,
"truncated": false
}
}
status is "completed" or "cancelled" (see POST /generate/{request_id}/cancel below) — a cancelled response still carries whatever partial score was generated before the cancel was observed. truncated: true means generation stopped because it hit the context budget, not because it reached a natural end — it may be cut off mid-bar/mid-track.
Response (config.num_candidates > 1)
num_candidates independent full generations from the same prompt (one seed each, no accept/reject filtering) — for "give me a few takes, I'll pick one" workflows.
{
"request_id": "a1b2c3d4...",
"model": "yellow_medium",
"status": "completed",
"base_seed": 12345,
"candidates": [
{"score": { ... }, "seed": 12345, "gen_count": 4, "error": null, "truncated": false},
{"score": null, "seed": 12346, "gen_count": 0, "error": "...", "truncated": false}
],
"summary": {"requested": 2, "succeeded": 1, "failed": 1, "failures": [{"seed": 12346, "reason": "..."}]},
"timing": { ... },
"tokens": { ... }
}
Error codes
| Status | Meaning |
|---|---|
400 |
Malformed score/request dict, or unknown model id |
422 |
RequestValidationError — structurally invalid generation request |
500 |
Inference failure |
503 |
Server busy — max_queue requests already admitted; retry after the Retry-After header (seconds) |
Failed requests (500) are appended to a JSONL log for offline analysis — see MIDIGPT_FAILED_REQUESTS_LOG (default failed_requests.jsonl).
POST /generate/{request_id}/cancel¶
Stop an in-flight generation. Returns the partial result via the original /generate call's response (streaming) or its eventual HTTP response (non-streaming), with status: "cancelled", instead of the caller having to wait for a natural finish or drop the connection.
404 if request_id has no matching in-flight request (already finished, already cancelled, or never existed).
Streaming (stream: true)¶
Instead of one JSON response at the end, POST /generate with "stream": true returns a text/event-stream of newly-completed notes as they're generated, ending with exactly one terminal event carrying the same response shape a non-streaming call would have returned:
data: {"type": "notes", "notes": [...]}
data: {"type": "notes", "notes": [...]}
data: {"type": "done", "response": { ...same shape as the non-streaming response... }}
type is one of "notes" (a batch of newly-generated notes), "done" (completed normally), "cancelled" (stopped via the cancel endpoint or a dropped connection), or "error" ({"type": "error", "request_id": ..., "error": "..."}). Scoped to the single-candidate path — stream: true with config.num_candidates > 1 returns 400.
Client example¶
The server accepts plain JSON — no midigpt dependency needed on the client side.
curl -s -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{
"score": {
"resolution": 480, "tempo": 500000,
"tracks": [{
"instrument": 0, "track_type": "melodic",
"bars": [
{"ts_numerator": 4, "ts_denominator": 4, "notes": []},
{"ts_numerator": 4, "ts_denominator": 4, "notes": []},
{"ts_numerator": 4, "ts_denominator": 4, "notes": []},
{"ts_numerator": 4, "ts_denominator": 4, "notes": []}
]
}]
},
"request": {
"tracks": [{"id": 0, "bars": [0, 1, 2, 3]}],
"config": {"model_dim": 4}
}
}'
The response score field contains the filled-in notes in the same JSON shape.
Concurrency¶
Each model gets its own semaphore, sized by that model's max_parallel (default 1 — fully serialized). Two requests against different models never wait on each other at all; within one model, calls overlap up to max_parallel — each overlapping call is still a full sequential decode sharing the box's CPU/GPU via the OS scheduler and torch's own intra-op thread pool, not a throughput multiplier, so raise it only after benchmarking on the target machine.
On top of per-model concurrency, max_queue bounds how many requests may be admitted (queued + running) across all models at once — once that many are admitted, further POST /generate calls get an immediate 503 (with Retry-After) instead of hanging on an unbounded wait, mirroring Ollama's OLLAMA_MAX_QUEUE backpressure. GET /health and GET /info remain responsive throughout regardless of queue depth.