一言で言うと:1回の呼び出しでモデルに数万文字の出力を要求する場合(エピソードのアウトライン、長編小説、大規模な翻訳、巨大なコードなど)、非ストリーミングではなく、レスポンスをストリーミングしてください。クライアントの読み取りタイムアウトは合計生成時間ではなく、データイベント間の間隔(数十秒 — 90〜120秒が安全な値)に設定してください。また、
max_tokensに余裕を持たせ、テキストを使用する前にstop_reasonを確認してください。これら4点を行うことで、長文出力の呼び出しで「何も返ってこない」状態を回避できます。2万〜3万文字を超える場合は、接続を20分間維持するのではなく、処理を複数の呼び出しに分割するという対策をもう1つ追加してください。/v1/messagesを使用していますが、OpenAI互換フォーマットとの違いについては個別に注記しています。
最初に知っておくべき3つのこと
- 10k語の出力では、実際の生成に10~20分かかるのは正常です。 モデルは数万文字をtokenごとに出力し、さらに推論/thinkingフェーズもあるため、エンドツーエンドのレイテンシーは実際に長くなります。これはゲートウェイが遅いのではなく、生成そのものに時間がかかっているためです。
-
非ストリーミングでは、すべてをバッファリングしてから送信します。 非ストリーミング(
streamが省略されている、またはfalseの場合)では、サーバーはモデルが生成全体を完了するまで待機し、その後にレスポンスボディ全体を一度に返す必要があります。その数分間、クライアントの読み取りタイムアウトとの競争になり、生成が長いほど結果が到着する前に切断される可能性が高くなります。また、例外は空であることが多く(httpx.ReadErrorのstr(e)が空欄)、原因を確認できません。 - 接続が切断されても課金されるため、無計画なリトライは二重課金になります。 サーバーが出力を生成した時点で、結果が届かなかった場合でも呼び出しは課金されます。すでにボディの一部を受信した後にリトライすると、モデルは再度実行され、再び料金が発生します。
ストリーミングを使用し、非ストリーミングは使用しない
ストリーミング(stream: true)では、最初のバイトが数秒以内に到着し、その後は数十秒ごとにデータイベントが到着します。読み取りタイムアウトは、何分も実行される生成全体ではなく、イベント間の間隔だけをカバーすればよいため、ストリーミングは長い出力を確実に配信できます。
2つのプロトコルには異なる終端記号があります。混同しないでください。
アダプティブthinkingが有効な場合、Claude ネイティブは最初に
type: "thinking" ブロック(その増分は thinking_delta)を出力し、その後に text ブロックを出力します。レンダリング時は、thinking_delta と text_delta を別々にルーティングし、thinkingを本文テキストに連結しないでください。
最小限のClaude ネイティブ /v1/messages ストリーミング例(プレーンなhttpx、行ごとのSSE解析):
読み取りタイムアウトは合計時間ではなく、イベント間の間隔に合わせて設定する
多くの開発者は、生成全体をカバーするために読み取りタイムアウトを非常に大きな値(例えば1800秒)に設定しても、依然としてタイムアウトに見舞われます。非ストリーミングの場合、その値は生成プロセス全体と競合することになり、わずかな遅延でも中断してしまうためです。正しいアプローチは、ストリーミングを採用し、イベント間の間隔に合わせた読み取りタイムアウトを設定することです。 実測の参考値(約1.5万文字の入力から約2万文字のエピソードのアウトラインをclaude-opus-5で生成した場合):
したがって、90〜120秒の読み取りタイムアウトを設定すれば、余裕を持って最大のイベント間隔をカバーできます。何分もの長大な値を設定する必要はありません。3段階のタイムアウト設定により、各フェーズを分割してそれぞれ適切な値を設定します:
各クライアントでの設定方法
多くのクライアントライブラリには、読み取りごとのタイムアウトと合計タイムアウトの両方が用意されており、類似した名前が付けられていることがよくあります。長い出力の場合は前者を使用し、後者は未設定にするか無制限にしておきます:Streams も停止することがある:途中で接続の受信が止まる現象
ストリーミングは「送信前にすべてをバッファリングする」という問題を解決しますが、警戒すべきもう1つの障害モードが存在します。それはstream の途中で停止する接続です。- 症状:最初は通常どおりテキストが届きますが、その後何も届かなくなります。それにもかかわらず、接続はエラーにも切断にもなりません。クライアントは自身のタイムアウトまでハングします。タイムアウトが1時間の場合、丸々1時間待たされた挙句、最初のわずかな断片しか得られません。
- 原因:リージョン間や国境をまたぐネットワーク経路において、長時間維持されている接続が中間ネットワーク機器でスタックすることがあります。サーバー側ではとっくに生成を完了して接続を切断しているにもかかわらず、その切断通知がクライアントに届きません。接続時間が長くなるほど、この現象が発生する可能性が高くなります(3分の接続よりも20分の接続の方が頻繁に発生します)。
- 課金:サーバー側で生成が完了し、通常どおり課金されます(「最初に知っておくべき3つのこと」のポイント3を参照)。
- 読み取りタイムアウトをイベント間隔に合わせて設定する(前セクション参照)。読み取りタイムアウトを120秒に設定していれば、停止が発生してもリトライまでに2分しかかかりませんが、1時間の読み取りタイムアウトでは丸々1時間かかります。これが最も重要です。
- 各接続の時間を短くする:非常に長い出力は複数の呼び出しに分割してください(下記の「非常に長い出力を複数の呼び出しに分割する」を参照)。
- 中断された stream を停止した位置から再開する — 下記の「リトライ戦略」を参照してください。
max_tokens に余裕を持たせ、stop_reason を確認する
長い出力はmax_tokens の上限に達して切り詰められやすくなります。これは特に Claude のような thinking を有効化したモデルで顕著です — thinking 自体が max_tokens の予算を消費します。長い文章では、その予算を使い切ることがあります。
max_tokensは 64000 から開始してください(高い effort / 深い thinking ではさらに多く指定してください。claude-opus-5は 128K 出力に対応しています)。- レスポンスを使用する前に
stop_reasonを確認してください:end_turn— 正常に完了しており、テキストは完全です。これが唯一の成功です。max_tokens— 切り詰められており、テキストが不完全、または空の場合もあります。これは切り詰めであり、「空の結果」ではありません —max_tokensを増やして再試行してください。refusal— 安全ポリシーにより拒否されました。別途処理してください。
str(e) だけ、または「テキストが空」であることだけで成功を判断するのは誤解を招きます — 空の本文は通常、max_tokens による切り詰めです。
非常に長い出力を複数の呼び出しに分割する
1回の呼び出しで5万文字を要求し、20分間実行し続ける構成は、ストリーミングを利用していても最も不安定なパターンです。途中でわずかなネットワークの不調が発生しただけでも全体が無効になり、かつ全体に対して課金されます。出力が2万〜3万文字を超える場合は、次のように分割してください。- まずアウトライン / エピソード一覧 / 章リストを生成します(短時間の呼び出し)。
- 次に、各出力を1万〜2万文字未満に抑えつつ、1回の呼び出しにつき1つの章またはエピソードを生成します。各パートの一貫性を保つため、アウトラインと必要なコンテキスト(キャラクター設定や直前のパートの結末など)を含めます。
- アウトラインが確定すれば、各パートは独立しているため並列実行が可能です。
Retry strategy
Be conservative with retries on long output, so “retry on failure” does not become “double billing plus a second long run”:- Retry only on failures before the response headers arrive, and on
5xx/429(with backoff, at most twice). Those are connection or transient issues where a retry makes sense. - If a stream drops after you already received part of the body, resume from where it stopped instead of starting over. The server has generated and billed it; restarting runs it again and pays again. Keep the text you received, include it in a new call as what has been written so far, and let the model continue — you pay only for the remainder.
- Treat a read timeout (a stalled connection) the same way: if you already have a good amount of text, resume; if you received almost nothing, retry the whole call.
- Log the request id from the response headers for reconciliation and troubleshooting.
シナリオ別チートシート
1回で3万文字を超える場合は、章やエピソードごとに分割してください(「非常に長い出力を複数の呼び出しに分割する」を参照)。常に stream を使用してください。
api.apiyi.com(またはバックアップの b.apiyi.com)を使用し、api-cf.apiyi.com は使用しないでください(CDNノードは約100秒で 524 を返し、長時間のリクエストを維持できません)。
関連
APIのタイムアウトを回避する方法
シナリオ別のタイムアウト値
ストリーミングと非ストリーミングの比較
トレードオフと選択方法
Claudeの推論とエフォート
適応型推論、エフォート層、max_tokens、切り詰め
Claudeのレスポンス処理
ネイティブレスポンス形式、SSEイベント、stop_reason