Error & Debugging

Error & Debugging

Halaman ini adalah referensi lengkap kegagalan pada API Nexotao: bentuk body error, arti tiap kode, mana yang boleh diulang, dan cara melacak satu permintaan setelah kejadian.

Base URL: https://api.nexotao.com.

Amplop error

Setiap error dari API dikembalikan sebagai satu objek JSON dengan bentuk yang sama, apa pun endpoint-nya.

{
  "error": "invalid api key",
  "code": "auth.api_key_invalid",
  "type": "api_key_invalid",
  "request_id": "8997bda3-65c4-45c3-ab38-c909f905fc4d",
  "retryable": false
}
FieldTipeArti
errorstringPesan untuk manusia. Teksnya bisa berubah sewaktu-waktu — jangan dipakai sebagai kondisi program
codestringIdentifier bertitik yang stabil, mis. auth.api_key_invalid. Inilah kontraknya — cabangkan logika di sini
typestringPotongan setelah titik terakhir dari code (mis. api_key_invalid). Ada demi kompatibilitas klien lama
request_idstringId permintaan, sama persis dengan header X-Request-Id
retryablebooltrue bila mengulang permintaan yang sama masuk akal
hintstringOpsional. Hanya muncul pada sebagian error, berisi saran tindakan konkret

Dua header selalu menyertai respons error:

HeaderIsi
X-Error-CodeSama dengan field code. Bisa dibaca tanpa mem-parsing body
X-Request-IdSama dengan field request_id

type diturunkan otomatis dari code, jadi ia tidak pernah membawa informasi tambahan. Kalau Anda menulis klien baru, pakai code (atau header X-Error-Code) dan abaikan type.

Membaca kode error

curl -sS -D /tmp/h.txt https://api.nexotao.com/v1/chat/completions \
  -H "Authorization: Bearer sk-nexo-CONTOH-PALSU" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"halo"}]}'
 
grep -i '^x-error-code\|^x-request-id' /tmp/h.txt

request_id

Setiap permintaan — berhasil maupun gagal — mendapat request_id unik yang dikirim di header X-Request-Id dan dicatat di sisi server.

  • Pada respons error, id yang sama juga ada di body sebagai request_id.
  • Simpan id ini di log aplikasi Anda. Saat melapor ke dukungan, satu request_id jauh lebih berguna daripada tangkapan layar pesan error.
  • Untuk permintaan streaming, X-Request-Id dikirim sebelum byte pertama stream, jadi ia tetap terbaca meski stream gagal di tengah jalan.
curl -sS -D - -o /dev/null https://api.nexotao.com/v1/messages \
  -H "Authorization: Bearer sk-nexo-CONTOH-PALSU" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-4-6","max_tokens":16,
       "messages":[{"role":"user","content":"halo"}]}' \
  | grep -i x-request-id

Katalog kode error

Dikelompokkan per status HTTP. Kolom Retry adalah nilai field retryable pada body.

400 — Permintaan tidak valid

codeKapan terjadiTindakanRetry
request.invalidBody tidak terbaca, model kosong/bukan string, input kosong pada /v1/responses, model tidak tersedia di endpoint tersebut, atau gambar dikirim ke model tanpa dukungan visionPerbaiki body permintaanfalse
request.invalid_requestBentuk permintaan ditolak sebelum mencapai backend — mis. body bukan objek JSON, pesan user berisi konten kosong, atau tools tidak berbentuk array objek dengan input_schema yang punya type di akarPerbaiki struktur pesan/toolfalse
request.unsupported_featureFitur yang diminta tidak dilayani untuk model itu di endpoint itu — mis. structured outputs atau web search Claude di luar /v1/messages, atau tool-use pada model DeepSeekPindah ke endpoint/model yang mendukung; lihat hint bila adafalse
model.unknownmodel tidak ada di katalogCek id model di GET /v1/models atau Model & Hargafalse
upstream.context_length_exceededPrompt melebihi context window modelKecilkan input atau aktifkan kompaksi — lihat Context Windowfalse
upstream.invalid_requestBackend model menolak bentuk permintaanPeriksa parameter yang tidak lazim (mis. sampling di luar rentang)false
upstream.unsupported_featureParameter/fitur tidak didukung backend model tersebutHapus parameter itu atau ganti modelfalse

401 — Autentikasi gagal

codeKapan terjadiTindakanRetry
auth.api_key_missingTidak ada Authorization: Bearer maupun x-api-keyKirim API key — lihat Autentikasifalse
auth.api_key_invalidKey tidak dikenaliPakai key yang benar; buat baru di dashboardfalse
auth.api_key_revokedKey pernah ada tapi sudah dicabutBuat key barufalse
auth.unauthorizedToken sesi dashboard hilang/tidak valid (endpoint dashboard, bukan jalur API key)Login ulang di dashboardfalse
⚠️

Tiga kode auth.* di atas tidak akan pernah berhasil bila diulang dengan kredensial yang sama. Klien yang mengulangnya otomatis hanya membakar kuota RPM dan memperlambat pemulihannya sendiri.

402 — Saldo

codeKapan terjadiTindakanRetry
billing.insufficient_balanceSaldo tidak cukup untuk menutup estimasi input plus jawaban minimalTop-up saldo, lalu kirim ulangfalse

Pemeriksaan ini terjadi sebelum permintaan diteruskan ke model, jadi permintaan yang ditolak 402 tidak menagih apa pun.

403 — Ditolak

codeKapan terjadiTindakanRetry
auth.account_deletedAkun pemilik key berstatus terhapusHubungi dukunganfalse
auth.forbiddenAkses ditolak pada endpoint dashboardHubungi dukunganfalse

404 / 405 — Rute

codeStatusKapan terjadiTindakanRetry
route.not_found404Path tidak ada. Termasuk /v1/images/generations dan /v1/audio/transcriptions, yang tidak dilayaniCek daftar endpoint di API Referencefalse
resource.not_found404Id model pada GET /v1/models/{id} tidak adaCek id modelfalse
usage.not_found404request_id tidak dikenal pada GET /usage/{request_id} — endpoint sesi dashboard, bukan jalur API keyPastikan id-nya benar dan milik akun yang samafalse
request.method_not_allowed405Path benar, method salah (mis. GET ke /v1/messages)Pakai POSTfalse

413 — Body terlalu besar

codeKapan terjadiTindakanRetry
request.too_largeBody permintaan melewati batas ukuranJalankan /compact atau mulai sesi baru; pada integrasi agent ini biasanya berarti riwayat percakapan sudah terlalu panjangfalse

429 — Rate limit dan kuota

codeKapan terjadiTindakanRetry
rate_limit.rpm_exceededBatas permintaan per menit untuk key terlampauiBackoff, lalu ulangitrue
rate_limit.tpm_exceededBatas token per menit untuk key terlampauiBackoff, atau kecilkan prompt/max_tokenstrue
gateway.capacityGerbang konkurensi Nexotao sedang penuh. Membawa Retry-After singkatUlangi setelah Retry-Aftertrue
upstream.rate_limitedBackend model melempar throttle. Retry-After diteruskan bila backend mengirimnyaUlangi setelah Retry-After; lihat hint bila adatrue
quota.key_limit_exceededBatas belanja per key terlampaui — ini pagu anggaran, bukan jendela lajuNaikkan/atur limit key di dashboard, atau tunggu periode berikutnyafalse
⚠️

quota.key_limit_exceeded berstatus 429 tapi retryable false. Ini satu-satunya 429 yang tidak boleh di-retry otomatis: menunggu beberapa detik tidak mengembalikan anggaran. Untuk limit harian/bulanan, Retry-After menunjukkan sisa detik sampai periode berganti; limit total (sepanjang masa) tidak pernah reset dan tidak membawa Retry-After.

Pada jalur API key, respons juga membawa header observabilitas laju bila fitur terkait aktif untuk key tersebut:

HeaderArti
X-RateLimit-Limit / -Remaining / -ResetKuota permintaan per menit
X-RateLimit-Limit-Tokens / -Remaining-Tokens / -Reset-TokensKuota token per menit
X-Key-Limit-Micro / X-Key-Usage-Micro / X-Key-Remaining-Micro / X-Key-Limit-PeriodBatas belanja per key, dalam micro-rupiah

5xx — Gangguan sisi server

codeStatusKapan terjadiTindakanRetry
internal.error500Kegagalan tak terdugaUlangi dengan backofftrue
internal.panic500Kegagalan tak tertangani; sudah tercatat lengkap di sisi serverUlangi; laporkan dengan request_id bila berulangtrue
dependency.upstream_unavailable502Backend model gagal, kehabisan percobaan, atau koneksi putus. Ini juga kelas penampung untuk kegagalan backend yang tidak dikenaliUlangi dengan backoff; pertimbangkan model laintrue
dependency.database_unavailable503Basis data sementara tidak bisa dihubungi (autentikasi, katalog model, saldo, atau pembacaan usage)Ulangi dengan backofftrue
dependency.upstream_unconfigured503Tidak ada backend terkonfigurasi untuk model tersebutPakai model lain; laporkan bila menetaptrue
billing.reconcile_failed503Pencatatan tagihan gagal setelah panggilan modelUlangi; saldo tidak terpotong pada kasus initrue

Isi respons dari backend model tidak pernah diteruskan apa adanya. Status, body, dan identitas backend diklasifikasikan dulu menjadi salah satu kode di atas dengan pesan tetap, lalu detail aslinya hanya disimpan di log server. Jadi jangan membangun logika di atas asumsi bahwa Anda akan melihat pesan asli dari penyedia model.

Error in-band pada streaming

Ini bagian paling mudah terlewat di seluruh API.

⚠️

Untuk permintaan streaming, status === 200 bukan tanda sukses. Begitu gerbang mulai mengirim heartbeat, status HTTP 200 sudah terkirim dan tidak bisa ditarik kembali. Kegagalan setelah titik itu dikirim di dalam stream sebagai event error. Klien yang hanya memeriksa response.ok akan menganggap permintaan gagal sebagai jawaban kosong.

Bentuk event error berbeda menurut endpoint:

Satu chunk data: berisi objek error, lalu ditutup data: [DONE].

data: {"error":{"message":"upstream service unavailable after retries; please retry","type":"server_error","code":"upstream_unavailable","request_id":"8997bda3-65c4-45c3-ab38-c909f905fc4d"}}

data: [DONE]

Deteksi: chunk data: yang punya kunci error di level teratas (bukan choices).

Pesannya adalah teks tetap yang dipilih dari klasifikasi kegagalan — mis. “upstream capacity exhausted after waiting; please retry” untuk throttle, “upstream timed out before starting the stream; please retry” untuk timeout, atau pesan spesifik untuk context window yang terlampaui. Kalau ada saran yang relevan (mis. arahan pemilihan model), saran itu ditempelkan ke akhir pesan, karena stream tidak punya tempat untuk field hint terpisah.

⚠️

Jebakan yang sama berlaku pada /v1/responses non-streaming. Endpoint itu mengirim keepalive agar koneksi panjang tidak diputus perantara, sehingga HTTP 200 bisa sudah terkirim sebelum hasilnya diketahui. Bila kemudian gagal, Anda menerima HTTP 200 dengan body JSON {"status":"failed","error":{...}}. Selalu periksa status pada body /v1/responses, bukan hanya kode HTTP.

Contoh deteksi

import json, httpx
 
payload = {
    "model": "claude-sonnet-4-6",
    "messages": [{"role": "user", "content": "halo"}],
    "stream": True,
}
 
with httpx.stream(
    "POST", "https://api.nexotao.com/v1/chat/completions",
    headers={"Authorization": "Bearer sk-nexo-CONTOH-PALSU"},
    json=payload, timeout=None,
) as r:
    request_id = r.headers.get("x-request-id")
    r.raise_for_status()  # perlu, TAPI tidak cukup
 
    for line in r.iter_lines():
        if not line.startswith("data: "):
            continue
        data = line[6:]
        if data == "[DONE]":
            break
        chunk = json.loads(data)
        if "error" in chunk:                     # kegagalan in-band
            err = chunk["error"]
            raise RuntimeError(
                f"{err.get('code')}: {err.get('message')} (request_id={request_id})"
            )
        # ... proses chunk normal

Panduan retry

Cek retryable lebih dulu

Ulangi hanya bila body error menyatakan "retryable": true. Kode dengan retryable: false — seluruh keluarga auth.*, billing.insufficient_balance, model.unknown, route.not_found, request.*, quota.key_limit_exceededtidak akan pernah berhasil dengan permintaan yang identik.

Hormati Retry-After bila ada

Bila responsnya membawa header Retry-After, pakai angkanya sebagai jeda minimum alih-alih jadwal backoff Anda sendiri. Nilai dari backend divalidasi dan dibatasi maksimum 3600 detik; nilai yang tidak masuk akal dibuang sehingga headernya tidak dikirim sama sekali.

Backoff eksponensial dengan jitter

Tanpa Retry-After, gandakan jeda tiap percobaan dan tambahkan keacakan supaya banyak klien tidak menyerbu bersamaan setelah satu insiden.

Batasi jumlah percobaan

Empat sampai lima percobaan sudah cukup. Setelah itu, kembalikan error ke pemanggil beserta request_id-nya.

import json, random, time, httpx
 
NON_RETRYABLE_HTTP = {400, 401, 402, 403, 404, 405, 413}
 
def call_with_retry(client, url, headers, payload, max_attempts=5):
    delay = 1.0
    for attempt in range(max_attempts):
        r = client.post(url, headers=headers, json=payload)
        if r.status_code < 400:
            return r
 
        body = {}
        try:
            body = r.json()
        except json.JSONDecodeError:
            pass
 
        # Kontraknya adalah `retryable`, bukan status HTTP.
        if not body.get("retryable", r.status_code not in NON_RETRYABLE_HTTP):
            raise RuntimeError(
                f"{body.get('code')}: {body.get('error')} "
                f"(request_id={r.headers.get('x-request-id')})"
            )
 
        wait = delay
        if ra := r.headers.get("retry-after"):
            try:
                wait = max(wait, min(int(ra), 3600))
            except ValueError:
                pass
        time.sleep(wait + random.uniform(0, 0.5 * wait))   # jitter
        delay *= 2
 
    raise RuntimeError("kehabisan percobaan")
⚠️

Jangan pernah membungkus auth.api_key_invalid (atau auth.api_key_missing / auth.api_key_revoked) dalam loop retry. Key yang salah tetap salah pada percobaan ke-100, dan setiap percobaan tetap dihitung terhadap kuota RPM key tersebut.

Biaya dan penelusuran setelah kejadian

Respons non-streaming membawa biaya aktual di header:

HeaderArti
X-Cost-RpBiaya permintaan ini dalam Rupiah
X-Request-IdId permintaan

Pada respons SSE (streaming), X-Cost-Rp sengaja tidak dikirim: header harus ditulis sebelum byte pertama, sementara biaya baru diketahui setelah stream selesai.

Jadi untuk permintaan streaming, cara mengetahui biayanya adalah:

Simpan X-Request-Id dari header respons

Header ini dikirim sebelum byte pertama stream, jadi tetap tersedia bahkan ketika stream kemudian gagal. Catat bersama panggilannya.

Cari permintaan itu di halaman Usage pada dashboard

Temukan id permintaan yang cocok untuk melihat angka final — model, jumlah token, dan tagihan persis yang memotong saldo Anda.

⚠️

Ada endpoint GET /usage/{request_id}, tetapi ia bukan bagian dari permukaan API key. Endpoint itu diautentikasi dengan token sesi dashboard, sehingga mengirim Authorization: Bearer sk-nexo-... ke sana gagal autentikasi dengan 401 auth.unauthorized, bukan mengembalikan data pemakaian. Kalau Anda hanya memegang API key, pakai halaman Usage di dashboard.

Lewat jalur mana pun, catatan finalnya memuat field berikut. Dua di antaranya perlu dipahami:

FieldArti
charge_microTotal tagihan dalam micro-rupiah (Rp 1 = 1.000.000 micro)
turnsJumlah giliran upstream yang ditagih. Normalnya 1; lebih dari 1 ketika gerbang menjalankan tool loop sisi server — inilah penjelasan tagihan yang terlihat lebih besar daripada perkiraan dari satu giliran saja

Field status memberi tahu hasil akhirnya, khususnya apakah permintaan yang gagal itu ditagih atau tidak:

statusArti
okBerhasil dan ditagih
upstream_errorBackend model gagal; tidak ada tagihan
aborted_streamStream terbuka lalu putus sebelum event penutup tanpa keluaran berguna; tidak ditagih
usage_missingBackend membalas 2xx tapi jumlah token tidak terbaca; ditandai dan tidak ditagih
billing_errorJawaban sudah terkirim tapi pencatatan tagihan gagal; tercatat sebagai 0
fallback_429 / fallback_oversizePenanda nol-biaya saat permintaan dialihkan ke model berikutnya dalam rantai

Troubleshooting

401 auth.api_key_missing — header autentikasi tidak terkirim. Pada curl, periksa tanda kutip dan baris lanjutan (\). Pada SDK, pastikan base_url/baseURL menunjuk ke https://api.nexotao.com/v1 dan key benar-benar ter-set (bukan variabel lingkungan kosong).

401 auth.api_key_invalid — key tidak dikenali. Penyebab paling sering: spasi atau baris baru ikut tersalin, key milik lingkungan lain, atau key sudah dihapus. Buat key baru di dashboard. Lihat Autentikasi.

402 billing.insufficient_balance — saldo tidak cukup untuk menutup estimasi input plus jawaban minimal. Nexotao memeriksa ini sebelum memanggil model, jadi permintaan yang ditolak tidak menagih apa pun. Top-up lewat dashboard; lihat Penagihan & Harga.

429 rate_limit.rpm_exceeded / rate_limit.tpm_exceeded — batas per menit key terlampaui. Turunkan konkurensi, terapkan backoff, atau kecilkan prompt/max_tokens. Header X-RateLimit-* menunjukkan sisa kuota dan detik sampai jendela berganti.

429 quota.key_limit_exceeded — batas belanja key tercapai. Ini pagu anggaran yang Anda sendiri pasang di dashboard, bukan throttle laju. Naikkan atau matikan limitnya di sana.

400 model.unknown — id model salah ketik atau model sudah tidak aktif. Ambil daftar yang sah:

curl -sS https://api.nexotao.com/v1/models \
  -H "Authorization: Bearer sk-nexo-CONTOH-PALSU"

404 route.not_found — path tidak dilayani. Yang paling sering: klien OpenAI-generik memanggil /v1/images/generations atau /v1/audio/transcriptions. Kedua endpoint itu tidak tersedia di Nexotao — lihat daftar endpoint yang dilayani di API Reference.

Stream berakhir tanpa teks, tanpa error — hampir selalu error in-band yang tidak diparse klien Anda. Kembali ke bagian Error in-band pada streaming di atas, lalu cari id permintaan itu di halaman Usage pada dashboard: status aborted_stream atau upstream_error menegaskan permintaan itu gagal dan tidak ditagih.

Masih buntu? Kumpulkan request_id, code, dan waktu kejadian, lalu hubungi dukungan lewat dashboard. Tanpa request_id, satu permintaan hampir tidak mungkin ditemukan di log.