Prompt Fingerprint
A prompt fingerprint is a short hash computed from everything that defines how a model call is configured, so that two calls can be compared for “same prompt release or not” without reading the text. It answers a question that version numbers cannot: is the thing that ran yesterday byte-for-byte the same as the thing running today?
Why a version number is not enough
A human-assigned version such as 2.0.0 records intent. It fails in two directions. Someone edits the instructions and forgets to bump the number, so two different releases share a label. Or someone bumps the number after a change that altered nothing, so two identical releases look different. When an evaluation score moves, or an output turns out wrong, the first question is which prompt produced it. A fingerprint attached to every logged request answers that mechanically.
What goes into it
Include every input that changes the model’s behavior and that is fixed at release time:
- the system instructions and the user-message template with its slots unfilled;
- few-shot examples, if any;
- the exact model identifier;
- sampling parameters such as temperature and the output token limit;
- the output schema, either its content or a name or version that always refers to unchanged content (the example below uses the label
answer-v1, so editing that schema without renaming it would not change the fingerprint).
Runtime data, meaning the user’s question and retrieved evidence, is not part of the release fingerprint, because it differs on every request. It can be recorded separately as ids and counts.
How it is computed
The contents are serialized to a canonical byte string, then hashed:
import hashlib, json
def fingerprint(spec: dict) -> str:
canonical = json.dumps(spec, sort_keys=True, ensure_ascii=True,
separators=(",", ":"))
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:12]
spec = {
"model": "model-x-2025-01",
"system": "Answer only from the supplied evidence.",
"template": "Evidence: {evidence}\nQuestion: {question}",
"temperature": 0.0, "max_tokens": 300,
"examples": [], "schema": "answer-v1",
}
variants = {
"none": spec,
"reordered keys": dict(reversed(list(spec.items()))),
"temperature 0.2": {**spec,"temperature":0.2},
"trailing space": {**spec,"system":spec["system"]+" "},
"model -02": {**spec,"model":"model-x-2025-02"},
}
for label,value in variants.items(): print(label,fingerprint(value))
for question in ("When is a refund allowed?","What is the shipping fee?"):
rendered = spec["system"]+"\n"+spec["template"].format(
evidence="Returns are allowed within 30 days. Shipping costs 5 dollars.",
question=question)
print("rendered",hashlib.sha256(rendered.encode("utf-8")).hexdigest()[:12])
Canonical means that equal contents always produce equal bytes. Sorting keys and fixing the encoding and separators removes differences that carry no meaning. A hash function then maps any input to a fixed-length digest in which a one-byte change gives an unrelated result.
Running this on a small hypothetical spec (model model-x-2025-01, the exact system prompt and template above, with {evidence} and {question} slots, temperature 0.0 and a 300-token limit) gave these results:
| Change to the spec | Fingerprint |
|---|---|
| none | 11ea553565b5 |
| keys listed in a different order | 11ea553565b5 |
| temperature 0.0 to 0.2 | 24676f5fcab9 |
| one trailing space added to the system prompt | 57ed3161cb29 |
model id ...-01 to ...-02 | 258f1af6af2e |
Reordering keys leaves the value unchanged because of the canonical form, while a trailing space, which no reader would notice, produces a different release. That is the intended behavior: the fingerprint tracks bytes, not meaning. Without sort_keys, the reordered spec serializes differently. Likewise, writing temperature as 0 instead of 0.0 changes the hash in this scheme, so a pipeline should normalize types before hashing.
Twelve hex characters carry 48 bits, enough to tell apart the handful of releases a team keeps. Use the full digest when the fingerprint is used as a lookup key across a large history.
Template fingerprint versus rendered-prompt hash
Two different hashes are in use and they answer different questions.
- The template fingerprint identifies the release. In the example it stayed
11ea553565b5for both requests. - A hash of the fully rendered prompt, with evidence and question filled in, differed per request (
c04355e94e4fande8340be26305for two different questions). It identifies the exact text sent, which is useful for cache keys and for finding repeated inputs, and, together with the stored prompt text, it lets you check which content a specific request used. A hash alone cannot reconstruct that text or authenticate a log.
Logging both gives a way to group requests by release and still pin down a single request.
Limits
A fingerprint covers what the application controls. It does not capture a provider changing the weights behind an alias such as latest, which is why the model identifier in the spec should be a dated snapshot where one exists. It does not make sampling deterministic either: the same fingerprint at nonzero temperature can still give different outputs. And it is not a secret or an integrity guarantee against tampering, since anyone can recompute a plain hash. If the hash must be unforgeable, use a keyed hash (see HMAC).
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
