← Kembali ke Blog

Batch API: Inferensi Massal Setengah Harga di OpenAI dan Anthropic

Job klasifikasi nightly nggak seharusnya makan enam jam. Seringnya iya, karena ada yang nulis loop dengan 40 worker, kena 429 di menit ketiga, nambahin sleep, terus dianggap beres. Modelnya bukan bottleneck. Pola request-nya yang salah.

Batch API ngebenerin pola itu. Kamu serahin satu file request, provider jalanin di kapasitas yang nggak rebutan sama traffic interaktif, dan kamu dapet file hasil dalam 24 jam. OpenAI dan Anthropic dua-duanya nagih setengah harga sinkron buat ini, jadi ini tuas termurah yang paling sering dilewatin.

Kapan batch cocok

Yang cocok:

  • Klasifikasi atau tagging backlog tiket, review, atau dokumen
  • Backfill embedding setelah ganti strategi chunking
  • Jalanin eval suite ke ribuan kasus
  • Moderasi tumpukan konten
  • Bikin data sintetis buat training

Yang nggak cocok: apa pun yang ditunggu user, apa pun yang streaming, apa pun yang di-retry dalam hitungan detik. Deadline 24 jam itu bagian dari produknya, bukan batasan yang harus diakalin.

OpenAI juga misahin kuota batch dari kuota sinkron, jadi job gede nggak bakal makan jatah yang dibutuhin produk kamu jam 9 pagi.

Yang perlu kamu siapin

  • Python 3.10+ dengan openai dan anthropic terpasang
  • API key provider yang kamu pakai
  • Dataset dengan primary key stabil per baris

Poin terakhir kelihatan sepele, padahal bukan. Seluruh alur ini bergantung pada pencocokan respons balik ke input, dan ID dari database kamu lebih aman daripada posisi array.

Langkah 1: bikin JSONL dengan ID kamu sendiri

OpenAI pakai JSONL: satu request per baris, dengan custom_id yang kamu tentuin.

import json

MODEL = "gpt-6-luna"  # satu model per file, cuma satu

with open("batch_input.jsonl", "w") as f:
    for row in load_tickets():
        f.write(json.dumps({
            "custom_id": f"ticket-{row['id']}",
            "method": "POST",
            "url": "/v1/chat/completions",
            "body": {
                "model": MODEL,
                "messages": [
                    {"role": "system", "content": "Klasifikasikan tiket ini. Balas JSON saja."},
                    {"role": "user", "content": row["text"][:6000]},
                ],
                "response_format": {"type": "json_object"},
            },
        }) + "\n")

Ada tiga keputusan penting di situ. custom_id pakai ID tiket kamu, bukan counter, jadi hasilnya tetap kecocokan walau urutannya berubah. Teks dipotong di 6000 karakter, karena satu outlier 400 ribu token bakal nolak barisnya dan kamu lebih baik tahu itu di kode daripada di laporan validasi. Dan response_format diset per baris, karena tiap baris request independen dengan opsi body yang sama kayak endpoint sinkron.

Langkah 2: upload dan bikin batch

from openai import OpenAI

client = OpenAI()

with open("batch_input.jsonl", "rb") as fh:
    uploaded = client.files.create(file=fh, purpose="batch")

batch = client.batches.create(
    input_file_id=uploaded.id,
    endpoint="/v1/chat/completions",
    completion_window="24h",
)
print(batch.id, batch.status)

/v1/responses juga jalan kalau kamu lebih suka API itu. Endpoint yang diterima batch daftarnya tetap: responses, chat completions, completions, embeddings, moderations, images, dan videos.

Batas per batch 50.000 request dan 200 MB input. Batch embedding juga dibatasi 50.000 input di semua request. Kamu bisa bikin 2.000 batch per jam, jadi job 500 ribu baris jadi sepuluh file, bukan satu.

Langkah 3: polling tanpa nyiksa API

import time

while True:
    b = client.batches.retrieve(batch.id)
    counts = b.request_counts
    print(b.status, counts.completed, "/", counts.total, "failed:", counts.failed)
    if b.status in {"completed", "failed", "expired", "cancelled"}:
        break
    time.sleep(60)

Status yang bakal kamu lihat: validating, in_progress, finalizing, completed, expired, failed, cancelling, cancelled. Batch yang kelewat jendela masuk expired, dan request yang sudah selesai tetap ada di file output.

Jeda 60 detik sudah cukup. Jendelanya 24 jam. Polling tiap detik nggak nambah apa-apa dan ngubah satu job jadi ribuan panggilan management.

Langkah 4: cocokin hasil, retry cuma yang gagal

raw = client.files.content(batch.output_file_id).text
rows = {}

for line in raw.splitlines():
    item = json.loads(line)
    body = item["response"]["body"]
    rows[item["custom_id"]] = body["choices"][0]["message"]["content"]

Baris yang sukses bawa body respons lengkap. Baris yang gagal nggak muncul di situ sama sekali; mereka masuk file error, alamatnya error_file_id:

if batch.error_file_id:
    errors = client.files.content(batch.error_file_id).text
    for line in errors.splitlines():
        item = json.loads(line)
        print(item["custom_id"], item["error"]["code"])

Request yang expired balik dengan batch_expired, dan karena custom_id itu ID tiket kamu, bikin file retry jadi kerjaan join, bukan arkeologi. Request yang selesai tetap ditagih walau batch-nya expired, jadi ngabaikan file error artinya bayar dua kali buat baris yang sama.

Langkah 5: job yang sama di Anthropic, bentuknya beda

Anthropic nggak minta file. Kamu kirim list request langsung di panggilan create, maksimal 100.000 request atau 256 MB, mana yang kena dulu.

import anthropic

ac = anthropic.Anthropic()

batch = ac.messages.batches.create(requests=[
    {
        "custom_id": f"ticket-{row['id']}",
        "params": {
            "model": "claude-sonnet-5-5",
            "max_tokens": 300,
            "messages": [{"role": "user", "content": row["text"][:6000]}],
        },
    }
    for row in load_tickets()
])
print(batch.id, batch.processing_status)

Hasilnya keluar sebagai JSONL lewat ac.messages.batches.results(batch.id), dan tiap entri bilang apa yang terjadi:

for entry in ac.messages.batches.results(batch.id):
    if entry.result.type == "succeeded":
        text = entry.result.message.content[0].text
        rows[entry.custom_id] = text
    else:
        failed[entry.custom_id] = entry.result.type

result.type isinya succeeded, errored, canceled, atau expired, yang lebih jelas daripada ngurai envelope respons. Kebanyakan batch selesai di bawah satu jam, dan hasilnya bisa diunduh sampai 29 hari, jadi job yang crash masih bisa diselamatkan tanpa submit ulang.

Dua batasan yang perlu masuk desain: stream: true ditolak, dan max_tokens minimal 1. Tool use, vision, dan extended thinking semuanya jalan di dalam batch, jadi satu run massal bisa lebih dari sekadar klasifikasi.

Langkah 6: hitungan duitnya, pakai angka nyata

Ambil 500.000 tiket dengan rata-rata 800 token input dan 150 token output. Itu 400 juta token input dan 75 juta token output per run.

Claude Sonnet 5.5 dihargai $2 per juta token input dan $10 per juta output di API standar, dan setengahnya di Batches API: $1 dan $5.

  • Standar: 400 x $2 + 75 x $10 = $1.550
  • Batch: 400 x $1 + 75 x $5 = $775

Model sama, prompt sama, hemat $775 per run. Kalau jalan tiap malam, itu bedanya antara pos anggaran yang dipertanyakan dan yang diabaikan.

Batasan yang perlu dicatat

  • Satu model per file input OpenAI. Campur model berarti pisah file.
  • custom_id harus unik. Duplikat bikin baris hilang diam-diam di dict kamu.
  • Hasil batch nggak abadi. Anthropic simpan 29 hari; OpenAI kasih file ID, bukan janji. Tulis ke database di hari batch selesai.
  • Anthropic memperingatkan batch bisa sedikit lewat spend limit workspace karena pemrosesan paralel. Jangan jadwalin batch 100.000 request di jam budget kamu reset lalu berharap ada rem mendadak.
  • Endpoint management punya rate limit sendiri. Polling ketat di lima puluh batch itu insiden tersendiri.

Kalau validasi gagal, bentuknya begini

Penolakan paling sering bukan karena prompt jelek, tapi karena file-nya salah:

  • Baris kosong atau JSON yang di-pretty-print jadi beberapa baris. Satu objek per baris, tanpa pengecualian.
  • URL di luar daftar endpoint yang diterima.
  • custom_id nggak ada atau dobel.
  • Body yang memang bakal ditolak endpoint target, misalnya stream: true.

Batch-nya nyangkut di validating, lalu pindah ke failed dengan file error yang nunjuk baris mana. Print sepuluh ribu karakter pertama file itu sebelum submit ulang; error-nya biasanya berulang.

Kapan jangan pakai ini

Kalau ada manusia yang nunggu, batch itu alat yang salah, segede apa pun diskonnya. Buat traffic interaktif, prompt caching dan model lebih kecil ngasih penghematan lebih besar daripada batch.

Kalau kamu jalanin GPU sendiri, langkah setaranya offline inference di vLLM ke queue yang kamu kendalikan. Prinsipnya sama: pisahin kerjaan massal dari kerjaan yang sensitif latency, jangan dicampur.

Dan kalau job-nya sekali jalan dengan 200 baris, loop sinkron aja. Waktu engineering-nya lebih mahal daripada diskonnya.

Mulai dari seribu baris

Jalanin batch 1.000 baris sampai selesai sebelum nembak dataset penuh. Cek tiga hal: semua custom_id balik, JSON-nya bisa diparse, dan file error kosong. Benerin join-nya selagi dampaknya masih kecil.

Kegagalan yang perlu dihindari bukan batch yang failed. Tapi batch 500.000 baris yang selesai jam 3 pagi dengan 4.000 ID nggak kecocokan, file error nggak pernah dibaca, dan nggak ada cara tahu customer mana yang kelewat.

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis