

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 フィールドはラベルにすぎません。
ホストはバックエンドを差し替えたり、トークナイザーをラップしたり、ルーターを
前に置いたりできます。パーソナリティプローブはシステムプロンプトが変われば動きます。
変わらないのは配管です:サーバーがトークンをどう数えるか、隠れたテンプレート
トークンをいくつ追加するか、バリデータが書いた文言、SSEの方言、
受け付けるロール、max_tokens が不合理なときに返す数値、
Server / プロバイダーのヘッダー。
これがこのツールが答える問いです:このチャットパスを処理しているのはどのスタックか? どのチェックポイントか、どのプロセスがポートにバインドされているか、 誰が補完を書いたかではありません。
ModelPrint はブラウザで9つのプローブを使って 同じ問いを立てました。このパッケージはCLIとライブラリの形です:バージョン管理されたプローブ、 登録可能なライブラリ、ペアワイズの証拠、ドリフト、そしてホスト型Inference APIを 一切呼び出さないローカルトークナイザーパス。
フィンガープリントはプローブの value を記録したバージョン管理されたJSONレコードです。
各プローブは、同じスタックに2回ヒットしたときに同じ値を返さなければなりません。
タイムスタンプ、リクエストID、レイテンシ、キーは除去されます。
トークナイザーのカウントはModelPrint 1.0.0のテキスト(MIT、unclecode/modelprint)を
再利用し、バイト単位で正確に、1文字の "a" ベースラインを差し引くことで
隠れたテンプレートを相殺します。これら4つの数値はエンコードマップ (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
同じプローブスイートへの2つの経路:
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 互換のベース URL + キー | チャット HTTP: usage、エラー、ヘッダー、SSE | fingerprint / compare / match / drift |
| ローカルのトークナイザーファイル | encode と chat_template | fingerprint-hf / verify-hf。重みはディスク上に留まる |
| host:port 以外に何もない | バナー / /health / /api/tags | 対象外。Julius を使用 |
一致は共有インフラを意味する。1つのラボが1つのスタック上で 2つのチェックポイントを提供できる。ルーターは自身のエラーラッパーで 応答することがある。カウントの一致は (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。これらはフィクスチャであり、スクレイピングされた
プロバイダではありません。ネットワークなしでそのうちの2つを比較します:
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"])
同じword tokenizerでも、エラーとヘッダーが異なります。スコアは約0.62になります。ModelPrintの4つのカウントは一致しますが、このツールはそれを共有スタックとは見なしません。
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)