
对 OpenAI 兼容聊天端点背后的服务栈进行指纹识别。
它记录分词器计数、隐藏模板偏移、校验器文本、 JSON/SSE 结构、角色接受情况、命名限制以及网关头部。它不会 加载权重、存储 API 密钥,也不会询问模型它是谁。
| 导入 | lm_fingerprint |
| CLI | lm-fingerprint |
| 功能 | 针对 /chat/completions 的版本化基础设施探测。离线合成库。本地 Hugging Face 分词器验证(不加载权重)。 |
| 它不是什么 | 不是梯度攻击。不是越狱。不是编码映射身份识别(tokprint)。不是“这个端口上运行的是哪个二进制文件”(Julius)。 |
仅限授权使用:你拥有的端点、你控制的实验室、范围内的 漏洞赏金、书面渗透测试。参见 SECURITY.md。
--api-key-env指定一个环境变量。密钥永远不会被记录。
OpenAI 兼容聊天是一种通用的通信协议。vLLM、SGLang、实验室网关以及
转售商都能说 /chat/completions。model 字段只是一个标签。
主机可以替换后端、包装分词器,或在前面放置一个路由器。
当系统提示变化时,人格探测的结果也会随之变化。
保持不变的是底层管道:服务器如何计数 token、它添加了多少个隐藏
模板 token、其校验器写出的文本、SSE 方言、
它接受哪些角色、当 max_tokens 荒谬时它报出的数字、
Server / 提供商头部。
这就是本工具回答的问题:处理这条聊天路径的是什么栈? 不是哪个检查点,不是哪个进程绑定到该端口,也不是谁写了某条补全。
ModelPrint 曾在浏览器中用九个探测 问过同样的问题。本包是 CLI 和库形式:版本化探测、可注册库、成对证据、 漂移检测,以及一条从不调用托管 Inference API 的本地分词器路径。
指纹是探测 value 的版本化 JSON 记录。当同一栈被命中两次时,每个探测
必须返回相同的值。时间戳、请求 ID、延迟和密钥都会被剥离。
分词器计数复用 ModelPrint 1.0.0 的文本(MIT,unclecode/modelprint),
字节精确,减去一个单字符 "a" 基线,以便隐藏模板相互抵消。
这四个数字是编码映射 (T) 的一个投影。
计数相等并不意味着 (T_1 \equiv T_2)。GPT-2 和 OPT 在这些数字上会碰撞;
而 rust 管道哈希不会。
hf_identity(仅限本地 HF 路径)是该管道承诺加上聊天模板哈希的 SHA-256。
相同的 (T) 和相同的模板匹配(tiny-gpt2 / gpt2)。那是家族 + 模板,而不是检查点。
布局:
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
进入同一探测套件的两条路径:
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
探针编写:docs/PROBES.md。模块映射: docs/ARCHITECTURE.md。
| 你持有 | 你能观察到什么 | 此工具做什么 |
|---|---|---|
| 一个已授权的 OpenAI 兼容 base URL + 密钥 | 聊天 HTTP:用量、错误、响应头、SSE | fingerprint / compare / match / drift |
| 本地分词器文件 | encode 和 chat_template | fingerprint-hf / verify-hf。权重保留在磁盘上 |
| 仅一个 host:port | Banner / /health / /api/tags | 超出范围。请使用 Julius |
匹配意味着共享基础设施。一个实验室可以在同一套栈上服务两个检查点。路由器可以用自己的错误包装器来应答。计数一致并不等于 (T) 的身份。
Python 3.10+。核心运行时是标准库。
git clone https://gitlab.com/WattoCyber/lm-fingerprint.git
cd lm-fingerprint
pip install -e ".[dev]"
PYTHONPATH=src python3 scripts/repro.py
预期得到 REPRO_OK。该门禁为离线运行:不使用 GPU,也不调用生产 API。
如果分词器未缓存,本地 HF 测试将被跳过。
pip install -e ".[hf]" # transformers, for fingerprint-hf / verify-hf
lm-fingerprint list-probes
lm-fingerprint list
list 会打印打包的堆栈:openai-chat-strict、glm-compat、
router-openai、permissive-compat。这些是固定测试数据,并非抓取得到的
提供方。无需网络即可比较其中两个:
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"])
相同的分词器,不同的错误和请求头。得分约为
0.62。ModelPrint 的四项计数一致;本工具并不据此认定二者共享同一技术栈。
lm-fingerprint fingerprint \
--base-url https://your-lab.example/v1 \
--model the-label \
--api-key-env OPENAI_API_KEY \
--out lab.json
lm-fingerprint match --fingerprint lab.json
lm-fingerprint enroll --fingerprint lab.json --stack-id my-lab --library my-lib.json
不要通过命令行传递密钥。不要抓取生产 API 来填充公共库。
lm-fingerprint fingerprint-hf --model sshleifer/tiny-gpt2 --local-files-only
lm-fingerprint verify-hf --local-files-only
计数来自 apply_chat_template(当模板存在时),否则来自
encode(..., add_special_tokens=False)。错误分类来自一个
命名的合成配置(默认 openai-chat-strict),因此运行保持
离线。远程句柄(https://、api://、……)会被拒绝。
lm-fingerprint list-probes
lm-fingerprint list [--library PATH]
lm-fingerprint fingerprint --base-url URL --model MODEL --api-key-env VAR [--out PATH]
lm-fingerprint fingerprint-hf --model HANDLE [--profile NAME] [--local-files-only]
lm-fingerprint verify-hf [--models a,b,c] [--local-files-only]
lm-fingerprint prove [--local-files-only]
lm-fingerprint compare --a a.json --b b.json
lm-fingerprint match --fingerprint a.json [--library PATH]
lm-fingerprint drift --baseline old.json --current new.json
lm-fingerprint enroll --fingerprint a.json --stack-id ID --library PATH
lm-fingerprint export --fingerprint a.json
没有 --api-key 标志。较旧的 wireprint-fingerprint / stackprint-fingerprint JSON 仍可加载。
from lm_fingerprint import (
StackLibrary,
compare_fingerprints,
fingerprint_endpoint,
fingerprint_hf,
match_library,
)
from lm_fingerprint.client import HttpTransport
fp = fingerprint_endpoint(HttpTransport(url, api_key=key), model="x", base_url=url)
hit = match_library(fp, StackLibrary.load())
local = fingerprint_hf("sshleifer/tiny-gpt2", local_files_only=True)
schema_version = 1,probe_suite_version = lm-fingerprint-probes-v2。
| 字段 | 含义 |
|---|---|
kind | lm-fingerprint |
target.model / target.base_url_host | 指向的目标。仅主机名;不含密钥 |
features.tokenizer_norm | 四个 ModelPrint 类别的计数(english、chinese、code、emoji) |
features.template_offset | 隐藏的 serving-template 大小 |
probes.<id>.value | 可比较的单元格。必须稳定 |
probes.<id>.weight | 对相似度的贡献 |
keys_stored | 始终为 false |
weights_loaded | 始终为 false |
比较方式:对双方均为 ok 且 stable 的探针进行加权精确匹配。置信度为 score × coverage。库匹配会报告 certainty(exact / strong / possible / unknown)以及与第二近邻的差距。
重新运行:
lm-fingerprint prove --local-files-only
与 ModelPrint 存在相同的遗留问题
(probes/index.js 中有九个探针)。他们的 README 就是评分规则:
指纹匹配只能证明共享基础设施,不能证明身份相同。