ModelMaskCheck:检测 AI 模型“挂羊头卖狗肉”的开源工具
SearchXEngine/modelmaskcheck
ModelMaskCheck 是一款检测 AI 模型 masking 的开源工具,通过 10 层探针识别端点宣称的模型与实际应答模型不一致的情况,输出 MASKED、ROUTER、CLEAN、OPAQUE、UNREACHABLE 五种判定。
Tool cek "masking" model AI — mendeteksi endpoint yang mengiklankan satu nama model tapi sebenarnya yang menjawab model lain (versi beda, vendor beda, atau router yang ganti tiap request).
Ada dashboard web — tinggal isi Base URL, API key, dan Model ID di browser, tekan tombol, keluar verdict.
ModelMaskCheck/
├─ engine/maskcheck.py # INTI: 10 lapis probe + verdict + pengaman anti-SSRF
├─ local/server.py # server lokal + REST API + mode hosted (stdlib only)
├─ local/index.html # dashboard LOKAL (yang publik ada di docs/)
├─ Dockerfile # buat container (whitelist: cuma engine/ + local/)
├─ .dockerignore # daftar putih — apa pun di luar itu tidak masuk image
├─ run.bat # launcher lokal (127.0.0.1)
├─ run-lan.bat # launcher LAN (0.0.0.0 + token acak)
├─ selftest.bat # jalanin semua self-test
├─ .github/
│ ├─ workflows/probe.yml # workflow Actions: dipicu dashboard, jalankan engine
│ └─ tools/run_probe.py # runner CI: baca Secret -> engine -> sapu kunci -> tulis hasil
├─ docs/ # yang dilayani GitHub Pages
│ ├─ index.html # dashboard PUBLIK (mobile-first, bisa di-Add to Home Screen)
│ ├─ targets.json # daftar target — TANPA satu pun API key
│ └─ results/ # hasil probe (latest.json + index.json + history/)
├─ tests/
│ ├─ run_selftest.py # gateway tiruan -> assert tiap verdict
│ ├─ mock_gateway.py # gateway palsu (MASKED / ROUTER / CLEAN)
│ ├─ test_hosted.py # pengaman cloud: anti-SSRF, no-key-input, token, /healthz
│ ├─ test_lan.py # guard LAN: token, cookie, refresh
│ ├─ test_dashboard_node.js # jalanin <script> docs/index.html di DOM tiruan
│ └─ check_wiring.py # cek id + handler + route dashboard
├─ test_http.py # smoke test lewat HTTP
└─ reports/ # hasil export (.md + .json) — tidak di-commit
Jalanin
run.bat
atau
python local/server.py # auto buka http://127.0.0.1:8787/
python local/server.py --port 9000 --no-browser
Stdlib-only — tidak perlu pip install apa pun. Butuh Python 3.9+.
Pakai di dashboard
- Klik preset di panel kiri (sumopod, River, DeepSeek resmi, OpenRouter, LLM7 keyless, …) atau + endpoint kosong.
- Isi Model ID yang diiklankan (yang mau kamu bedah).
- API key: tempel langsung, atau kosongkan supaya dibaca dari environment variable
(
SUMOPOD_API_KEY,RIVER_API_KEY,OPENAI_API_KEY, …). - Pilih mode:
- Quick — ~15 request, cukup untuk mayoritas kasus.
- Deep — ~25 request, fingerprint tokenizer penuh (10 string kanonik) + repeat router 3×.
- ▶ Probe semua & bandingkan.
Hasilnya muncul sebagai kartu verdict + tab Perbandingan (matriks tokenizer antar endpoint)
- tab Raw JSON + tombol export .md/.json ke folder
reports/.
Key disimpan di localStorage browser (bisa dimatikan lewat checkbox). Tidak ada data yang
dikirim ke mana pun selain ke endpoint yang kamu isi sendiri.
Verdict
| Verdict | Artinya |
|---|---|
MASKED |
Nama yang diiklankan ≠ model yang menjawab. Ada bukti keras (header bocor / admin endpoint / version drift / keluarga beda). |
ROUTER |
Endpoint mengarahkan tiap request ke model berbeda. Bukan satu model sama sekali. |
CLEAN |
Model yang menjawab persis sama dengan yang diminta. |
OPAQUE |
Tidak ada kebocoran — gateway bersih atau model cloaked. Pakai mode Deep + endpoint pembanding. |
UNREACHABLE |
Endpoint mati / key salah / kuota habis. |
10 lapis probe
| # | Lapis | Yang dicari |
|---|---|---|
| 1 | Admin & katalog | GET <base>/model/info, /models, /health, / — gateway LiteLLM / one-api bocorkan pemetaan nama publik → upstream asli lewat litellm_params.model dan api_base. Ini bacaan tunggal paling mematikan. |
| 2 | Chat dasar | field model di body respons: sama atau tidak dengan yang kita minta. |
| 3 | Header identity bocor | x-litellm-model-name, x-litellm-model-group, x-litellm-model-api-base, x-upstream-* → identitas + upstream asli. Header telemetri (x-envoy-upstream-service-time, cf-ray, server) dipisah — bukan bukti identitas. |
| 4 | Version drift | minta deepseek-v4-flash, dijawab deepseek-v4.1-flash → angka versi beda = masked. |
| 5 | Swap keluarga | minta keluarga A, dijawab keluarga B → ditukar total (skor tertinggi). |
| 6 | ID respons | pola chatcmpl- / msg_ / uuid / numerik menunjuk gaya vendor yang menjawab. |
| 7 | Error leak | kirim model ngawur + max_tokens raksasa → pesan error sering menyebut model/limit asli. |
| 8 | Reasoning | reasoning_content terbuka vs reasoning_details terenkripsi (ciri model cloaked). |
| 9 | Router test | N request identik; kalau body.model ganti-ganti → endpoint itu router, bukan model. |
| 10 | Fingerprint tokenizer | jumlah prompt_tokens untuk 10 string kanonik (ASCII, zh, ja, code, sql, math, emoji, longword, digits, mixed). Dua endpoint dengan signature identik = kemungkinan besar tokenizer/model yang sama walau namanya diiklankan berbeda. Ini bukti terkuat saat tidak ada header bocor — dan satu-satunya cara membuktikan X di provider A = Y di provider B. |
Bobot bukti: header leak 5.0 · admin mapping 4.0 · version drift 4.0 · swap keluarga 5.0 · router 5.0 · error/param leak 2.5 · reasoning 2.0 · self-report 0.0 (tidak dihitung).
Kenapa self-report nol
Model sering salah mengaku. Contoh nyata: model DeepSeek di River menjawab
"I'm Claude 3.5 Sonnet (new), made by Anthropic." Self-report tetap ditampilkan
sebagai anomali (indikasi distilasi/pelabelan ulang), tapi tidak pernah masuk skor.
CLI
python engine/maskcheck.py \
--base-url https://ai.sumopod.com/v1 \
--api-key "$SUMOPOD_API_KEY" \
--model deepseek-v4-flash \
--mode quick --repeat 3 \
--out reports/sumopod.jsonREST API (kalau mau dipakai dari script lain)
| Method | Path | Body |
|---|---|---|
| GET | /api/health |
— |
| GET | /api/presets |
— |
| GET | /api/reports |
— |
| POST | /api/probe |
{"endpoint":{...},"mode":"quick","repeat":3} |
| POST | /api/compare |
{"endpoints":[{...},{...}],"mode":"deep"} |
| POST | /api/export |
{"report":{...},"comparison":{...}} → tulis .md+.json ke reports/ |
Bentuk endpoint:
{
"name": "sumopod",
"base_url": "https://ai.sumopod.com/v1",
"model": "deepseek-v4-flash",
"api_key": "sk-...",
"key_env": "SUMOPOD_API_KEY",
"extra_headers": {"X-Foo": "bar"},
"timeout": 45
}api_key kosong → dibaca dari key_env. base_url tanpa path otomatis ditambah /v1;
kalau sudah ada path (mis. https://api.deepseek.com/beta) dipakai apa adanya.
Self-test (bukti deteksinya nyala)
selftest.bat
Menjalankan tests/mock_gateway.py — gateway tiruan yang meniru tiga perilaku nyata —
lalu menembaknya dengan engine asli dan meng-assert verdict:
| Skenario tiruan | Verdict yang harus keluar |
|---|---|
alias yang bocor x-litellm-model-name + ganti nama model |
MASKED |
| endpoint yang ganti model tiap request | ROUTER |
| model jujur | CLEAN |
| endpoint mati | UNREACHABLE |
Hasil terakhir: 6 pass, 0 fail. Selain itu check_wiring.py memverifikasi setiap
id yang dirujuk JS ada di HTML, setiap handler onclick/oninput punya definisi fungsi,
dan setiap route /api/* yang dipanggil JS ada di local/server.py.
Akses dari HP / device lain (mode LAN)
run-lan.bat
Bind ke 0.0.0.0 + token acak otomatis. Jendelanya mencetak URL lengkap:
http://<ip-lan-anda>:8787/?k=<token>
Buka URL itu (harus ada ?k=-nya) dari HP. Tanpa token, halaman dan seluruh /api/*
dibalas 401 — ini disengaja: dashboard ini bisa nembak URL mana pun pakai API key, jadi
tanpa kunci siapa pun yang sejaringan bisa memakai mesinmu sebagai proxy.
Token disimpan di sessionStorage browser dan otomatis dibuang dari address bar setelah
halaman dimuat. Tiap kali run-lan.bat dijalankan, tokennya baru.
Refresh tidak minta token lagi
Cookie, bukan sessionStorage, yang bikin refresh aman. Waktu halaman di-reload, browser
minta GET / tanpa ?k= — dan JavaScript belum jalan, jadi sessionStorage tidak menolong
sama sekali: server sudah balas 401 sebelum satu baris skrip pun dieksekusi. Jadi server
memasang cookie mmc_t (HttpOnly, SameSite=Lax, 30 hari) begitu request pertama lolos
autentikasi. Sejak itu reload, restore tab, dan pindah dari in-app browser ke browser penuh
semuanya jalan tanpa token.
Urutan yang dicek server: header X-MMC-Token → cookie mmc_t → query ?k=.
Manual:
python local/server.py --host 0.0.0.0 --token # token acak
python local/server.py --host 0.0.0.0 --token RAHASIA # token sendiri
python local/server.py --host 0.0.0.0 # TANPA token (jangan di jaringan publik)
Kalau HP-nya tidak bisa connect
- Windows Firewall. Cek dulu apakah
python.exeyang dipakai sudah punya izin inbound di profil jaringan yang aktif:
Perhatikan barisnetsh advfirewall firewall show rule name="python.exe" verboseProfiles,Direction: In,Action: Allow, danProgram:— path-nya harus persis python yang menjalankan server.run-lan.batsengaja memakai Python 3.13 karena di mesin ini rule-nya sudah ada untuk path itu. Kalau python-mu beda, tambahkan rule sendiri dari PowerShell admin:New-NetFirewallRule -DisplayName "ModelMaskCheck" -Direction In -Action Allow ` -Protocol TCP -LocalPort 8787 -Profile Private,Public
- Kategori jaringan. Cek dengan
Get-NetConnectionProfile. KalauPublic, firewall lebih ketat dan jaringannya mungkin dipakai orang lain — wajib pakai token. - Mullvad. Kalau Mullvad nyala dengan lockdown mode, kill switch-nya memblok paket Tailscale/LAN juga (dua-duanya WireGuard). Nyalakan "Local network sharing" di Mullvad, atau tambahkan pengecualian lewat split tunneling. Jangan pakai exit node Tailscale bersamaan dengan Mullvad — dua-duanya memperebutkan default route.
Batasnya
Ini bukan cara buat diakses dari luar rumah — begitu HP pindah ke data seluler, IP LAN
192.168.x.x tidak bisa dijangkau lagi. Untuk itu butuh Tailscale (atau Headscale/WireGuard
sendiri), bukan cuma bind 0.0.0.0. Lihat bagian "Kalau mau diakses dari mana saja".
Kalau mau diakses dari mana saja
Dashboard publiknya jalan di GitHub Pages, engine-nya jalan di GitHub Actions. Nol kartu, laptop boleh mati.
HP / browser --dispatch--> Actions: engine/maskcheck.py + kunci dari Secret
^ |
+------ baca latest.json <-------+ commit hasil
- Dashboard: https://searchxengine.github.io/modelmaskcheck/
- Kunci API cuma hidup di GitHub Secret
MMC_TARGETS_JSON(JSON array:[{name, base_url, model, api_key}]). Halaman tidak punya kolom kunci sama sekali, dan runner menyapu setiap kunci dari seluruh output — termasuk traceback. - Bikin fine-grained token (
Actions: Read and write+Contents: Read-only, repo ini saja), tempel sekali di dashboard, disimpan dilocalStorageHP. - Repo harus publik — Pages gratis tidak tersedia untuk repo privat (dan Actions di repo
privat langsung
startup_failure). - Tiap probe nunggu ~1–2 menit (boot runner), bukan instan.
Dari terminal:
gh workflow run probe.yml -f target="River — DeepSeek-V4.1-Flash" -f mode=deep -f repeat=3
gh run watchKenapa nggak di PaaS gratis? Semuanya sekarang digembok kartu — ini hasil nembak API mereka, bukan hafalan:
| Platform | Jawaban aslinya |
|---|---|
| Render | 402 Payment information is required (tiga workspace, termasuk yang baru dibuat) |
| Zeabur | Shared clusters are deprecated. Please rent a Server |
| Northflank | wajib tambah payment method sebelum bisa bikin resource |
| Koyeb | pre-auth hold $29 |
| Hugging Face Spaces | compute Space butuh PRO |
| GCP/Azure/Oracle/Fly | kartu saat daftar |
| PythonAnywhere free | tanpa kartu, tapi outbound dibatasi daftar putih |
Kalau nanti butuh yang instan tanpa nunggu CI, jalurnya Cloudflare Workers — perlu port engine ke TypeScript, dan itu belum dikerjakan.
Kalau mau tetap di mesin sendiri, bind 0.0.0.0 saja tidak cukup: IP 192.168.x.x cuma
hidup di jaringan lokal, jadi begitu HP pindah ke data seluler nggak bisa dijangkau. Butuh
overlay network:
| Opsi | Yang terjadi | Catatan |
|---|---|---|
| Tailscale (WireGuard mesh) | tiap device dapat IP 100.x.y.z, saling nyapa dari mana pun, tanpa port forward |
paling cepat; control plane-nya hosted (lihat metadata: daftar device, IP publik, waktu konek — isi trafik tidak) |
| Headscale | Tailscale tapi control plane-nya sendiri | butuh VPS |
| WireGuard polos | tunnel manual ke satu VPS | paling repot, paling tidak ada pihak ketiga |
Kalau tool ini diekspos publik, --block-private wajib nyala: tanpa itu, base_url bebas
jadi SSRF proxy terbuka ke jaringan rumahmu dan endpoint metadata cloud.
Catatan lapangan
200belum tentu ada. CDN/envoy sering balas200dengan body kosong untuk path yang tidak ada — engine menandainyaphantom. Jangan anggap itu endpoint hidup.- Proxy LiteLLM adalah sumber kebocoran terbaik. Kalau
/model/infokebuka, kamu dapat pemetaan lengkap sekali baca. Kalau tertutup,x-litellm-model-namedi chat response biasanya masih bocor. - Router
*/autodan*/freesering ganti model tiap request — probe 1× bisa menyesatkan. Naikkan repeat ke 3–5. - Saat tidak ada kebocoran apa pun, satu-satunya jalan adalah membandingkan fingerprint tokenizer dengan endpoint yang sudah kamu percaya. Tambah endpoint referensi (mis. API resmi vendor) di daftar, lalu lihat tab Perbandingan.
- Self-report jangan dipercaya. Contoh nyata: model DeepSeek di River menjawab
"I'm Claude 3.5 Sonnet (new), made by Anthropic."Karena itu bobotnya 0.0 — hanya muncul sebagai anomali, bukan bukti.
Hasil uji nyata (12 Okt 2026)
| Endpoint | Model diminta | Verdict | Catatan |
|---|---|---|---|
api.river.ai/v1 |
deepseek-ai/DeepSeek-V4.1-Flash |
CLEAN |
served persis sama; tokenizer sig 24|41|21|19; anomali self-report (mengaku Claude 3.5) |
api.river.ai/v1 |
zai-org/GLM-5.3-Flash |
CLEAN |
tokenizer sig 32|47|31|28 — beda jelas dari DeepSeek, konsisten dua model berbeda |
api.llm7.io/v1 |
GLM-5.3-Flash |
CLEAN (sebagian) |
6/7 sub-probe kena HTTP 429 kuota harian; tokenizer tidak terukur |
ai.sumopod.com/v1 |
deepseek-v4-flash |
UNREACHABLE |
HTTP 401 Key is blocked — key sumopod sudah diblokir, bukan bug tool |
来源:GitHub · github.com