Stable canonical sha256 hash of LLM request/message structures. Recursive key-sorted JSON canonicalization with per-provider presets that drop noise fields. For cache keys and idempotency. Zero runtime deps.
Project description
llm-message-hash-py
Stable canonical sha256 hash of LLM request/message structures.
Two semantically identical Anthropic requests can produce different
sha256(json.dumps(req)) results because Python dict iteration order is
not part of the value, and fields like cache_control change the bytes
without changing what gets sent to the model. This library walks the
value tree, sorts dict keys recursively, drops a configurable set of
fields, and sha256s the canonical bytes.
Useful for prompt-cache lookups, idempotency keys, and dedupe.
Sibling to the Rust crate
llm-message-hash.
Install
pip install llm-message-hash-py
Use
Default (no fields dropped):
from llm_message_hash import hash_request
a = {"model": "claude", "messages": [{"role": "user", "content": "hi"}]}
b = {"messages": [{"content": "hi", "role": "user"}], "model": "claude"}
assert hash_request(a) == hash_request(b)
Per-provider preset (drops cache_control, response-only fields, etc.):
from llm_message_hash import HashOpts, hash_request
with_cc = {
"messages": [{
"role": "user",
"content": [{"type": "text", "text": "hi", "cache_control": {"type": "ephemeral"}}],
}],
}
without_cc = {
"messages": [{
"role": "user",
"content": [{"type": "text", "text": "hi"}],
}],
}
h1 = hash_request(with_cc, HashOpts.for_anthropic())
h2 = hash_request(without_cc, HashOpts.for_anthropic())
assert h1 == h2
You can also get the canonical bytes directly:
from llm_message_hash import canonical_json
s = canonical_json({"b": 1, "a": 2})
assert s == '{"a":2,"b":1}'
Presets
Each preset drops the response-side metadata that varies per call plus provider-specific request fields that do not change semantics:
| Preset | Drops |
|---|---|
HashOpts.for_anthropic() |
cache_control, id, usage, stop_reason, stop_sequence |
HashOpts.for_openai() |
created, id, object, system_fingerprint, usage, finish_reason |
HashOpts.for_bedrock() |
cache_control, usage, stopReason, metrics |
HashOpts.for_gemini() |
usageMetadata, safetyRatings, finishReason |
Extend any preset:
opts = HashOpts.for_anthropic()
opts.drop_keys.add("metadata")
Or build your own:
opts = HashOpts(drop_keys={"trace_id", "request_id"})
Drop key behavior
drop_keys matches exact key names at any depth. A key named in
drop_keys is removed from every dict it appears in, no matter how
deeply nested. List order is preserved (a list is structurally
significant). Strings are case sensitive: "hi" and "Hi" hash
differently. Numbers compare by their JSON representation: 42 and
42.0 are different strings and so hash differently.
What it does NOT do
- No tokenization. The hash is over structure, not token count.
- No semantic equivalence beyond key-order normalization and the drop list.
- No streaming. Pass a complete Python object.
- No HTTP. Does not talk to any LLM provider.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llm_message_hash_py-0.1.0.tar.gz.
File metadata
- Download URL: llm_message_hash_py-0.1.0.tar.gz
- Upload date:
- Size: 7.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14a585668783c8fd818e3b447ffe8a5d55c4d40ac44e6fc1037abf2eef55cfe5
|
|
| MD5 |
74587f1691b23f8862aac4d1755873ae
|
|
| BLAKE2b-256 |
b61b6d9828f00f9ae8872f3217d11557bd87fb77b539b5096ffa6aae4cf0679a
|
File details
Details for the file llm_message_hash_py-0.1.0-py3-none-any.whl.
File metadata
- Download URL: llm_message_hash_py-0.1.0-py3-none-any.whl
- Upload date:
- Size: 6.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a07dabd2e458d467609f57ddc75a9261e595751864ea18aff7fe58c91d52a51c
|
|
| MD5 |
ab687f2080293bbdb6d2858a93157578
|
|
| BLAKE2b-256 |
c6717acce332c81d2fcfaeede3698f03cbe84c78dad13fa2e4b6ff286494b390
|