← Kembali ke Blog

Satu Gateway untuk Semua LLM: LiteLLM Self-Hosted dengan Budget per Aplikasi

Awalnya cuma satu API key provider di .env. Enam bulan kemudian ada empat service, tiga provider, dan satu thread Slack yang nanya kenapa tagihan Anthropic naik tiga kali lipat. Tiap service punya salinan key sendiri, loop retry sendiri, dan definisi sendiri soal model mana yang murah.

Bikin shared client library kelihatan seperti solusinya, sampai logic retry-nya beda antar repo dan pinning versinya jadi perang kecil. Gateway menyelesaikan ini di layer network: semua service nembak satu endpoint yang kompatibel OpenAI, dan gateway yang mutusin provider mana yang nanganin request, apa yang terjadi kalau provider itu balikin 500, dan berapa budget tiap key.

LiteLLM Proxy adalah gateway itu, dan cukup jalan di VM yang sama dengan aplikasi kamu. Di akhir tutorial ini kamu punya dua alias model, jalur fallback yang tetap hidup saat satu provider mati, dan virtual key dengan limit $5 per 30 hari. Semua langkah di bawah jalan lokal. Buat production kamu cuma ganti tag image, bind address, dan password Postgres.

Yang perlu disiapkan

  • Docker Engine 24+ dengan Compose v2 (docker compose version)
  • API key dari dua provider, di sini OpenAI dan Anthropic
  • curl dan jq
  • Python 3.10+ cuma buat tes SDK di bagian akhir

LiteLLM 1.108.3 (versi yang dipakai saat artikel ini ditulis) support Python 3.10 sampai 3.14. Container-nya bawa runtime sendiri, jadi batasan itu cuma relevan kalau kamu install SDK langsung.

Langkah 1: jalankan gateway dan Postgres

Budget dan pencatatan spend disimpan di database. Tanpa Postgres, proxy tetap melayani request, tapi semua pengecekan budget gagal terbuka. Ini lebih bahaya daripada nggak punya budget sama sekali, karena kamu merasa sudah aman.

Bikin file env dulu. LITELLM_SALT_KEY mengenkripsi API key provider yang disimpan di database dan nggak bisa dirotasi di tempat, jadi simpan file ini baik-baik.

printf 'LITELLM_MASTER_KEY=sk-%s\nLITELLM_SALT_KEY=sk-%s\nPOSTGRES_PASSWORD=%s\n' \
  "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" "$(openssl rand -hex 16)" > .env

Tambahkan key provider ke file yang sama:

printf 'OPENAI_API_KEY=%s\nANTHROPIC_API_KEY=%s\n' \
  "sk-key-openai-kamu" "sk-ant-key-anthropic-kamu" >> .env

Sekarang docker-compose.yml:

services:
  litellm:
    image: docker.litellm.ai/berriai/litellm:latest
    ports:
      - "127.0.0.1:4000:4000"
    environment:
      LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
      LITELLM_SALT_KEY: ${LITELLM_SALT_KEY}
      DATABASE_URL: postgresql://litellm:${POSTGRES_PASSWORD}@db:5432/litellm
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
    volumes:
      - ./config.yaml:/app/config.yaml:ro
    command: ["--config", "/app/config.yaml", "--port", "4000"]
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: litellm
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U litellm"]
      interval: 5s
      timeout: 5s
      retries: 10
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Dua hal penting di sini. Port-nya di-bind ke 127.0.0.1 supaya gateway nggak bisa diakses dari internet sebelum kamu pasang TLS di depannya. Dan tag image pakai latest karena itu yang dipakai quickstart resmi; cek release notes dan pin versi tertentu sebelum dipakai buat traffic nyata.

Langkah 2: definisikan alias model

config.yaml adalah tempat semua routing. Aplikasi kamu cuma akan nembak fast atau smart.

model_list:
  - model_name: fast
    litellm_params:
      model: openai/gpt-6-luna
      api_key: os.environ/OPENAI_API_KEY
  - model_name: smart
    litellm_params:
      model: anthropic/claude-sonnet-5
      api_key: os.environ/ANTHROPIC_API_KEY

router_settings:
  routing_strategy: latency-based-routing
  num_retries: 2
  timeout: 30
  allowed_fails: 3
  cooldown_time: 30
  fallbacks: [{"fast": ["smart"]}]

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY

model_name adalah alias yang dikirim aplikasi. litellm_params.model adalah string provider asli, dan prefix-nya ngasih tahu LiteLLM SDK serta gaya auth mana yang dipakai. Memisahkan keduanya bikin ganti provider jadi urusan config, bukan perubahan kode.

Setting router-nya layak dibaca sekali. num_retries: 2 retry di deployment yang sama. allowed_fails: 3 masukin deployment ke cooldown selama cooldown_time detik setelah tiga kali gagal, jadi satu key rusak nggak makan semua request semenit ke depan. fallbacks ngirim traffic fast ke smart setelah retry habis.

Jalankan:

docker compose up -d
curl -s http://localhost:4000/health/readiness | jq

Output yang diharapkan:

{"status": "healthy", "db": "connected"}

Endpoint itu ngecek proses dan database. Buat ngecek provider benar-benar jawab atau nggak, panggil route health yang butuh auth. Perlu diingat route ini ngirim request nyata ke tiap deployment, jadi ada biaya beberapa token setiap panggilan:

curl -s http://localhost:4000/health \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq '.healthy_count, .unhealthy_count'

Langkah 3: terbitkan key dengan budget

Service kamu jangan pernah pegang master key. Key itu bisa bikin dan hapus key lain, baca semua spend log, dan ubah routing. Terbitkan virtual key per service.

curl -s http://localhost:4000/key/generate \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key_alias": "checkout-service",
    "models": ["fast", "smart"],
    "max_budget": 5,
    "budget_duration": "30d",
    "rpm_limit": 60
  }' | jq -r .key

Simpan nilai yang muncul sebagai LITELLM_KEY. Response itu satu-satunya kesempatan lihat key lengkapnya.

List models itu allowlist, bukan saran. Dua alias dimasukkan karena fallback bisa mendarat di salah satunya. max_budget dalam USD dan budget_duration reset hitungannya tiap 30 hari, jadi key ini dapat $5 per bulan, bukan $5 total. rpm_limit membatasi 60 request per menit, cukup buat nahan loop liar yang mau habisin budget dalam semenit.

Langkah 4: panggil dari aplikasi

Gateway ini ngomong format chat completions OpenAI, jadi kode SDK yang sudah ada tetap jalan cuma dengan ganti base URL.

curl -i http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer $LITELLM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "fast", "messages": [{"role": "user", "content": "Say hello in five words."}]}'

Cek header response. x-litellm-model-api-base kasih tahu upstream mana yang benar-benar melayani request, dan itu cara tercepat memastikan fallback jalan.

import os
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:4000/v1",
    api_key=os.environ["LITELLM_KEY"],
)

resp = client.chat.completions.create(
    model="fast",
    messages=[{"role": "user", "content": "Classify this ticket: my card was charged twice."}],
)
print(resp.choices[0].message.content)

Nggak ada satu baris pun di file itu yang tahu soal Anthropic. Kalau besok kamu putuskan fast pakai model Gemini, edit config.yaml dan restart container. Aplikasi tetap kirim alias yang sama.

Langkah 5: buktikan fallback-nya jalan

Fallback yang belum pernah dipicu cuma tebakan. Rusak key utama dengan sengaja dan lihat header-nya berubah.

OPENAI_API_KEY=sk-invalid docker compose up -d --force-recreate litellm

Variable shell menang atas .env saat interpolasi, jadi container restart dengan key yang bakal gagal auth. Kirim request yang sama dan periksa header:

curl -s -D - -o /dev/null http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer $LITELLM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "fast", "messages": [{"role": "user", "content": "ping"}]}' \
  | grep -i x-litellm-model-api-base

Harusnya muncul API base Anthropic, bukan OpenAI. Balikin key aslinya setelah selesai:

docker compose up -d --force-recreate litellm

Tes ini juga ngasih gambaran soal latency. Satu percobaan auth gagal plus retry nambah delay ke request itu, jadi jaga num_retries tetap kecil dan timeout tetap ketat. Fallback yang butuh 40 detik bukan fallback buat traffic interaktif.

Langkah 6: baca spend-nya

curl -s "http://localhost:4000/key/info?key=$LITELLM_KEY" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq '.info.spend'

Biaya diambil dari price map LiteLLM, jadi ini estimasi berdasarkan harga per token yang dipublikasikan, bukan kontrak hasil nego kamu. Buat dashboard tim biasanya cukup akurat. Buat nagih ke customer, cocokkan dengan invoice provider sebulan sekali.

Saat key lewat max_budget, request mulai gagal dengan error budget, bukan lanjut diam-diam. Sambungkan error itu ke log aplikasi, kalau nggak, orang pertama yang sadar adalah user.

Yang jebol di production

  • Cuma satu replika. Dengan dua container proxy, cooldown dan rate limit hidup di memory masing-masing. Tambah Redis dan arahkan router_settings.redis_host ke situ sebelum scale out.
  • Master key ikut dikirim ke aplikasi. Perlakukan seperti root. Rotasi kalau sampai masuk ke bundle frontend atau log CI.
  • Rotasi LITELLM_SALT_KEY. Nggak ada rotasi di tempat. Ganti key ini bikin semua API key provider yang tersimpan nggak terbaca sampai kamu masukin ulang.
  • Budget tanpa database. Pengecekannya dilewati, dan kamu cuma dapat satu baris warning saat startup.

Kapan pakai ini, kapan pakai alternatif

SDK provider langsung di tiap service aman kalau kamu cuma punya satu provider dan satu tim. Begitu service kedua butuh fallback, kamu bakal nyalin kode routing ke repo kedua.

OpenRouter lebih cepat dimulai kalau kamu mau routing hosted dan nggak mau ngurus Postgres. Tukarnya, traffic dan data spend kamu ada di gateway orang lain, dan budget per key ikut aturan mereka.

Wrapper FastAPI buatan sendiri masuk akal cuma kalau routing-nya tergantung logic bisnis, misalnya tenant enterprise diarahkan ke deployment tertentu. Meski begitu kamu tetap bakal bikin ulang retry, cooldown, dan tabel harga, yang justru sebagian besar isi proxy ini.

Langkah berikutnya

Tambah deployment kedua dengan model_name yang sama buat load balance dua API key dari satu provider. Pindah dari key per service ke team kalau mau budget bareng dengan limit per anggota. Dan sebelum replika kedua, pasang Redis supaya state router-nya dibagi.

Config di artikel ini sekitar 30 baris. Itu intinya: routing, failover, dan kontrol spend berhenti jadi kode aplikasi dan jadi infrastruktur yang bisa dibaca dalam satu layar.

Referensi

Butuh Bantuan Implementasi?

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

Konsultasi Gratis