AI
Optimasi Biaya AI Agent: Anggarkan Per Task, Bukan Per Call
Oktober 202611 menit baca

Agent menjalankan loop, dan setiap turn mengirim ulang prompt, tool call, dan hasil tool sebelumnya sebagai input. Bahkan dengan previous_response_id, OpenAI menagih ulang setiap input token sebelumnya dalam chain sebagai input. Karena itu tiket yang butuh delapan turn bisa berbiaya berkali-kali lipat dari satu request.
Setelah run selesai, baca result.context_wrapper.usage, yang menjumlahkan requests, input, output, dan cached token dari setiap model call. Hitung input baru, cached input, dan output dengan tarif model Anda, lalu catat apakah run tersebut selesai. Bagi total pengeluaran semua run dengan jumlah run yang selesai, sehingga run yang gagal tetap terhitung.
Agents SDK melempar MaxTurnsExceeded ketika sebuah run melewati batas max_turns. Anda bisa mengirim error_handlers dengan handler max_turns yang mengembalikan RunErrorHandlerResult, misalnya pesan eskalasi, alih-alih exception. Mengisi include_in_history dengan False menjaga fallback tersebut tidak masuk ke session history.
Tidak secara langsung. Parameter itu membatasi berapa subagent yang aktif bersamaan, dengan default 3, tetapi Responses API tidak menetapkan batas tetap untuk jumlah total subagent maupun kedalaman tree. max_tool_calls juga tidak didukung pada multi-agent, jadi pasang batas pengeluaran di kode Anda sendiri sebelum request dimulai.
Function yang ditandai defer_loading tidak masuk context sampai model mencarinya, sehingga schema tool yang tidak terpakai tidak dikirim di setiap turn. Tool yang dimuat disisipkan di akhir context window, sehingga cached prefix dari request sebelumnya tetap berlaku. Tool search membutuhkan gpt-5.4 atau yang lebih baru di Responses API.

Ringkasan Utama
Optimasi biaya AI agent dimulai dari biaya per task yang selesai, bukan per API call, karena setiap turn agent menagih ulang context yang terus membesar sebagai input. Hitung biaya tiap run dari usage, bagi dengan task selesai, lalu batasi max_turns dan konkurensi subagent, tunda tool yang jarang dipakai, lakukan compaction, dan arahkan langkah sederhana ke model murah.
Bayangkan sebuah helpdesk agent ERP yang menjawab pertanyaan seperti mengapa sebuah invoice belum dibayar. Spreadsheet yang dipakai untuk menyetujuinya hanya menghitung satu request: prompt 6.000 token dan jawaban 300 token, kira-kira Rp322 dengan tarif gpt-5.4. Tagihan bulanan pertama dari penyedia API ternyata sepuluh kali lipat lebih besar, dan tidak ada yang bisa menjelaskan tiket mana penyebabnya.
Selisih itu bukan salah hitung harga. Agent adalah sebuah loop, dan setiap turn mengirim ulang semua yang sudah terjadi sebelumnya. Artikel ini membahas optimasi biaya AI agent untuk loop tersebut menggunakan OpenAI Agents SDK dan Responses API: cara mengukur biaya per task yang selesai, dan lima kontrol dari dokumentasi resmi untuk menekannya, lengkap dengan contoh perhitungan dalam rupiah yang bisa Anda ulang dengan angka sendiri.
Sebuah chat completion punya satu input dan satu output. Satu run agent punya banyak, dan semuanya saling bergantung. Turn kedua berisi prompt turn pertama, tool call-nya, dan hasil tool tersebut. Turn kedelapan berisi ketujuh turn sebelumnya. Panduan conversation state dari OpenAI menyatakannya secara eksplisit untuk versi server-side: walaupun memakai previous_response_id, semua input token sebelumnya dalam chain tetap ditagih lagi sebagai input token. Tidak mengirim history bukan berarti tidak membayarnya.
Jadi unit yang penting adalah task yang selesai: satu tiket terselesaikan, satu rekening koran yang sudah direkonsiliasi, satu purchase order yang sudah di-draft. Itu juga unit yang dipahami bisnis. Seorang manajer keuangan bisa memutuskan apakah Rp2.000 per tiket terselesaikan layak dibayar; tidak ada yang bisa memutuskan apa pun dari harga per sejuta token.
Asumsikan helpdesk agent dengan prefix 6.000 token berisi instruksi dan definisi tool. Setiap turn menambah sekitar 1.500 token output tool dan 300 token output model, dan satu tiket biasanya butuh delapan turn. Harga yang dipakai adalah tarif Standard di halaman pricing OpenAI: gpt-5.4 sebesar 2,50 USD input, 0,25 USD cached input, dan 15 USD output per sejuta token; gpt-5.4-mini sebesar 0,75, 0,075, dan 4,50. Kurs diasumsikan Rp16.500 per USD. Baris dengan cache mengasumsikan setiap turn memakai ulang seluruh input turn sebelumnya sebagai cached prefix, yaitu skenario terbaik, bukan jaminan.
| Skenario | Input token | Output token | Biaya per task |
|---|---|---|---|
| Estimasi spreadsheet: satu request | 6.000 | 300 | Rp322 |
| 8 turn, gpt-5.4, tanpa cache hit | 98.400 | 2.400 | Rp4.653 |
| 8 turn, gpt-5.4, cached prefix | 98.400, di antaranya 79.800 cached | 2.400 | Rp1.690 |
| 8 turn, gpt-5.4-mini, cached prefix | 98.400, di antaranya 79.800 cached | 2.400 | Rp507 |
| 12 turn, mentok di batas, gpt-5.4, cached | 190.800, di antaranya 165.000 cached | 3.600 | Rp2.636 |
Run delapan turn tanpa cache memakan biaya sekitar 14 kali estimasi spreadsheet, dan itu terjadi tanpa bug apa pun. Caching memulihkan sebagian besar selisihnya, itulah sebabnya kontrol-kontrol di bagian berikutnya sebagian besar tentang menjaga prefix tetap stabil. Perhatikan arti baris dua belas turn: run yang berputar sampai batasnya lalu menyerah justru lebih mahal daripada run yang berhasil.
Sekarang hitung untuk satu bulan. Dari 1.000 tiket, misalkan 800 selesai dalam delapan turn dan 200 mentok di batas dua belas turn lalu dieskalasi ke manusia. Total pengeluaran adalah 800 kali Rp1.690 ditambah 200 kali Rp2.636, sekitar Rp1,88 juta. Dibagi 800 task yang selesai, biaya sebenarnya per tiket terselesaikan sekitar Rp2.349, kira-kira 39 persen di atas angka happy path. Angka inilah yang dipakai untuk menyusun anggaran.
Agents SDK mengakumulasi usage dari setiap model call dalam satu run. Setelah Runner.run selesai, result.context_wrapper.usage berisi requests, input_tokens, output_tokens, dan total_tokens, ditambah input_tokens_details.cached_tokens dan output_tokens_details.reasoning_tokens, sementara request_usage_entries memberi rincian per request. Itu sudah cukup untuk menghitung harga sebuah run tanpa perlu menyalin angka dari dashboard penyedia.
from agents import Agent, Runner, RunErrorHandlerInput, RunErrorHandlerResult
# USD per 1M tokens, Standard tier, OpenAI pricing page (October 2026).
PRICES = {
"gpt-5.4": {"input": 2.50, "cached": 0.25, "output": 15.00},
"gpt-5.4-mini": {"input": 0.75, "cached": 0.075, "output": 4.50},
}
IDR_PER_USD = 16_500 # assumption: use your finance team's booking rate
def task_cost_idr(usage, model: str) -> float:
p = PRICES[model]
cached = usage.input_tokens_details.cached_tokens
fresh = usage.input_tokens - cached # cached tokens are NOT free,
usd = (fresh * p["input"] # just cheaper; reasoning tokens
+ cached * p["cached"] # are already inside output_tokens
+ usage.output_tokens * p["output"]) / 1_000_000
return usd * IDR_PER_USD
helpdesk = Agent(
name="ERP helpdesk",
model="gpt-5.4",
instructions="Answer ERP questions with the tools. Never post documents.",
tools=[get_invoice, list_payments, get_customer], # your function tools
)
def run_ticket(ticket_text: str) -> dict:
hit_cap = False
def on_max_turns(_data: RunErrorHandlerInput[None]) -> RunErrorHandlerResult:
nonlocal hit_cap
hit_cap = True
return RunErrorHandlerResult(
final_output="Escalated to a human: the agent ran out of steps.",
include_in_history=False, # keep the fallback out of session storage
)
result = Runner.run_sync(
helpdesk, ticket_text,
max_turns=12, # p95 of observed turns, plus headroom
error_handlers={"max_turns": on_max_turns},
)
usage = result.context_wrapper.usage
return {
"completed": not hit_cap,
"requests": usage.requests, # one per model call in the loop
"cost_idr": task_cost_idr(usage, "gpt-5.4"),
}
# The number to put on a dashboard and in a budget:
# sum(cost_idr over ALL runs) / count(runs where completed)
# A capped run still cost money; dividing by attempts hides it.Ada dua detail penting dalam kode itu. Cached token dikurangkan dari input dan dihitung dengan tarif cached, karena token tersebut lebih murah, bukan gratis. Lalu setiap run mencatat apakah ia selesai, sehingga agregasinya bisa membagi total pengeluaran semua run dengan jumlah run yang selesai saja. Membagi dengan jumlah percobaan diam-diam memberi nilai bagus pada agent yang cepat menyerah.
Simpan satu baris per task: id tiket, flag selesai, requests, field token, dan hasil hitungan rupiah. Dengan beberapa ratus baris Anda sudah bisa membaca distribusi turn secara langsung, melihat tipe tiket mana yang menghasilkan ekor panjang, dan menetapkan batas di bagian berikutnya berdasarkan data, bukan intuisi.
max_turns membatasi berapa banyak iterasi agent loop yang boleh diambil sebuah run, dengan satu turn berarti satu model call. Jika run melewatinya, SDK melempar MaxTurnsExceeded. Mengisi max_turns=None mematikan batas tersebut, dan itu satu-satunya pengaturan yang tidak boleh dibawa ke production oleh agent yang sensitif terhadap biaya.
Exception adalah pengalaman pengguna yang buruk, jadi setiap entry point Runner juga menerima error_handlers, sebuah dict yang dikunci berdasarkan jenis error. Handler max_turns mengembalikan RunErrorHandlerResult dengan final_output pilihan Anda, misalnya pesan eskalasi, dan include_in_history=False menjaga fallback itu tidak masuk ke result history maupun session storage. Pengguna mendapat jawaban yang jelas, tiket berpindah ke manusia, dan pengeluaran berhenti.
Tetapkan max_turns dari distribusi yang terukur, bukan dari angka bulat. Batas di sekitar persentil ke-95 jumlah turn pada task yang selesai, ditambah sedikit ruang, menghentikan loop yang lepas kendali sambil hanya menyentuh task yang memang kecil kemungkinannya selesai. Tinjau ulang setiap kali menambah tool, karena tool baru mengubah jumlah langkah sebuah task.
Batas ini juga sinyal kualitas. Tipe tiket yang sering mentok di batas biasanya menandakan ada tool yang kurang, instruksi yang ambigu, atau task yang memang butuh manusia. Memperbaikinya lebih murah daripada menaikkan batas, karena setiap turn tambahan lebih mahal daripada turn sebelumnya.
Fitur multi-agent di Responses API memungkinkan root agent memanggil subagent dalam satu request. Satu-satunya kontrol lebarnya adalah max_concurrent_subagents, yang membatasi berapa subagent aktif bersamaan di seluruh tree, termasuk anak dan turunan yang lebih dalam tetapi tidak termasuk root. Nilai default-nya 3, yang direkomendasikan panduan tersebut untuk sebagian besar workload. Tiga fakta lain dari panduan yang sama membentuk anggarannya:
from openai import OpenAI
client = OpenAI()
DAILY_BUDGET_IDR = 250_000
spent_today_idr = ledger.sum_today("reconciliation") # your own store
# Multi-agent has no fixed limit on total subagents or tree depth, and
# max_tool_calls is not supported with it. Refuse to START work you cannot afford.
if spent_today_idr >= DAILY_BUDGET_IDR:
raise BudgetExhausted("reconciliation agents paused until tomorrow")
response = client.beta.responses.create(
model="gpt-6.1-sol",
input="Reconcile September bank lines against open AR invoices for PT Contoh. "
"Use at most two subagents: one per bank account.",
multi_agent={
"enabled": True,
"max_concurrent_subagents": 2, # default is 3; this caps width, not total
},
betas=["responses_multi_agent=v1"],
)
ledger.record("reconciliation", response.usage) # price it like any other taskPerlakukan max_concurrent_subagents sebagai kontrol laju, bukan kontrol pengeluaran. Ia menentukan seberapa cepat token dipakai, bukan berapa banyak. Letakkan batas sebenarnya di luar request: batasi cakupan task di input, catat usage setiap response di ledger Anda sendiri, dan tolak memulai pekerjaan multi-agent baru begitu anggaran harian habis.
Dalam praktiknya, multi-agent cocok untuk task yang bagian-bagiannya memang paralel dan terbatas, misalnya satu subagent per rekening bank dalam rekonsiliasi. Untuk pekerjaan yang terbuka, satu agent dengan max_turns, atau agents-as-tools di Agents SDK, di mana setiap tool agent bisa diberi model sendiri yang lebih murah, menjaga anggaran tetap bisa ditegakkan.
Definisi tool adalah bagian dari prompt prefix. ERP agent dengan empat puluh tool membawa semua schema di setiap turn, dan panduan tool search mencatat bahwa definisi yang tidak terpakai tetap memakan context. Tool search, tersedia untuk gpt-5.4 ke atas di Responses API, memungkinkan Anda menandai function dengan defer_loading sehingga tidak masuk context sampai model mencarinya.
erp_namespace = {
"type": "namespace",
"name": "erp",
"description": "ERP tools for invoices, payments, stock and purchasing.",
"tools": [
{ # used by almost every ticket: load it up front
"type": "function",
"name": "get_invoice",
"description": "Fetch an AR invoice by number.",
"parameters": {
"type": "object",
"properties": {"invoice_no": {"type": "string"}},
"required": ["invoice_no"],
"additionalProperties": False,
},
},
{ # needed by a minority of tickets: defer it
"type": "function",
"name": "list_stock_moves",
"description": "List stock moves for an item and warehouse.",
"defer_loading": True,
"parameters": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"warehouse": {"type": "string"},
},
"required": ["sku", "warehouse"],
"additionalProperties": False,
},
},
# ...more deferred tools; the guide suggests under 10 per namespace
],
}
response = client.responses.create(
model="gpt-5.4", # tool_search needs gpt-5.4 or later
input="Why is INV-2026-0912 still unpaid?",
tools=[erp_namespace, {"type": "tool_search"}],
)
# Wrong: appending list_stock_moves to tools mid-task. The tool block sits in
# the prefix, so the change invalidates the cache for everything after it.
# Right: let tool search load it; loaded tools go to the END of the context.Detail biayanya ada pada lokasi tool dimuat. Panduan tersebut menyatakan bahwa semua tool, baik lewat hosted maupun client-executed tool search, dimuat di akhir context window, sehingga cached prefix dari request sebelumnya tetap berlaku. Menambahkan tool ke array tools di tengah task justru sebaliknya: blok tool berada di awal prompt, sehingga cache hilang untuk semua yang ada setelahnya. Panduan itu juga mengingatkan bahwa mengubah set tool yang sudah dimuat akan merusak cache sejak titik itu, jadi muat sekali lalu biarkan.
Kelompokkan tool ke dalam namespace, yang menurut panduan memang menjadi permukaan utama yang dilatih untuk dicari model, dan jaga setiap namespace di bawah sekitar sepuluh function. Biarkan dua atau tiga tool yang dibutuhkan hampir setiap task tetap tidak ditunda, agar jalur umum tidak pernah membayar langkah pencarian.
Task panjang pada akhirnya menghadapi masalah kedua: walaupun cache sempurna, context terus membesar, dan cached token memang murah tetapi tidak gratis. Server-side compaction menangani hal ini. Anda mengirim context_management berisi entri bertipe compaction dengan compact_threshold, dan ketika jumlah token yang dirender melewatinya, server memadatkan context menjadi compaction item terenkripsi yang membawa state sebelumnya dengan token lebih sedikit.
Threshold adalah tuas biaya, bukan sekadar jaring pengaman. Contoh di panduan compaction memakai 200.000 token, padahal pada tiket dua belas turn di atas, input per turn sudah mendekati 26.000 token di akhir. Pilih threshold dari log usage Anda sendiri, di titik ketika input per turn mulai mendominasi biaya sebuah task.
prev_id = None
for step in ticket_steps: # your own loop over tool results
response = client.responses.create(
model="gpt-5.4",
previous_response_id=prev_id, # only the NEW input is sent...
input=[{"role": "user", "content": step}],
# ...but the whole chain is billed as input every turn. Compact
# well before the context window, not at it.
context_management=[{"type": "compaction", "compact_threshold": 60_000}],
)
prev_id = response.id
log_usage(response.usage) # watch input_tokens fall after compactionCompaction mengganti context sebelumnya dengan representasi yang lebih pendek, sehingga request setelah compaction tidak bisa memakai ulang cached prefix lama. Compaction yang terlalu sering menukar context besar dengan cache miss yang berulang. Selain itu compaction item bersifat opaque dan tidak bisa dibaca manusia, jadi catat semua yang dibutuhkan untuk audit, misalnya invoice mana yang sudah dicek, sebelum hilang terpadatkan.
Jika Anda merangkai turn dengan previous_response_id, ingat aturan penagihan dari bagian pertama: seluruh chain ditagih sebagai input di setiap turn. Compaction adalah tuas yang memendekkan chain itu. Tanpanya, chain yang berjalan lama terus tumbuh sampai yang menghentikannya adalah context window, bukan anggaran.
Tidak setiap turn butuh model terkuat. Mengekstrak nomor invoice, memilih tool, atau memformat balasan adalah langkah kecil; mendiagnosis selisih pembayaran tidak. Selisih harga antar tier cukup lebar sehingga routing sama pentingnya dengan caching. Tarif di bawah adalah harga Standard per sejuta token dari halaman pricing OpenAI per Oktober 2026.
| Model | Input, USD per 1 juta | Cached input, USD per 1 juta | Output, USD per 1 juta |
|---|---|---|---|
| gpt-5.4 | 2,50 | 0,25 | 15,00 |
| gpt-5.4-mini | 0,75 | 0,075 | 4,50 |
| gpt-5.4-nano | 0,20 | 0,02 | 1,25 |
Dalam contoh perhitungan, tiket delapan turn yang sama turun dari Rp1.690 menjadi Rp507 dengan gpt-5.4-mini. Apakah mini menyelesaikan porsi tiket yang sama adalah pertanyaan yang harus dijawab oleh log usage dan eval Anda, karena model yang lebih murah tetapi menyelesaikan lebih sedikit task bisa lebih mahal per task yang selesai. Jika digabungkan, anggaran per task terlihat seperti ini:
Aturan yang perlu dibawa sederhana: harga sebuah agent adalah biaya per task yang selesai, dan angka itu sudah termasuk run yang gagal. Ukur itu dulu, dari objek usage dan bukan dari dashboard, maka kontrol-kontrolnya berhenti menjadi tebakan. max_turns membatasi ekor distribusi, batas konkurensi membatasi laju tetapi bukan total, tool search dan compaction menjaga context tetap murah, dan routing memilih model termurah yang masih bisa menyelesaikan pekerjaan.