Skip to main content
The fast server isn’t the one with the biggest tok/s number. It’s the one that still emits a valid tool call on the fortieth turn of an agent loop.
Any OpenAI-compatible server can answer a chat request. The ones that matter for agentic work are the ones that hold up under sustained, structured, multi-turn pressure — continuous batching, a paged KV cache, and a tool-call parser that doesn’t drift. That’s why this stack runs vllm-mlx.

The landscape

Why vllm-mlx

Three properties decide it:
  1. Prefix + paged KV cache. Multi-turn and tool-loop workloads re-send a growing, mostly-unchanged context. Reusing the already-prefilled prefix instead of re-running it is the biggest real-world speedup there is — and it’s what makes a local model tolerable as an agent backend.
  2. Continuous batching. Concurrent callers fold into one GPU forward pass instead of serializing.
  3. OpenAI-shaped tool calling. Every caller on the workstation already speaks the OpenAI API, so the model is a drop-in for the cloud providers in the same router.

Rapid-MLX: evaluate, don’t switch on hype

Rapid-MLX is real, actively developed, and — importantly — a hardened derivative of vllm-mlx, not a rival engine. Its headline speed multipliers are author-reported and should be discounted. Its genuinely interesting feature is tool-call auto-repair: it detects malformed tool output and reshapes it back into a valid tool_calls structure, plus a prompt cache. Because it shares DNA with the current backend, the right move is a scoped A/B on tool-calling reliability under multi-round load — not a swap chasing a tokens-per-second headline.

Tool calling is the real failure mode

The number-one way a local agent breaks isn’t speed — it’s a tool call that doesn’t parse. Two things cause most of it:
  • Parser mismatch. The server’s tool-call parser has to match the model’s chat template. The wrong parser produces correct-looking JSON that the server mangles, or silently drops the call. This stack defaults to the hermes parser, which in vllm-mlx reads the <tool_call> XML its resident Qwen-family models emit; a different model family needs its own parser.
  • Quantization drift under long loops. A 4-bit model can format tool calls perfectly for the first several turns and then start emitting subtly invalid structure deep into an agentic trace. This is where auto-repair earns its keep.
  • No parser at all for the model’s format. This is the one that gets misdiagnosed as a bad model. A large open-weights model emitted its own channel markup as raw text in the assistant content field, with finish_reason: stop and no tool_calls array — the call was semantically correct and the serving layer simply dropped it, because the server ships no parser for that format (verified 2026-07-26/27). It leaks into ordinary completions too. This is the same failure class as an agent runtime leaking raw <function=…> markup into a summary: the reasoning was fine, the plumbing lost it.
Per-model tool-call verification is a required gate, not a nice-to-have. Before a model is used for agent work, send a real tool-calling request to /v1/chat/completions through the deployed server and confirm a populated tool_calls array — across several turns, not one. Read content on the same response: if it holds anything that looks like call markup, the parser is missing, no matter how healthy the HTTP status looks. A model that cannot emit a structured tool call through this server is disqualified for agent work no matter how good it is, because the agent never sees the call.
Speed has a second dimension here. A model that does return valid structured calls can still burn several hundred reasoning tokens before the first one — measured at roughly 400 on one candidate. That is latency an agent pays on every single action, not once per conversation, so it belongs in the tool-calling verdict rather than in the raw tokens-per-second column. A parser gap also cuts the other way: writing the missing parser can unlock a model that the plumbing, not the model, was dropping. Check whether the failure is the model or the server before you retire the model. That is what happened with the channel-markup case above, and the parser was written as a declarative patch rather than the model being written off.
Do not justify a parser on a throughput claim you have not re-measured. The original case for that parser cited a ~4.5x cumulative throughput lead over the incumbent. That figure was taken in a different regime and possibly through a proxy that was serving the wrong weights, so it is not comparable. Re-measured in one stated regime, the incumbent leads on decode by roughly 2.6x — the opposite direction.The parser is still correct and still worth having: the markup leakage is real and independently measured on an isolated worker. It just needed a reason that survived scrutiny, and “fixes a genuine plumbing defect” was always the stronger one. A correct fix argued from a wrong number is one re-measurement away from being reverted.
Per-model configuration divergence is the intended end state, not a smell. A tool-call parser, a reasoning parser, and a chat-template flag are properties of the model’s own output format. Forcing every model through one shared setting is what produces silent leaks; a per-model toggle is the correct shape, and a catalog that looks inconsistent across models is usually just accurate.

Apple Silicon stack

The llama-swap + vllm-mlx stack these backends slot into.

Models & quantization

The quants whose tool formatting the parser has to keep up with.

AI development pipeline

How local serving is routed alongside the cloud providers.

Benchmarking

Where backend claims get measured instead of believed.

Verifying the instrument

Proving which weights a server actually loaded before you trust its numbers.