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.
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)| Endpoint | What it does |
|---|---|
GET/health | Liveness probe — 200 when the server is up. |
GET/v1/models | Every model the server can serve — loaded or discoverable — with a capabilities array (chat, embeddings, image, audio, music, video, 3d) so clients know what each one can do. |
POST/v1/chat/completions | Chat with streaming (SSE), tool calling, vision (image parts on multimodal models), JSON mode / response_format, and thinking models. The workhorse endpoint. |
POST/v1/completions | Raw text completion (no chat template) — FIM / code-completion friendly. |
POST/v1/embeddings | Batched embeddings from encoder models (e.g. bge-small-en-v1.5-8bit) — the whole input array runs as one padded GPU forward. |
All optional; every endpoint stays spec-compatible without them.
| Field | Meaning |
|---|---|
enable_thinking | Toggle thinking/reasoning on models that support it; reasoning arrives as reasoning_content, never leaked into the visible text. |
reasoning_budget | Cap thinking tokens per request. |
kv_quant | Per-request KV-cache quantization: "off", 4, 8, "turbo2", "turbo4". |
enable_pld / enable_drafter / enable_mtp | Per-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.
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| Endpoint | What it does |
|---|---|
POST/v1/responses | The 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/compact | Compact a long conversation into an opaque token-light blob that round-trips through the next request. |
WS/v1/responses | Same endpoint over WebSocket: send response.create-shaped JSON frames, receive each SSE event as one text frame. |
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.
| Endpoint | What it does |
|---|---|
POST/api/chat · /api/generate | Ollama chat / generate, NDJSON streaming, tool calls with object arguments, think, images. |
POST/api/embed · /api/embeddings | Embeddings, new and legacy shapes. |
GET/api/tags · /api/ps · /api/version | Model listing, loaded models, version. |
POST/api/show · /api/pull | Model metadata; native HuggingFace pull by short name. |
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.
/v1/images/generationsFLUX.2-klein and Krea-2-Turbo (deep dive). Returns base64 PNG.
| Field | Meaning |
|---|---|
prompt | Required. |
size | "WxH". Krea: any multiple of 16 in 256–2048; FLUX: 1024×1024. |
image + mode | Base64 source image with "variation" (img2img, plus strength 0–1) or "edit" (FLUX.2 instruction edit — "make the hair blue" keeps subject and scene). |
ref_images | Up 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_path / lora_scale | Runtime style LoRA (.safetensors, diffusers layout) — no re-quantization, applied unfused. |
cond_gain / cond_weights | Text-conditioning rebalance knobs. |
steps, seed, safety | Sampler steps, reproducibility, NSFW filter (false to disable per request). |
/v1/audio/speechQwen3-TTS text-to-speech and zero-shot voice cloning (deep dive). Returns WAV bytes.
| Field | Meaning |
|---|---|
input | The text to speak. Required. |
ref_audio | Optional base64 WAV (24 kHz mono) — a few seconds of any voice clones it zero-shot, no transcript needed. |
/v1/audio/music-generationsACE-Step text-to-music — describe a track, get 48 kHz stereo WAV (deep dive). New in v26.7.2.
| Field | Meaning |
|---|---|
prompt | Style / genre / mood description. Required. |
lyrics | Optional lyrics; empty means instrumental. |
duration_seconds | 10–600, default 60. |
bpm / keyscale / timesignature | Musical steering — e.g. 120, "C major", "4/4". |
vocal_language, seed | Vocal language (default "en"); reproducibility. |
/v1/video/generationsLTX-Video — text, image, or audio to video with synced sound (deep dive).
| Field | Meaning |
|---|---|
prompt | Required. Quoted lines become spoken dialogue. |
width / height / num_frames | Output 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_image | Base64 photo pinned as frame 0 — image-to-video. |
audio | Base64 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 / seed | Guidance and sampler knobs; sensible per-pipeline defaults. |
/v1/3d/generationsHunyuan3D-2.1 — one photo in, a GLB mesh out; optional full-PBR texturing (deep dive). New in v26.7.2.
| Field | Meaning |
|---|---|
image | Base64 PNG/JPEG of the subject. Required. A cutout with transparency conditions best. |
texture | true runs the multiview paint stage — albedo + metallic-roughness baked into a 2K atlas (texture_steps to tune). |
octree_resolution | Mesh detail, 64–512 (default 256; the app uses 384). |
steps / guidance_scale / seed | Diffusion steps (default 30), guidance 0–20 (default 5), reproducibility. |
Response: {"created": …, "format": "glb", "data": "<base64 GLB>"} — standard glTF, opens anywhere.
| Endpoint | What it does |
|---|---|
POST/v1/load-model | {"model": "<id or absolute path>"} — load a discovered model, or register + load one from anywhere on disk. Multiple models stay resident (LRU-evicted under a memory cap). |
POST/v1/unload-model | {"model": "<id>"} — free a model's memory now. |
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.
Both are opt-in and independent. Start the server with --metrics, --api-key, or neither — the app exposes the same two switches in Settings.
| Endpoint | What it does |
|---|---|
GET/metrics | Prometheus 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.json | The 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.
Download MLX Core or brew install mlx-serve, load a model, and every endpoint above is live on localhost.