Skip to main content

Management API — Endpoint Index

A single-page index of every /api endpoint. Each area has a dedicated page with request/response examples — follow the section links.

All endpoints return the management envelope: {success, data, error}.

Models (/api/models) — full reference​

EndpointMethodDescription
/api/modelsGETList all models
/api/modelsPOSTDownload a model (HF or local path)
/api/models/previewPOSTPreview model metadata & memory estimates
/api/models/{id}GETGet model details
/api/models/{id}DELETEDelete model from disk
/api/models/{id}/loadPOSTLoad model to GPU
/api/models/{id}/unloadPOSTUnload model from GPU
/api/models/{id}/lockPOSTLock model (prevent unload)
/api/models/{id}/unlockPOSTUnlock model
/api/models/{id}/cancelPOSTCancel in-progress download
/api/models/{id}/leasePOSTLease the resident model (returns lease_id once)
/api/models/{id}/leaseGETLive lease and last ended lease (never the ID)
/api/models/{id}/lease/renewPOSTRenew (header X-miLLM-Lease)
/api/models/{id}/leaseDELETERelease (header X-miLLM-Lease)

POST …/load and …/unload read X-miLLM-Lease; see Model lease.

SAEs (/api/saes) — full reference​

EndpointMethodDescription
/api/saesGETList SAEs + attachment status
/api/saes/downloadPOSTDownload SAE from HuggingFace
/api/saes/previewPOSTPreview SAE repository files
/api/saes/attachmentGETSingle-SAE attachment status (incl. steering_apply_count)
/api/saes/attachmentsGETMulti-SAE attachment status — every attached (sae_id, layer) entry, total VRAM and the vram_envelope_mb warning (Feature 12 cross-layer circuit serving)
/api/saes/attach-setPOSTAttach a set of SAEs at once for cross-layer circuit serving — loads only the referenced SAEs (fp16), installs one hook per (sae_id, layer), idempotent per key
/api/saes/{id}GET / DELETEGet / delete an SAE
/api/saes/{id}/compatibilityGETDry-run compatibility check
/api/saes/{id}/attachPOSTAttach to loaded model
/api/saes/{id}/detachPOSTDetach
/api/saes/{id}/cancelPOSTCancel in-progress download
/api/saes/monitoringPOSTConfigure monitoring (alias of /api/monitoring/configure)

Steering (/api/saes/steering) — full reference​

EndpointMethodDescription
/api/saes/steeringGETGet steering status and values
/api/saes/steeringPOSTSet single feature value
/api/saes/steering/batchPOSTSet multiple feature values
/api/saes/steering/enablePOSTEnable steering
/api/saes/steering/disablePOSTDisable steering (keep values)
/api/saes/steering/{idx}DELETERemove single feature
/api/saes/steeringDELETEClear all values & disable

Monitoring (/api/monitoring) — full reference​

EndpointMethodDescription
/api/monitoringGETGet monitoring state
/api/monitoring/configurePOSTConfigure (features, history size, top-k)
/api/monitoring/enablePOSTToggle without reconfiguring
/api/monitoring/historyGET / DELETEActivation history / clear
/api/monitoring/statisticsGET / DELETEFeature statistics / reset
/api/monitoring/statistics/topPOSTTop features by metric

Profiles (/api/profiles) — full reference​

EndpointMethodDescription
/api/profilesGET / POSTList / create
/api/profiles/save-currentPOSTSave live steering as a profile
/api/profiles/activeGETGet active profile
/api/profiles/{id}GET / PATCH / DELETEGet / update (partial) / delete
/api/profiles/{id}/activatePOSTActivate (replaces current steering)
/api/profiles/{id}/deactivatePOSTDeactivate
/api/profiles/{id}/exportGETExport as JSON
/api/profiles/importPOSTImport from JSON

Clusters (/api/clusters) — imported cluster definitions (concepts)​

Clusters are cluster-typed steering documents imported from miWorkbench (or a Hugging Face pack). One cluster serves at a time, behind a global intensity dial.

EndpointMethodDescription
/api/clustersGETList imported clusters with bound state, warnings and intensity
/api/clusters/importPOSTImport a cluster definition or bundle
/api/clusters/hub/searchGETSearch public cluster packs on Hugging Face
/api/clusters/hub/{repo_id}/definitionsGETList a Hub repo's cluster definitions
/api/clusters/hub/importPOSTImport one definition from a Hub repo
/api/clusters/{cluster_id}/activatePOSTActivate a cluster (hard compatibility gate)
/api/clusters/{cluster_id}/deactivatePOSTDeactivate a cluster
/api/clusters/{cluster_id}DELETEDelete a cluster profile
/api/clusters/active/intensityPUTSet the active cluster's intensity (global λ dial)
/api/clusters/{cluster_id}/intensityPUTSet a specific cluster's intensity (λ)
/api/clusters/{cluster_id}/exportGETRe-export the lossless original definition

Circuits (/api/circuits) — imported multi-layer circuits (concepts)​

Circuits are multi-layer interventions spanning several SAEs, each carrying an evidence rung (0–3). A circuit below rung 2 is refused unless activation passes acknowledge_unvalidated. Serving requires an SAE attached on every member layer (Feature 12 multi-SAE).

EndpointMethodDescription
/api/circuitsGETList imported circuits with evidence rung, layers and serveability (?min_rung=, ?serveable=, ?limit=, ?offset=)
/api/circuits/activeGETThe currently serving circuit(s) and their steering state
/api/circuits/claimsGETWhich circuit holds which layer
/api/circuits/claims/releasePOSTRelease a stuck layer claim
/api/circuits/importPOSTImport a circuit definition
/api/circuits/{circuit_id}/activatePOSTActivate (serve) a circuit — refused UNVALIDATED_CIRCUIT below rung 2 unless acknowledged; CIRCUIT_LAYER_CONTENTION when another circuit already holds a layer
/api/circuits/{circuit_id}/deactivatePOSTStop serving a circuit
/api/circuits/active/intensityPUTSet the active circuit's global intensity (λ) — NO_ACTIVE_CIRCUIT / AMBIGUOUS_ACTIVE_CIRCUIT when zero or several circuits serve
/api/circuits/{circuit_id}DELETEDelete an imported circuit
/api/circuits/{circuit_id}/exportGETExport the raw circuit definition

Circuit sensing (/api/circuit-sensing) — observed edge firings (WebSocket event)​

Records when a served circuit's edges co-activate during generation. This is an observation of firing, not a causal claim — an edge earns a causal reading only from rung-2 edge validation upstream in miWorkbench, never from a sensing event. Instrument, not gate — context text is never sent over WebSocket; fetch event detail for the ±K window.

EndpointMethodDescription
/api/circuit-sensing/statusGETEdge-sensing status: sensable/unsensable edges, per-request overhead, enabled_circuits
/api/circuit-sensing/eventsGETList observed edge firings newest-first (?limit=, ?since=, scoping params)
/api/circuit-sensing/events/{event_id}GETOne observed edge firing incl. the ±K context window (404 CIRCUIT_SENSING_EVENT_NOT_FOUND when pruned/cleared)
/api/circuit-sensing/eventsDELETEClear observed edge firings
/api/circuit-sensing/{circuit_id}/enablePOSTEnable edge sensing for a circuit
/api/circuit-sensing/{circuit_id}/disablePOSTDisable edge sensing for a circuit

Sensing (/api/sensing) — cluster co-activation events (concepts)​

EndpointMethodDescription
/api/sensing/statusGETArmed state, threshold mode, per-request overhead, retention limits, and enabled_clusters (persistent intent, distinct from armed)
/api/sensing/eventsGETEvents newest-first (?profile_id=, ?limit=, ?since=); age-expired rows are pruned on read
/api/sensing/events/{id}GETEvent detail incl. the ±K context window (404 when pruned/cleared)
/api/sensing/eventsDELETEClear events (?profile_id= scopes to one cluster)
/api/sensing/{profile_id}/enablePOSTEnable sensing for a cluster (arms live when that cluster is active with an SAE attached)
/api/sensing/{profile_id}/disablePOSTDisable sensing (disarms live)
/api/sensing/{profile_id}/configPUTRuntime overrides: {"min_k": n} sets the quorum (validated against the sensable-member ceiling), {"min_k": null} restores the default (all sensable members). Stored miServer-locally — exports stay lossless. Re-arms live

Event detail responses include context_parts {before, span, after} — the window split at the fired span so clients can highlight it (older events predate the field and carry null).

Adapters (/api/weights/adapter) — serve a LoRA adapter on the resident base​

Apply one LoRA adapter to the loaded base model, and take it off again, without reloading anything and without writing a single base weight: forward hooks add scaling · B·(A·x) to each targeted linear layer, computed in float32 (the adapter's A and B are kept in float32) and added to the layer's output. No trainer needs to be attached. An adapter from miForge is served as it is; one trained with peft directly, in a notebook or downloaded from the Hub needs an adapter_manifest.json written first (below). One adapter is applied at a time.

What an adapter costs. Measured on the RTX 3090 (2026-10-16) with LFM2.5-1.2B-Instruct in bfloat16 and a rank-16 adapter on 28 modules (q_proj, v_proj, w2): greedy decode at 145.5 tokens/s bare and 118.4 tokens/s with the adapter (0.81×; median of 5 runs of 256 tokens). Applying it held serving for 753 ms; removing it, under 1 ms.

EndpointMethodTokenDescription
/api/weights/adapter/loadPOSTtrainer or operatorVerify and apply the adapter at {"path": "<dir>"} (unknown fields are a 422)
/api/weights/adapter/unloadPOSTtrainer or operatorRemove the applied adapter (no body, or {})
/api/weights/adapterGETnoneThe adapter block: applied, name, base_name, manifest_sha256, manifest_base_name, manifest_base_sha256, weights_sha256, rank, target_count, loaded_at, base_steps_since_load (fields below)

A token is required, even with no trainer. Load and unload send Authorization: Bearer <token> matching a trainer or operator entry in WEIGHTS_TOKENS (format in the configuration reference). With WEIGHTS_TOKENS empty every call is refused 401 WEIGHTS_UNAUTHORIZED (no_tokens_configured), so to serve adapters on a deployment without a trainer, configure an operator token. Both routes honour the model lease: while another holder leases the resident model they are refused 409 MODEL_LEASED, and the holder passes by sending its lease ID in X-miLLM-Lease. A Batch API batch leases the model while it runs, so an adapter cannot be loaded or unloaded under a running batch: every row of a batch is served by one model.

The directory. path is relative to a checkpoint root (WEIGHTS_CHECKPOINT_DIR, or one of WEIGHTS_CHECKPOINT_READ_ROOTS; a path present under two roots is refused as ambiguous) and holds three regular files, each resolving inside the directory (a symlink out of it is refused):

<root>/r9/adapter-step-000200/
adapter_model.safetensors # PEFT keys base_model.model.<module>.lora_{A,B}.weight
adapter_config.json # PEFT's own config; r, lora_alpha, target_modules must agree
adapter_manifest.json # miforge.adapter-manifest v1: name, base, rank, alpha, weights_sha256

A plain PEFT directory has no manifest. Write one (it reads the config, checks the weights' keys against it, hashes the weights and validates the result against the vendored schema):

python -m millm.tools.write_adapter_manifest /checkpoints/r9/adapter-step-000200 \
--base-name meta-llama/Llama-3.1-8B-Instruct

It refuses configurations v1 cannot serve as trained: use_rslora, use_dora, trained biases, rank_pattern/alpha_pattern, fan_in_fan_out, modules_to_save, and a regex or all-linear target_modules. An existing manifest is replaced only with --force.

Which base it fits. --base-name is the base the adapter was trained against. When the resident base has a manifest hash (a checkpoint, or a local model directory holding a manifest.json), that hash is its identity and the adapter must name it in base.manifest_sha256 (pass --base-manifest-sha256). Otherwise base.manifest_sha256 must be null and base.name must be the resident row's name or its HF repo id. The Hub revision is not checked.

Refusal order. Cheapest first; every refusal before the last steps changes nothing, and the artifact is read, hashed and placed before serving is paused:

#CheckRefusal
1a trainer or operator token (checked before the body)401 WEIGHTS_UNAUTHORIZED (missing, invalid_token, no_tokens_configured, tokens_malformed)
2a model is loaded400 MODEL_NOT_LOADED
3transformers engine, unquantized weights400 ADAPTER_NOT_SERVABLE (engine, quantization)
4continuous batching off400 ADAPTER_NOT_SERVABLE (cbm)
5one admission slot (MAX_CONCURRENT_REQUESTS=1)409 BARRIER_UNAVAILABLE (concurrency)
6the lease409 MODEL_LEASED
7weights clean409 WEIGHTS_NOT_CLEAN
8no adapter applied or loading409 ADAPTER_ALREADY_LOADED (applied, load_in_progress)
9directory inside one root, three regular files inside it400 ADAPTER_INVALID (bad_path, not_found, outside_root, ambiguous, missing_file)
10the schema is vendored409 ADAPTER_SCHEMA_NOT_VENDORED
11each JSON file is a regular file of at most 1 MB; the manifest validates; adapter_config.json is servable and agrees with it400 ADAPTER_INVALID (not_regular_file, too_large, unreadable) / 400 ADAPTER_MANIFEST_INVALID (details.errors by JSON path; config_unservable, config_disagrees)
12some linear module matches target_modules, and the weights file is no larger than A and B for those modules can be400 ADAPTER_SHAPE_MISMATCH (target_unused) / 400 ADAPTER_INVALID (too_large)
13weights SHA-256 equals the manifest's (the file is read once; the tensors come from those bytes)400 ADAPTER_HASH_MISMATCH (both digests)
14the manifest's base is the resident base400 ADAPTER_BASE_MISMATCH (both identities)
15every tensor is a PEFT LoRA A or B for a floating-point linear module, at the manifest's one rank, with the right shapes400 ADAPTER_SHAPE_MISMATCH (details.first names the module and both shapes; every offender in details.offenders); a tensor that is not a LoRA A or B, or an unreadable safetensors header: 400 ADAPTER_INVALID (unsupported_tensor, unreadable)
16tensors placed (finite, floating point)400 ADAPTER_INVALID (non_finite, dtype), 507 INSUFFICIENT_MEMORY
17serving paused: a request being generated finishes first503 QUEUE_FULL, 409 MODEL_BUSY, 409 PAUSE_IN_PROGRESS, 409 BARRIER_UNAVAILABLE, 409 WEIGHTS_NOT_CLEAN
18inside the pause: the base was not replaced meanwhile409 ADAPTER_BASE_CHANGED
19install, all or nothing500 ADAPTER_INSTALL_FAILED (every hook removed again)

A load refused at steps 17–19 releases the tensors it placed. Unload refuses, in order, 409 NO_ADAPTER_LOADED, 400 ADAPTER_NOT_SERVABLE (cbm), 409 BARRIER_UNAVAILABLE (concurrency), the lease, 409 WEIGHTS_NOT_CLEAN and the pause's own refusals (step 17).

Responses. Load returns adapter (name, manifest_sha256, weights_sha256, base, rank, alpha, scaling, target_count), resolved_model_name (the name /v1 now reports) and held_ms. Unload returns unloaded (name, manifest_sha256), resolved_model_name (the bare base's) and held_ms. held_ms runs from asking for the pause to releasing it, so it includes waiting for the request that was being generated.

The served name. While an adapter is applied, every /v1 response body's model, every stream chunk's model and the terminal chunk's millm_model, the X-miLLM-Model header, each Batch API output line, and /v1/models report the adapter manifest's own name. A request naming the adapter's name, or the bare base name, is served by the resident model with no reload. The name and the hooks change together inside the pause, so a request is never answered by one and labelled with the other. Unloading restores the base name, and the base's answers return exactly. GET /v1/models/{model} answers for every listed id, including names containing / (an adapter named after a Hub base, <org>/<repo>:<run>:adapter-<step>). While an adapter is applied, X-miLLM-Model and the stream's millm_model read origin=adapter. A request that names the adapter but is admitted after it has been unloaded is refused as an unknown model (404 model_not_found), never answered by the base; a Batch API line in the same situation fails with that 404 line and is not run.

What else changes. Any replacement of the base model — a model unload, or loading another model — drops the adapter first: the hooks are removed, the status reads applied: false, and the drop is logged (adapter_dropped_by_base_change) and audited (adapter/drop). An adapter is not kept across a restart. Probe verdicts produced while an adapter is applied are provisional, and probe parity is refused with 409 ADAPTER_APPLIED, because both compare against the base. base_steps_since_load counts commits to the base since the adapter was applied; null means not recorded.

The status block's fields (GET /api/weights/adapter, also embedded in GET /api/weights/status). Every field is null when applied is false.

FieldMeaning
appliedWhether an adapter is applied
nameThe adapter's own name, which /v1 serves while it is applied
base_nameThe resident base's served name: the model actually under the adapter
manifest_sha256SHA-256 of the adapter's adapter_manifest.json
manifest_base_nameThe base the adapter's manifest claims it was trained on (base.name)
manifest_base_sha256The base manifest hash the adapter's manifest claims (base.manifest_sha256; null for a Hub base)
weights_sha256SHA-256 of adapter_model.safetensors, verified at load
rankThe LoRA rank
target_countHow many linear modules carry a hook
loaded_atWhen the adapter was applied
base_steps_since_loadCommits to the base since the adapter was applied; null = not recorded

An adapter name is refused at load when it is empty, contains whitespace or non-printable characters, is longer than 255 characters, or names the resident base or any registered model (ADAPTER_MANIFEST_INVALID, details.problem = empty, not_printable, too_long, base_name or registered_name).

Health & operations (/api/health) — full reference​

EndpointMethodDescription
/api/healthGETLiveness
/api/health/readyGETReadiness
/api/health/detailedGETComponent breakdown, queue state (inference) and the live lease
/api/health/gpusGETPer-card memory, including what miServer holds
/api/health/inferenceGETActive backend & capabilities
/api/health/metricsGETApp metrics
/api/health/metrics/prometheusGETPrometheus format
/api/health/circuitsGETCircuit breakers
/api/health/circuits/{name}/resetPOSTReset a breaker
/api/health/versionGETVersion

Queue state: the inference block of /api/health/detailed​

A stable, typed contract (Feature 29). The block is always present; when it cannot be read it carries error and the other fields are null. Unmeasured is null, never 0.

FieldTypeMeaning
backend"queue" | "cbm"The serial queue, or continuous batching while it runs
cbm_enabled, cbm_runningboolContinuous batching configured / serving now
queue_pendingintInteractive requests waiting plus holding a slot (it is incremented before the slot is taken — not "waiting" alone)
queue_max_concurrent, queue_max_pendingintMAX_CONCURRENT_REQUESTS, MAX_PENDING_REQUESTS
in_flightint | nullSlots held now. The idle cache release holds a slot too and counts, because a request arriving then waits for it. Batch chunks count once the batch API ships. null while continuous batching runs
queue_waitingint | nullInteractive requests waiting: queue_pending − holding. null while continuous batching runs
batch_backlog_rowsint | nullRows not yet run across in-progress batches; null until a batch API exists (not 0)
estimated_wait_secondsfloat | nullAn estimate of how long a request arriving now waits for a slot
errorstring | nullSet when the block could not be read: unavailable (error id …); the reason is in the server log under that id

The estimate is

estimated_wait_seconds = median(last 50 slot-holding durations) × (queue_waiting + in_flight) / queue_max_concurrent

null with fewer than 3 measured durations, and while continuous batching runs (its requests hold no queue slot). The batch backlog is not added: interactive requests go first at every batch chunk boundary. It is an estimate, not a promise.

/api/health/detailed also carries lease: the live lease on the resident model (model_id, model_name, holder, reason, acquired_at, renewed_at, expires_at, ttl_seconds, seconds_remaining) or null. Never the lease ID. No nvidia-smi runs on this endpoint.

Per-card memory: GET /api/health/gpus​

{"read_at": "2026-10-06T12:00:00Z", "reason": null, "cards": [
{"smi_index": 0, "uuid": "GPU-247a…", "name": "NVIDIA GeForce RTX 3090",
"total_mb": 24576, "used_mb": 17396, "free_mb": 7180,
"torch_index": 0, "torch_measured": true, "millm_allocated_mb": 0, "millm_reserved_mb": 16820,
"engine_memory": null, "processes": [{"pid": 4242, "used_mb": 17100}], "processes_reason": null}]}
  • Total, used and free come from nvidia-smi. torch_index is torch's index, matched by UUID; a card torch cannot see is listed with torch_index: null, not dropped.
  • millm_allocated_mb / millm_reserved_mb are torch's allocator on that card. They are read only on cards miServer has placed a transformers model on (torch_measured: true); on any other card they are null. Reading a card never creates a CUDA context on it. torch's reserved figure excludes the CUDA context itself, which nvidia-smi counts per process.
  • A resident GGUF model's memory is llama.cpp's, not torch's: its cards say engine_memory: "not_measured_by_torch", and processes (nvidia-smi's per-process list, where the container can read it) is the only measurement of it.
  • Without nvidia-smi the answer is 200 with cards: [] and reason — never zeros.
  • The read runs in a worker thread and takes no request-queue slot, so it is safe to poll during a generation.