跳到正文
GitHub·· 6 天前AI 评分48

ModelMaskCheck:检测 AI 模型“挂羊头卖狗肉”的开源工具

SearchXEngine/modelmaskcheck

AI 导读

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

  1. Klik preset di panel kiri (sumopod, River, DeepSeek resmi, OpenRouter, LLM7 keyless, …) atau + endpoint kosong.
  2. Isi Model ID yang diiklankan (yang mau kamu bedah).
  3. API key: tempel langsung, atau kosongkan supaya dibaca dari environment variable (SUMOPOD_API_KEY, RIVER_API_KEY, OPENAI_API_KEY, …).
  4. Pilih mode:
    • Quick — ~15 request, cukup untuk mayoritas kasus.
    • Deep — ~25 request, fingerprint tokenizer penuh (10 string kanonik) + repeat router 3×.
  5. ▶ 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.json

REST 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

  1. Windows Firewall. Cek dulu apakah python.exe yang dipakai sudah punya izin inbound di profil jaringan yang aktif:
    netsh advfirewall firewall show rule name="python.exe" verbose
    
    Perhatikan baris Profiles, Direction: In, Action: Allow, dan Program: — path-nya harus persis python yang menjalankan server. run-lan.bat sengaja 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
  2. Kategori jaringan. Cek dengan Get-NetConnectionProfile. Kalau Public, firewall lebih ketat dan jaringannya mungkin dipakai orang lain — wajib pakai token.
  3. 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 di localStorage HP.
  • 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 watch

Kenapa 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

  • 200 belum tentu ada. CDN/envoy sering balas 200 dengan body kosong untuk path yang tidak ada — engine menandainya phantom. Jangan anggap itu endpoint hidup.
  • Proxy LiteLLM adalah sumber kebocoran terbaik. Kalau /model/info kebuka, kamu dapat pemetaan lengkap sekali baca. Kalau tertutup, x-litellm-model-name di chat response biasanya masih bocor.
  • Router */auto dan */free sering 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