yoi/tickets/llm-worker-transient-retry.md

71 lines
3.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# llm-worker: HTTP transient リトライ
## 背景
`crates/llm-worker/src/llm_client/transport.rs` はリトライを持たず、
upstream が一時的に不調だったときのエラーがそのまま `WorkerError`
伝播して Run が中断する。実セッションでも以下が観測されている:
- セッション `019de419-6f8b-71a0-9e56-f0b9a6c7098b.jsonl:85`
`API error (status: 503): upstream connect error ... Connection refused`
これは `transport.rs:194-216``response.status().is_success()` 前に
返される pre-stream の経路。リクエストはまだ消費されていない。
- Anthropic の `overloaded_error` (529) や Codex backend の 503 も
同経路で観測される transient な事象。
これらは「ヘッダが返る前の段階」で出るため、SSE を読み始めて
出力トークンを発生させる前であり、素朴な再送でべき等に復旧できる典型ケース。
ストリームが途中で切れた場合のリカバリは別の話(→ `llm-worker-stream-continuation`)。
## 方針
`transport.rs` の HTTP 送信層に transient エラー向けの再送を追加する。
SSE 読み出し開始後 (`response.bytes_stream()` 以降) のエラーは対象外で、
従来どおり `ClientError::Sse` として上に流す。
schemeOpenAI / Anthropic / Responses 等)に依存しない共通処理として、
すべての client から同じ振る舞いで使える形にする。
## 要件
### リトライ対象
- HTTP ステータス: 408 / 425 / 429 / 500 / 502 / 503 / 504 / 529
- `reqwest::Error::is_connect()` / `is_timeout()` 由来の送信失敗
- それ以外の `ClientError::Api { status }` および `ClientError::Json`
ストリーム開始後の `ClientError::Sse` は対象外
判定は `is_retryable(&ClientError) -> bool``error.rs` に置いて一箇所に集約する。
### バックオフ
- フルジッタ付き指数base/cap は実装時に妥当な値で固定。後で manifest 化したくなったら別ticket
- `Retry-After` ヘッダがあれば指数バックオフを上書きしてその時間待つ
- 上限: 最大試行回数 + 累積タイムアウトの両方を持つ
### ログ
- リトライ発火ごとに `warn!`ステータス、attempt 番号、次の wait
### 既存挙動の温存
- ストリーム途中で切れた場合の挙動には手を入れない
`transport.rs:231` の `ClientError::Sse` 経路はそのまま)
- 成功時のレイテンシに観測可能なオーバヘッドを足さない
## 完了条件
- `is_retryable` のテーブル駆動 unit test
- 503 / 529 / connect refused をモックした unit test が、
規定回数までリトライして「最終的に成功」「上限到達でエラー」の両ケースを通る
- `Retry-After: 5` を返すモックでは指数を上書きして 5s 待っている
(仮想時間で検証)
- mid-stream で `ClientError::Sse` を起こすモックでリトライが発火しない
- `cargo check` / `cargo test``llm-worker` で通る
## 範囲外
- mid-streamSSE 読み中)の継続再開 → `llm-worker-stream-continuation`
- プロバイダ別の細かい retry policy共通既定で十分
- リトライ上限値の manifest からの上書き必要になったら別ticket