Claude Media
Anthropic SDK 1.14のstream()に未知のイベントが届く条件

Anthropic SDK 1.14のstream()に未知のイベントが届く条件

Anthropic Python SDK 1.14でmessages.stream()の挙動が変わった。未知のイベントがイテレーターに届く条件と、message_start前のイベントで例外が出なくなった点を差分から特定した。

結論: 条件は「SSEのイベント名」と「message_startの有無」の2段

Anthropic Python SDK v1.14.0(2026年10月14日リリース)で、client.messages.stream() が返すストリームヘルパーは、知らない type のイベントを捨てずにそのまま返すようになりました。同じリリースで、message_start より前にイベントが来ても Unexpected event order で落ちなくなっています。

ただし、未知のイベントなら何でも届くわけではありません。届くのは次の2条件を満たしたものだけです。

  1. SSEの event: 行の名前が、SDKが受け付ける名前の一覧に入っている(新しい type をデータ側に持つ message 名のイベントなど)
  2. データの type が既知のどれにも当たらない(message_start の前でも後でも、そのまま返される)

この記事は、リリースノートの2行と、v1.13.0からv1.14.0までの差分(ストリーム関連のソースとテスト)を突き合わせて、どの層で何が起きるかを書きます。

くらべる

v1.13.0とv1.14.0のヘルパーの違い

変更前

v1.13.0

message_start より前に別のイベントが来ると、RuntimeError で止まります。既知でない type はイベント生成の分岐に当たらず、呼び出し側には届きません。

変更後

v1.14.0

message_start 前のイベントは、集計せずそのまま返されます。既知でない type も、イベント生成の最後の分岐でそのまま返されます。

v1.14.0の2つの修正はリリースノートでこう書かれている

Bug Fixes欄の該当は次の2行です。

  • streaming: pass unknown event types through the message stream helpers
  • streaming: stop raising "Unexpected event order" for an event before message_start

同じリリースにはOpenTelemetryのトレース機能の追加(後述)や、落とされたストリームを閉じる修正も入っています。ストリームの挙動を見るときは、これらが同じバージョンで一緒に入る点を頭に置いてください。

条件1: SSEのイベント名が受け付け一覧に入っていること

ストリームが2層になっていることが、条件を読み解く鍵です。

下の層は、HTTPのSSEを読む Stream クラスです。ここでは event: 行の名前が一覧と一致したものだけをパースして上へ渡します。v1.14.0のソースでは、一覧に message_start や content_block_delta などの基本イベントのほか、message、Managed Agents系(agent.message や session.status_idle など)、workflow_run.* といった名前が並んでいます。ping は読み捨てられ、error は例外に変換されます。

上の層が、messages.stream() で使うヘルパー(MessageStream)です。ヘルパーは下の層が返したイベントを順に受け取り、メッセージを組み立てながらイベントを返します。

今回の修正が触ったのは、上の層だけです。下の層の名前の一覧は、_streaming.py の差分を見る限り、計測用のコードが足されただけで変わっていません。つまり次のことが言えます。

  • event: future_event のように、名前そのものが一覧にないイベントは、ヘルパーに届く前に下の層で読み飛ばされる
  • event: message で、データの type が未知(future_event など)なイベントは、ヘルパーまで届き、v1.14.0からは呼び出し側に返される

SDKのテストも、この前提で書かれています。テスト用のSSEデータには「未知のイベントは名前を message にする。そうすればクライアントがSSEイベント名をフィルターするかどうかにかかわらずヘルパーに届く」という趣旨のコメントがあり、未知イベントはすべて event: message で送られています。

条件2: message_startの前か後かで扱いが分かれる

ヘルパーは、イベントを受けるたびに「ここまでのメッセージ」(スナップショット)を更新しています。v1.14.0の __stream__ は、受け取ったイベントごとに次の順で判断します。

  1. スナップショットがまだ無く、イベントの type が message_start ではない: 集計せず、そのままイベントを返して次へ進む
  2. それ以外: これまでどおりスナップショットを更新し、更新後に対応するイベントを作って返す
  3. イベント生成の最後の分岐(既知のどれにも当たらない type): v1.13.0までは型チェック用の網羅確認があるだけでしたが、v1.14.0ではイベントをそのまま返す

1が「message_start 前のイベント」の修正、3が「未知イベントの素通し」の修正にあたります。

message_startの前に来たイベント

v1.13.0までは、スナップショットがない状態で message_start 以外が来ると、集計関数が Unexpected event order, got <type> before "message_start" という RuntimeError を出していました。v1.14.0では、このケースは集計の前で分岐し、イベントを返すだけになります。

注意したいのは、返されたイベントがスナップショットに反映されない点です。message_start より前のイベントは、get_final_message() で得られるメッセージには入りません。

message_startの後に来た未知のイベント

message_start を受け取った後に未知の type が来た場合は、イベント生成の最後の分岐でそのまま返されます。テストでは、ストリームの途中と最後に future_event を混ぜたレスポンスを使い、次の3点を確かめています。

  • 返されたイベントの type の並びに、future_event が元の位置のまま入る
  • 先頭の未知イベントは to_dict() で元のデータ(type と usage)に戻せる
  • 最終メッセージの本文は Hello there!、stop_reason は end_turn、usage は input_tokens 11と output_tokens 6で、未知イベントが持っていた usage(99/99)は混ざらない

最後の点が実務では重要です。未知イベントが使用量らしきフィールドを持っていても、v1.14.0のヘルパーはそれを最終メッセージの usage に足しません。

自分のコードを守る書き方

素通しになったことで、for event in stream: のループは、これまで見たことのない type を受け取る可能性があります。次のように、未知は無視する分岐を明示しておくと、サーバー側の拡張に巻き込まれません。以下は一般的な書き方の例で、SDKの実出力ではありません。

import anthropic
 
client = anthropic.Anthropic()
 
with client.messages.stream(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Say hello there!"}],
) as stream:
    for event in stream:
        if event.type == "text":
            print(event.text, end="", flush=True)
        elif event.type == "message_stop":
            print()
        elif event.type not in KNOWN_TYPES:
            # 知らない type は中身だけ記録して読み飛ばす
            logger.info("unknown stream event: %s", event.to_dict())
    message = stream.get_final_message()

text_stream で文字だけを受け取る使い方なら、この問題は表に出ません。for event in stream: でイベントを直接見ている場合と、match 文で case _: に例外を投げている場合が対象です。後者は、アップグレード後に新しい type が届いた時点で止まる可能性があります。

未知イベントの中身を残したいときは、上の例のように to_dict() が使えます。KNOWN_TYPES と logger は自前で用意する想定です。

型チェッカーは、返されるイベントが型の宣言上の和集合に含まれないケースを想定しません。SDKは未知イベントを型のキャストで通しているため、event.type を文字列として比較する書き方のほうが安全です。

create(stream=True)との違い

client.messages.create(..., stream=True) は、ヘルパーを通さない素のイベント列を返します。ヘルパーのドキュメントでも、こちらは最終メッセージを組み立てない分メモリー消費が少ないと説明されています。

今回の修正はヘルパー側(同期と非同期、通常版とbeta版の4クラス)に入っています。差分ではbetaの _beta_messages.py にも同じ message_start 前の分岐が入っているので、client.beta.messages.stream() も同じ挙動になります。素のストリームには、集計がないので「message_start 前で落ちる」問題自体がありません。

SSEイベントを自分で読む手順は、SSEイベント種別をSDKなしで生パースする方法にまとめています。

同じリリースで一緒に変わる3点

ストリームの挙動を確認するときに、同じv1.14.0の変更を把握しておくと切り分けが楽になります。

同梱の変更

v1.14.0でストリームに触れる変更

  • OpenTelemetryのトレース

    アプリがグローバルなトレーサープロバイダーを登録していると、アップグレード後はSDKがスパンを作って他のトレースと一緒に送ります。リクエストにはトレースコンテキストのヘッダーも付きます。止めるにはクライアントに open_telemetry=False を渡すか、環境変数 ANTHROPIC_OPEN_TELEMETRY=false を設定します。

  • 落とされたストリームの終了

    close dropped streams without waiting for the collector の修正です。途中で読むのをやめたストリームを、ガベージコレクターを待たずに閉じられるようになります。

  • ストリームの観測

    差分では、ストリームの開始・要素・終了を観測する処理が下の層に入っています。ヘルパーが集計したスナップショットを共有する処理も、同じ差分に含まれます。

トレーサーを使っている環境でバージョンを上げると、ストリーム1本ごとにスパンが増えます。未知イベントの挙動と、トレース量の増加を同じ原因として扱わないようにしてください。

アップグレード後に確かめる手順

手元の環境で挙動を見るなら、バージョンを確認してからテストします。

pip install -U "anthropic>=1.14.0"
python -c "import anthropic; print(anthropic.__version__)"

そのうえで、確認しておきたい点は次のとおりです。

  • ストリームを受けるコードに、未知の type を例外にする分岐がないか
  • message_start 以外が先頭に来たケースを、テストの入力に加えているか
  • get_final_message() を使う処理が、ストリームの途中のイベント数に依存していないか

ツール呼び出しのストリームで入力JSONが壊れて届いたときの対処は、別の問題としてツールストリーミングでinvalid jsonが来たときの直し方に書いています。

まとめ

未知イベントがヘルパーに届く条件は、SSEの名前が受け付け一覧にあることです。届いたイベントは、message_start の前なら集計の外で、後なら元の位置のまま返されます。最終メッセージの usage や本文には混ざりません。

アップグレード前に決めておきたいのは、未知の type をログに残して読み飛ばすのか、止めて気づくのか、の1点です。for event in stream: を直接書いているコードは、この方針を分岐として明示しておくとよいでしょう。

Python SDKの全体像はClaudeのPython SDKで非同期実行とtool_runnerを実装するを、リクエストに未対応項目を足す方法はAnthropic Python SDKのextra_bodyとmodel_extraで未対応項目を扱うを参照してください。

この記事を共有:XはてブLinkedIn