mlx-serve/API documentation

One local server.
Three APIs your tools already speak.

Everything below is served by one process on one port — chat, embeddings, and image / video / music / voice / 3D generation. OpenAI-compatible, Anthropic-compatible, and Ollama-compatible, so existing SDKs and apps connect unchanged. No API key on localhost — it's your machine — and one optional key the moment you expose it to the network.

Base URL http://localhost:11234 No auth on localhost SSE streaming everywhere
Getting started OpenAI-compatible Extensions Anthropic Messages Responses API Ollama API Media generation Model management Utilities Metrics & auth

Getting started

Start the server from the MLX Core app (it runs whenever a model is loaded) or from the CLI:

mlx-serve serve          # serves everything under ~/.mlx-serve/models, loads on demand
mlx-serve run gemma4     # or: download one model, serve it, and chat

The default port is 11234 (--port to change it). Models are addressed by name — the model field accepts a model's directory name, an org/repo id, or an Ollama-style short name with tag. A request naming a discovered-but-unloaded model loads it on the fly.

curl http://localhost:11234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemma-4-e4b-it-8bit",
    "messages": [{"role": "user", "content": "Why is the sky blue?"}],
    "stream": true
  }'

Or with the OpenAI SDK — just point base_url at your Mac:

from openai import OpenAI
client = OpenAI(base_url="http://localhost:11234/v1", api_key="unused")
r = client.chat.completions.create(
    model="gemma-4-e4b-it-8bit",
    messages=[{"role": "user", "content": "Hello!"}])
print(r.choices[0].message.content)

OpenAI-compatible endpoints

EndpointWhat it does
GET/healthLiveness probe — 200 when the server is up.
GET/v1/modelsEvery model the server can serve — loaded or discoverable — with a capabilities array (chat, tool_use, streaming, vision, audio, reasoning, json_schema, embeddings, image, music, video, 3d) so clients know what each one can do.
POST/v1/chat/completionsChat with streaming (SSE), tool calling, vision (image parts on multimodal models), JSON mode / response_format, logprobs / top_logprobs, and thinking models. The workhorse endpoint.
POST/v1/completionsRaw text completion (no chat template) — FIM / code-completion friendly. logprobs here is an integer and returns OpenAI's legacy four-array shape.
POST/v1/embeddingsBatched embeddings from encoder models (BERT/bge, EmbeddingGemma, Qwen3-Embedding) — the whole input array runs as one padded GPU forward. Pooling is read from the checkpoint, and dimensions truncates + renormalizes like text-embedding-3.

Every response carries usage.prompt_tokens_details.cached_tokens, so you can see what the prefix cache saved. stream_options: {"include_usage": true} adds the usage chunk at the end of a stream. When a reply is cut because the model went in circles, finish_details: {"type": "repetition_loop"} sits beside finish_reason instead of the cut being reported as a plain length stop.

Request extensions beyond OpenAI

All optional; every endpoint stays spec-compatible without them.

FieldMeaning
enable_thinkingToggle thinking/reasoning on models that support it; reasoning arrives as reasoning_content, never leaked into the visible text.
reasoning_effortOpenAI's "low" / "medium" / "high", mapped to a thinking budget (or to the model's own effort setting where it has one, like DeepSeek V4).
reasoning_budget_tokensCap thinking tokens per request. An explicit value outranks reasoning_effort.
kv_quantPer-request KV-cache quantization: "off", 4, 8, "turbo2", "turbo4".
kv_attn_modeRead path for a quantized KV cache: "auto" (default), "dense", "fused". Fused reads the packed cache in place and pays off at long context.
enable_pld / enable_drafter / enable_mtpPer-request control over the three speculative-decoding paths (deep dive). Defaults are already sensible — outputs stay equivalent either way.

Sampling defaults resolve in this order: request body → server launch flags → the model's own generation_config.json → engine defaults. Clients that omit temperature/top_p/top_k get what the model's authors recommend.

Anthropic Messages API

POST /v1/messages implements the Anthropic Messages API — typed content blocks, tool_use / tool_result, thinking blocks, and the full SSE event lifecycle (message_start, content_block_delta, …). It's what lets Claude Code run against your local model:

export ANTHROPIC_BASE_URL=http://localhost:11234
export ANTHROPIC_API_KEY=unused
claude   # Claude Code now talks to your Mac

OpenAI Responses API

EndpointWhat it does
POST/v1/responsesThe stateful Responses API: typed output items, previous_response_id chaining, tool calls, sequence-numbered SSE events.
GET/v1/responses/{id}Fetch a stored response.
DELETE/v1/responses/{id}Delete a stored response.
POST/v1/responses/compactCompact a long conversation into an opaque token-light blob that round-trips through the next request.
WS/v1/responsesSame endpoint over WebSocket: send response.create-shaped JSON frames, receive each SSE event as one text frame.

Ollama-compatible API

Tools built for Ollama — Raycast, Obsidian, Enchanted, Open WebUI, ollama-python/js — connect unchanged (the swap, in depth). Run on Ollama's port and nothing needs configuring at all: mlx-serve serve --port 11434.

EndpointWhat it does
POST/api/chat · /api/generateOllama chat / generate, NDJSON streaming, tool calls with object arguments, think, images.
POST/api/embed · /api/embeddingsEmbeddings, new and legacy shapes.
GET/api/tags · /api/ps · /api/versionModel listing, loaded models, version.
POST/api/show · /api/pullModel metadata; native HuggingFace pull by short name.

Media generation

Each media endpoint accepts "stream": true for SSE progress events ending in a base64 complete payload; without it the response is the finished artifact. Load and unload media models like any other (below) — chat and media coexist in one process.

POST/v1/images/generations

FLUX.2-klein (4B and 9B), Krea-2-Turbo and Mage-Flow Turbo / Edit (deep dive). Returns base64 PNG.

FieldMeaning
promptRequired.
size"WxH", rounded to the backend's own grid: FLUX to a multiple of 32 in 256–1536, Krea and Mage-Flow to a multiple of 16 in 256–2048. Omitted in "edit" mode means "match the source".
image + modeBase64 source image with "variation" (img2img, plus strength 0–1) or "edit" (instruction edit on FLUX.2 or Mage-Flow Edit — "make the hair blue" keeps subject and scene).
ref_imagesUp to 3 extra base64 reference images beside image in "edit" mode — refer to them by number in the prompt: "replace the face of the man in image 1 with the face from image 2". Each keeps its own aspect ratio.
lora_paths / lora_scalesRuntime style LoRAs (.safetensors, diffusers / kohya / PEFT layouts), up to 8, absolute paths. They stack: every adapter is summed at forward time, never merged, so nothing is re-quantized. Each file's own declared alpha is honored. The singular lora_path / lora_scale still works.
cond_gain / cond_weightsText-conditioning rebalance knobs.
steps, seedSampler steps, reproducibility.

POST/v1/images/edits

The OpenAI SDK's edit shape, so client.images.edit(image=…, prompt=…) works unchanged against any edit-capable model you have loaded. This one is multipart/form-data, not JSON, and it is pure translation into the mode:"edit" body above — there is no second inference path.

client.images.edit(
    model="mage-flow-edit-turbo-8bit",
    image=[open("room.png","rb"), open("chair.png","rb")],   # repeated image[] = multi-reference
    prompt="put the chair from image 2 into image 1")

Accepted fields: model, prompt, image / image[] (up to 4), size. Anything we can't honor is a named 400 rather than a silent no-op: mask (the editors are maskless), n > 1, response_format: "url", an output_format other than png, and stream.

POST/v1/audio/speech

Qwen3-TTS (voice cloning) or Kokoro-82M (54 built-in voices), whichever is loaded (deep dive). Returns WAV bytes. The two backends share the endpoint but not the controls, and asking one for the other's control is a named 400 rather than a quietly plain-voiced answer.

FieldMeaning
inputThe text to speak. Required (text also accepted).
ref_audioQwen3-TTS only. Base64 WAV (24 kHz mono) — a few seconds of any voice clones it zero-shot, no transcript needed. {"warm_only": true} with a ref_audio caches the speaker embedding without synthesizing, so the first sentence of a session isn't cold.
voiceKokoro only. One of the 54 packs (default af_heart); a comma-separated list blends them, e.g. "af_bella,af_sky". An unknown name is a 400.
speed / seedKokoro only. Speaking rate in (0, 5], default 1.0; seed for reproducibility.

POST/v1/audio/music-generations

Text-to-music, WAV out (deep dive): ACE-Step (48 kHz stereo, fast) or MiniMax Music 3 (sings full songs at 44.1 kHz, up to six minutes). On Music 3, lyrics is required and the musical-steering fields below don't exist; put tempo and key in the caption.

FieldMeaning
promptStyle / genre / mood description. Required.
lyricsOptional on ACE-Step (empty means instrumental); required on MiniMax Music 3, structure tags like [verse] on their own lines.
duration_seconds10–600, default 60.
bpm / keyscale / timesignatureMusical steering — e.g. 120, "C major", "4/4". ACE-Step only.
vocal_language, seedVocal language (default "en"); reproducibility.

POST/v1/video/generations

One endpoint, two backends: LTX-Video 2.3 / 2.5 (text, image or audio to video with synced sound; on 2.5, "decoder": "diffusion" picks its sharper diffusion decoder) and MiniMax-H3 / Hailuo 3.0 (video and its stereo soundtrack denoised jointly in one pass, long clips chained via chain_windows). Whichever is loaded serves the request, and the field a backend can't honor is a named 400 rather than a silently dropped setting (deep dive).

The response is raw frames plus raw audio, not a container: {"frames": N, "width", "height", "fps": 24, "format": "rgb8", "data": "<base64>"}, and when the model produced sound, audio_format: "pcm_s16le", audio_channels: 2, audio_sample_rate and audio_data beside it. Mux to MP4 on your side. These runs take minutes, so use "stream": true for progress and give your client a generous timeout.

Opt-in per-step preview (issue #208): "preview": true with "stream": true attaches a JPEG to each denoise progress event — Latent2RGB of the predicted clean latent, not a full VAE decode. Default off, so existing clients keep the {type, stage, step, total} shape. preview_frames (default 1, max 8) packs extra temporal slices as a horizontal filmstrip; preview_max_side (default 256, 0 = latent native) caps the long edge. Cached-velocity H3 steps emit progress without an image. complete is still the full rgb8 + PCM.

LTX-Video

FieldMeaning
promptRequired. Quoted lines become spoken dialogue.
width / height / num_framesOutput geometry and length (24 fps).
pipeline"one_stage" (fast default), "two_stage", "two_stage_hq" — dev-model guidance at half resolution, learned 2× upsample, distilled refine.
first_frame_imageBase64 photo pinned as frame 0 — image-to-video.
audioBase64 WAV soundtrack the video performs to (two-stage only); the original clip is muxed into the MP4.
cfg_scale / stg_scale / cfg_audio_scale / steps / stage2_steps / seedGuidance and sampler knobs; sensible per-pipeline defaults.
lora_paths / lora_scalesSame stacked-LoRA grammar as the image endpoint.

MiniMax-H3 — narrower on purpose: no CFG scale and no pipeline mode (it is CFG-distilled and single-pipeline), and frame counts live on its own ladder. New in v26.8.2. It ships as two partitions that carry the same files and differ only in which conditioning they were trained for, so the server reads the pack's declared tasks and refuses what that build would ignore: FL2VA does keyframes and chained windows, REF2VA does references.

FieldMeaning
promptRequired. Describe the scene, then the sound after overall_soundscape:.
width / heightMultiples of 32, default 256. 1344×768 is the recommended size and takes about 50 minutes for 124 frames on an M4 Max.
num_framesSnapped to the model's 17k+5 ladder and the delivered count is reported back, so an audio mux can't drift. 24 fps, up to 15 seconds.
turbotrue attaches the 4-step distillation adapter (about 2× faster end to end, slightly softer). steps then defaults to 4 and 4 is the floor; without turbo the default is 30.
fastThe fast recipe (velocity cache + attention broadcast, about 2.8× at 768p) is on by default. false keeps every forward dense.
first_frame_image / last_frame_imageFL2VA only. Base64 photo anchoring the start and/or end of the clip. An undecodable image is a 400, never a silent text-to-video.
chain_windowsFL2VA only, 1–6. Generates that many back-to-back windows of num_frames, each continuing from the last frame of the one before, for a longer clip.
ref_images / ref_videos / ref_audiosREF2VA only. Base64 PNG/JPEG strings, {"frames": […], "audio": "…"} objects, and WAV strings. Refer to them in the prompt by number: <Picture 1>, <Video 1>, <Audio 1>. ref_image_size is "match" (default) or "max".
lora_paths / lora_scalesSame stacked grammar again, and turbo is simply file 0 of that stack, so a style LoRA rides on top of it.
steps / seedSampler steps and reproducibility.

POST/v1/3d/generations

Hunyuan3D-2.1 — one photo in, a GLB mesh out; optional full-PBR texturing (deep dive). New in v26.7.2.

FieldMeaning
imageBase64 PNG/JPEG of the subject. Required. A cutout with transparency conditions best.
texturetrue runs the multiview paint stage — albedo + metallic-roughness baked into a 2K atlas (texture_steps to tune).
octree_resolutionMesh detail, 64–512 (default 256; the app uses 384).
steps / guidance_scale / seedDiffusion steps (default 30), guidance 0–20 (default 5), reproducibility.

Response: {"created": …, "format": "glb", "data": "<base64 GLB>"} — standard glTF, opens anywhere.

Model management

EndpointWhat it does
POST/v1/load-model{"model": "<id or absolute path>"} — load a discovered model, or register + load one from anywhere on disk. Add "default": true to make it the serving default without a restart. Multiple models stay resident (LRU-evicted under a memory cap).
POST/v1/unload-model{"model": "<id>"} — free a model's memory now.
POST/v1/models/rescanPick up models downloaded while the server runs; the app calls it after every download.

Requests are queued per model and batch-decode together where the architecture allows; a media generation runs on the same GPU and briefly pauses chat decode — one machine, one memory budget, no surprises.

--model-dir is repeatable, so a server can serve several folders at once. They merge first-wins, and every one of them shows up in /v1/models.

Utility endpoints

EndpointWhat it does
POST/tokenize{"content": "…"} — token ids for a string, from the loaded model's own tokenizer.
POST/detokenize{"tokens": [1,2,3]} — back to text.
GET/propsllama.cpp-style server properties: chat template, context length, live memory counters. Keeps answering on a server with no chat model loaded.

Metrics & auth

Both are opt-in and independent. Start the server with --metrics, --api-key, or neither — the app exposes the same two switches in Settings.

EndpointWhat it does
GET/metricsPrometheus text format. Standard vllm: names (throughput, time-to-first-token, tokens in and out) sit beside Apple-specific mlx_serve: ones (GPU utilization, memory, prefix-cache reuse), so an off-the-shelf vLLM Grafana dashboard works with no configuration. 503 unless --metrics is on.
GET/metrics.jsonThe same figures as JSON — what the server's own live index-page panel polls.

Recording happens once per request, never per token, so the metrics have no measurable effect on tokens/sec.

With --api-key <token> set, every request arriving from another machine must present the key — the OpenAI, Anthropic, and Ollama APIs, the index page, and the endpoints above. Any of these work:

curl http://mac.local:11234/v1/models -H "Authorization: Bearer <key>"
curl http://mac.local:11234/v1/models -H "x-api-key: <key>"
curl "http://mac.local:11234/v1/models?api_key=<key>"

Requests from the machine itself are trusted and need no key, so the app, your editor, and local scripts are unaffected — the key guards only what is reachable off the box. /health stays open either way. Because localhost is exempt, curl localhost will never show you a 401; test from another machine.

Point your code at your Mac.

Download MLX Core or brew install mlx-serve, load a model, and every endpoint above is live on localhost.