首页
/ Jan 项目中的 mlx-server:基于 MLX-Swift 的 OpenAI 兼容本地推理服务器详解

Jan 项目中的 mlx-server:基于 MLX-Swift 的 OpenAI 兼容本地推理服务器详解

2026-09-05 13:33:32作者:滕妙奇

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 中提到的三个核心组件在仓库中都有明确对应:

  1. ModelRunnerModelRunner.swift):以 Swift actor 实现,负责模型加载与非流式/流式推理;
  2. MLXHTTPServerServer.swift):基于 Hummingbird 框架的 HTTP 服务器,注册全部 OpenAI 兼容端点;
  3. ActiveGenerationsServer.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):提供 MLXMLXNN 核心库;
  • mlx-swift-lm(精确锁定 exact: "3.31.4"):提供 MLXLLM(文本模型)、MLXLMCommonMLXVLM(视觉语言模型)三个产品;
  • 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.maskFillDType.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.swiftlog() 函数把每条日志直接写入 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() 的完整调用链为:

  1. 设置 Memory.cacheLimit(20GB 上限);
  2. 打印启动信息(模型路径、端口、上下文大小、缓存上限);
  3. 解析模型 ID(--model-id 优先,否则取父目录名);
  4. 创建 ModelRunnertry await modelRunner.load(modelPath:) 加载模型;
  5. 构造 MLXHTTPServer(modelRunner:modelId:apiKey:),调用 buildRouter() 生成路由;
  6. 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.jinjachat_template.jsontokenizer_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 配置,这是当前实现的一个已知边界。

响应结构为 ChatCompletionResponseid 形如 chatcmpl-<12位UUID前缀>object 固定为 chat.completionfinish_reason 在有工具调用时为 tool_calls,否则为 stopServer.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-streamCache-Control: no-cacheConnection: keep-alive,帧格式为标准的 data: <json>\n\n,结束时以 data: [DONE]\n\n 收尾。每个 chunk 的 objectchat.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 + 注册取消"的模式:

  1. 通过 AsyncStream<ByteBuffer>.makeStream() 得到 (responseStream, continuation)
  2. 在独立 Task 中消费 modelRunner.generateStream(...) 产出的事件流(StreamEvent.chunk / .toolCall / .done,定义见 ModelRunner.swift),把每个 token 编码为 chat.completion.chunk JSON,经 buildSSEFrame() 包装成 SSE 帧 yield 给 continuation;
  3. 生成 Task 注册进 ActiveGenerations(以 responseId 为键),使 /v1/cancel 能外部取消它;
  4. continuation.onTermination 回调中调用 task.cancel()——当客户端断开、流消费者终止时自动取消生成任务(Server.swift)。ModelRunner.generateStream() 内部也设有一层 onTerminationModelRunner.swift),并在每个事件循环里检查 Task.isCancelled

性能细节:SSE 帧共用一个进程级 ByteBufferAllocatorsseBufferAllocator)以减少分配;JSON 编码使用共享的 jsonEncodersecondsSince1970 + convertToSnakeCase 策略,Server.swift)。最终 chunk(含 finish_reason)会额外携带 usagetimings(prompt/predicted 每秒 token 数,来自 GenerateCompletionInfoModelRunner.swift)——这一 timings 字段是 llamacpp 风格的扩展,非 OpenAI 标准。

8. 取消机制的三种触发路径

综合 ActiveGenerations actor 与两处 onTermination 钩子,mlx-server 中一次生成可以被取消的完整路径有三条:

  1. 客户端断开连接:SSE 流消费终止 → continuation.onTerminationtask.cancel()
  2. 显式 API 取消POST /v1/cancelcancelAll() 遍历所有注册任务执行 cancel()
  3. 生成侧自检ModelRunner 内的事件循环在取消后 break 并正常 finish()(不抛错)。

ActiveGenerationsServer.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."

拆分器要处理三种情况:

  1. 显式:输出含 think...think 对——丢弃开标签,中间内容归 reasoning,丢弃闭标签,之后归 content;
  2. 隐式:chat template 已把开标签注入提示词(Qwen3 家族),模型输出只有闭标签——闭标签之前的全部内容归 reasoning;
  3. 无标记:全部按 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"),对应源码实现:

  • ChatMessagecontent(字符串或 OpenAI 式 part 数组)外,还支持独立的 imagesvideos 字符串数组字段;
  • ContentType 枚举支持 image_url 类型的 part(OpenAITypes.swift),imageUrls 计算属性从中提取 URL;
  • ModelRunner.toChatMessage() 把两种来源(content 数组中的 image_url part 与独立的 images 字段)合并,并对 file:// 前缀做了剥离处理,最终转为 UserInput.Image.url(...) / UserInput.Video.url(...) 传入 ChatSessionstreamDetails(to:images:videos:) 调用(ModelRunner.swift);
  • VLM 能力来自 mlx-swift-lm 的 MLXVLM 产品依赖(Package.swift)。

11. 附赠:Anthropic Messages 兼容端点

源码中还有一个 README 未列出的端点 POST /v1/messagesServer.swift),用于让 Anthropic SDK 客户端直连本服务器。其转换管线在 AnthropicTypes.swift

  • 请求侧:anthropicToInternalMessages() 把 Anthropic 的 system(字符串或 text block 数组)转为前置 system 消息,tool_use block 转成 OpenAI 格式的 tool_callstool_result 转成独立 role: "tool" 消息,base64 图片转成 data:<media_type>;base64,... URL;anthropicToolsToOpenAI()input_schema 映射为 OpenAI 的 parametersAnthropicTypes.swift);
  • 响应侧:非流式返回 content 块数组(text / tool_use)+ stop_reasonend_turn / tool_use);流式按 Anthropic SSE 事件序列输出 message_startpingcontent_block_startcontent_block_delta(text_delta 或 input_json_delta)→ content_block_stopmessage_deltamessage_stopServer.swift)。

一个值得注意的实现差异:Anthropic 编码器 anthropicEncoder 刻意不使用 convertToSnakeCase 键策略,以免破坏工具 input schema 中原本就是 snake_case 的键。

12. 故障排查

继承 README 的 Troubleshooting 章节并结合源码:

12.1 模型加载失败

确保模型目录包含以下文件:

  • config.json — 模型配置(加载逻辑以它为存在性判据,见 ModelRunner.load());
  • tokenizer.json — 分词器词表;
  • model.safetensorsmodel.safetensors.index.json — 模型权重;
  • 可选:generation_config.jsonchat_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(推理面)依次展开。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384