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条件を満たしたものだけです。
- SSEの
event:行の名前が、SDKが受け付ける名前の一覧に入っている(新しいtypeをデータ側に持つmessage名のイベントなど) - データの
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 helpersstreaming: 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__ は、受け取ったイベントごとに次の順で判断します。
- スナップショットがまだ無く、イベントの
typeがmessage_startではない: 集計せず、そのままイベントを返して次へ進む - それ以外: これまでどおりスナップショットを更新し、更新後に対応するイベントを作って返す
- イベント生成の最後の分岐(既知のどれにも当たらない
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_tokens11とoutput_tokens6で、未知イベントが持っていた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で未対応項目を扱うを参照してください。