Claude Media
ClaudeのMessage Batches SDKをPython・TypeScript・Ruby・Javaで実装する

ClaudeのMessage Batches SDKをPython・TypeScript・Ruby・Javaで実装する

Message Batches SDKはPOST /v1/messages/batchesを呼ぶ薄いラッパーです。create・retrieve・resultsの書き方を4言語で並べ、メソッド名と型の違いを示します。

Message Batches SDKで書く前の準備

Message Batches SDKのcreateは、POST /v1/messages/batchesにリクエストを送るメソッドです。Message Batches APIは、複数のMessagesリクエストを1つのバッチにまとめて非同期に処理するClaude APIの機能です。SDKを使えば、リクエストの組み立てから結果の受け取りまでを言語ネイティブのオブジェクトで書けます。料金や24時間の処理枠、始め方の詳細はClaude Batch APIの使い方にまとまっています。ここでは、Python・TypeScript・Ruby・Javaの公式SDKで、バッチの作成(create)・状態確認(retrieve)・結果取得(results)をどう書くかを並べます。

4言語ともclient.messages.batches(Rubyも同じ、Javaのみclient.messages().batches())という共通の名前空間の下にAPIが並びます。ただしメソッド名や戻り値の型は言語ごとに細部が違い、Pythonのコードをそのまま他言語に「翻訳」すると動きません。違いが出るのは、状態の比較、結果取得のメソッド名、エラーへのアクセスの深さの3か所です。

SDKのメソッドとHTTPエンドポイントの対応は次のとおりです。

操作HTTPPython / TypeScriptRubyJava
作成HTTPPOST /v1/messages/batchesPython / TypeScriptcreateRubycreateJavacreate
状態確認HTTPGET /v1/messages/batches/{id}Python / TypeScriptretrieveRubyretrieveJavaretrieve
結果取得HTTPresults_urlが指すURLPython / TypeScriptresultsRubyresults_streamingJavaresultsStreaming
キャンセルHTTPPOST /v1/messages/batches/{id}/cancelPython / TypeScriptcancelRubycancelJavacancel
削除HTTPDELETE /v1/messages/batches/{id}Python / TypeScriptdeleteRubydeleteJavadelete

インストールと動作要件は次のとおりです。

言語インストール必要バージョン
Pythonインストールpip install anthropic必要バージョンPython 3.10以上
TypeScriptインストールnpm install @anthropic-ai/sdk必要バージョンNode.js 20 LTS以上
Rubyインストールbundle add anthropic必要バージョンRuby 3.2.0以上
JavaインストールGradle: com.anthropic:anthropic-java:2.73.0必要バージョンJava 8以上
pip install anthropic
npm install @anthropic-ai/sdk
bundle add anthropic

paramsにはVision・ツール利用(Web検索やコード実行などのサーバーツールを含む)・System messages・複数ターンの会話・拡張思考など、通常のMessagesリクエストとほぼ同じ内容を渡せます。バッチで使えない主な指定は3点です。stream: trueはバッチでは使えません(結果が1本のファイルで返るため)。speed(Fast mode)も同期呼び出し向けのオプションなので無効です。max_tokens: 0(プロンプトキャッシュのプリウォーム用途)も、バッチ処理中にキャッシュが失効しうるため対応していません。既存の同期呼び出し用コードからparamsを流用するときは、この3つが残っていないか確認します。含めるとバリデーションエラーで返ってきます。

ステップ1: バッチを作成する(create)

requests配列に、custom_idと通常のMessages APIと同じparamsを持つオブジェクトを並べて渡します。ここではサポートチケットの一次回答を2件まとめて生成する例で統一します。1つのバッチの上限は次のとおりです。

数字

Message Batchesの上限と保管期間

  • リクエスト数

    100,000件

    256MBと、どちらか先に達した方が上限

  • バッチのサイズ

    256MB

    リクエスト数の上限より先に達することがある

  • 処理の期限

    24時間

    過ぎると未処理のリクエストはexpiredになる

  • 結果の保管

    29日

    作成から数える。その後はバッチ自体は見えても結果はダウンロードできない

1回の処理で扱える量と、結果が残る期間です

件数かサイズが上限を超えるなら、requests配列を分割してcreateを複数回呼び出します。

Python — 辞書リテラルをそのままrequestsに渡せます。型チェッカーを効かせたいときは、anthropic.types.messages.batch_create_params.RequestとMessageCreateParamsNonStreamingで包む書き方もあります。

import anthropic
 
client = anthropic.Anthropic()
 
message_batch = client.messages.batches.create(
    requests=[
        {
            "custom_id": "ticket-1042",
            "params": {
                "model": "claude-opus-5-5",
                "max_tokens": 512,
                "messages": [
                    {"role": "user", "content": "次の問い合わせに一次回答の下書きを書いてください: 配送が3日遅れています。"}
                ],
            },
        },
        {
            "custom_id": "ticket-1043",
            "params": {
                "model": "claude-opus-5-5",
                "max_tokens": 512,
                "messages": [
                    {"role": "user", "content": "次の問い合わせに一次回答の下書きを書いてください: 返品方法を教えてください。"}
                ],
            },
        },
    ]
)
print(message_batch.id)

TypeScript — オブジェクトの形はPythonの辞書とほぼ同じですが、paramsの各キーに型が付くため、存在しないフィールド名を書くとエディタの補完・型チェックの段階で気づけます。

import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic();
 
const messageBatch = await client.messages.batches.create({
  requests: [
    {
      custom_id: "ticket-1042",
      params: {
        model: "claude-opus-5-5",
        max_tokens: 512,
        messages: [{ role: "user", content: "次の問い合わせに一次回答の下書きを書いてください: 配送が3日遅れています。" }]
      }
    },
    {
      custom_id: "ticket-1043",
      params: {
        model: "claude-opus-5-5",
        max_tokens: 512,
        messages: [{ role: "user", content: "次の問い合わせに一次回答の下書きを書いてください: 返品方法を教えてください。" }]
      }
    }
  ]
});
 
console.log(messageBatch.id);

Ruby — ハッシュのキーはシンボルで書けるため、Pythonの辞書リテラルに近い感覚で書けます。

client = Anthropic::Client.new
 
batch = client.messages.batches.create(
  requests: [
    {
      custom_id: "ticket-1042",
      params: {
        model: "claude-opus-5-5",
        max_tokens: 512,
        messages: [
          { role: "user", content: "次の問い合わせに一次回答の下書きを書いてください: 配送が3日遅れています。" }
        ]
      }
    },
    {
      custom_id: "ticket-1043",
      params: {
        model: "claude-opus-5-5",
        max_tokens: 512,
        messages: [
          { role: "user", content: "次の問い合わせに一次回答の下書きを書いてください: 返品方法を教えてください。" }
        ]
      }
    }
  ]
)
 
puts batch.id

Java — ビルダーパターンで各フィールドを明示的に組み立てる必要があり、他の3言語よりコード量が増えます。そのぶん、paramsに何を渡しているかがコード上で追いやすくなります。

AnthropicClient client = AnthropicOkHttpClient.fromEnv();
 
BatchCreateParams params = BatchCreateParams.builder()
  .addRequest(
    BatchCreateParams.Request.builder()
      .customId("ticket-1042")
      .params(
        BatchCreateParams.Request.Params.builder()
          .model(Model.CLAUDE_OPUS_5_5)
          .maxTokens(512)
          .addUserMessage("次の問い合わせに一次回答の下書きを書いてください: 配送が3日遅れています。")
          .build()
      )
      .build()
  )
  .addRequest(
    BatchCreateParams.Request.builder()
      .customId("ticket-1043")
      .params(
        BatchCreateParams.Request.Params.builder()
          .model(Model.CLAUDE_OPUS_5_5)
          .maxTokens(512)
          .addUserMessage("次の問い合わせに一次回答の下書きを書いてください: 返品方法を教えてください。")
          .build()
      )
      .build()
  )
  .build();
 
MessageBatch messageBatch = client.messages().batches().create(params);
System.out.println(messageBatch.id());

作成直後のprocessing_statusは必ずin_progressです。ここから完了までポーリングします。

ステップ2: 処理状況をポーリングする(retrieve)

ほとんどのバッチは1時間以内に終わります。retrieve(id)を一定間隔で呼び、processing_statusが終了を表す値になるまで待ちます。この「終了を表す値」の型が言語ごとに違う点が最初の分岐点です。

Python(文字列比較) — 標準ライブラリのtime.sleep()をそのまま使えるので、追加の依存なしにポーリングループを書けます。

import time
 
while True:
    message_batch = client.messages.batches.retrieve(message_batch.id)
    if message_batch.processing_status == "ended":
        break
    time.sleep(60)
print(message_batch)

TypeScript(文字列比較) — PromiseとsetTimeoutを組み合わせて待機を書きます。このwhileループごとasync関数に切り出しておくと、呼び出し側からawait pollBatch(batch.id)のように使えます。

let batch = messageBatch;
while (true) {
  batch = await client.messages.batches.retrieve(batch.id);
  if (batch.processing_status === "ended") {
    break;
  }
  await new Promise((resolve) => setTimeout(resolve, 60_000));
}
console.log(batch);

Ruby(シンボル比較) — loop do ... endはRubyの慣用的な無限ループの書き方で、break ifで抜ける条件を1行で書けます。

loop do
  batch = client.messages.batches.retrieve(batch.id)
  break if batch.processing_status == :ended
 
  sleep 60
end

Java(enum比較) — MessageBatch.ProcessingStatusはenumなので、ENEDEDのような綴り間違いはコンパイル時に検出できます。

import com.anthropic.models.messages.batches.MessageBatch;
 
MessageBatch batch = messageBatch;
while (true) {
    batch = client.messages().batches().retrieve(batch.id());
    if (batch.processingStatus().equals(MessageBatch.ProcessingStatus.ENDED)) {
        break;
    }
    Thread.sleep(60000);
}

PythonとTypeScriptは"ended"という文字列と比較しますが、Rubyは:endedというシンボル、JavaはMessageBatch.ProcessingStatus.ENDEDというenum値です。Javaのenumに.equals("ended")と文字列を渡して比較すると、equalsはObjectを受け取るのでコンパイルは通りますが、常にfalseになります。上のループなら終了せずに回り続けます。

ステップ3: 結果をストリーミングで取得する(results)

処理が終わったバッチには、リクエストごとに次のいずれかの結果が付きます。バッチ全体の件数内訳はrequest_countsで見られます。

結果

4種類の結果と課金

  • succeeded

    メッセージが生成された成功です。

  • errored

    メッセージが作られなかった失敗です。不正なリクエストと内部サーバーエラーが含まれ、課金されません。

  • canceled

    バッチのキャンセルが、モデルへ送られる前に間に合ったリクエストです。課金されません。

  • expired

    24時間の期限までに、モデルへ送られなかったリクエストです。課金されません。

結果は.jsonl形式で、results_urlから取得できます。サイズが大きくなりうるため、一括で落とさずストリーミングで1件ずつ処理するのが基本です。

Python — match文(Python 3.10以降)でtypeごとに分岐できます。

for result in client.messages.batches.results(message_batch.id):
    match result.result.type:
        case "succeeded":
            print(f"成功: {result.custom_id}")
        case "errored":
            print(f"エラー: {result.custom_id}")
        case "expired":
            print(f"期限切れ: {result.custom_id}")

TypeScript — for await...ofで非同期イテレータを1件ずつ処理します。全件を配列にまとめてから処理すると、大きなバッチではメモリを圧迫するため避けます。

for await (const result of await client.messages.batches.results(batch.id)) {
  switch (result.result.type) {
    case "succeeded":
      console.log(`成功: ${result.custom_id}`);
      break;
    case "errored":
      console.log(`エラー: ${result.custom_id}`);
      break;
    case "expired":
      console.log(`期限切れ: ${result.custom_id}`);
      break;
  }
}

Ruby — メソッド名がresults_streamingになり、ブロックを渡して1件ずつ処理します。結果の分岐はtypeのシンボルではなく、結果の型クラスで書きます。

client.messages.batches.results_streaming(batch.id).each do |result|
  case result.result
  when Anthropic::Models::Messages::MessageBatchSucceededResult
    puts "成功: #{result.custom_id}"
  when Anthropic::Models::Messages::MessageBatchErroredResult
    puts "エラー: #{result.custom_id}"
  when Anthropic::Models::Messages::MessageBatchExpiredResult
    puts "期限切れ: #{result.custom_id}"
  end
end

Java — StreamResponseはAutoCloseableを実装しているためtryブロックで使います。閉じ忘れるとコネクションが残ったままになります。

import com.anthropic.core.http.StreamResponse;
import com.anthropic.models.messages.batches.BatchResultsParams;
import com.anthropic.models.messages.batches.MessageBatchIndividualResponse;
 
try (
  StreamResponse<MessageBatchIndividualResponse> stream = client
    .messages()
    .batches()
    .resultsStreaming(
      BatchResultsParams.builder().messageBatchId(batch.id()).build()
    )
) {
  stream.stream().forEach(result -> {
    switch (result.result().type().value()) {
      case SUCCEEDED -> System.out.println("成功: " + result.customId());
      case ERRORED -> System.out.println("エラー: " + result.customId());
      case EXPIRED -> System.out.println("期限切れ: " + result.customId());
    }
  });
}

Python/TypeScriptはbatches.results()という同じメソッド名ですが、Rubyはresults_streaming()、JavaはresultsStreaming()です。移植するときにメソッド名を混同しやすい箇所です。

errored結果の中身も、そのままリトライしていいエラーか、リクエストを直さないと再送しても失敗するエラーかを見分けるために構造を確認する必要があります。ここも言語ごとにアクセスの深さが違います。

言語invalid_request_errorの判定
Pythoninvalid_request_errorの判定result.result.error.error.type == "invalid_request_error"(2段ネスト)
TypeScriptinvalid_request_errorの判定result.result.error.type === "invalid_request_error"(1段)
Rubyinvalid_request_errorの判定result.result.error.type == :invalid_request(1段・シンボルは_errorが付かない)
Javainvalid_request_errorの判定result.result().asErrored().error().error().isInvalidRequestError()(2段ネスト)

判定の結果で、次の動きが分かれます。

くらべる

erroredのあとにやること

直してから再送

invalid_request_error

paramsを直さないと、再送しても失敗し続けます。custom_idで元のリクエストを引き当て、直した内容を次のバッチに入れます。

そのまま再送

それ以外

サーバー側の問題として、同じ内容のまま再送できます。

Rubyだけシンボルの綴りが_error抜きになっています。

expiredになった分は、custom_idを控えておき、同じ内容で新しいバッチを作り直して再送できます。succeeded・errored・expiredのどれになったかをcustom_idごとに記録しておくと、再送が必要な分だけを次のバッチにまとめられます。

結果の順序と保管期間、キャンセルの扱い

結果は、リクエストした順に並んで返るとは限りません。2件目の結果が1件目より先に返ることがあるため、配列のインデックスではなくcustom_idを突き合わせのキーにします。この点は4言語とも同じです。

結果の取得期限は、上の表のとおり作成から29日です。バッチはWorkspace単位で見えるため、同じWorkspaceのAPIキーなら別のプログラムが作ったバッチも一覧に出ます。処理が終わったバッチはDELETE /v1/messages/batches/{batch_id}で削除でき、処理中のバッチは先にキャンセルする必要があります。

バッチをキャンセルしたいときは、Python・TypeScript・Rubyはbatches.cancel(id)、Javaはbatches().cancel(id)で、この操作では言語差分は出ません。キャンセル直後のprocessing_statusはcancelingになり、同じポーリングでendedまで待ちます。キャンセルされたバッチも最終的にはendedになり、キャンセル前に処理が済んだリクエストの結果は残ります。

まとめ — 移植で見る箇所は状態の比較と結果の取得

Rubyでは:endedとの比較、results_streaming()、:invalid_requestのシンボルが書き換え先です。JavaならProcessingStatus.ENDEDとの比較、resultsStreaming()、result().type().value()の分岐に変わります。残りはPythonの例を読み替えられます。バッチの料金体系・24時間の処理枠・拒否レスポンスの扱いなど運用面はClaude Batch APIの使い方、キャッシュヒット率を上げる工夫はバッチ処理でプロンプトキャッシュのヒット率を上げる方法にまとめています。

Python固有の非同期実装やtool_runnerの組み方はClaudeのPython SDKで非同期実行とtool_runnerを実装する、TypeScriptのStreamingやMCPヘルパーはClaudeのTypeScript SDKで実装するStreaming・Tool・MCPヘルパー、Rubyのツール実行ループとページネーションはClaudeのRuby SDKでツール実行ループとページネーションを実装するで扱っています。

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