Laravel12 OpenAIのAPIをBatchコールする方法

Laravel12 OpenAIのAPIをBatchモードでコールする方法

Batchモードとは

通常のAPIコールとの最大の違いはリアルタイムではなくリクエストを「予約」して後でまとめて結果を受け取る点にあります。

メリット

通常のAPI料金の50%オフで利用可能です。

デメリット

非同期処理になるためリアルタイムコールに比べかなり実装がややこしくなります。

実装の全体像

0.前準備
1.リクエストの配列を作成
2.リクエストファイルのアップロード
3.バッチジョブの作成
4.バッチジョブステータスの確認
5.結果の取得

0.前準備

  • envにOpenAIのAPIキーを設定
  • バッチジョブ管理用のテーブルをcreate
    以下のようなテーブルを作成
Schema::create('openai_batches', function (Blueprint $table) {
    $table->id();
    $table->string('batch_id')->unique(); // OpenAIから返されるID
    $table->string('status');              // 実行状況
    $table->json('result')->nullable();    // 完了後のレスポンス
    $table->timestamps();
});

1.リクエストの配列を作成

Batchモードでは、複数のリクエストを1つの .jsonl ファイル(1行1リクエストの形式)にまとめてアップロードします。

        $apiKey = env('OPENAI_API_KEY');  // OpenAIのAPIキーをセット
        $prompt = $request->input('prompt');

        // リクエスト作成
        $requests = [
            [
                "custom_id" => "req-" . uniqid(),
                "method" => "POST",
                "url" => "/v1/chat/completions",
                "body" => [
                    "model" => "gpt-5-nano",  // モデルを指定
                    "messages" => [
                        ["role" => "user", "content" => $prompt]
                    ],
                ]
            ]
        ];

        // JSONL形式の文字列を作成
        $jsonl = "";
        foreach ($requests as $req) {
            $jsonl .= json_encode($req) . "\n";
        }

2.リクエストファイルのアップロード

作成したJSONL形式のファイルをアップロード。
purpose を ‘batch’ にするのがポイントです。

        $uploadResponse = Http::withToken($apiKey)
            ->attach('file', $jsonl, 'batch.jsonl')
            ->post('https://api.openai.com/v1/files', ['purpose' => 'batch']);

        if (!$uploadResponse->successful()) {
            return back()->with('error', 'Upload failed: ' . $uploadResponse->body());
        }

        $fileId = $uploadResponse->json('id');    // アップロードしたファイルのファイルIDを取得

3.バッチジョブの作成

ファイルIDを指定して、バッチの実行を指示します。

        $batchResponse = Http::withToken($apiKey)
            ->post('https://api.openai.com/v1/batches', [
                'input_file_id' => $fileId,
                'endpoint' => '/v1/chat/completions',
                'completion_window' => '24h',
            ]);

        if (!$batchResponse->successful()) {
            return back()->with('error', 'Batch creation failed: ' . $batchResponse->body());
        }

        $batchData = $batchResponse->json();

バッチジョブ情報をデータベースに保存

        OpenAIBatch::create([
            'batch_id' => $batchData['id'],
            'status' => $batchData['status'],
        ]);

4.バッチジョブステータスの確認

バッチは即時完了しないため、cronなどで定期的にステータスを監視し”completed“になるのを待って結果を取得します。

        $batchModel = OpenAIBatch::findOrFail($id);
        $apiKey = env('OPENAI_API_KEY');

        // ステータス確認
        $response = Http::withToken($apiKey)
            ->get("https://api.openai.com/v1/batches/{$batchModel->batch_id}");

        if (!$response->successful()) {
            return back()->with('error', 'Status check failed: ' . $response->body());
        }

        $data = $response->json();
        $batchModel->status = $data['status'];

$data[‘status’]が”completed“になっていたら結果を取得しDBに格納します。

        if ($data['status'] === 'completed' && isset($data['output_file_id'])) {
            $batchModel->output_file_id = $data['output_file_id'];
            
            // 結果の取得
            $fileResponse = Http::withToken($apiKey)
                ->get("https://api.openai.com/v1/files/{$data['output_file_id']}/content");
            
            if ($fileResponse->successful()) {
                $lines = explode("\n", trim($fileResponse->body()));
                $results = array_map(fn($line) => json_decode($line, true), $lines);
                $batchModel->result = $results;
            }
        }

        // 結果をDBに格納
        $batchModel->save();

ステータスは以下が存在します。

validating入力ファイルの検証中
failed入力ファイルは検証プロセスに失敗しました
in_progress実行中
finalizing結果を準備中
completed完了
expired24時間以内に完了できなかった
cancellingキャンセル中
cancelledキャンセル済み

感想

リアルタイム性が必要ないものはバッチで十分だと思いました。
コスト半額というのは大きい!しかし、実装がかなりややこしい。

返信を残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です