Dua 9Router di Railway: Error "API Key Invalid" yang Ternyata Tabrakan Prefix
Merantai dua instance 9Router di Railway berakhir dengan error "API key is invalid" di semua request yang lewat rantai, padahal semua key valid dan test di dashboard hijau semua. Ini kronologi diagnosisnya, akar masalah tabrakan prefix-nya, dan fix yang akhirnya membuat rantai itu jalan.
Beberapa waktu setelah punya satu instance 9Router yang berjalan baik di Railway, muncul ide eksperimen yang kedengarannya sederhana: deploy satu instance lagi, lalu rantai instance kedua itu ke instance utama sebagai provider. 9Router di dalam 9Router.
Motivasinya soal kuota. Di 9Router, free model dari OpenCode otomatis ter-include di setiap instalasi, provider OpenCode Free, tanpa perlu login. Pertanyaannya: kalau instalasinya dua, apakah jatah free model-nya terasa dua kali lipat?
Catatan riset pendampingnya sebenarnya sudah memberi peringatan: rate limit free model OpenCode dihitung per alamat IP publik (satu bucket harian bersama, bukan per API key atau per instalasi), dan IP egress Railway dipakai bersama antar tenant serta bisa bergeser setelah redeploy. Jadi katalog model memang akan muncul dobel, tapi kuotanya belum tentu. Apa pun hasilnya, eksperimennya tetap dijalankan, dan di tengah jalan, rantainya gagal total dengan error yang menyesatkan. Artikel ini adalah catatan diagnosisnya.
Topologinya
Satu project Railway berisi dua environment yang sepenuhnya terpisah: database, volume, API key, dan konfigurasi masing-masing tidak tersinkron sama sekali.
- Instance utama, yang dipakai client sehari-hari.
- Instance kedua, yang dirantai masuk ke instance utama.
Client-nya adalah DeepSeek Harness (dsh), dikonfigurasi dengan satu Base URL menunjuk ke instance utama (https://domain-instance-utama/v1), API protocol OpenAI Chat Completions, dan satu API key milik instance utama.
Di dalam instance utama, isinya:
- Provider lokal OpenCode Free dengan prefix
oc, contoh model:oc/big-pickle,oc/mimo-v2.6-flash-free. - Provider Gemini, contoh:
gemini/gemini-3.7-flash. - Satu provider node OpenAI-compatible yang menunjuk ke instance kedua, berisi 2 connections dengan Round Robin aktif.
- Empat combo, semuanya Round Robin mode rotate:
opencode(model OpenCode lokal),gemini,opencode-production(model hasil rantai dari instance kedua), dan satu combo gabungan yang berisi ketiga combo tersebut.
Di instance kedua, katalog GET /v1/models-nya berisi total 797 model dari puluhan provider. Yang relevan di sini: provider OpenCode Free dengan prefix oc berisi tepat 10 model, dan satu combo miliknya sendiri yang juga bernama opencode-production.
Gejalanya: Hijau di Dashboard, Gagal di Chat
Semua tes awal dilakukan dari dsh, lewat satu Base URL dan satu API key yang sama:
- Combo
opencode-production→ GAGAL:This turn failed: API key is invalid, kategori error AUTH. - Model langsung dari provider OpenCode lokal → BERHASIL, selesai 2 detik.
- Combo
gemini→ BERHASIL, selesai 5 detik. - Model tunggal hasil rantai
oc/oc/big-pickledipanggil langsung (tidak lewat combo) → tetap GAGAL dengan error yang sama. Jadi combo bukan tersangka utamanya.
Yang membuat diagnosisnya lama adalah kontradiksi ini:
- Di dashboard instance utama, halaman provider node instance kedua: Test Connection hasilnya Passed 2, Failed 0, kedua connections hijau, status
activedansuccess. - Instance kedua dites isolasi (langsung, tidak lewat rantai) bisa merespons normal.
- Tapi request chat beneran lewat rantai selalu gagal AUTH.
Kronologi Diagnosis
Langkah 1: Cek kedua instance dari luar, tanpa key
GET /v1/models ke instance utama dan instance kedua, keduanya tanpa API key, sama-sama membalas HTTP 401. Artinya gerbang API key kedua instance aktif normal dan kedua service hidup. Ini bukan masalah DNS, domain, atau service mati.
Langkah 2: Hipotesis key rantai, terpatahkan
Hipotesis pertama yang paling masuk akal: key yang tersimpan di koneksi provider node instance kedua ternyata bukan key milik instance kedua. API key 9Router memang terbit per instance dan tidak bisa saling tukar antar environment.
Tapi dua bukti melemahkannya. Test Connection dashboard hijau 2/2, autentikasi di level provider lolos. Dan ada pembanding yang menentukan: request POST langsung ke instance kedua memakai key instance utama (key asing bagi instance kedua) membalas HTTP 401 dengan body polos seperti ini:
{"error": "API key required for remote API access"}
Kalau masalahnya memang key rantai, bentuk errornya adalah yang ini. Error yang muncul lewat rantai bentuknya beda total, jadi key yang tersimpan bukan penyebabnya, dan dashboard yang hijau tadi ternyata benar.
Langkah 3: Potong client, tes curl langsung
Supaya dsh tidak ikut campur, tes dilakukan dengan curl langsung ke instance utama memakai key-nya:
- Model
oc/big-pickle(lokal) → HTTP 200, dijawab normal. Key client valid, instance utama sehat. - Model
oc/oc/big-pickle→ HTTP 401, tapi isi body-nya bukan penolakan key:
{"error": {"message": "[401]: {\"type\":\"error\",\"error\":{\"type\":\"ModelError\",\"message\":\"Model oc/big-pickle is not supported\"}}", "type": "authentication_error", "code": "invalid_api_key"}}
Itu error aslinya. Bukan key yang ditolak, isinya ModelError: Model oc/big-pickle is not supported.
- Combo
opencode-production→ HTTP 401 serupa; model yang sedang kena giliran rotasi saat ituoc/longcat-2.5-preview-free, dan pesannya:Model oc/longcat-2.5-preview-free is not supported. - Model
oc/ocz/big-pickle→Model ocz/big-pickle is not supported. - Model
oc/opencode-production→Model opencode-production is not supported.
Polanya konsisten dan ini sidik jarinya: semua ID yang seharusnya diteruskan ke instance kedua gagal, dan pesan error selalu mengutip sisa string setelah segmen oc/ pertama.
Langkah 4: Tes instance kedua dengan key-nya sendiri
GET /v1/models→ 200, total 797 model.POSTmodeloc/big-pickle→ HTTP 200, dijawab normal.POSTmodelopencode-production(combo milik instance kedua sendiri) → HTTP 200.POSTmodel polosbig-pickletanpa prefix → HTTP 404No active credentials for provider: openai, wajar, nama polos ter-route ke provideropenaiyang memang tidak punya credentials.POSTmodelocz/big-pickle→ HTTP 404No active credentials for provider: opencode-zen, katalognya ada, tapi provideroczdi instance kedua tidak pernah di-connect, jadi jalurnya mati.
Kesimpulan langkah ini: instance kedua 100% sehat untuk jalur oc/* dan combo-nya sendiri.
Akar Masalah: Tabrakan Prefix oc
Di instance utama ada dua hal yang sama-sama memakai prefix oc:
- Provider OpenCode Free lokal, prefix
oc. - Provider node yang menunjuk ke instance kedua, saat dibuat, prefix-nya juga
oc. Model hasil import dari instance kedua pun terdaftar sebagaioc/ditambah ID model aslinya, sehingga lahir ID dobel sepertioc/oc/big-pickle,oc/ocz/big-pickle, danoc/opencode-production.
Mekanisme kegagalannya:
Request masuk ke instance utama: model = oc/oc/big-pickle
segmen pertama "oc" dibaca sebagai provider OpenCode LOKAL
(bukan node instance kedua)
sisa string "oc/big-pickle" dikirim mentah sebagai nama model
ke OpenCode Zen
Zen menolak: HTTP 401 + ModelError
"Model oc/big-pickle is not supported"
gateway membungkus error 401 itu sebagai
authentication_error / invalid_api_key
client menampilkannya sebagai: "API key is invalid"
Request-nya tidak pernah sampai ke node instance kedua, apalagi ke instance keduanya. Semua bukti cocok: error selalu mengutip sisa string setelah oc/ pertama, tes langsung ke instance kedua dengan ID yang sama persis berhasil 200, dan gerbang key di kedua ujung terbukti bekerja dengan bentuk penolakan yang berbeda dan konsisten.
Kenapa Gejalanya Menipu
OpenCode Zen memakai status HTTP 401 untuk nama model yang tidak didukung, bukan 404. Error 401 dari lapisan model itu diteruskan gateway sebagai authentication_error / invalid_api_key, lalu client merendernya sebagai kegagalan autentikasi.
Akibatnya gejala tampil sebagai "API key invalid", padahal key client valid, key antar-instance valid, dan yang salah murni resolusi nama model. Pelajarannya: jangan percaya label kategori error di client. Selalu ambil body error mentah dari lapisan gateway lewat POST /v1/chat/completions, di situlah pesan aslinya tinggal.
Fix-nya
Di dashboard instance utama:
Langkah 1: Ganti prefix node instance kedua
Provider node yang menunjuk ke instance kedua → Edit → ganti Prefix dari oc menjadi prod. Prefix harus unik, satu segmen, dan tidak bertabrakan dengan provider lokal mana pun.
Langkah 2: Tambah model secara manual, bukan import
Model di node itu ditambahkan satu per satu lewat + Add Model, bukan Import from /models, karena import akan menarik seluruh 797 model dari semua provider di instance kedua. Yang dimasukkan hanya 10 model OpenCode Free milik instance kedua:
oc/big-pickle
oc/mimo-v2.6-flash-free
oc/space-bunny-free
oc/longcat-2.5-preview-free
oc/step-5-preview-free
oc/nemotron-3-ultra-free
oc/nemotron-3.5-lightning-free
oc/muse-spark-1.2-contributor-free
oc/muse-spark-1.3-contributor-free
oc/jev-1.13-free
Langkah 3: Perbarui combo
Setelah prefix diganti, ID final model di instance utama menjadi prod/oc/big-pickle, prod/oc/mimo-v2.6-flash-free, dan seterusnya, plus prod/opencode-production. Combo opencode-production di instance utama diperbarui isinya ke ID-ID baru itu. Alternatif yang juga valid: isi combo cukup dengan satu member prod/opencode-production, yaitu memanggil combo internal milik instance kedua.
Langkah 4: Refresh katalog di client
Di dsh, fetch ulang model catalog pada provider yang menunjuk ke instance utama (ID model berubah karena prefix berubah), lalu pilih combo opencode-production.
Yang harus dihindari
Jangan masukkan member berpola prod/ocz/* ke combo. Jalur ocz di instance kedua mati karena provider-nya tidak punya credentials aktif, rotasi yang kena member itu akan gagal 404.
Verifikasi
- Katalog instance utama setelah fix memuat ID baru:
prod/oc/big-pickle,prod/opencode-production, dan comboopencode-production. - Tes curl independen lewat instance utama:
POST /v1/chat/completionsdengan modelopencode-production→ HTTP 200, dilayani modelspace-bunny-free, salah satu free model OpenCode dari instance kedua yang kena giliran Round Robin. - Dari dsh, combo
opencode-productionakhirnya merespons juga.
Rantai lengkapnya bekerja utuh: client → instance utama → instance kedua → OpenCode Free.
Pelajaran Buat yang Merantai Gateway
- Prefix provider adalah kunci routing, bukan sekadar label. Dua provider atau node dalam satu instance tidak boleh berbagi prefix yang sama, apalagi saat men-chain gateway ke gateway, karena ID model upstream sudah membawa prefix-nya sendiri. ID dobel seperti
oc/oc/...adalah sinyal bahaya, bukan hal normal. - Bedakan tiga lapis autentikasi saat debugging gateway berantai: key client ke gateway utama, key gateway utama ke gateway kedua (yang tersimpan di koneksi provider), dan credentials gateway kedua ke provider akhir. Masing-masing punya bentuk penolakan berbeda, cocokkan bentuk errornya sebelum menuduh salah satu.
- Test Connection dan daftar
/modelsmembuktikan konektivitas dan katalog, bukan eksekusi chat. Bukti tertinggi tetap requestPOST /v1/chat/completionsdengan model dan key yang persis dipakai client. - Katalog besar bukan berarti semuanya bisa dieksekusi. Dari 797 model hasil import, sebagian hanya katalog tanpa credentials aktif, contohnya jalur
ocz/*tadi. - Pisah environment berarti pisah total. Database, volume, dan daftar API key masing-masing tidak sinkron. Merantai antar-environment tetap bisa karena domain publiknya saling terjangkau; yang tidak otomatis adalah sinkronisasi konfigurasinya.
- Setelah sesi debugging panjang, regenerate API key kedua instance lalu update key-nya di client dan di koneksi node. Kebiasaan sehat setiap kali key banyak dipakai untuk tes.
Yang Masih Terbuka
Pertanyaan asal eksperimen ini belum terjawab: apakah kuota OpenCode Free terasa dua kali lipat lewat rantai ini? Rantainya sudah jalan, tapi jawabannya baru bisa dibuktikan lewat pemakaian berat, apakah kedua instance berbagi satu bucket harian per IP egress, atau kebetulan berbeda IP. Cara membuktikannya sederhana: pakai satu jalur sampai kena 429, lalu langsung tes jalur satunya untuk model yang sama.
Satu quirk kecil yang sempat tercatat: respons dari rantai kadang memiliki artefak teks data: [DONE] menempel setelah JSON completion, gabungan perilaku streaming dan non-streaming gateway-nya. Tidak memengaruhi keberhasilan request, tapi layak diketahui kalau client-mu sensitif soal format respons.
Penutup
Error paling membingungkan dalam kasus ini ternyata bukan error autentikasi sama sekali, ia cuma menyamar jadi satu. Satu segmen prefix yang bertabrakan cukup untuk membuat seluruh rantai terlihat seperti masalah key, lengkap dengan dashboard yang hijau di semua tempat yang salah. Kalau kamu merantai gateway dan bertemu "API key is invalid" padahal semua key sudah dicek berkali-kali, curigai routing nama modelnya dulu, dan selalu baca body error mentahnya.
Komentar (0)
Belum ada komentar. Tulis tanggapan pertama di bawah.