finish_reason: length で応答が途中で切れる原因と対処法

この記事の要点
finish_reason: "length"は、モデルが指定された最大トークン数に達して生成を止めたことを示します。HTTPは200で、例外は発生しません。- Anthropic形式では
stop_reasonのmax_tokensが対応します。枠の端で止まった場合はmodel_context_window_exceededになります。 - 推論モデルでは思考のトークンも同じ出力の枠を使います。思考で枠を使い切ると、本文が空のまま止まることがあります。
- 対処は、
max_tokensを上げる、思考の量を下げる、続きを取得する、のいずれかです。
JSONの解析が失敗する、文章が語の途中で終わる、content が空で返ってくる。こうした現象が出たら、まずレスポンスの finish_reason を確かめます。
finish_reason: length のレスポンス
エラーではないので、本文は通常の応答と同じ形です。choices[0].finish_reason だけが違います(値は一例です)。
{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "結論から言うと、この構成には3つの"
},
"finish_reason": "length"
}
],
"usage": {
"prompt_tokens": 420,
"completion_tokens": 256,
"total_tokens": 676
}
}
OpenAIのAPIリファレンスでは、length はリクエストで指定した最大トークン数に達した場合の値とされています。
stop_reason との対応
| 形式 | 値 | 意味 |
|---|---|---|
| OpenAI Chat Completions | finish_reason: "length" | 最大トークン数に達した |
| OpenAI Responses | status: "incomplete"、incomplete_details.reason: "max_output_tokens" | 上限に達した |
| Anthropic Messages | stop_reason: "max_tokens" | max_tokens に達した |
| Anthropic Messages | stop_reason: "model_context_window_exceeded" | コンテキストウィンドウの端に達した |
Anthropicの値の一覧はStop reasonsのドキュメント、Responses APIの挙動はOpenAIのReasoningガイドにあります。
思考トークンが出力の枠を使い切る
推論モデルは、回答の前に考えた分もトークンとして消費します。Anthropicのドキュメントは、思考トークンは max_tokens の一部で、出力として課金されると説明しています。OpenAIも、見える出力が1文字も生成される前に上限に達することがあり、その場合も入力と思考の分は課金されると明記しています。
FastMetal経由でも、思考のトークンは出力の単価で課金され、completion_tokens に含まれます。内訳は usage.completion_tokens_details.reasoning_tokens で確認できます。表示されない場合も課金の対象です。
reasoning オブジェクトで思考の量を調整できます。指定できる値はモデルごとに違い、既定の量もモデルごとに異なります。Kimi K3は既定で最大まで考えます(推論の設定)。
切れた応答の続きを取得するコード
切れたら、ここまでの出力をassistantの発言として渡し、続きを頼みます。本文が空で切れたときは、続きを頼んでも意味がないので例外にしています。
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["FASTMETAL_API_KEY"],
base_url="https://api.fastmetal.ai/v1",
)
def complete_all(messages: list[dict], max_tokens: int = 4096, rounds: int = 3) -> str:
parts: list[str] = []
for _ in range(rounds):
history = list(messages)
if parts:
history += [
{"role": "assistant", "content": "".join(parts)},
{"role": "user", "content": "直前の出力の最後の文字から、そのまま続きを書いてください。"},
]
resp = client.chat.completions.create(
model="kimi-k3",
messages=history,
max_tokens=max_tokens,
extra_body={"reasoning": {"effort": "low"}},
)
choice = resp.choices[0]
if choice.finish_reason == "length" and not choice.message.content:
raise RuntimeError("思考で出力の枠を使い切りました。max_tokens を増やしてください。")
parts.append(choice.message.content or "")
if choice.finish_reason != "length":
return "".join(parts)
raise RuntimeError("指定した回数では最後まで生成できませんでした。")
JSONなど構造のある出力は、途中で継ぎ足すと壊れやすくなります。その場合は続きを頼まず、max_tokens を増やして全体を生成し直します。
FastMetalで確かめる
FastMetalは、OpenAI互換とAnthropic互換のAPIを持つ日本のAIゲートウェイです。同じコードのまま model を変えて、思考の量や出力の長さの違いを比べられます。思考トークンの単価を含む円建ての料金は料金ページで確認できます。
よくある質問
finish_reason が length のとき課金されますか
はい。生成された分は出力トークンとして課金されます。切れた応答を捨てて生成し直すと、その分の費用が重なります。
max_tokens を大きくすれば必ず解決しますか
出力が足りないだけなら解決します。入力と合わせてコンテキスト長を超える場合は、別の対処が必要です。原因が思考の量なら、reasoning で調整します。
content が空で finish_reason が length になるのはなぜですか
思考のトークンで出力の枠を使い切り、本文を生成する前に止まった可能性が高いです。usage.completion_tokens_details.reasoning_tokens を確かめ、max_tokens を増やすか、思考の量を下げます。
ストリーミングでも確認できますか
できます。最後のチャンクの finish_reason に同じ値が入ります。切れた場合の処理は、ストリームの終了時に分岐させます。
まとめ
finish_reason: "length" は例外にならないので、確かめるコードがないと切れた応答がそのまま流れます。レスポンスごとに値を見て、思考の量と max_tokens を調整します。
FastMetalでは、1つのAPIキーでモデルごとの挙動を比べられます。無料でアカウントを作成して試せます。