Jan 项目中的 mlx-server:基于 MLX-Swift 的 OpenAI 兼容本地推理服务器详解
Jan(开源离线桌面 AI 应用)内置了一条专为 Apple Silicon 打造的本地推理链路:位于 mlx-server/ 目录下的 mlx-server,是一个用 Swift 编写的高性能推理服务器,它为 MLX 格式的大语言模型(LLM)与视觉语言模型(VLM)提供 OpenAI 兼容的 Chat Completions API,支持 SSE 流式输出与函数调用(Tool Calling)。读完本文,你将掌握 mlx-server 的构建方式、全部命令行参数、五个 HTTP 端点的完整用法,以及流式 SSE 帧、取消机制、推理内容拆分(reasoning_content)等源码级实现细节,从而能够独立部署、调试并对接该服务。
1. 定位与核心特性
根据 mlx-server/README.md 的定义,mlx-server 是 "A high-performance inference server for MLX models, providing an OpenAI-compatible API for running large language models on Apple Silicon"。其四大核心特性为:
| 特性 | 说明 |
|---|---|
| OpenAI-Compatible API | OpenAI API 调用的直接替代品(drop-in replacement) |
| Streaming Support | 基于 Server-Sent Events 的实时 token 流 |
| Tool Calling | 支持 OpenAI 兼容格式的函数调用 |
| Multi-Model Support | 同时支持 LLM 与 VLM(视觉语言模型) |
从源码结构看,README 中提到的三个核心组件在仓库中都有明确对应:
- ModelRunner(ModelRunner.swift):以 Swift actor 实现,负责模型加载与非流式/流式推理;
- MLXHTTPServer(Server.swift):基于 Hummingbird 框架的 HTTP 服务器,注册全部 OpenAI 兼容端点;
- ActiveGenerations(Server.swift):同样是 actor,追踪所有可取消的生成任务,支撑
/v1/cancel端点。
2. 技术栈与依赖管理(Package.swift 解读)
mlx-server 是一个标准 Swift Package。Package.swift 给出了运行环境下限与完整依赖清单:
- 平台要求:
platforms: [.macOS(.v14)],即 macOS 14.0(Sonoma)及以上、Apple Silicon 芯片(M1/M2/M3/M4)、Xcode 15+ 构建环境,README 建议至少 8GB 统一内存; - mlx-swift(
>= 0.31.4 < 0.32.0):提供MLX、MLXNN核心库; - mlx-swift-lm(精确锁定
exact: "3.31.4"):提供MLXLLM(文本模型)、MLXLMCommon、MLXVLM(视觉语言模型)三个产品; - swift-transformers(
>= 1.3.0):提供Tokenizers; - swift-jinja(
"2.3.0"..<"2.4.0"); - swift-argument-parser(
>= 1.7.0):CLI 参数解析; - hummingbird(
>= 2.19.0):HTTP 服务器框架。
Package.swift 中特意保留了版本锁定理由的注释,值得注意:
- mlx-swift-lm 被钉死在发布标签 3.31.4 而非
main,因为main分支已经用到了 mlx-swift 0.31.5+ 的 API(MLXArray.maskFill、DType.greatestFiniteMagnitudeArray),低于该下限的版本无法保证可编译; - swift-jinja 被限制在 2.4.0 之前,因为 2.4.0 将
Value.object的键类型从String改为ObjectKey,与 swift-transformers 1.3.3 的jinjaValue()不兼容。
这种精确锁版本的写法是理解"为什么升级依赖会构建失败"的关键依据。
3. 构建与安装
README 给出的构建步骤如下:
# Clone the repository
cd mlx-server
# Build in release mode
xcodebuild -scheme mlx-server -configuration Release
# The binary and metallib will be in the Xcode derived data build products
由于该目录是 Swift Package(含 Package.swift),使用 SwiftPM 的等价命令为:
cd mlx-server
swift build -c release
构建产物(可执行文件 mlx-server)位于 .build/arm64-apple-macosx/release/,README 的 Quick Start 正是引用该路径:
# Run with a local MLX model
./.build/arm64-apple-macosx/release/mlx-server \
--model "/path/to/your/model" \
--port 8080
启动后服务器仅绑定 127.0.0.1(README 的 Notes 明确说明这是出于安全考虑),日志中会打印就绪信号。MLXServerCommand.swift 中的注释 "Print readiness signal (monitored by Tauri plugin)" 表明:Jan 桌面端的 Rust 侧 tauri-plugin-mlx(见 src-tauri/plugins/tauri-plugin-mlx)正是通过监视这行日志来判断服务器就绪的——Swift 端 Logger.swift 的 log() 函数把每条日志直接写入 stdout 并立即刷新,就是为了被 Rust 父进程捕获。
4. 命令行参数
README 提供了四个参数,而 MLXServerCommand.swift 的 @Option 定义中实际有五个(多出一个 --model-id,README 未列出):
| 选项 | 默认值 | 说明 |
|---|---|---|
-m, --model |
Required | 模型目录路径或 HuggingFace 模型 ID |
--port |
8080 |
HTTP 服务端口 |
--ctx-size |
4096 |
上下文窗口大小 |
--api-key |
""(空,即不鉴权) |
可选的 API 鉴权密钥 |
--model-id |
""(缺省时取模型路径的父目录名) |
API 中上报的模型 ID |
补充源码层面的实现细节:
- 鉴权:当
--api-key非空时,请求必须携带Authorization: Bearer <key>,否则返回 401 与authentication_error错误体(Server.swift); - 模型 ID 推导:
--model-id为空时,取--model路径去掉扩展名后的最后一级目录名(MLXServerCommand.swift),这也解释了/v1/models返回的id字段为何是目录名; - GPU 显存上限:进程启动时会设置
Memory.cacheLimit = 20 * 1024 * 1024(20GB),代码注释明确写的是 "prevent OOM issues"(MLXServerCommand.swift),这是防止 MLX 缓存把统一内存占满导致系统级 OOM 的保护措施。
5. 启动流程:从 CLI 到就绪
MLXServerCommand.run() 的完整调用链为:
- 设置
Memory.cacheLimit(20GB 上限); - 打印启动信息(模型路径、端口、上下文大小、缓存上限);
- 解析模型 ID(
--model-id优先,否则取父目录名); - 创建
ModelRunner并try await modelRunner.load(modelPath:)加载模型; - 构造
MLXHTTPServer(modelRunner:modelId:apiKey:),调用buildRouter()生成路由; Application(router:configuration: .init(address: .hostname("127.0.0.1", port: port)))启动服务并阻塞运行。
模型加载的路径解析逻辑在 ModelRunner.load() 中,比 README 描述的更健壮:
- 若
--model指向的目录内存在config.json,直接使用该目录; - 否则向上一级目录探测
config.json("Try parent directory"); - 若指向单个文件,则回退到其父目录。
确定目录后调用 mlx-swift-lm 的 loadModel(from:using:),并传入 TokenizerAdapter.swift 中定义的 SwiftTransformersTokenizerLoader——该适配器把 huggingface/swift-transformers 的 AutoTokenizer 适配为 MLXLMCommon.Tokenizer 协议,文件头部注释说明了原因:mlx-swift-lm 的 PR #118 将分词器/下载器从核心库解耦,loadModel 要求调用方自行提供 TokenizerLoader。
加载完成后,ModelRunner 还会执行 detectInjectsThinkingOpener(in:):依次检查 chat_template.jinja、chat_template.json、tokenizer_config.json 中的 chat_template,若模板里注入了 think 开标签(Qwen3 系列等模型),则置位 injectsThinkingOpener。这个标志位会被流式管线读取,决定推理拆分器是否"一开始就处于 reasoning 模式",详见第 9 节。
6. HTTP API 端点总览
MLXHTTPServer.buildRouter() 共注册了五个端点(README 文档化的 5 个之外,源码中还存在一个额外的 Anthropic 兼容端点,见第 10 节):
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /health |
健康检查,返回 {"status":"ok"} |
| GET | /v1/models |
列出已加载模型 |
| POST | /v1/chat/completions |
Chat Completions(支持流式/非流式/工具调用) |
| POST | /v1/cancel |
取消全部进行中的生成 |
| POST | /v1/messages |
Anthropic Messages 兼容端点(源码扩展) |
6.1 Chat Completions(非流式)
README 的完整示例:
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "model",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello, how are you?"}
],
"temperature": 0.7,
"max_tokens": 100
}'
请求体由 ChatCompletionRequest 结构定义。结合 Server.swift 中的默认值解析逻辑,完整参数表如下:
| 字段 | 类型 | 默认值(缺失时) | 说明 |
|---|---|---|---|
model |
String | 必填 | 模型名(任意值均可,服务器只加载一个模型) |
messages |
Array | 必填 | 对话消息数组 |
temperature |
Float | 0.7 |
采样温度 |
top_p |
Float | 1.0 |
核采样 |
max_tokens / n_predict |
Int | 无限制 | 两者均可,max_tokens ?? n_predict |
stream |
Bool | false |
是否 SSE 流式 |
stop |
String[] | [] |
停止词(见下方限制说明) |
repetition_penalty |
Float | 1.0 |
重复惩罚 |
tools |
Array | nil |
OpenAI 格式的工具定义 |
需要注意的限制:请求体收集上限为 10MB(request.body.collect(upTo: 10 * 1024 * 1024),Server.swift)。另外 ModelRunner.generateStream() 的参数注释明确写着 stop 目前被跳过("Skipped for now, later should pass thru model load where container preparation - context.configuration.extraEOSTokens")——即该字段已被解析并透传,但尚未真正接入生成器的 EOS 配置,这是当前实现的一个已知边界。
响应结构为 ChatCompletionResponse:id 形如 chatcmpl-<12位UUID前缀>,object 固定为 chat.completion,finish_reason 在有工具调用时为 tool_calls,否则为 stop(Server.swift),并附带 usage(prompt/completion/total token 数)。
6.2 流式响应
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "model",
"messages": [{"role": "user", "content": "Tell me a story."}],
"stream": true
}'
响应头为 text/event-stream、Cache-Control: no-cache、Connection: keep-alive,帧格式为标准的 data: <json>\n\n,结束时以 data: [DONE]\n\n 收尾。每个 chunk 的 object 为 chat.completion.chunk,首个 chunk 的 delta 只含 role: "assistant"。
6.3 取消生成
curl -X POST http://localhost:8080/v1/cancel
该端点调用 ActiveGenerations.cancelAll(),返回 {"status":"cancelled"|"no_active_generation","cancelled_count":<n>}(Server.swift)。
6.4 模型列表与健康检查
curl http://localhost:8080/v1/models
curl http://localhost:8080/health
/v1/models 返回单元素数组:{id: <modelId>, object: "model", created: <unix时间戳>, owned_by: "mlx"};/health 返回 {"status":"ok"},可供客户端轮询判断就绪状态。
7. 流式输出实现:AsyncStream 与 SSE 帧
README 的 Notes 说"客户端断开会自动取消当前生成",其完整实现值得展开。handleStreamingRequest() 采用了"外部创建 Task + 注册取消"的模式:
- 通过
AsyncStream<ByteBuffer>.makeStream()得到(responseStream, continuation); - 在独立
Task中消费modelRunner.generateStream(...)产出的事件流(StreamEvent:.chunk/.toolCall/.done,定义见 ModelRunner.swift),把每个 token 编码为chat.completion.chunkJSON,经 buildSSEFrame() 包装成 SSE 帧 yield 给 continuation; - 生成 Task 注册进
ActiveGenerations(以 responseId 为键),使/v1/cancel能外部取消它; continuation.onTermination回调中调用task.cancel()——当客户端断开、流消费者终止时自动取消生成任务(Server.swift)。ModelRunner.generateStream()内部也设有一层onTermination(ModelRunner.swift),并在每个事件循环里检查Task.isCancelled。
性能细节:SSE 帧共用一个进程级 ByteBufferAllocator(sseBufferAllocator)以减少分配;JSON 编码使用共享的 jsonEncoder(secondsSince1970 + convertToSnakeCase 策略,Server.swift)。最终 chunk(含 finish_reason)会额外携带 usage 与 timings(prompt/predicted 每秒 token 数,来自 GenerateCompletionInfo,ModelRunner.swift)——这一 timings 字段是 llamacpp 风格的扩展,非 OpenAI 标准。
8. 取消机制的三种触发路径
综合 ActiveGenerations actor 与两处 onTermination 钩子,mlx-server 中一次生成可以被取消的完整路径有三条:
- 客户端断开连接:SSE 流消费终止 →
continuation.onTermination→task.cancel(); - 显式 API 取消:
POST /v1/cancel→cancelAll()遍历所有注册任务执行cancel(); - 生成侧自检:
ModelRunner内的事件循环在取消后break并正常finish()(不抛错)。
ActiveGenerations(Server.swift)提供 register / cancel / cancelAll / remove 四个方法,任务在流结束后通过 defer 自动 remove(responseId) 清理。由于它声明为 actor,跨任务的字典操作是并发安全的。
9. 推理内容拆分:reasoning_content 与 ReasoningSplitter
一个 README 未覆盖、但源码中实现完整的细节:mlx-server 会把带 think/think 标记的模型输出拆分为 reasoning_content(思考过程)与 content(正文),分别写入 SSE delta 的两个字段(ChatDelta 定义了 reasoning_content: String?)。实现位于 ReasoningSplitter.swift,其文件头注释概括了设计:
"Splits a streaming model output into reasoning_content and content based on think/think markers, mirroring how llamacpp's --reasoning-format option exposes the split to OpenAI-compatible clients."
拆分器要处理三种情况:
- 显式:输出含
think...think对——丢弃开标签,中间内容归 reasoning,丢弃闭标签,之后归 content; - 隐式:chat template 已把开标签注入提示词(Qwen3 家族),模型输出只有闭标签——闭标签之前的全部内容归 reasoning;
- 无标记:全部按 content 输出。
其状态机细节:
- 用一个
resolveWindow = 32字符的头部窗口来判定上述三种情况,窗口内未出现任何标签则认定为纯 content(ReasoningSplitter.swift); drainSafe()始终保留缓冲区末尾think的长度(8 个字符),处理闭标签横跨 token 边界的场景(ReasoningSplitter.swift);- 流开始时若
ModelRunner.injectsThinkingOpener为真,则startInReasoning: true,拆分器直接进入 reasoning 模式(Server.swift 读取该标志)。
这套机制与第 5 节提到的 detectInjectsThinkingOpener 模板探测(ModelRunner.swift)共同构成完整的推理内容透传链路。
10. 视觉输入:VLM 支持路径
README 声明支持 VLM("vision-language models are supported via image/video URL inputs"),对应源码实现:
- ChatMessage 除
content(字符串或 OpenAI 式 part 数组)外,还支持独立的images、videos字符串数组字段; ContentType枚举支持image_url类型的 part(OpenAITypes.swift),imageUrls计算属性从中提取 URL;- ModelRunner.toChatMessage() 把两种来源(
content数组中的image_urlpart 与独立的images字段)合并,并对file://前缀做了剥离处理,最终转为UserInput.Image.url(...)/UserInput.Video.url(...)传入ChatSession的streamDetails(to:images:videos:)调用(ModelRunner.swift); - VLM 能力来自 mlx-swift-lm 的
MLXVLM产品依赖(Package.swift)。
11. 附赠:Anthropic Messages 兼容端点
源码中还有一个 README 未列出的端点 POST /v1/messages(Server.swift),用于让 Anthropic SDK 客户端直连本服务器。其转换管线在 AnthropicTypes.swift:
- 请求侧:
anthropicToInternalMessages()把 Anthropic 的system(字符串或 text block 数组)转为前置 system 消息,tool_useblock 转成 OpenAI 格式的tool_calls,tool_result转成独立role: "tool"消息,base64 图片转成data:<media_type>;base64,...URL;anthropicToolsToOpenAI()把input_schema映射为 OpenAI 的parameters(AnthropicTypes.swift); - 响应侧:非流式返回
content块数组(text / tool_use)+stop_reason(end_turn/tool_use);流式按 Anthropic SSE 事件序列输出message_start→ping→content_block_start→content_block_delta(text_delta 或 input_json_delta)→content_block_stop→message_delta→message_stop(Server.swift)。
一个值得注意的实现差异:Anthropic 编码器 anthropicEncoder 刻意不使用 convertToSnakeCase 键策略,以免破坏工具 input schema 中原本就是 snake_case 的键。
12. 故障排查
继承 README 的 Troubleshooting 章节并结合源码:
12.1 模型加载失败
确保模型目录包含以下文件:
config.json— 模型配置(加载逻辑以它为存在性判据,见 ModelRunner.load());tokenizer.json— 分词器词表;model.safetensors或model.safetensors.index.json— 模型权重;- 可选:
generation_config.json、chat_template.jinja。
12.2 端口被占用
更换端口即可:
--port 8081
12.3 从源码结构可进一步观察到的排查线索
- 服务器只监听
127.0.0.1,从其他机器访问必然失败——这是设计约束而非故障; - 所有关键事件都以
[mlx]前缀写入 stdout(由 Rust 侧捕获),模型加载失败会打印Failed to load model: <error>(MLXServerCommand.swift)后进程退出; - 生成吞吐信息(prompt/generation tokens per second)会以
[mlx] Generation: x tokens/sec形式出现在日志中(ModelRunner.swift),可据此判断是模型问题还是硬件瓶颈。
13. 项目结构与文件索引
README 给出的结构如下,补充源码中实际存在的全部文件及其职责:
mlx-server/
├── Sources/
│ └── MLXServer/
│ ├── MLXServerCommand.swift # CLI 入口(swift-argument-parser @main)
│ ├── ModelRunner.swift # 核心推理引擎(actor:加载/生成/流式)
│ ├── Server.swift # HTTP 服务器、路由、SSE 流、取消管理
│ ├── OpenAITypes.swift # OpenAI API 类型定义
│ ├── AnthropicTypes.swift # Anthropic API 类型与请求转换
│ ├── ReasoningSplitter.swift # think 标签流式拆分器
│ ├── TokenizerAdapter.swift # swift-transformers → MLXLMCommon 分词器适配
│ └── Logger.swift # stdout 日志(供 Rust 进程捕获)
├── Package.swift # Swift package manifest(依赖与版本锁定)
└── README.md # 本说明文件
在 Jan 整体架构中,mlx-server 属于 Apple Silicon 推理链路的终端组件:Rust 侧的 tauri-plugin-mlx 负责拉模型、拉起该 Swift 进程并监视就绪日志,Web 前端则通过 OpenAI 兼容协议调用 127.0.0.1:<port> 上的端点。这种"Rust 进程管理 + Swift 推理服务 + OpenAI 协议"的解耦,使得 Jan 可以复用整个 OpenAI 生态的客户端代码而不感知底层是 MLX 还是 llama.cpp(llama.cpp 链路对应仓库中的 tauri-plugin-llamacpp)。
14. 小结
mlx-server 用约 2000 行 Swift 代码实现了三层能力:一是完整的 OpenAI Chat Completions 兼容面(含流式、工具调用、用量统计);二是本地化运维语义(127.0.0.1 绑定、stdout 就绪信号、20GB 显存缓存上限、客户端断开即取消);三是对新兴模型特性的适配(reasoning_content 拆分、VLM 图/视频输入、Anthropic Messages 协议转换)。对其源码的阅读入口建议从 MLXServerCommand.swift(入口)→ Server.swift(协议面)→ ModelRunner.swift(推理面)依次展开。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00