1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//! # llm-message-hash
//!
//! Stable canonical hash of LLM request/message structures.
//!
//! Two semantically-identical Anthropic requests can produce different
//! `sha256(serde_json::to_string(&req))` results because:
//!
//! - JSON key order isn't guaranteed.
//! - `cache_control` fields don't change request semantics but change the bytes.
//! - Optional metadata (`metadata.user_id`, etc.) varies between callers.
//!
//! This crate fixes that. It walks the value tree, sorts keys recursively,
//! drops a configurable set of fields, and sha256s the canonical bytes.
//!
//! ## Quick example
//!
//! ```
//! use serde_json::json;
//! use llm_message_hash::{hash_canonical_hex, HashOpts};
//!
//! let a = json!({
//! "model": "claude-sonnet-4-5",
//! "messages": [{"role": "user", "content": "hi"}],
//! });
//! let b = json!({
//! "messages": [{"content": "hi", "role": "user"}],
//! "model": "claude-sonnet-4-5",
//! });
//!
//! assert_eq!(hash_canonical_hex(&a), hash_canonical_hex(&b));
//! ```
//!
//! ## With per-provider ignore lists
//!
//! ```
//! use serde_json::json;
//! use llm_message_hash::{hash_canonical_hex_with, HashOpts};
//!
//! let req_a = json!({
//! "model": "claude-sonnet-4-5",
//! "messages": [{
//! "role": "user",
//! "content": [{"type": "text", "text": "hi", "cache_control": {"type": "ephemeral"}}],
//! }],
//! });
//! let req_b = json!({
//! "model": "claude-sonnet-4-5",
//! "messages": [{
//! "role": "user",
//! "content": [{"type": "text", "text": "hi"}],
//! }],
//! });
//!
//! // `HashOpts::anthropic()` drops cache_control + metadata.user_id
//! let h1 = hash_canonical_hex_with(&req_a, &HashOpts::anthropic());
//! let h2 = hash_canonical_hex_with(&req_b, &HashOpts::anthropic());
//! assert_eq!(h1, h2);
//! ```
pub use ;
pub use HashOpts;