mlx-openai-server

May 3, 2026 ยท View on GitHub

MIT License Python 3.11

OpenAI-compatible API server for local MLX models on Apple Silicon. It serves text, multimodal, image, embedding, and Whisper models through familiar OpenAI SDK endpoints.

Requires macOS on Apple Silicon and Python 3.11+.

Contents

Feature Launch

Darwin 36B Opus is now available in MLX format for local text inference:

Launch it with the required reasoning and tool-call parsers:

mlx-openai-server launch --model-path Darwin-36B-Opus-mlx-text-only-8bit --reasoning-parser qwen3_moe --tool-call-parser qwen3_coder --debug --served-model-name Darwin-36B-Opus

Darwin 36B Opus should be served with both --reasoning-parser qwen3_moe and --tool-call-parser qwen3_coder; without them, reasoning and tool-call output will not be parsed correctly.

Install

python3.11 -m venv .venv
source .venv/bin/activate

uv pip install mlx-openai-server

Install from GitHub instead:

uv pip install git+https://github.com/cubist38/mlx-openai-server.git

Whisper transcription also needs ffmpeg:

brew install ffmpeg

Start a Server

Text model:

mlx-openai-server launch \
  --model-type lm \
  --model-path mlx-community/Qwen3-Coder-Next-4bit \
  --reasoning-parser qwen3_moe \
  --tool-call-parser qwen3_coder

Point OpenAI-compatible clients to:

http://localhost:8000/v1

Use any non-empty API key, for example not-needed.

Common launch modes:

# Multimodal text/image/audio
mlx-openai-server launch \
  --model-type multimodal \
  --model-path <mlx-vlm-model>

# Image generation
mlx-openai-server launch \
  --model-type image-generation \
  --model-path <flux-or-qwen-image-model> \
  --config-name flux-dev \
  --quantize 8

# Image editing
mlx-openai-server launch \
  --model-type image-edit \
  --model-path <flux-or-qwen-image-edit-model> \
  --config-name flux-kontext-dev \
  --quantize 8

# Embeddings
mlx-openai-server launch \
  --model-type embeddings \
  --model-path <embedding-model>

# Whisper transcription
mlx-openai-server launch \
  --model-type whisper \
  --model-path mlx-community/whisper-large-v3-mlx

Supported model types:

TypeBackendEndpoint family
lmmlx-lmchat, responses
multimodalmlx-vlmchat, responses
image-generationmfluximage generation
image-editmfluximage editing
embeddingsmlx-embeddingsembeddings
whispermlx-whisperaudio transcription

Image --config-name values:

  • Generation: flux-schnell, flux-dev, flux-krea-dev, flux2-klein-4b, flux2-klein-9b, qwen-image, z-image-turbo, fibo
  • Editing: flux-kontext-dev, flux2-klein-edit-4b, flux2-klein-edit-9b, qwen-image-edit

API Usage

Chat

import openai

client = openai.OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")

response = client.chat.completions.create(
    model="mlx-community/Qwen3-Coder-Next-4bit",
    messages=[{"role": "user", "content": "Say hello in one sentence."}],
)
print(response.choices[0].message.content)

Vision

import base64
import openai

client = openai.OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")

with open("image.jpg", "rb") as f:
    image = base64.b64encode(f.read()).decode("utf-8")

response = client.chat.completions.create(
    model="local-multimodal",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "What is in this image?"},
                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image}"}},
            ],
        }
    ],
)
print(response.choices[0].message.content)

Images

import base64
from io import BytesIO

import openai
from PIL import Image

client = openai.OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")

response = client.images.generate(
    model="local-image-generation-model",
    prompt="A mountain lake at sunset",
    size="1024x1024",
)

image = Image.open(BytesIO(base64.b64decode(response.data[0].b64_json)))
image.show()

Embeddings

import openai

client = openai.OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")

response = client.embeddings.create(
    model="local-embedding-model",
    input=["The quick brown fox jumps over the lazy dog"],
)
print(len(response.data[0].embedding))

Responses API

import openai

client = openai.OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")

response = client.responses.create(
    model="local-model",
    input="Write a three sentence story.",
)

for item in response.output:
    if item.type == "message":
        for part in item.content:
            if getattr(part, "text", None):
                print(part.text)

Supported endpoints:

EndpointModel types
GET /v1/modelsall
POST /v1/chat/completionslm, multimodal
POST /v1/responseslm, multimodal
POST /v1/images/generationsimage-generation
POST /v1/images/editsimage-edit
POST /v1/embeddingsembeddings
POST /v1/audio/transcriptionswhisper

The request model should be the model path, --served-model-name, or YAML served_model_name.

Server Options

OptionDefaultNotes
--model-pathrequiredLocal path or Hugging Face repo
--model-typelmlm, multimodal, image-generation, image-edit, embeddings, whisper
--served-model-namemodel pathAlias accepted in API requests
--host0.0.0.0Bind host
--port8000Bind port
--context-lengthmodel defaultLM and multimodal context/cache length
--max-tokens100000Default generated tokens when request omits max_tokens
--temperature1.0Default sampling temperature
--top-p1.0Default nucleus sampling
--top-k20Default top-k sampling
--repetition-penalty1.0Default repetition penalty
--config-namemodel-dependentImage model preset
--quantizeunsetImage model quantization: 4, 8, or 16
--log-levelINFODEBUG, INFO, WARNING, ERROR, CRITICAL
--no-log-filefalseDisable file logging

LM-specific memory and batching options:

OptionDefaultNotes
--decode-concurrency32Max concurrent batch decode sequences
--prompt-concurrency8Max prompts prefilled together
--prefill-step-size2048Tokens per prefill step
--disable-batchingfalseDisable continuous batching; required if per-request positive seed values must be honored
--prompt-cache-size10Retained prompt KV cache entries
--max-bytesunboundedPrompt KV cache byte budget
--prompt-cache-dirtemp dirDirectory for disk-backed prompt KV cache payloads
--kv-bitsunsetKV cache quantization bits, usually 4 or 8
--kv-group-size64KV quantization group size
--quantized-kv-start0Token step where KV quantization starts
--draft-model-pathunsetSmaller draft model for speculative decoding
--num-draft-tokens2Draft tokens proposed per step

When continuous batching is enabled, LM and multimodal requests stay on the batched generation path whenever the backend supports it. Per-request positive seed values are ignored in this mode because the batch scheduler shares one RNG lane; launch with --disable-batching if request-level seed reproducibility is required.

Prompt KV caches are reused only by the generation path that created them. Batched and non-batched requests use separate cache entries because MLX cache and stream objects are thread-affine.

Multimodal (mlx-vlm) batching intentionally does not use prompt-prefix caching; current mlx-vlm prompt-cache support is limited, so VLM batching focuses on admitting parallel requests into the shared decode/prefill batch.

Multi-Model Config

Use YAML when you want several models behind one server:

mlx-openai-server launch --config config.yaml

Example:

server:
  host: "0.0.0.0"
  port: 8000
  log_level: INFO

models:
  - model_path: mlx-community/MiniMax-M2.5-4bit
    model_type: lm
    served_model_name: minimax
    tool_call_parser: minimax_m2
    reasoning_parser: minimax_m2

  - model_path: black-forest-labs/FLUX.2-klein-4B
    model_type: image-generation
    served_model_name: flux2-klein-4b
    config_name: flux2-klein-4b
    quantize: 4
    on_demand: true
    on_demand_idle_timeout: 120

Important YAML keys:

KeyNotes
model_path, model_type, served_model_nameModel identity and routing
context_lengthLM/multimodal context length
prompt_cache_size, prompt_cache_max_bytes, prompt_cache_dirPrompt KV cache limits and disk location
batch_completion_size, batch_prefill_size, batch_prefill_step_size, disable_batchingContinuous batching controls
kv_bits, kv_group_size, quantized_kv_startKV cache quantization
default_max_tokensDefault generated tokens
on_demand, on_demand_idle_timeoutLoad large models only when requested

In multi-model mode, each model runs in a spawned subprocess. This isolates MLX/Metal runtime state and avoids process-fork semaphore issues on macOS.

Long Context and Metal OOM

Large prompts, high concurrency, and long generations all increase KV-cache memory. If Metal runs out of memory, macOS may terminate the process with:

libc++abi: terminating due to uncaught exception of type std::runtime_error: [METAL] Command buffer execution failed: Internal Error (0000000e:Internal Error)

Start with conservative settings:

mlx-openai-server launch \
  --model-type lm \
  --model-path <model-path> \
  --context-length 8192 \
  --decode-concurrency 4 \
  --prompt-concurrency 1 \
  --prefill-step-size 512 \
  --max-tokens 2048 \
  --prompt-cache-size 1 \
  --max-bytes 2147483648 \
  --kv-bits 4 \
  --kv-group-size 64 \
  --quantized-kv-start 0

Tune in this order:

  1. Lower --prompt-concurrency and --prefill-step-size to reduce prefill spikes.
  2. Lower --decode-concurrency to reduce active KV caches.
  3. Lower --max-tokens to bound generation growth.
  4. Lower --prompt-cache-size and set --max-bytes to limit retained caches.
  5. Use --kv-bits 4 for supported LM/multimodal models.
  6. Reduce --context-length if the model still does not fit.

YAML equivalent:

models:
  - model_path: <model-path>
    model_type: lm
    context_length: 8192
    batch_completion_size: 4
    batch_prefill_size: 1
    batch_prefill_step_size: 512
    default_max_tokens: 2048
    prompt_cache_size: 1
    prompt_cache_max_bytes: 2147483648
    kv_bits: 4
    kv_group_size: 64
    quantized_kv_start: 0

For large models on macOS 15+, you can also raise the wired memory limit:

bash configure_mlx.sh

Advanced LM Options

Tool and Reasoning Parsers

Some models need parser flags for tool calls or reasoning blocks:

mlx-openai-server launch \
  --model-type lm \
  --model-path <model> \
  --tool-call-parser qwen3 \
  --reasoning-parser qwen3 \
  --enable-auto-tool-choice

Common parser names include qwen3, qwen3_5, glm4_moe, qwen3_coder, qwen3_moe, qwen3_next, qwen3_vl, harmony, and minimax_m2.

Message converters are auto-detected from parser selection when a compatible converter exists.

Custom Chat Templates

mlx-openai-server launch \
  --model-type lm \
  --model-path <model> \
  --chat-template-file /path/to/template.jinja

Speculative Decoding

mlx-openai-server launch \
  --model-type lm \
  --model-path <main-model> \
  --draft-model-path <smaller-draft-model> \
  --num-draft-tokens 4

Speculative decoding is only available for lm. It is not used by the continuous batch path.

Structured Outputs

Chat completions accept OpenAI-style response_format JSON schema. The Responses API also supports client.responses.parse() with Pydantic models. See examples/structured_outputs_examples.ipynb and examples/responses_api.ipynb.

Troubleshooting

IssueFix
Model does not fit in memoryUse a smaller or pre-quantized model, lower --context-length, and see Long Context and Metal OOM.
Metal OOM during batchingLower --prompt-concurrency, --prefill-step-size, --decode-concurrency, and --max-tokens.
There is no Stream(gpu, N) in current threadKeep prompt-cache persistence on the worker thread and avoid sharing cache payloads across batch/non-batch paths; use --disable-batching when request seeds or single-request behavior are required.
Port already in usePass --port 8001 or another free port.
Image model memory is too highUse --quantize 4 or --quantize 8.
Model loading says parameters are missing or unexpectedUpgrade the backend package from source.
Hugging Face download failsCheck network access and Hugging Face authentication for gated models.

Upgrade backend packages for newly released model architectures:

uv pip install git+https://github.com/ml-explore/mlx-lm.git
uv pip install git+https://github.com/Blaizzy/mlx-vlm.git
uv pip install git+https://github.com/Blaizzy/mlx-embeddings.git

Examples and Demos

Example notebooks live in examples/:

AreaNotebooks
Text and Responses APIresponses_api.ipynb, simple_rag_demo.ipynb
Visionvision_examples.ipynb
Audioaudio_examples.ipynb, transcription_examples.ipynb
Embeddingsembedding_examples.ipynb, lm_embeddings_examples.ipynb, vlm_embeddings_examples.ipynb
Imagesimage_generations.ipynb, image_edit.ipynb
Structured outputsstructured_outputs_examples.ipynb

Demos:

Contributing

  1. Fork the repository.
  2. Create a feature branch.
  3. Make changes with tests.
  4. Submit a pull request.

Use conventional commit prefixes when possible, for example fix:, feat:, docs:, or test:.

Support

Acknowledgments

Built on MLX, mlx-lm, mlx-vlm, mlx-embeddings, mflux, mlx-whisper, and mlx-community.