Anthropic Python SDKのextra_bodyとmodel_extraで未対応項目を扱う
SDKの型にまだ無いパラメータを送るextra_body、返ってきた未知の項目を読むmodel_extra、nullと欠落を分けるmodel_fields_setの使い方と注意点です。
AnthropicのPython SDKは、文書化されたAPIに型を合わせて作られています。型にまだ無いパラメータを送りたいとき、レスポンスに見慣れない項目が混ざったときは、SDKを捨てずに3つの口から対応できます。
| やりたいこと | 使うもの |
|---|---|
| 文書化されていないエンドポイントを呼ぶ | 使うものclient.get / client.post など |
| 型に無いパラメータ・クエリ・ヘッダーを足す | 使うものextra_body / extra_query / extra_headers |
| 型に無いレスポンス項目を読む | 使うものresponse.unknown_prop / response.model_extra |
あわせて、nullが返ったのか項目自体が無いのかをmodel_fields_setで見分ける方法も使えます。非同期実行やtool_runnerはClaudeのPython SDKで非同期実行とtool_runnerを実装するで扱っています。ここでは型の外側に出るときの書き方と、事故を避ける線引きに絞ります。
型の外に出る前に: SDKが想定している変化
SDKの型は文書化されたAPIが基準です。一方でAPIのバージョン方針には、既存の入力・出力パラメータは保つ一方で、任意の入力の追加や出力への値の追加は行う場合があると書かれています。つまり、サーバーが新しい項目を返し始めても、SDK側の型更新が追いつくまでは「型に無い項目」として届く場面があります。
この前提があるので、レスポンスに知らない項目が混ざるのは異常ではありません。model_extraはその受け皿です。
ベータ機能は別の経路を先に確認します。ベータはclient.betaとbetas引数で有効にでき、SDKがanthropic-betaヘッダーを付けてくれます。
response = client.beta.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
betas=["context-management-2025-06-27"],
)ベータ名をextra_headersで手書きする前に、betasに書けないかを見ます。型に載っている経路を使えば、extra_系の注意点を負わずに済みます。
文書化されていないエンドポイントを呼ぶ
SDKにメソッドが無いエンドポイントは、client.postのようなHTTP動詞のメソッドで呼べます。クライアントに設定した再試行などのオプションは、この呼び出しにも効きます。
import httpx2
response = client.post(
"/foo",
cast_to=httpx2.Response,
body={"my_param": True},
)
print(response.json())/fooとmy_paramは説明用のダミーです。cast_to=httpx2.Responseを渡すと、型変換されない生のレスポンスが返り、.json()で本文を読めます。SDKが提供するPydanticモデルへの変換は働かないため、返り値の検証は自分で書くことになります。
型に無いパラメータを足す: extra_body・extra_query・extra_headers
メソッドは存在するのに、引数の型に無い項目を送りたいときは、リクエストオプションのextra_body、extra_query、extra_headersを使います。次は書き方の一例です(my_new_optionは説明用のダミー名です)。
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
extra_body={"my_new_option": True},
extra_query={"debug": "1"},
extra_headers={"x-my-trace": "abc123"},
)これらは送信を足すだけです。受け取る側のAPIがその項目を理解するかどうかは、APIの仕様次第です。
同名の引数を上書きする
SDKの解説には、extra_系の引数は同じ名前の文書化済みパラメータを上書きするという注意があります。あわせて、信頼できる入力にだけ使うよう求めています。
たとえばextra_body={"max_tokens": 100000}のように書くと、max_tokens=1024と書いたはずの値が置き換わります。ユーザー入力をそのままextra_bodyに流すと、呼び出し側が決めたはずのモデルや上限まで、入力者に書き換えられます。
Claude Codeにこの種のコードを書かせるときは、次の2点を先に決めておくと安全です。
extra_bodyに渡せるキーを固定の許可リストで絞る- ユーザー入力は
messagesの中身にだけ入れ、extra_系には渡さない
ALLOWED_EXTRA_KEYS = {"my_new_option"}
def safe_extra_body(extra: dict) -> dict:
unknown = set(extra) - ALLOWED_EXTRA_KEYS
if unknown:
raise ValueError(f"許可されていないキー: {sorted(unknown)}")
return extraプロジェクトのCLAUDE.mdには「extra_body / extra_query / extra_headersには外部入力を渡さない。キーはALLOWED_EXTRA_KEYSで固定する」と1行書いておけば、後から別のコードを足させるときにも守られます。
ヘッダーの上書きで型が合わなくなる場合がある
anthropic-versionは、SDKが2023-06-01で自動送信します。クライアントのdefault_headersか、リクエストごとのextra_headersで上書きできますが、SDKの解説は、ヘッダーの上書きが誤った型やその他の未定義の挙動につながる場合があると警告しています。
client.messages.with_raw_response.create(
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
model="claude-opus-5-5",
extra_headers={"anthropic-version": "My-Custom-Value"},
)この例は上書きができることを示すためのもので、本番で使う値ではありません。バージョンを変える理由が特にないなら、既定のままにします。
型に無いレスポンス項目を読む
レスポンスはPydanticモデルです。モデルに定義されていない項目は、属性としてそのまま読めます。
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(message.unknown_prop) # 型に無い項目を直接読む(例の名前はダミー)
print(message.model_extra) # 型に無い項目をまとめて dict で取るmodel_extraは、未知の項目を辞書で返します。新しい項目が増えたかを調べるときは、これを1回ログに出すのが手早い方法です。
if message.model_extra:
logger.info("未知のレスポンス項目: %s", list(message.model_extra))値ではなくキー名だけを出しておくと、本文由来の内容がログに残りません。SDK側の型が追いついて項目が正式な属性になれば、この出力から消えます。
全体をまとめて扱いたいときは、to_dict()とto_json()が使えます。どちらも辞書やJSON文字列に直す補助メソッドです。
nullと欠落を区別する: model_fields_set
型のある項目がNoneに見えても、サーバーがnullを返した場合と、項目自体を返さなかった場合があります。model_fields_setは、レスポンスに実際に含まれていた項目名の集合で、この2つを分けられます。
if response.my_field is None:
if "my_field" not in response.model_fields_set:
print("field was not in the response")
else:
print("field was null")my_fieldは説明用のダミーです。「値が無い」を1つにまとめず、「返ってこなかった」と「明示的に空だった」を分けたい場面で使います。たとえば、項目が増える前のレスポンスと増えた後のレスポンスを同じコードで読むときです。
生のレスポンスが必要なときはwith_raw_response
ヘッダーなどの付随情報を読みたいときは、with_raw_responseを挟みます。request-idヘッダーを取る例です。
response = client.messages.with_raw_response.create(
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
model="claude-opus-5-5",
)
print(response.headers.get("request-id"))
message = response.parse() # create() が返すはずだったオブジェクト非同期クライアントでは返り値がAsyncAPIResponseになり、.parse()や.json()などはawaitが必要です。同期版のコードをそのまま移すと、awaitの付け忘れで動かないので注意します。
本文を最後まで読まずに順次処理したいときはwith_streaming_responseを使い、コンテキストマネージャーで包みます。どちらもextra_headersなどのオプションと組み合わせられます。
送った中身をログとヘッダーで確かめる
extra_bodyで足した項目が本当に送られたかは、SDKのログで確認できます。SDKは標準ライブラリのloggingを使い、環境変数ANTHROPIC_LOGをdebugかinfoにすると出力されます。
export ANTHROPIC_LOG=debug
python probe.pydebugには送信内容が含まれる場合があるため、共有ログやCIの出力に流す前に、APIキーや本文が混ざっていないかを見ます。
OpenTelemetryのHTTPXClientInstrumentor、Sentryのhttpx連携、respx、pytest-httpxのようにhttpxを直接パッチする道具は、既定ではSDKのリクエストを見ません。SDKが使うHTTPクライアントはhttpx2(httpxのAPI互換フォーク)だからです。これらでリクエストを捕まえたいときは、起動時にhttpx2.alias_httpx()を1回呼びます。httpxをimportするより前に呼ぶ必要があり、プロセス全体でimport httpxがhttpx2に解決されるようになります。
HTTPクライアントを自前のものに差し替える場合は、DefaultHttpxClient(非同期ならDefaultAsyncHttpxClient)を使います。素のhttpx2.Clientだと、SDKの既定のタイムアウトや接続数の上限が引き継がれません。http_clientにはhttpx2のクライアントしか渡せず、別パッケージのhttpxのクライアントを渡すとTypeErrorになります。1回の呼び出しだけ差し替えるならclient.with_options(http_client=...)で済みます。プロキシが原因の接続失敗はプロキシ環境変数が反映されない問題で扱っています。
確認用のスクリプトでは、with Anthropic() as client:の形で書くと、終了時にHTTP接続も閉じられます。
ヘッダーの足し方は2通りあります。全リクエストに付けたいなら、クライアントのdefault_headersです。1回の呼び出しだけならextra_headersを使います。トレース用のヘッダーのように毎回付けるものは前者、実験中の1回だけ付けるものは後者、という分け方が扱いやすくなります。
項目が受け付けられなかったときの切り分け
型に無い項目を送ると、APIが受け付けない場合があります。そのときSDKは、4xxや5xxの応答に対してAPIErrorのサブクラスを送出します。
- 400は
BadRequestError、422はUnprocessableEntityError、429はRateLimitError、500以上はInternalServerErrorです - 接続できなかったときは
APIConnectionErrorで、原因はe.__cause__に入ります - 上記以外のステータスは
APIStatusErrorで受け、e.status_codeとe.responseを表示できます
どのステータスで返るかは、送った項目と受け取るAPI次第です。決め打ちせず、probe.pyで実際に出たコードを見て分岐を書きます。
失敗した呼び出しを後から追うときは、_request_idが役に立ちます。成功したレスポンスのオブジェクトには、request-idヘッダーから_request_idが入ります。_で始まりますが、この属性だけは公開されています。
print(message._request_id) # 例: req_018EeWyXxfu5pfWkrYcMdjWG再試行にも気をつけます。接続エラー、408、409、429、500以上は、既定で2回まで短い指数バックオフを置いて自動で再試行されます。extra_bodyの付いた同じリクエストが繰り返し送られるので、ログに同じ内容が数回並んでも別々の呼び出しとは限りません。
実装前に手元で確かめる流れ
Claude Codeに実装を頼むなら、新しい項目をいきなり本番コードへ入れず、まず確認用のスクリプトを1本書かせると手戻りが減ります。
python probe.pyprobe.pyに入れるのは、次の3つです。
- 想定する呼び出しを1回実行する
model_extraのキーとmodel_fields_setを出力する- 例外が出たら、ステータスとレスポンス本文を表示する
出力を読んで、項目が実際に返っているか、名前が想定と合っているかを確かめてから、本実装に進みます。
つまずきやすい点
extra_bodyに書いたキーが、型付きの引数と同名だと上書きされる。外部入力を渡さないclient.postの返り値はPydanticモデルではない。cast_toで指定した型になるanthropic-versionの上書きは、SDKの型と実際の応答がずれる原因になることがありますmodel_extraに出た項目は、SDKの更新で正式な属性になることがある。属性名をコードに固定する場合は、更新時に見直す- バッチ処理や構造化出力のように型が整った機能は、先にMessage Batches SDKの実装の型付きの経路で足りないかを見る
まとめ
未対応の項目は、送る側はextra_系、読む側はmodel_extraで扱えます。ただしextra_系は文書化済みの引数を上書きするため、外部入力は通さず、キーは許可リストで固定するのが最低限の守りです。型に載った経路(betasなど)があるなら、それを先に使います。ネットワーク側の設定が絡む場合は、プロキシ環境変数が反映されない問題も合わせて見てください。