2026-11-02、Anthropic が Messages API で Message Threads のベータ提供を始めました。長い会話やエージェントループで、毎ターン会話全体を送り直さなくてよくなる機能です。
直接 Anthropic を呼ぶなら、公式 SDK のヘルパーを使うだけで済みます。ただ、間に API ゲートウェイやプロキシを挟んでいる場合は注意が必要です。ドキュメントを読み、手元のゲートウェイのソースを確認したところ、エラーを一切出さずに会話の文脈だけが消える経路がありました。以下は公式ドキュメント(2026-11-03 確認)とオープンソースのコードに基づいてまとめたものです。
Message Threads の仕組み
ベータヘッダーは message-threads-2026-08-12 です。
| ステップ | 送るもの |
|---|---|
| 作成 | 会話全体 + system + tools + "thread": {"type": "create"}
|
| 継続 | 直前の応答以降の新しいメッセージだけ + "thread": {"type": "continue", "previous_message_id": "<直前の id>"}。system と tools は送らない |
| 再送 | 404 thread_not_found が返ったら、手元の会話全体で作成し直す |
thread は最後のリクエストから 5 分で期限切れになり、遅くとも 1 時間で削除されます。どの応答も「このターンを保存した」とは教えてくれないので、会話の正本は常にアプリ側に持ち、再送の経路を最初から実装しておく必要があります。
もう 1 つ押さえておきたいのが「固定フィールド」です。model、thinking、tool_choice、トップレベルの cache_control、そして anthropic-beta の値の組み合わせなどは、thread の間ずっと作成時と同じ値で送る必要があります。変えると 400 thread_fingerprint_mismatch になります。
ゲートウェイがふだんやっていることが、ことごとく裏目に出る
ゲートウェイは負荷分散、フェイルオーバー、課金のために色々な処理をしています。それが thread の制約とぶつかります。
| ゲートウェイの処理 | 起きること |
|---|---|
| 複数の上流キーや workspace への負荷分散 | continue リクエストが別のキーに届くと 404。ドキュメントは「別の organization、workspace、ユーザー、サービスアカウント」からのリクエストを 404 の原因に挙げている |
| ボディを固定の構造体に読み込んで再シリアライズ |
thread フィールドが黙って捨てられる(後述) |
| ベータ値の書き換え・追加、モデルのマッピングや自動ダウングレード | 固定フィールドが変わり 400 thread_fingerprint_mismatch
|
| エラー時に別のキーでリトライ | 1 つの応答 id から継続できるのは 1 回だけなので thread_already_continued / thread_continue_in_flight
|
| 上流エラーを独自形式に書き換え |
error.details.error_code が消え、SDK が再送の判断をできない(存在しないモデルも 404 を返す) |
| OpenAI 互換エンドポイントだけを提供 | Chat Completions には thread を表すフィールドがない |
| ボディのサイズから使用量をローカル推定 | continue リクエストは小さいので推定値が大きく下振れする |
一番怖いのは「何も起きない」パターン
ほとんどのパターンはエラーになるので気づけます。厄介なのは構造体のパターンです。
continue リクエストは、そもそも「新しいメッセージだけを送り、system も tools も送らない」設計です。ゲートウェイが thread を持たない構造体でボディを読むと、このフィールドは転送前に消えます。上流には、最後の 1、2 件のメッセージだけを含む普通のリクエストが届きます。
-
tool_resultで始まる場合(エージェントループではこれが普通です)、対応するtool_useがないので 400 になります。まだましなほうです。 - 普通の
userメッセージで始まる場合、モデルはそのまま回答します。システムプロンプトもツールも、それまでの会話もない状態での回答です。
実例として、オープンソースのゲートウェイ new-api の v1.0.0-rc.46 を読むと、anthropic-beta ヘッダーは結合してそのまま転送されます。一方、ClaudeRequest 構造体には thread フィールドがありません。ヘッダーは届くのにボディのフィールドだけが消えるので、リクエストのパススルーを有効にしていないチャネルでは、まさにこの状態になります。型付きの構造体で Anthropic API を実装しているプロキシは、一度確認しておく価値があります。
クライアント側で気づくための目安は 2 つあります。
- continue のターンで、応答がそれまでの会話を明らかに忘れている。
-
usageの入力規模(input_tokens+cache_read_input_tokens+cache_creation_input_tokens)が、そのターンで送った数件のメッセージと同じくらいしかない。
thread 使用時の usage の数え方はドキュメントに書かれていないので、アラート用の経験則として使ってください。
ゲートウェイ側の最小限の対処
-
threadをそのまま転送する。 上流が対応していないなら、フィールドを捨てずにthread_unsupported_requestのような明示的なコードで 400 を返します。そうすれば SDK はunsupported_request_behavior: continue_unthreadedの設定で通常のリクエストに切り替えられます。 -
previous_message_idで振り分ける。 「応答id→ 上流のキー」を記録し、continue リクエストを同じキーに送ります。TTL は thread の最長保持時間に合わせて 1 時間で十分です。 - continue リクエストを別のキーでリトライしない。 エラーをそのまま返し、クライアントに作成リクエストで再送してもらいます。
-
エラーを書き換えない。 ステータス、
error.type、error.detailsを残します。 - 固定フィールドを変えない。 ベータ値をリクエストごとに足さない、thread の途中でモデルを差し替えない。
- 課金は上流の
usageで行う。
クライアント側では、公式 SDK の thread ヘルパーを使い、unsupported_request_behavior を continue_unthreaded にしておくのが無難です。ストリーミングでは、message_start で id が届いても、message_stop を受け取るまで継続しないでください。
thread はキャッシュではない
「新しいメッセージしか送らないならキャッシュは要らない」と思いがちですが、ドキュメントの説明は逆で、thread はプロンプトをキャッシュしません。
- すべてのリクエストに同じトップレベルの
cache_controlを付けるのが最も簡単です。 -
thread付きのリクエストに置けるブレークポイントは最大 4 つです。作成時にsystemやtoolsに置いたものは、continue のたびにもう一度数えられます。 - Anthropic のキャッシュは、書き込みが入力料金の 1.25 倍(5 分)または 2 倍(1 時間)、読み取りが 0.1 倍です。位置が悪くヒット率が低いと、キャッシュしないより高くつきます。
キャッシュも上流アカウント単位で保持されるので、ゲートウェイの「同じキーに固定する」対処は、thread とキャッシュヒット率の両方に効きます。
使えない場面
Message Batches、トークンカウント、オンデマンドのコンパクション、大半の context editing では使えません。HIPAA readiness や Inference hooks を有効にした組織でも使えず、ZDR の対象外です。対応プラットフォームに Amazon Bedrock は含まれていません。また、SDK のベータ版 tool runner はデフォルトで thread を使うので、自分で有効にした覚えがなくても、リクエストに thread が含まれていることがあります。
開示:私は API ゲートウェイ(byesu)の開発・運用に関わっています。この記事は、自分の関わるゲートウェイで同じ点を確認する過程でまとめたものです。ゲートウェイを使っている方は、どのゲートウェイであっても上の 2 つの目安で一度確かめてみてください。
出典:
フィールド表、エラーコード一覧、FAQ を含む詳細版はこちらにまとめてあり、ベータの仕様が変わったら更新します:
https://docs.byesu.com/ja/articles/claude-message-threads-gateway