API Contract tahan evolusi dibutuhkan ketika aplikasi Anda bergantung pada model AI yang perilakunya sering berubah: nama field berganti, format output meluas, error baru muncul, timeout meningkat, atau mode streaming dan async berubah tanpa banyak peringatan. Jika kontrak integrasi Anda terlalu menempel ke detail vendor, setiap perubahan kecil di provider bisa berubah menjadi incident produksi.

Solusi yang paling tahan lama adalah membungkus provider AI dengan kontrak internal yang sempit, eksplisit, dan kompatibel ke depan. Artikel ini membahas pola desain backend yang bisa langsung diterapkan: capability versioning, schema request/response yang stabil, error model yang konsisten, idempotency untuk submit job, retry aman, webhook completion, penanganan timeout dan partial failure, serta observability untuk mendeteksi drift integrasi lebih awal.

Masalah Utama: Vendor AI Cepat Berubah, Kontrak Anda Tidak Boleh Ikut Rapuh

Dalam praktiknya, kegagalan integrasi AI jarang disebabkan oleh satu bug besar. Yang lebih sering terjadi adalah akumulasi perubahan kecil:

  • provider mengganti nama parameter atau memindahkannya ke nested object,
  • field respons baru ditambahkan dan parser Anda gagal karena terlalu ketat,
  • struktur error berubah dari string ke object,
  • perilaku timeout berbeda antara model sinkron dan job asinkron,
  • provider mulai mengembalikan hasil parsial atau warning,
  • webhook terkirim lebih dari sekali atau datang sebelum polling selesai,
  • fitur tertentu hanya tersedia pada model tertentu meski endpoint-nya sama.

Karena itu, target desain bukan hanya “API yang bekerja hari ini”, tetapi API contract yang bisa menyerap perubahan vendor tanpa memaksa perubahan besar pada klien internal.

Prinsip Desain Kontrak yang Tahan Evolusi

1. Gunakan kontrak internal sebagai lapisan anti-korosi

Jangan ekspos payload vendor langsung ke service lain di sistem Anda. Buat provider adapter yang menerjemahkan request/response vendor ke kontrak internal yang Anda kontrol. Dengan begitu, perubahan vendor berhenti di satu tempat.

Prinsip ini sangat penting jika Anda ingin bisa:

  • mengganti provider tanpa mengubah semua consumer,
  • menjalankan fallback ke model lain,
  • menambahkan observability yang konsisten,
  • mengendalikan validasi dan error semantics sendiri.

2. Bedakan versi kontrak dari versi model

Kesalahan umum adalah menyamakan versi API Anda dengan versi model provider. Padahal keduanya bergerak dengan ritme berbeda. Kontrak Anda sebaiknya memiliki versi sendiri, misalnya v1, sementara model provider ditangani sebagai detail implementasi atau capability.

Contoh buruk:

  • klien harus tahu nama model vendor tertentu,
  • perubahan model memaksa perubahan endpoint publik,
  • fitur diikat ke satu provider, bukan ke kemampuan.

Contoh lebih baik:

  • klien meminta capability seperti structured_output, tool_calling, atau async_jobs,
  • router internal memilih provider/model yang sesuai,
  • jika provider berubah, kontrak ke klien tetap sama.

3. Kompatibilitas ke depan lebih penting daripada rigid parser

Untuk respons dari provider, parser internal Anda harus toleran terhadap field tambahan. Untuk kontrak publik Anda sendiri, klien sebaiknya hanya diwajibkan memproses field yang didokumentasikan sebagai stabil. Menambahkan field baru umumnya non-breaking; mengganti tipe atau menghapus field biasanya breaking.

Capability Versioning: Versi Berdasarkan Kemampuan, Bukan Merek Model

Jika integrasi Anda harus mendukung open-weights maupun vendor API, desain request berbasis capability lebih tahan lama dibanding hardcode parameter vendor.

Contoh request internal

{
  "contract_version": "v1",
  "request_id": "req_01JABCXYZ",
  "mode": "async",
  "capabilities": {
    "response_format": "json_schema",
    "max_latency_ms": 15000,
    "supports_webhook": true,
    "priority": "standard"
  },
  "input": {
    "system_instruction": "Ekstrak entitas dari dokumen.",
    "user_content": "PT Maju Jaya menandatangani kontrak pada 12 Mei 2026.",
    "attachments": []
  },
  "output_schema": {
    "type": "object",
    "properties": {
      "company_name": { "type": "string" },
      "contract_date": { "type": "string" }
    },
    "required": ["company_name"]
  },
  "webhook": {
    "url": "https://api.example.com/ai/webhooks/completion",
    "secret_ref": "whsec_prod_ai"
  },
  "idempotency_key": "extract-entity-doc-8842-v1"
}

Di sini, klien tidak perlu tahu provider mana yang dipakai. Mereka hanya menyatakan kebutuhan. Adapter internal bertugas memetakan capability tersebut ke parameter vendor yang tersedia.

Kapan capability perlu diberi versi?

Beri versi saat makna capability berubah, bukan hanya saat daftar model berubah. Misalnya:

  • response_format=json_schema awalnya hanya menjamin JSON valid,
  • lalu Anda menambah jaminan validasi terhadap schema penuh,
  • itu perubahan semantik dan layak diberi versi atau capability flag baru.

Contoh pendekatan yang aman:

  • contract_version=v1
  • capabilities.structured_output.level = "basic" | "strict"

Dengan pola ini, Anda tidak perlu membuat versi besar setiap kali vendor menambah model baru.

Desain Schema Request/Response yang Stabil

Request: eksplisit, minimal, dan dapat divalidasi

Request contract yang baik memiliki ciri:

  • field wajib sedikit dan jelas,
  • field opsional punya default semantics yang terdokumentasi,
  • tipe data tidak ambigu,
  • nilai enum dibatasi,
  • metadata vendor dipisahkan dari field inti.

Jika Anda butuh escape hatch untuk fitur vendor tertentu, jangan mencampurnya ke field inti. Simpan dalam namespace terisolasi seperti provider_hints. Dengan begitu, klien tahu bahwa field itu best effort, bukan bagian inti dari kontrak stabil.

{
  "contract_version": "v1",
  "mode": "sync",
  "input": {
    "user_content": "Ringkas teks berikut..."
  },
  "provider_hints": {
    "preferred_provider": "provider-a",
    "preferred_model_tier": "fast"
  }
}

Catatan penting: field di bawah provider_hints sebaiknya tidak mengubah makna bisnis utama. Kalau provider mengabaikan hint tersebut, hasil masih harus valid menurut kontrak.

Response: bedakan hasil inti, metadata, dan debug info

Kesalahan umum adalah mencampur hasil model, metrik, warning, dan data operasional dalam satu object datar. Pisahkan dengan jelas:

{
  "job_id": "job_01JABCPQRS",
  "status": "completed",
  "result": {
    "output": {
      "company_name": "PT Maju Jaya",
      "contract_date": "2026-05-12"
    }
  },
  "usage": {
    "input_units": 1200,
    "output_units": 180
  },
  "warnings": [
    {
      "code": "PARTIAL_SCHEMA_COERCION",
      "message": "Sebagian field dinormalisasi ke tipe string."
    }
  ],
  "provider": {
    "name": "provider-a",
    "model": "internal-routing-choice"
  },
  "timestamps": {
    "accepted_at": "2026-07-16T10:00:00Z",
    "completed_at": "2026-07-16T10:00:08Z"
  }
}

Pemisahan ini berguna karena:

  • result menjadi area yang paling stabil bagi consumer,
  • usage dan provider boleh berkembang tanpa mengganggu parser bisnis,
  • warnings memberi sinyal drift tanpa mengubah status menjadi gagal.

Aturan kompatibilitas non-breaking yang praktis

  • Menambah field baru di response: umumnya aman.
  • Menambah enum value baru: aman hanya jika klien siap menghadapi nilai tak dikenal.
  • Mengubah field dari scalar ke object: breaking.
  • Menghapus field existing: breaking.
  • Mengubah default perilaku tanpa flag baru: hampir selalu breaking secara semantik.

Untuk enum, selalu siapkan bucket seperti unknown atau fallback handling. Jangan asumsikan daftar status selalu tertutup selamanya.

Error Model yang Konsisten di Atas Banyak Provider

Provider AI sering memiliki struktur error yang tidak seragam. Ada yang mengembalikan HTTP 429 dengan body sederhana, ada yang memberi kode granular, ada yang membedakan soft limit dan hard limit, dan ada yang mencampur validation error dengan transient infra error.

Kontrak internal perlu menormalkan error ke kategori yang bisa ditindaklanjuti oleh sistem Anda.

Contoh error envelope

{
  "error": {
    "code": "RATE_LIMITED",
    "category": "transient",
    "message": "Provider sementara menolak request karena limit tercapai.",
    "retryable": true,
    "retry_after_ms": 2000,
    "provider_error": {
      "provider": "provider-a",
      "raw_code": "too_many_requests"
    }
  },
  "request_id": "req_01JABCXYZ"
}

Kategori error yang berguna

  • validation: request kontrak Anda salah; jangan retry otomatis.
  • auth: kredensial atau otorisasi bermasalah; butuh intervensi konfigurasi.
  • transient: timeout sementara, 429, upstream unavailable; retry aman jika request idempotent.
  • provider_bug: respons vendor tidak sesuai ekspektasi, parser gagal, atau field wajib hilang.
  • policy: konten ditolak oleh moderation atau aturan provider.
  • quota: limit akun atau budget habis; retry biasanya tidak membantu tanpa perubahan kondisi.

Dengan model ini, komponen queue, workflow engine, atau HTTP client internal dapat mengambil keputusan retry tanpa menebak-nebak dari string error vendor.

Jangan bocorkan detail vendor sebagai kontrak inti

Menyimpan provider_error.raw_code berguna untuk debugging, tetapi jangan jadikan nilai itu sebagai basis logika aplikasi lain. Semua keputusan bisnis sebaiknya berdasarkan code, category, dan retryable dari kontrak internal.

Idempotency untuk Submit Job dan Retry Aman

Pada integrasi AI asinkron, masalah klasik adalah request submit berhasil di provider tetapi timeout di sisi client. Tanpa idempotency, retry bisa membuat dua job identik berjalan dan webhook ganda memicu proses bisnis dua kali.

Kapan idempotency wajib?

  • submit job asinkron,
  • operasi yang memicu biaya,
  • operasi yang mengubah state downstream,
  • retry dilakukan otomatis oleh client, gateway, worker, atau queue.

Pola implementasi

  1. Klien mengirim idempotency_key yang stabil untuk intent yang sama.
  2. Server menyimpan fingerprint request penting bersama status submit.
  3. Jika key yang sama datang lagi dengan payload setara, kembalikan respons submit yang sama.
  4. Jika key sama tetapi payload berbeda, balas konflik agar bug cepat terlihat.
{
  "job_id": "job_01JABCPQRS",
  "status": "accepted",
  "idempotency_key": "extract-entity-doc-8842-v1",
  "accepted_at": "2026-07-16T10:00:00Z"
}

Penting: idempotency bukan sekadar deduplikasi berdasarkan body mentah. Anda perlu mendefinisikan field mana yang menjadi identitas intent. Misalnya, metadata observability mungkin boleh berubah tanpa dianggap request berbeda.

Retry aman: bedakan submit, poll, dan callback handling

  • Submit job: retry hanya jika idempotent.
  • Polling status: umumnya aman di-retry, tetapi perlu backoff.
  • Webhook receiver: harus tahan event duplikat dan out-of-order.

Gunakan exponential backoff with jitter untuk error transient. Hindari retry agresif saat provider sedang rate limited, karena hanya memperparah antrean dan biaya.

Kesalahan umum adalah menganggap HTTP timeout berarti request pasti gagal. Pada provider AI, timeout sering berarti hasil submit tidak diketahui. Di sinilah idempotency key menjadi mekanisme pemulihan utama.

Webhook Completion, Timeout, dan Partial Failure

Webhook completion: desain untuk duplikasi dan urutan yang tidak pasti

Webhook dari provider tidak selalu datang sekali, dan tidak selalu datang setelah status internal Anda siap. Karena itu:

  • verifikasi signature atau shared secret,
  • simpan event_id jika tersedia atau bentuk fingerprint event sendiri,
  • buat handler yang idempotent,
  • jangan mengasumsikan urutan event,
  • ack webhook secepat mungkin lalu proses berat di queue internal.
{
  "event_type": "job.completed",
  "event_id": "evt_01JABCTUVW",
  "job_id": "job_01JABCPQRS",
  "status": "completed",
  "result": {
    "output": {
      "company_name": "PT Maju Jaya"
    }
  },
  "occurred_at": "2026-07-16T10:00:08Z"
}

Jika provider tidak menyediakan event_id, simpan kombinasi seperti job_id + status + checksum(result) sebagai dedupe key best effort.

Timeout: buat status eksplisit, jangan pakai boolean sederhana

Jangan hanya punya status success dan failed. Untuk integrasi AI, Anda hampir selalu butuh state yang lebih kaya:

  • accepted
  • running
  • completed
  • failed
  • timed_out
  • cancelled
  • unknown

Status unknown berguna saat submit timeout dan Anda belum tahu apakah provider menerima job. State ini memungkinkan workflow recovery yang lebih aman, misalnya lewat lookup by idempotency key atau reconciliation worker.

Partial failure: jangan paksa semua warning menjadi hard failure

Model AI sering menghasilkan output yang “cukup berguna” walau tidak sempurna: satu field kosong, schema coercion terjadi, attachment kedua gagal dibaca, atau confidence turun. Jika semua kondisi ini dianggap gagal total, availability aplikasi Anda justru memburuk.

Gunakan pola:

  • status=completed bila hasil utama tersedia,
  • warnings[] untuk degradasi yang masih dapat diterima,
  • failed_parts[] bila operasi terdiri dari beberapa unit input.
{
  "job_id": "job_01JABCPQRS",
  "status": "completed",
  "result": {
    "output": {
      "summary": "Dokumen berisi perjanjian kerja sama."
    },
    "failed_parts": [
      {
        "part_id": "attachment-2",
        "reason_code": "UNREADABLE_ATTACHMENT"
      }
    ]
  },
  "warnings": [
    {
      "code": "PARTIAL_INPUT_PROCESSED",
      "message": "Satu lampiran tidak dapat diproses."
    }
  ]
}

Pola ini penting untuk pipeline dokumen, batch enrichment, atau ekstraksi multi-file.

Fallback Saat Provider Mengganti Field atau Perilaku

Fallback bukan hanya soal ganti provider ketika request gagal. Fallback yang baik juga menangani perubahan halus pada schema atau perilaku vendor.

Pola fallback yang realistis

  • Field aliasing: jika provider memindah field, adapter mencoba membaca nama lama dan baru.
  • Loose parsing di boundary: parser boundary toleran terhadap field tambahan dan perubahan urutan.
  • Semantic fallback: jika structured output gagal, turunkan ke plain text lalu validasi/parse internal.
  • Provider fallback: jika capability tidak terpenuhi atau error transient berkepanjangan, reroute ke provider lain.

Contoh adapter parsing defensif

function normalizeProviderResponse(raw) {
  const output = raw.output ?? raw.result ?? raw.data?.output;
  const usage = raw.usage ?? raw.metrics ?? {};

  if (!output) {
    throw new Error("Provider response missing output");
  }

  return {
    result: { output },
    usage: {
      input_units: usage.input_units ?? usage.prompt_tokens ?? null,
      output_units: usage.output_units ?? usage.completion_tokens ?? null
    }
  };
}

Contoh di atas sengaja sederhana, tetapi pesannya penting: logika kompatibilitas diletakkan di adapter boundary, bukan disebar ke seluruh aplikasi.

Trade-off fallback

  • Fallback meningkatkan availability, tetapi bisa menurunkan konsistensi hasil.
  • Fallback ke plain text dapat menjaga alur tetap hidup, tetapi menambah beban parsing internal.
  • Provider fallback perlu perhatian pada biaya, latency, dan perbedaan policy/moderation.

Karena itu, fallback sebaiknya eksplisit dan terukur. Simpan metadata execution_path agar tim bisa melihat kapan fallback sering aktif.

Checklist Review API Contract Sebelum Go-Live

Gunakan checklist ini saat meninjau kontrak integrasi AI:

  1. Versioning
    • Apakah versi kontrak dipisah dari versi model/provider?
    • Apakah perubahan semantik punya mekanisme flag atau versi baru?
  2. Request schema
    • Apakah field wajib minimum dan jelas?
    • Apakah enum dan default behavior terdokumentasi?
    • Apakah hint vendor dipisahkan dari field inti?
  3. Response schema
    • Apakah hasil inti dipisah dari metadata operasional?
    • Apakah klien bisa mengabaikan field tambahan tanpa gagal?
    • Apakah ada ruang untuk warnings dan partial results?
  4. Error model
    • Apakah semua error dipetakan ke kategori yang bisa ditindaklanjuti?
    • Apakah retryable vs non-retryable eksplisit?
  5. Idempotency
    • Apakah submit job menerima idempotency key?
    • Apakah payload mismatch pada key yang sama terdeteksi?
  6. Async completion
    • Apakah webhook handler idempotent?
    • Apakah polling dan webhook bisa hidup berdampingan?
  7. Timeout and recovery
    • Apakah ada status unknown atau mekanisme reconciliation?
    • Apakah retry policy aman untuk tiap jenis operasi?
  8. Compatibility
    • Apakah parser boundary toleran terhadap field tambahan?
    • Apakah ada strategi aliasing atau fallback saat schema vendor berubah?
  9. Observability
    • Apakah drift integrasi bisa terdeteksi sebelum jadi incident besar?

Observability untuk Mendeteksi Drift Integrasi Lebih Awal

Kontrak yang baik tetap perlu observability yang baik. Banyak drift tidak langsung memunculkan error 500, tetapi terlihat sebagai perubahan distribusi: warning naik, schema coercion meningkat, latency melonjak, atau fallback makin sering dipakai.

Sinyal yang perlu dipantau

  • Success rate per provider/model/capability
  • Rate of unknown enum / missing expected field
  • Schema validation failure rate
  • Warning rate, misalnya PARTIAL_SCHEMA_COERCION
  • Fallback activation rate
  • Idempotency replay rate
  • Webhook duplicate rate
  • Latency percentile untuk submit, processing, callback, dan end-to-end completion

Log yang tepat lebih penting daripada log yang banyak

Setiap eksekusi sebaiknya punya korelasi minimal:

  • request_id
  • job_id
  • idempotency_key
  • provider
  • contract_version
  • capability_profile
  • execution_path seperti primary, fallback_plain_text, fallback_provider_b

Dengan korelasi ini, Anda bisa menjawab pertanyaan operasional penting: “Apakah error meningkat hanya pada satu provider?”, “Apakah drift terjadi setelah provider mengganti format output?”, atau “Apakah fallback menyelamatkan request atau justru memperburuk kualitas?”

Canary dan contract test untuk provider adapter

Selain monitoring runtime, pertahankan contract test untuk adapter provider. Uji ini tidak harus rumit, tetapi perlu memverifikasi:

  • request yang sama dipetakan dengan benar ke provider,
  • respons vendor lama dan respons vendor dengan field tambahan tetap bisa dinormalisasi,
  • error vendor dipetakan ke error envelope internal yang tepat,
  • webhook sample yang duplikat tidak memicu side effect ganda.

Jika memungkinkan, jalankan canary request berkala pada capability penting untuk mendeteksi perubahan perilaku sebelum trafik utama terkena.

Referensi Arsitektur Implementasi yang Sederhana

Struktur backend yang umum dan efektif:

  • API layer: validasi kontrak publik, generate request_id, terima idempotency key.
  • Orchestrator: memilih mode sync/async, capability routing, retry policy.
  • Provider adapters: translasi request/response/error per vendor.
  • Job store: menyimpan status job, hasil, warning, metadata, webhook dedupe.
  • Webhook receiver: verifikasi, dedupe, enqueue processing.
  • Reconciliation worker: menangani status unknown, webhook yang hilang, atau polling fallback.
  • Observability pipeline: metrics, logs, traces, schema validation events.

Pemisahan ini membuat perubahan provider tidak menyebar ke seluruh codebase.

Penutup

Membangun API Contract tahan evolusi untuk integrasi model AI vendor berarti menerima bahwa provider akan berubah lebih cepat daripada aplikasi Anda. Karena itu, kontrak publik harus fokus pada kebutuhan stabil: capability, schema yang eksplisit, error model yang konsisten, idempotency, async completion yang aman, dan observability untuk mendeteksi drift.

Jika Anda hanya mengambil satu keputusan desain dari artikel ini, pilihlah ini: jangan jadikan payload vendor sebagai kontrak aplikasi Anda. Bungkus provider di balik adapter, definisikan semantik internal yang stabil, dan ukur drift sejak awal. Langkah itu akan mengurangi incident, mempermudah fallback, dan memberi ruang untuk mengganti model atau vendor tanpa merombak seluruh sistem.