Error [json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)]

OpenAI 互換 API(/v1)で Ollama に構造化出力を頼むと `reasoning_effort:"none"` のとき `response_format` が無視される

FIX SUMMARY verified
Applies when
ollama/ollama:0.31.1ollama /v1 OpenAI-compat with response_format(json_schema) + reasoning_effort:none on a thinking model (qwen3.5:9b), CPU

Verified: reproduced in ollama/ollama:0.31.1, then the json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) signature was gone after the fix (exit 0).

OpenAI の SDK をそのまま Ollama に向けて、response_format に JSON Schema を渡し、モデルの出力を JSON に固定する。よくある使い方です。ところが reasoning_effort"none" を添えた瞬間、返ってくるのが JSON ではなく英語の文章になり、受け取った側の json.loads() が落ちることがあります。

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

原因はあなたのスキーマでも SDK でもありません。Ollama サーバの版です。reasoning_effort:"none" は Ollama の内部で「思考(thinking)を切る」指定に変換され、古いサーバではこのとき response_format の制約が最後まで適用されません。同じスクリプトが、サーバの版を上げるだけで通ります。

結論:サーバを 0.31.2 以上に上げる

直すのはクライアントではなくサーバです。コードは変更しません。

docker rm -f ollama                       # 稼働中の旧コンテナを止めて削除する(名前は docker ps で確認)
docker run -d --name ollama \
  -v ollama:/root/.ollama \
  -p 127.0.0.1:11434:11434 \
  ollama/ollama:0.31.2

この docker run は、既存コンテナが名前付き volume ollama を使い、追加の設定を持たない構成を前提にした例です。GPU 指定(--gpus)や別のマウント、環境変数(-e)を使っているなら、先に docker inspect ollama で今の設定を控え、それらを新しい docker run に引き継いでください(引き継がないと、モデルの置き場所や GPU 設定を失います)。Docker Compose で動かしているなら、docker rm -f はせず、image: のタグを ollama/ollama:0.31.2 に書き換えて作り直すのが安全です。

-p にホスト側 IP を書かないと Docker は全インターフェース(0.0.0.0)へ公開します。ローカルで使うだけなら 127.0.0.1: を付けて loopback に閉じてください。モデルは名前付き volume ollama に残るので、同じ volume を使う限り再ダウンロードは要りません。作り直したら curl http://127.0.0.1:11434/api/version で版を確かめます。

再現するコード

OpenAI SDK を Ollama の /v1 に向け、response_format(JSON Schema)と reasoning_effort:"none" を同時に送るだけで再現します。スキーマに反する出力をモデルに促すため、プロンプトでは「JSON を使うな」と指示しました。response_format が守られていればスキーマが勝ち、無視されればプロンプトが勝つ、という切り分けです。

import json

from openai import OpenAI

SCHEMA = {
    "type": "object",
    "properties": {
        "answer": {"type": "string", "enum": ["ok"]},
        "count": {"type": "integer", "enum": [7]},
    },
    "required": ["answer", "count"],
    "additionalProperties": False,
}

PROMPT = (
    "Do not use JSON. Answer as one friendly English sentence. "
    "The answer value should be ok and the count should be 7."
)

client = OpenAI(base_url="http://127.0.0.1:11434/v1", api_key="ollama")
resp = client.chat.completions.create(
    model="qwen3.5:9b",
    messages=[{"role": "user", "content": PROMPT}],
    response_format={
        "type": "json_schema",
        "json_schema": {"name": "answer", "schema": SCHEMA},
    },
    reasoning_effort="none",
    temperature=0,
    max_tokens=64,
)

content = resp.choices[0].message.content
print("raw content:", repr(content))

data = json.loads(content)
assert data == {"answer": "ok", "count": 7}, data
print("FORMAT_OK", data)

ollama/ollama:0.31.1 のサーバに対して実行すると、content には自然文が入り、次の行の json.loads()JSONDecodeError を送出して終了コード 1 になります。ollama/ollama:0.31.2 に向ければ、同じスクリプトが {"answer": "ok", "count": 7} を返して終了コード 0 で終わります。api_key は Ollama では検証されないので、任意の文字列で構いません。

なぜ /v1 でも起きるのか:翻訳層を通ると /api/chat と同じ要求になる

Ollama の /v1/chat/completions は、OpenAI 形式のリクエストを Ollama ネイティブの /api/chat の形へ翻訳してから処理します。この翻訳で、あなたが送った2つのパラメータは次のように読み替えられます(0.31.1 の openai/openai.go で確認しました)。

  • response_format: {"type": "json_schema", ...} → ネイティブの format(JSON Schema による出力制約)。json_schema の中で読まれるのは schema フィールドだけで、name は無視されます。
  • reasoning_effort: "none" → ネイティブの think"none" のときだけ think=false になり、"low" / "medium" / "high" / "max" は thinking を有効にする側へ回ります。

ここで注意したいのは、Ollama の /v1reasoning_effort に受理する値が、openai/openai.go の中で high / medium / low / max / none の5つに限られている点です。このうち "none" だけが think=false(思考を明示的に切る)に対応し、ほかの4つは thinking を有効にする側へ回ります。範囲外の値を送ると 400 で弾かれます。

thinking モデルから速く JSON を取り出したくて reasoning_effort:"none" を付けた——その指定こそが、翻訳を通ると think=false になり、古いサーバで response_format を無視させる引き金です。これは、ネイティブ API で formatthink=false を同時に送ったときに起きる不具合と、入口が違うだけの同一事象です。ネイティブ API 側で踏んだ場合の詳しい機構は、姉妹記事「Ollama で think=false を送ると format が無視される」で扱っています。

版の境界

errfix の検証ハーネスで実測した2点です。モデルもスクリプトも固定し、入れ替えたのはサーバの版だけです。

Ollama サーバresponse_format + reasoning_effort:"none"返ってくる content
ollama/ollama:0.31.1スキーマが無視される自然文
ollama/ollama:0.31.2スキーマどおり{ "answer": "ok", "count": 7 }

境界は 0.31.1 と 0.31.2 のあいだにあります。修正は Ollama の server/routes.go に入った PR #15901 で、v0.31.1…v0.31.2 の差分に入っています。v0.31.2 のリリースノートの次の1行が、この修正を指しています。

Fixed structured output for thinking models when thinking is disabled

翻訳層の openai/openai.go ではなく、その下流でスキーマ制約をいつ適用するかを決める server/routes.go の変更なので、/api/chat/v1 の両方が同じ版で直ります。この不具合は、手元では出ないのに古い推論サーバに向けたときだけ出ます。OpenAI SDK(openai パッケージ)の版を上げても直りませんし、pip freeze にも痕跡が残りません。Docker Compose でタグを固定したまま共有している推論サーバだけが落ちる、という形になりやすいところです。

temperature:0 で測っており、0.31.1 が返した自然文はネイティブ API で同じ条件を送ったときと同一の文字列でした。あらゆる入力・あらゆる環境で同じ文字列が返るという意味ではありません。判定に使うのは文面の一致ではなく、JSON としてパースでき、スキーマに合うかどうかです。

切り分け

  • content が空文字で返る:これは版境界の不具合とは別です。thinking を有効にしたまま max_tokens(ネイティブの num_predict)を絞ると、思考が生成枠を使い切って本文に一文字も到達せず、finish_reasonlength で返ります。reasoning_effort:"none" を外して thinking を許した場合に起きやすく、0.31.2 に上げても枠が足りなければ続きます。見るべきは版ではなく生成枠です。
  • reasoning_effort:"none" を外したら JSON が返るようになった:これは翻訳層の写像から筋の通った挙動です。think=false になるのは "none" のときだけなので、"none" 以外の値(または reasoning_effort 自体を送らない)にすれば thinking が有効になり、古いサーバでも response_format が適用される側に回ります。ただし thinking のぶん応答は遅く(CPU 推論では姉妹記事のように十数分かかる例もあります)、生成枠も食います。速さのために思考を切りたいなら、回避策で粘るよりサーバの版を上げるのが確実です。
  • response_format を渡していないのに JSON が返らない:それは構造化出力ではなくプロンプトの問題で、この記事の対象外です。まず response_format を渡しているか、json_schema の中に schema が入っているかを確認してください。
  • reasoning_effort が 400 で弾かれる:Ollama が受理しない値(空文字や別の語など)を送っています。受理されるのは high / medium / low / max / none の5つです。
  • ネイティブの /api/chat を直接叩いているresponse_format/reasoning_effort ではなく format/think を送っているなら、入口は違っても同じサーバ側の不具合です。姉妹記事「Ollama で think=false を送ると format が無視される」を参照してください。直し方は同じくサーバの版上げです。

検証環境

  • ollama/ollama:0.31.1(再現)/ollama/ollama:0.31.2(消滅)
  • モデル qwen3.5:9b、GPU 無し(CPU 推論)
  • Python クライアント openai==2.45.0
  • リクエスト POST /v1/chat/completionsstream: falseresponse_format(json_schema)+reasoning_effort:"none"temperature:0max_tokens:64

再現から修正までは errfix の検証ハーネスが機械的に確認しています。再現時にシグネチャが出ること、修正後に消えること、終了コードが 1 から 0 に変わることの3点です。翻訳層の写像(response_formatformatreasoning_effort:"none"think=false)は 0.31.1 の openai/openai.go を、上流の版・PR・機構は GitHub の issue・PR・タグ差分・ソースで裏を取っています。

検証(machine-verified)

この修正は ollama/ollama:0.31.1 のバージョン固定コンテナ内で再現し、修正後に json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs python/ollama-v1-response-format
● reproduce json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) present ✓
● apply fix exit 0
● re-run json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) gone ✓
PASS verified · ollama/ollama:0.31.1 · signature gone