gpt-oss を MLX-LM の API サーバで Dify や Open WebUI からムリヤリ使う方法

新しく MXFP4 に対応した MLX/MLX-LM に関する記事を書いたので、そちらをご覧ください。

先日アップした記事では、MLX バージョンの gpt-oss を MLX-LM の API サーバで動かすと Dify や Open WebUI 等では正しく動作しないと書きましたが、server.pyに変更を加えることでチャットだけはできるようになりました。手元の環境では動いていますが、ビビって MLX-LM には PR せずに Issue だけあげてあります (↓ 2コ)。本家が解決したら不要になる情報ですがせっかくなので共有します。

https://github.com/ml-explore/mlx-lm/issues/364 → (2025/08/15 加筆) 修正がコミットされたようなので、近くバージョンアップで修正されそうです

https://github.com/ml-explore/mlx-lm/issues/365 → (2025/08/15 加筆) 「クライアント側で対応すべき内容」と言うことでクローズされました

フォークした repo (↓) には変更済みのserver.pyを置いてあるので、よかったらどうぞ。Dify や Open WebUI 等の API クライアント側の対応を待たずに MLX-LM で gpt-oss が動きます。

https://github.com/tokyohandsome/mlx-lm

前回の記事はこれ:

各種バージョン等

Issue に書いてますが一応。

  • Open Web UI: v0.6.20
  • Dify: 1.7.1
% pip list|grep mlx
mlx 0.28.0
mlx-lm 0.26.3
mlx-metal 0.28.0
%
% python -V
Python 3.12.11

不具合の内容

詳細は上の issue を見てもらいたいのですが、ざっくり以下の内容です:

  1. LLM からのレスポンスが途中で終わってしまう: 何か制御コードみたいなものが含まれているのかと思ったら、ただのカラ文字が原因だった感じです。カラ文字は送らないようにしたら動くようになりました。トークナイザの不具合?
  2. チャットで 2つ目のプロンプトを投げるとサーバでエラーが発生する: 本来 API クライアント側で<|channel|>から<|message|>の思考部分をサーバに送り返さないのが正解だと思います (なので MLX-LM では上記 issue は対応無し)。ただまぁボクの場合、ローカルで動かしているだけなので、サーバで該当部分を捨ててしまうようにしました。

素人目にはそんなに根が深いわけではなさそうなので、かなり近いうちに修正されるんじゃないかと思ってます。(2025/08/15 追記) 1. は新しい MLX-LM のバージョンで修正されそうです。2. はクライアント側での対応が必要です。

手っ取り早く使うには

上記の通り現状 MLX-LM では gpt-oss 個別の対応はされないようで、Open WebUI や Dify などの API クライアント側での MLX 版 gpt-oss 対応を待たなければならないようです。ボクのように MLX の速さやにとりつかれていて API で MLX 版を使いたいという人は、server.pyだけ上書きして使ってみてください。mlxmlx-lmmlx-metalのバージョンは上記と合わせたほうが良いと思います。

仮想環境にクローンして使うならこんな感じです (ポートなどはご自由に)。

git clone https://github.com/tokyohandsome/mlx-lm
pip install -r requirements.txt
python -m mlx_lm.server --host 0.0.0.0 --port 9999 --log-level DEBUG

本家の MLX-LM を入れて、ボクがいじったserver.pyだけを差し替える方法も参考として貼っておきます。pipenvを使ってますがお使いの仮想環境でどうぞ:

mkdir gpt-oss_mlx-lm
cd gpt-oss_mlx-lm
pipenv --python 3.12 # 3.8 以上なら OK
pipenv shell
pip install mlx==0.28.0 mlx-lm==0.26.3

# インストールされたバージョンの確認
pip list|grep mlx

# 元のファイルを .original としてコピーしておく
mv .venv/lib/python3.12/site-packages/mlx_lm/server.py .venv/lib/python3.12/site-packages/mlx_lm/server.py.original
curl https://raw.githubusercontent.com/tokyohandsome/mlx-lm/refs/heads/main/mlx_lm/server.py -o .venv/lib/python3.12/site-packages/mlx_lm/server.py

上の最後の 2行で元のファイルをserver.py.originalとして保存し、変更済みのserver.pyをダウンロードしています。これで準備完了です。

以下コマンドで OpenAI API コンパチの MLX-LM サーバが起動します (例ではポート9999)。

mlx_lm.server --host 0.0.0.0 --port 9999 --log-level DEBUG

Open WebUI 等から gpt-oss に接続し、Terminal に流れるトークン全てが表示され、2回目以降もチャットが続けられれば成功です!

MLX-LM API Server のモデルを Open WebUI や Dify から使う方法は別記事に詳しく書いていますのでどうぞ:

ついでにserver.pyの変更箇所 (diff) を貼っておきます。+の行にあるのが今回追加した部分です:

diff .venv/lib/python3.12/site-packages/mlx_lm/server.py.original .venv/lib/python3.12/site-packages/mlx_lm/server.py
--- .venv/lib/python3.12/site-packages/mlx_lm/server.py.original	2025-08-15 21:05:24
+++ .venv/lib/python3.12/site-packages/mlx_lm/server.py	2025-08-15 21:15:34
@@ -694,2 +694,8 @@
             logging.debug(gen_response.text)
+
+            # --- Added from here ---
+            if not gen_response.text:
+                logging.debug("Skipping empty token.")
+                continue
+            # --- to here ---
 
@@ -837,3 +843,37 @@
             messages = body["messages"]
+
+            # --- Changes from here ---
+            # Modify message based on the `mlx-lm` chat template.
+            for message in messages:
+                if message["role"] == "assistant":
+                    content = message.get("content", "")
+                    if "<|channel|>analysis<|message|>" in content and "<|channel|>final<|message|>" in content:
+                        try:
+                            analysis_start_tag = "<|channel|>analysis<|message|>"
+                            analysis_end_tag = "<|end|>"
+                            final_start_tag = "<|channel|>final<|message|>"
+
+                            analysis_start = content.find(analysis_start_tag) + len(analysis_start_tag)
+                            analysis_end = content.find(analysis_end_tag)
+                            final_start = content.find(final_start_tag) + len(final_start_tag)
+
+                            analysis = content[analysis_start:analysis_end].strip()
+                            final = content[final_start:].strip()
+
+                            message["content"] = final
+                            message["thinking"] = analysis
+                        except Exception as e:
+                            logging.error(f"Failed to parse assistant message with analysis/final tags: {e}")
+                            # If parsing fails, leave the content and empty thinking
+                            message["thinking"] = ""
+            # --- to here ---
+
             process_message_content(messages)
+
+            # Moved response_format before `apply_chat_template`
+            if body.get("response_format", {}).get("type") == "json_object":
+                if self.tokenizer.chat_template is None:
+                    raise ValueError("JSON response format requested, but tokenizer has no chat template. Consider using `--use-default-chat-template`")
+                messages.append({"role": "user", "content": self.tokenizer.json_schema_prompt})
+
             prompt = self.tokenizer.apply_chat_template(

それでもまだ LM Studio が優れているところ

というわけでムリヤリながら Dify や Open WebUI でも MLX 版 gpt-oss でチャットができるようになったわけですが、OpenAI 社が推奨する思考部分をユーザから隠すということができません。そこは正式対応済みの LM Studio が勝っていますね。Dify や Open WebUI も Qwen/Qwen3-32B-MLX-4bit なんか使ってると思考部分は隠せているので、gpt-oss (というか Harmony response format) の正式対応が進んでくれたらいいな、と思っています。

単純に思考部分を完全に見えなくするだけであれば、どうせ今回紹介している方法では乱暴にオリジナルのスクリプトを書き換えて使っているので、server.py<|channel|>から<|message|>までのメッセージをクライアントに返さないように改造してしまっても良いかもしれません。

ところで今回どうやって直したか、とか

せっかくなので LM Studio で gpt-oss を動かして協力してもらいながら解決まで持って行きたかったんですが、テストするときには MLX-LM でも gpt-oss をロードする事になりメモリキャパオーバによるクラッシュの危険性が高いので避けました。で、ChatGPT に相談を始めたものの全然解決に近づいている感じがなく時間ばかりがかかりギブアップ。次に Gemini (2.5 Flash) に相談し始めてからはほぼ最短コースで解決にたどり着いた感じです。この時には質問方法や内容に慣れて、深掘りすべきところにもある程度見当が付いてきたこともあったとは思いますが、Gemini を見直しました。

質問の時には、使っている環境、症状の詳細、関係している可能性が高い Python スクリプト全体 (server.py)、サーバのエラー、クライアント (Dify や Open WebUI) のエラー、等を詳細に伝えることで解決できた感じです。ChatGPT はコードの修正をお願いすると全く違うものが出てきたりして使えなかったです。もしかしたら動いたのかも知れませんがとても pull request には使えないものだったので (そういう意味では gpt-oss もそういう用途では使えないのかな)。Gemini は最小限の追加で、コードを差し込むところの説明含め正確でした。

余談ですが、最近プログラマ不要論みたいなのがありますよね。生成 AI で置き換え可能、とかなんとか。確かに最近は 20B~30B 程度のサイズの LLM でもざっくりとしたプロンプトから一発でブロック崩しゲームを書いてくれたりしますが、狙ったとおりの変更やバグの修正などを上手に行うにはプログラムの知識は必要だと思いますね。

おまけ: gpt-oss-20B と Qwen3 30B A3B の SVG 対決

プロンプト: SVG で UFO が牛をさらっている画像を作ってください

(貼ったのは PNG にしたものです)

まずは inferencerlabs/openai-gpt-oss-20b-MLX-6.5bit

文章で説明するのはズルイ。ま、やっちゃダメとも言わんかったか

次に nightmedia/Qwen3-30B-A3B-Thinking-2507-dwq4-mlx

雰囲気がヨイ!けど人さらってますね

現場からは以上となります!

Image by Stable Diffusion (Mochi Diffusion)

リンゴに絆創膏、というイメージで書いてもらいました。バンドエイドは商標ですが、全くそう見えないものができたのでセーフと自己判断して採用。そろそろリンゴ以外を使った方がいいかもと思いつつも結局こんな感じで、生成 AI ばかり使いすぎて頭がアレになってきた人の特徴でしょうかね。

Date:
2025年8月10日 23:07:43

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
small band-aid patches on a red apple

Exclude from Image:

Seed:
1709363568

Steps:
21

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

© Peddals.com