
Huella de LLMs compatibles con OpenAI a partir de señales de tokenizador y comportamiento.

Toma la huella de la pila de servicio detrás de un endpoint de chat compatible con OpenAI.
Registra la contabilidad del tokenizador, el desplazamiento oculto de la plantilla, la prosa de validación, la forma JSON/SSE, la aceptación de roles, los límites con nombre y las cabeceras de la puerta de enlace. No carga pesos, no almacena claves de API ni le pregunta al modelo quién es.
| Import | lm_fingerprint |
| CLI | lm-fingerprint |
| Qué | Sondas de infraestructura versionadas contra /chat/completions. Biblioteca sintética offline. Verificación local del tokenizador de Hugging Face (sin pesos). |
| Qué no es | No es un ataque de gradiente. No es un jailbreak. No es identidad de mapa de codificación (tokprint). No es "qué binario está en este puerto" (Julius). |
Solo para uso autorizado: endpoints que posees, laboratorios que controlas, recompensas dentro del alcance, pentests por escrito. Consulta SECURITY.md.
--api-key-envnombra una variable de entorno. La clave nunca se registra.
El chat compatible con OpenAI es un cable común. vLLM, SGLang, una puerta de enlace de laboratorio y un
revendedor pueden hablar todos /chat/completions. El campo model es una etiqueta.
El host puede intercambiar el backend, envolver el tokenizador o colocar un enrutador
delante. Las sondas de personalidad se mueven cuando se mueve el prompt del sistema.
Lo que permanece fijo es la fontanería: cómo cuenta tokens el servidor, cuántos tokens
ocultos de plantilla añade, la prosa que escribió su validador, el dialecto SSE,
qué roles acepta, el número que nombra cuando max_tokens es absurdo,
las cabeceras Server / del proveedor.
Esa es la pregunta que responde esta herramienta: ¿qué pila está gestionando esta ruta de chat? No qué checkpoint, no qué proceso está vinculado al puerto, no quién escribió una finalización.
ModelPrint planteó la misma pregunta en un navegador con nueve sondas. Este paquete es la forma de CLI y biblioteca: sondas versionadas, biblioteca inscribible, evidencia por pares, deriva y una ruta de tokenizador local que nunca llama a una API de inferencia alojada.
Una huella es un registro JSON versionado de values de sondas. Cada sonda
debe devolver el mismo valor cuando se golpea la misma pila dos veces. Las marcas de tiempo,
los ids de solicitud, la latencia y las claves se eliminan.
Los recuentos del tokenizador reutilizan los textos de ModelPrint 1.0.0 (MIT, unclecode/modelprint),
byte a byte, menos una línea base "a" de un carácter para que una plantilla oculta
se cancele. Esos cuatro números son una proyección del mapa de codificación (T).
Recuentos iguales no significan (T_1 \equiv T_2). GPT-2 y OPT colisionan en
ellos; el hash del pipeline de rust no.
hf_identity (solo ruta HF local) es SHA-256 de ese compromiso del pipeline
más el hash de la plantilla de chat. La misma (T) y la misma plantilla coinciden
(tiny-gpt2 / gpt2). Eso es familia + plantilla, no un checkpoint.
Disposición:
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
Dos rutas hacia la misma suite de pruebas:
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
Creación de sondas: docs/PROBES.md. Mapa de módulos: docs/ARCHITECTURE.md.
| Lo que tienes | Lo que puedes observar | Lo que hace esta herramienta |
|---|---|---|
| Una URL base compatible con OpenAI autorizada + clave | HTTP de chat: uso, errores, cabeceras, SSE | fingerprint / compare / match / drift |
| Archivos de tokenizador locales | encode y chat_template | fingerprint-hf / verify-hf. Los pesos permanecen en disco |
| Nada más que un host:puerto | Banner / /health / /api/tags | Fuera de alcance. Usa Julius |
Una coincidencia es infraestructura compartida. Un laboratorio puede servir dos checkpoints sobre una misma pila. Un router puede responder con su propio envoltorio de errores. La coincidencia en el conteo no es la identidad de (T).
Python 3.10+. El entorno de ejecución principal es la biblioteca estándar.
git clone https://gitlab.com/WattoCyber/lm-fingerprint.git
cd lm-fingerprint
pip install -e ".[dev]"
PYTHONPATH=src python3 scripts/repro.py
Espera REPRO_OK. Esa puerta es offline: sin GPU, sin APIs de producción.
Las pruebas locales de HF se omiten si el tokenizador no está en caché.
pip install -e ".[hf]" # transformers, for fingerprint-hf / verify-hf
lm-fingerprint list-probes
lm-fingerprint list
list imprime las pilas empaquetadas: openai-chat-strict, glm-compat,
router-openai, permissive-compat. Esos son fixtures, no proveedores
extraídos. Compare dos de ellos sin red:
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"])