← ブログに戻る
コラム

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

FastMetal

金属の刃で途中から断ち切られた一本の光の筋のイラスト。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 Completionsfinish_reason: "length"最大トークン数に達した
OpenAI Responsesstatus: "incomplete"、incomplete_details.reason: "max_output_tokens"上限に達した
Anthropic Messagesstop_reason: "max_tokens"max_tokens に達した
Anthropic Messagesstop_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キーでモデルごとの挙動を比べられます。無料でアカウントを作成して試せます。

最新のAIモデルを今すぐ試す

最新のAIモデルはFastMetalのAPIキー1つで利用できます。ブラウザですぐに試す、またはOpenAI SDKからそのまま呼び出せます。