
Identifier les LLM compatibles OpenAI à partir de signaux liés au tokenizer et au comportement.

Empreinte de la pile de service derrière un point de terminaison de chat compatible OpenAI.
Il enregistre la comptabilité du tokenizer, le décalage de template masqué, la prose de validation, la forme JSON/SSE, l'acceptation des rôles, les limites nommées et les en-têtes de passerelle. Il ne charge pas de poids, ne stocke pas de clés API et ne demande pas au modèle qui il est.
| Import | lm_fingerprint |
| CLI | lm-fingerprint |
| Quoi | Sondes d'infrastructure versionnées contre /chat/completions. Bibliothèque synthétique hors ligne. Vérification locale du tokenizer Hugging Face (sans poids). |
| Ce qu'il n'est pas | Pas une attaque par gradient. Pas un jailbreak. Pas une identité par carte d'encodage (tokprint). Pas « quel binaire écoute sur ce port » (Julius). |
Usage autorisé uniquement : points de terminaison que vous possédez, labs que vous contrôlez, bounties dans le périmètre, pentests avec autorisation écrite. Voir SECURITY.md.
--api-key-envnomme une variable d'environnement. La clé n'est jamais journalisée.
Le chat compatible OpenAI est un protocole courant. vLLM, SGLang, une passerelle de lab et un
revendeur peuvent tous parler /chat/completions. Le champ model est une étiquette.
L'hôte peut échanger le backend, envelopper le tokenizer ou placer un routeur devant.
Les sondes de personnalité bougent quand le prompt système bouge.
Ce qui reste stable, c'est la plomberie : comment le serveur compte les tokens, combien de tokens
de template masqués il ajoute, la prose écrite par son validateur, le dialecte SSE,
quels rôles il accepte, le nombre qu'il nomme quand max_tokens est absurde,
les en-têtes Server / fournisseur.
C'est la question à laquelle cet outil répond : quelle pile traite ce chemin de chat ? Pas quel checkpoint, pas quel processus est lié au port, pas qui a écrit une complétion.
ModelPrint posait la même question dans un navigateur avec neuf sondes. Ce paquet en est la forme CLI et bibliothèque : sondes versionnées, bibliothèque enrôlable, preuves par paires, dérive, et un chemin de tokenizer local qui n'appelle jamais une API d'inférence hébergée.
Une empreinte est un enregistrement JSON versionné de values de sondes. Chaque sonde
doit renvoyer la même valeur lorsque la même pile est sollicitée deux fois. Les horodatages,
les identifiants de requête, la latence et les clés sont retirés.
Les comptes de tokenizer réutilisent les textes de ModelPrint 1.0.0 (MIT, unclecode/modelprint),
octet pour octet, moins une base d'un caractère "a" afin qu'un template masqué
s'annule. Ces quatre nombres sont une projection de la carte d'encodage (T).
Des comptes égaux ne signifient pas (T_1 \equiv T_2). GPT-2 et OPT entrent en collision sur
ceux-ci ; le hash du pipeline rust, non.
hf_identity (chemin HF local uniquement) est le SHA-256 de cet engagement de pipeline
plus le hash du template de chat. Même (T) et même template correspondent
(tiny-gpt2 / gpt2). C'est la famille + le template, pas un checkpoint.
Disposition :
src/lm_fingerprint/
cli.py argparse. --api-key-env only. No --api-key.
client.py HttpTransport, InProcessTransport, redact_secrets
probes.py 38 ProbeSpec rows (tokenizer / errors / shape / roles / limits / proxy / surface)
engine.py run suite → Fingerprint
schema.py schema_version=1, probe_suite_version=lm-fingerprint-probes-v2
similarity.py weighted exact-match, coverage, confidence, library rank, drift
align.py constant-shift MAE + residual on the 15-script suite
catalog.py offline supported_parameters twins
library.py enroll / match. Packaged rows are synthetic fixtures
synthetic.py in-process stacks for tests and the shipped library
hf.py local AutoTokenizer + named validation profile. No weights
localmap.py encode-pipeline-v2 commitment (rust tokenizer.json)
proof.py measured ModelPrint 9-probe identity
report.py text reports
data/library.json
Deux voies d'accès à la même suite de sondes :
live endpoint local Hub tokenizer (optional [hf])
| |
HttpTransport.chat HfChatTransport
POST /chat/completions encode / chat_template for counts
| synthetic profile for errors/shape
+------------------+----------------------+
|
engine.run_probes
|
Fingerprint (JSON, keyless)
|
compare | match | drift | enroll
Création de sondes : docs/PROBES.md. Carte des modules : docs/ARCHITECTURE.md.
| Vous détenez | Ce que vous pouvez observer | Ce que fait cet outil |
|---|---|---|
| Une URL de base OpenAI-compat autorisée + une clé | HTTP de chat : usage, erreurs, en-têtes, SSE | fingerprint / compare / match / drift |
| Des fichiers de tokenizer locaux | encode et chat_template | fingerprint-hf / verify-hf. Les poids restent sur le disque |
| Rien qu'un host:port | Bannière / /health / /api/tags | Hors périmètre. Utilisez Julius |
Une correspondance, c'est une infrastructure partagée. Un laboratoire peut servir deux checkpoints sur une seule pile. Un routeur peut répondre avec son propre wrapper d'erreur. L'accord des décomptes n'est pas l'identité de (T).
Python 3.10+. Le runtime principal est la bibliothèque standard.
git clone https://gitlab.com/WattoCyber/lm-fingerprint.git
cd lm-fingerprint
pip install -e ".[dev]"
PYTHONPATH=src python3 scripts/repro.py
Attendez-vous à REPRO_OK. Cette validation est hors ligne : pas de GPU, pas d'API de production.
Les tests HF locaux sont ignorés si le tokenizer n'est pas en cache.
pip install -e ".[hf]" # transformers, for fingerprint-hf / verify-hf
lm-fingerprint list-probes
lm-fingerprint list
list affiche les stacks empaquetées : openai-chat-strict, glm-compat,
router-openai, permissive-compat. Ce sont des fixtures, pas des
fournisseurs scrapés. Comparez deux d'entre eux sans réseau :
from lm_fingerprint.client import InProcessTransport
from lm_fingerprint.engine import fingerprint_endpoint
from lm_fingerprint.similarity import compare_fingerprints
from lm_fingerprint.synthetic import SYNTHETIC_STACKS, handler_for
def fp(stack_id):
t = InProcessTransport(handler_for(stack_id), headers=SYNTHETIC_STACKS[stack_id].headers)
return fingerprint_endpoint(t, model=f"synthetic/{stack_id}", base_url=f"inprocess://{stack_id}")
print(compare_fingerprints(fp("openai-chat-strict"), fp("router-openai"))["note"])