本文へ移動
Signalcraft
English

← Signalcraft一覧へ戻る

LLMストリーミングのtimeoutとretryを単位から調べる

LLMストリーミングのtimeoutとretryを単位から調べる

OpenCodeのようなエージェントからローカルvLLMや遅い推論APIを呼ぶと、最初のchunkまで長く待つことがあります。

ここで、ストリーミングをtokenごとの独立したリクエストだと考えると、timeoutとretryの意味を誤ります。

実際には、1つのHTTPリクエストが接続を開き、レスポンスボディを複数のchunkで受け取ります。

ストリームの通信単位を確認する

LLMのストリーミングは、概ね次の流れです。

client
  │ POST /v1/chat/completions
  ▼
API server
  │ response headers
  │ data: token chunk
  │ data: token chunk
  │ data: [DONE]
  ▼
client

受信したchunkは、同じHTTP接続を流れるレスポンスの一部です。

途中で接続が切れても、サーバーへ「N tokenの続き」を要求できるとは限りません。

再開位置、生成状態、リクエストの同一性を扱うプロトコルを、サーバーとクライアントの両方が実装している必要があります。

その仕組みがない場合、クライアントは元のリクエストを最初から再実行するか、失敗として扱います。

timeoutを全体とchunk間隔へ分ける

OpenCodeの現行設定資料には、リクエスト全体のtimeoutと、chunk間隔を監視するchunkTimeoutがあります。

公式資料ではtimeoutの既定値を300,000ミリ秒とし、falseで無効化できると説明しています。

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "local-vllm": {
      "options": {
        "timeout": 600000,
        "chunkTimeout": 120000
      }
    }
  }
}

2つの値を同じにする必要はありません。

初回chunkが遅いモデルでは、全体のtimeoutだけでなく、初回chunkまでのchunkTimeoutも十分に長くします。

OpenCodeのSSE処理は、現行ソースでは各readをchunkTimeoutで監視します。

そのため、最初のchunkを待つreadも対象になります。

例のchunkTimeoutが120秒なら、全体のtimeoutを600秒にしても、初回chunkが120秒を超えた時点で中断されます。

生成中に一時停止するモデルでは、chunk間隔を許容する値が必要です。

ただし、timeoutを大きくすると通信障害の検出も遅くなります。

chunkTimeoutを小さくすると、正常な計算中でも接続を切ります。

設定値は大きければよいのではなく、初回chunkまでの時間と生成中のchunk間隔の最大値を測って決めます。

通信再試行と意味的再試行を分ける

接続断の再試行可否は、失敗した時点で変わります。

レスポンスを1度も観測していない場合は、サーバーが処理を始めたか判断できません。

途中までレスポンスを受け取った場合も、サーバーが生成を終えたのか最後のchunkだけ失ったのか分かりません。

同じリクエストを再送すると、処理が二重になる可能性があります。

テキスト生成だけなら、主な影響は計算時間と料金です。

tool callや外部APIへの書き込みを含む処理では、副作用も二重になります。

最低限、次の2種類を分けます。

  • transport retry: 接続前または応答観測前の一時的な失敗を同じ入力で再送します。
  • semantic retry: 応答後にJSON形式や内容が不正だったため、指示を変えて再生成します。

OpenCodeのLLMパッケージ設計資料では、一時的な転送エラーやrate limitを再試行候補として扱います。

一方、観測可能な出力やtoolの副作用が発生した後は、暗黙に再試行しない方針を説明しています。

この挙動はハーネス、バージョン、provider SDKで変わり得ます。

利用中の設定とログで、実際のretry境界を確認します。

ローカルvLLMを直接呼んで層を分ける

OpenCodeからローカルvLLMを呼ぶ場合は、最初にAPIサーバーへ直接リクエストを送ります。

curl http://localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "MODEL_ID",
    "messages": [{"role": "user", "content": "短い応答を返してください。"}],
    "stream": true,
    "max_tokens": 32
  }'

直接のストリームが安定するなら、次にOpenCodeのprovider設定と起動ログを確認します。

問題は次の4つへ分けます。

  1. 初回chunkまでが遅い
  2. 生成中のchunk間隔が長い
  3. 一定時間後に接続自体が切れる
  4. 応答後のJSON検証やtool実行が失敗する

1と2はtimeout調整の候補です。

3はproxy、HTTP client、サーバー、ネットワークのどこが切断したかを調べます。

4はストリーミングではなく、出力検証とsemantic retryの問題です。

retryを識別子で追跡する

同じ論理処理を再実行するクライアントでは、少なくとも3つのIDをログへ残します。

run_id=20260819-001
operation_id=12
transport_attempt=2
semantic_attempt=1

これで、接続断による再送と、内容不正による再生成を同じretryとして混同しません。

外部APIへ書き込む場合は、サーバー側でoperation_idを冪等性キーとして扱う設計も検討します。

ログへaccess key、token、プロンプトに含まれる個人情報を無条件に書きません。

設定値を測定から決める

最初から極端に大きなtimeoutを設定せず、実際の応答を測ります。

  1. 短い非ストリーミングリクエストを送る
  2. streamingを有効にして初回chunkまでを測る
  3. 生成中の最大chunk間隔を測る
  4. 全体処理時間へ余裕を足してtimeoutを決める
  5. 最大chunk間隔へ余裕を足してchunkTimeoutを決める
  6. connection drop、429、5xx、出力形式エラーを別々に確認する
  7. retry回数と境界をログで確認する

切断を減らすには、timeoutだけでなく入力長、最大出力長、並列数も調整します。

ハーネス設定はサーバーの計算を速くしません。

正常に計算している時間と接続停止を区別し、失敗後に再実行する単位を決める境界です。

参考資料