llm-message-hash 0.1.0

Stable canonical hash of LLM request/message structures. Recursive key-sorting JSON canonicalization + sha256, with per-provider ignore-lists so semantically-equal Anthropic/OpenAI/Bedrock requests produce the same hash. Useful for cache keys and idempotency.
Documentation
//! # 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);
//! ```

#![deny(missing_docs)]

mod canonical;
mod hasher;
mod opts;

pub use hasher::{
    hash_canonical, hash_canonical_hex, hash_canonical_hex_with, hash_canonical_with,
};
pub use opts::HashOpts;