APIとSDKガイド
LLM APIから有効なJSONを取得する:response_formatと再試行
パース可能なJSONを返すコピー可能なPythonリクエスト、空または途中で切れた回答を処理するjson.loadsラッパー、JSONが壊れる理由のチェックリスト。

このページの内容
チャットモデルからJSONを引き出すには、3つのことを行います。response_を渡し、プロンプトに「json」という単語と構造の例を入れ、オブジェクト全体が収まるようにmax_tokensを十分に大きく設定します。そして、答えを盲信してはいけません。空でないことを確認し、finish_reasonを確認し、それから初めてjson.loadsを呼び出します。
これが短い説明です。このガイドの残りは、非常によくある作業、つまりメールからフィールドを抽出するための完全なスクリプトです。例ではベースURL https:/ とモデルID deepseek- を使用しています(ドキュメント)。
作業内容:メールを入力し、行を出力する#
スクリプトがサポートメールを読み取り、それぞれをレコードに変換する必要があるとします。送信者、日付、金額、送信者の要望です。次のメールを見てください。
こんにちは、Lumen Ltd.のAnna Petrovaです。10月14日に請求書#3381に1,250 EURを支払いましたが、領収書はまだ未払いと表示されています。金曜日までに修正してください。
次のものを、それ以外は何も返してほしくありません。
{"sender": "Anna Petrova", "date": "2026-10-14", "amount": 1250, "currency": "EUR", "action": "fix receipt status"}単純な「フィールドを抽出して」というプロンプトでは、親しみやすい文章、コードフェンス、またはその両方が返ってくる可能性があります。あなたのjson.loadsは最初のバッククォートで失敗します。これがresponse_formatと適切なプロンプトが一緒に解決する問題です。
Pythonとcurlでの同じリクエスト
Python
import json
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.cheaptokens.dev/v1",
api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"
SYSTEM = """Extract fields from the email and answer with json only.
Example of the json format:
{"sender": "Jane Roe", "date": "2026-01-31", "amount": 99.5, "currency": "USD", "action": "short phrase"}
Use null for a field that is not in the email."""
resp = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": email_text},
],
response_format={"type": "json_object"},
max_tokens=1000,
temperature=0,
)
print(resp.choices[0].message.content)curl
curl https://api.cheaptokens.dev/v1/chat/completions \
-H "Authorization: Bearer $CHEAPTOKENS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-ai/DeepSeek-V4-Flash-0731",
"messages": [
{"role": "system", "content": "Extract fields from the email and answer with json only. Example: {\"sender\": \"Jane Roe\", \"amount\": 99.5}"},
{"role": "user", "content": "Hi, this is Anna Petrova from Lumen Ltd. I paid invoice #3381 for 1,250 EUR..."}
],
"response_format": {"type": "json_object"},
"max_tokens": 1000
}'JSON Outputに関するDeepSeek APIリファレンス(2026年10月27日確認、ページに公開日なし)には、このリクエストが従うルールが記載されています。response_formatをjson_objectに設定し、システムまたはユーザープロンプトのどこかに「json」という単語を入れ、対象構造の例を含めます。例がないと、モデルはフィールド名を推測する必要があり、次のメールでは異なる推測をします。
フラグよりも例が重要な理由#
response_formatはAPIにJSONオブジェクトを返すよう要求します。どのオブジェクトかをモデルに伝えるわけではありません。同僚に「スプレッドシートを送って」と言い、どの列かを言わないようなものです。スプレッドシートは得られますが、あなたのものではありません。
プロンプト内の例は列ヘッダー行です。2つの習慣がよりうまく機能させます。
- 型を含む実際の形を示す:
1250は数値、"2026-10-14"は固定日付形式の文字列です。 - フィールドが欠けている場合の対処を指定する(上記の
null)。そうしないと、モデルは隙間を埋めるために値をでっち上げます。
response_formatは強いヒントとして扱い、保証とは考えないでください。モデルは時々空文字列を返したり、オブジェクトを文章で包んだりします。だから次の部分が存在します。
安全にパースする:空、途中で切れた、または単に壊れている#
実際には3つの失敗が現れ、それぞれ異なる痕跡を残します。
- 空のコンテンツ。 DeepSeekのドキュメントによると、JSONモードは時々空のコンテンツを返すことがあり、プロンプトを言い換えることを勧めています。あなた側では、
message.contentが""またはNoneであり、json.loads("")がExpecting value: line 1 column 1 (char 0)を発生させるように見えます。 - 途中で切れた。
max_tokensがオブジェクトの途中で尽きました。finish_reasonはlengthで、テキストは文字列の途中で終わります。詳細はAIの回答が途中で止まる理由をご覧ください。 - JSONではない。 オブジェクトの前の文章、またはそれを囲むフェンス。

1番目と3番目の修正は、より厳しいプロンプトでの再試行です。2番目については、同じ制限での再試行は何も変えないので、ラッパーは例外を発生させ、max_tokensを上げるよう伝えます。
def ask_json(email_text: str, retries: int = 1) -> dict:
user = email_text
for attempt in range(retries + 1):
resp = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": user},
],
response_format={"type": "json_object"},
max_tokens=1000,
temperature=0,
)
choice = resp.choices[0]
if choice.finish_reason == "length":
raise ValueError("JSON cut off by max_tokens: raise the limit")
text = (choice.message.content or "").strip()
if text:
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# empty or not JSON: ask again, more firmly
user = email_text + "\n\nReply with one json object only, in the format of the example."
raise ValueError("no valid JSON after retry")スクリプトには1回の再試行で十分です。2回失敗したら、メールIDをログに記録して次に進みます。3回目の同一リクエストはほとんど役に立ちません。
ペアを合わせる
どの症状がどの原因を指すか?
左の項目をタップしてから、そのペアをタップ
- finish_reasonが「length」max_tokensがオブジェクトに対して小さすぎる
- contentが空文字列JSONモードが何も返さなかった。プロンプトを言い換えて再試行する
- バッククォートである文字でのJSONDecodeErrorモデルがJSONをコードフェンスで包んだ
- メール間でフィールド名が異なるプロンプトに構造の例がない
構文だけでなく形を確認する#
json.loadsはテキストが有効なJSONであることだけを証明します。要求したフィールドがあることは証明しません。パース直後に安価なチェックを追加します。
REQUIRED = {"sender", "date", "amount", "currency", "action"}
data = ask_json(email_text)
missing = REQUIRED - data.keys()
if missing:
raise ValueError(f"missing fields: {sorted(missing)}")ここからdictはデータベース行またはスプレッドシートに直接入ります。固定レコードを埋める代わりにモデルに独自の関数を呼び出させたい場合は、別のツールです。Pythonでの関数呼び出しをご覧ください。

メールのバッチにかかるコスト#
短い抽出は安価な種類のリクエストです。入力は数百トークン、出力は数十トークンです。1日1,000通のメールで、入力約400トークン(プロンプト、例、メール)と出力約80トークン(JSON)と仮定します。DeepSeek-V4-Flashでは1日あたり$0.014で、表は他のモデルでの同じ負荷を示しています。
| モデル | 入力 $/1M | 出力 $/1M | リクエストあたり | 1,000 リクエスト | OpenRouter、1,000 リクエスト |
|---|---|---|---|---|---|
| GLM-5.3-Flash | $0.02 | $0.04 | $0.000011 | $0.0112 | $0.0226 |
| DeepSeek-V4-Flash | $0.015 | $0.10 | $0.000014 | $0.014 | $0.0315 |
| MiniMax-M2.7 | $0.05 | $0.15 | $0.000032 | $0.032 | $0.216 |
リクエストあたり 400 入力トークンと 80 出力トークン。100万トークンあたりの価格は現行のもので、当社の料金体系に合わせて変動します。OpenRouter 列は同じワークロードの参考価格を示しています。
合計で、入力と出力を合わせて数えると、DeepSeek-V4-Flashはこの負荷に対してOpenRouterの参照価格より安くなります。また、回答あたり最大32,768出力トークンを許可するので、長い明細リストが小さな制限のように途中で切れることはなく、価格はいつでも同じです。GLM-5.3-Flash(zai-)は同じAPIアドレスでの別の選択肢です。最大8,192出力トークンで、メールごとに1レコードには十分です。現在の価格は価格ページにあり、DeepSeekモデルページにここで使用したモデルの詳細があります。
クイックチェック#
クイックチェック
JSONが文字列の途中で切れています。最初に何を見ますか?
答えを表示
答え: finish_reasonとmax_tokens
切れたオブジェクトはfinish_reasonがlengthに設定されて終わります。制限に達したので、max_tokensを上げてください。温度やアドレスを変えても役に立ちません。
まだリクエストを送っていない場合は、最初のAPIリクエストから始めて、戻って上記のコードに置き換えてください。



