◉CLAUDE LABEN
●THREADS — Messages API に message threads がベータで登場しました(11/2)。会話をサーバーに保持し、以降は新しいメッセージだけを送ります●API — Messages API の最大リクエストサイズが 32MB から 128MB に拡大されました(10/28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り26日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST — Opus 4.8 の fast mode は非推奨になり、11/30 に除去されます。移行先は Opus 5.5●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●THREADS — Messages API に message threads がベータで登場しました(11/2)。会話をサーバーに保持し、以降は新しいメッセージだけを送ります●API — Messages API の最大リクエストサイズが 32MB から 128MB に拡大されました(10/28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り26日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST — Opus 4.8 の fast mode は非推奨になり、11/30 に除去されます。移行先は Opus 5.5●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/API & SDK
⬡ API & SDK/2026-11-05中級

message threads に移す前に、会話の正本を手元に残す形へ書き換えた3か所

11 月 2 日にベータで登場した message threads へ、毎ターン全履歴を送る実装を移すときの書き換え点を整理しました。create と continue の使い分け、404 や 400 が返ったときの切り分け、移さない判断の線引きまで、公式ドキュメントに沿って書いています。

Claude API143message threadsMessages APIベータ移行16

長い会話のループを書いていると、ログに並ぶ送信量が気になる夜があります。ターンが進むたびに、同じ履歴を一からすべて送り直しているからです。11 月 2 日のリリースノートに message threads の一行を見つけたとき、私はドキュメントを三往復して、ようやく仕組みを飲み込みました。

この記事は、その読み込みの記録です。毎ターン全履歴を送る実装を、threads に移すときに書き換えた点と、あえて書き換えなかった点を残します。

先に断っておくこと

内容は公式ドキュメントの記述に基づいています。threads はベータ(ベータヘッダーは message-threads-2026-08-12)で、私自身は本番の会話ループでまだ使っておりません。

手元で動かしたのは、記事の後半に載せる再送ロジックだけです。本物の API ではなく、404 や 400 を返す偽のクライアントを相手に、期待どおりの順で create と continue が呼ばれることを確かめました。応答の実際の速さや、本番でのエラーの出方は、まだ確かめられていません。

何が変わるのか

これまでは、毎ターン messages に会話の全部を入れて送っていました。threads では、最初の 1 回だけ全体を送り、次からは新しいメッセージと、直前の応答の id だけを送ります。

  • create: 全会話と system、tools を送り、thread に {"type": "create"} を付けます。
  • continue: 新しいメッセージだけを送り、thread に {"type": "continue", "previous_message_id": "..."} を付けます。
  • replay: スレッドは最後のリクエストから 5 分で期限が切れ、遅くとも 1 時間以内に削除されます。続きが送れないと、continue は 404 を返します。

注意したいのは、スレッドが「ベストエフォート」だという点です。応答のどれが保存されたかは、その場では分かりません。次の continue で初めて分かります。

書き換えた 3 か所

書き換えた場所は、結局三つでした。

  1. 会話の正本を自分で持つ。API が覚えていてくれるから、と履歴を捨てると、404 のときに復元できません。
  2. 送るフィールドを三つに仕分ける。create だけで送るもの、毎回同じ値で送るもの、毎回変えてよいものです。
  3. 404 や 400 を、やり直してよいものと、直すべきものに分ける。

仕分けの表は次のとおりです。ドキュメントの内容を、私の言葉で並べ直しました。

分類フィールドルール
create のみsystem、toolscontinue で送ると(null でも)400 になります
固定model、tool_choice、thinking、mcp_servers、context_management、output_config、トップレベルの cache_control、ベータヘッダーの組毎回 create と同じ値。違うと thread_fingerprint_mismatch の 400 になります
自由max_tokens、stop_sequences、stream、metadata、service_tierターンごとに変えてかまいません

固定フィールドは、コードの 1 か所に置いて、create と continue の両方がそこから読む形にしました。値を二箇所に書くと、いつか片方だけ直して 400 を踏むからです。

再送を入れたラッパー

ここからがコードです。何を解決するかを先に書くと、「continue が 404 などで拒まれたとき、手元の正本から create で送り直す」ことです。

import anthropic
 
BETA = ["message-threads-2026-08-12"]
REPLAY_CODES = {"thread_not_found", "thread_already_continued", "thread_continue_in_flight"}
 
 
def error_code(exc):
    """SDK 例外の body から error_code を取り出す。body の形は版で違うので両方を見る。"""
    body = getattr(exc, "body", None)
    if not isinstance(body, dict):
        return None
    err = body.get("error", body)
    return (err.get("details") or {}).get("error_code")
 
 
class ThreadChat:
    def __init__(self, client, *, model, system, tools=None, max_tokens=1024):
        self.client = client
        # 固定フィールドは 1 か所に置き、create と continue で同じ値を送る
        self.fixed = dict(model=model, betas=BETA, cache_control={"type": "ephemeral"})
        self.max_tokens = max_tokens
        self.system = system
        self.tools = tools
        self.history = []       # 会話の正本。API 側の thread とは別に、自分で必ず持つ
        self.prev_id = None     # None のときは次のターンを create で送る
        self.replays = 0        # 何回やり直したか。あとで数えるための印
 
    def _create(self, messages):
        kw = dict(self.fixed, max_tokens=self.max_tokens, system=self.system,
                  messages=messages, thread={"type": "create"})
        if self.tools:
            kw["tools"] = self.tools
        return self.client.beta.messages.create(**kw)
 
    def _continue(self, new_messages):
        # system と tools は渡さない(渡すと 400)
        return self.client.beta.messages.create(
            **self.fixed, max_tokens=self.max_tokens, messages=new_messages,
            thread={"type": "continue", "previous_message_id": self.prev_id})
 
    def send(self, text):
        new = {"role": "user", "content": text}
        if self.prev_id is None:
            resp = self._create(self.history + [new])
        else:
            try:
                resp = self._continue([new])
            except (anthropic.NotFoundError, anthropic.BadRequestError) as exc:
                if error_code(exc) not in REPLAY_CODES:
                    raise           # fingerprint 不一致などは、やり直しで隠さず直す
                self.replays += 1
                resp = self._create(self.history + [new])
        if resp.stop_reason == "refusal":
            self.prev_id = None     # 拒否のあとは続けない。この 1 ターンは正本にも入れない
            return resp
        self.history += [new, {"role": "assistant", "content": resp.content}]
        # フォールバックモデルが書いた応答は API が保存しないので、次は create に戻す
        iters = getattr(getattr(resp, "usage", None), "iterations", None) or []
        fell_back = any(getattr(i, "type", None) == "fallback_message" for i in iters)
        self.prev_id = None if fell_back else resp.id
        return resp

なぜこう書くのか、三点だけ添えます。

まず、thread_fingerprint_mismatch は REPLAY_CODES に入れていません。固定フィールドの食い違いは、やり直せば通ってしまうかもしれませんが、それは食い違いを隠すだけです。自分のコードのどこかで値が揺れている合図なので、例外のまま上へ投げます。

次に、replays を数えています。やり直しは安全ですが、毎回起きているなら、threads にした意味が薄れています。あとで一日分を数えるための印です。

最後に、usage.iterations の fallback_message を見る部分は、ドキュメントの記述(「usage.iterations に fallback_message があれば、フォールバックモデルが書いた応答」)から組みました。属性の形は手元の SDK の版で一度 print して、確かめてから使っていただければと思います。

偽のクライアントで確かめたこと

先ほどのコードは、404 や 400 を返す偽のクライアントに対して、次の四点が期待どおりになることを確かめました。

  1. 1 ターン目は create で、system を含めて送られる。
  2. 2 ターン目が thread_not_found の 404 を受けると、同じ入力を create で、手元の正本 3 件(過去の往復と新しい発言)を付けて送り直す。
  3. 送り直しのあとは、新しい id から continue に戻る。
  4. thread_fingerprint_mismatch はそのまま例外になる。拒否(refusal)のあとは prev_id が空になり、次のターンが create になる。

これは本物の API の振る舞いの検証ではありません。「ドキュメントを読んで私が理解した順番どおりに、コードが分岐するか」を確かめた、という範囲です。

エラーが返ったときの切り分け

エラーは、モデルが動く前に返り、トークンは使われず、スレッドの期限も変わりません。ドキュメントの表を、やることの順に並べ直しました。

ステータスerror_code最初にやること
404thread_not_found正本から create で送り直す。ただし、存在しないモデル名でも 404 になるので、先に error_code を見る
400thread_fingerprint_mismatchメッセージに出る項目名を見て、固定フィールドを揃える。変えたいなら新しい thread
400thread_already_continued各 id から続けられるのは 1 回だけ。最新の id から続ける。受け取れていなければ送り直す
400thread_continue_in_flight自分の前の試行が走っていることが多い。終わるのを待つか、送り直す
400thread_unsupported_request非対応の機能を外すか、thread なしで全会話を送る

404 の原因は一つに決まりません。期限切れのほか、max_tokens で切れたツール呼び出しの応答のように保存されなかったターン、別のキーや別のワークスペースから送ったリクエストも含まれます。原因を当てにいくより、送り直して、キーとワークスペースを揃えておくほうが早いのだと思います。

移さない判断

threads に移さないほうがよい場面も、ドキュメントにははっきり書かれています。

  • Message Batches と token counting では使えません。
  • compaction(on demand を含む)は使えません。長くなった会話は、要約を自分で書いて、新しい create で始め直します。
  • context editing も、一部を除いて使えません。
  • ZDR と HIPAA の対象外です。組織で HIPAA の設定が有効な場合は、thread_unsupported_request になります。
  • スレッドの一覧・取得・削除の API はありません。

私なら、会話がせいぜい数ターンで終わる用途や、バッチで回す処理は、今のまま全履歴を送る形に残します。移すのは、ターンが十を超えて長引くエージェントのループからです。

次の一手

会話の正本は、いつも自分の手元に置き、thread は速く送るための写しとして借ります。 個人開発の小さなループでも、この線引きにしておけば、404 が出ても慌てずに済みます。

最初の一歩として、全部を書き換えず、長いループを一つだけ選んで ThreadChat のような薄いラッパーで包み、一日ぶんの replays を数えていただければと思います。送り直しがほとんど起きないなら、そこから範囲を広げれば足ります。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⬡ API & SDK2026-10-31
旧 Usage and Cost API から Analytics API に替えたら、日別の表から日が消えました — 空の日は返らない仕様とゼロ埋めの書き方
10/29 にベータとなった Usage and Cost Analytics API は、使用量のない日の行を返しません。旧 API の日別レポートをそのまま置き換えると日が欠ける理由と、切り分けの順番、ゼロ埋めを足した Python の例を、手元で確かめた結果つきで書き残します。
⬡ API & SDK2026-10-27
Python SDK を 2.0.0 に上げて動かなくなる3か所 — モック越しに再現して決めた確認の順番
Python SDK 2.0.0 へ上げると、temperature の引数、認証ヘッダ、環境変数の二重指定の3か所で動作が変わります。手元のモック越しに再現した出力と、プロキシを挟む構成での直し方、確認の順番をお伝えします。
⬡ API & SDK2026-10-25
SDK を上げた途端に temperature が 400 になる前に、呼び出し箇所を洗いました
API バージョン 2026-10-21 では temperature・top_p・top_k を送ると、既定値でも 400 が返ります。SDK の更新で切り替わる仕組みと、先に洗っておく記述を拾う小さなスキャナ、直す順番をまとめました。