Skip to main content

prompt_hash/
lib.rs

1//! # prompt-hash
2//!
3//! Deterministic cache key for an LLM prompt.
4//!
5//! Inputs:
6//! - `model` — model id (already normalized by `claude-cost`/`openai-cost`
7//!   if you care).
8//! - `messages` — `[(role, content)]`.
9//! - `temperature` — quantized to 2 decimal places to avoid floating
10//!   point cache misses.
11//!
12//! Output: 64-char hex SHA-256.
13//!
14//! Use as a request-level cache key. For semantic cache keys (matching
15//! prompts with the same meaning but different wording), pair with
16//! [`semantic-cache-key`](https://crates.io/crates/semantic-cache-key).
17//!
18//! ## Example
19//!
20//! ```
21//! use prompt_hash::key;
22//! let k1 = key(
23//!     "claude-sonnet-4-5",
24//!     &[("user", "what is 2+2?")],
25//!     1.0,
26//! );
27//! let k2 = key(
28//!     "claude-sonnet-4-5",
29//!     &[("user", "what is 2+2?   ")], // trailing ws ignored
30//!     1.0,
31//! );
32//! assert_eq!(k1, k2);
33//! assert_eq!(k1.len(), 64);
34//! ```
35
36#![deny(missing_docs)]
37
38mod sha256;
39
40/// Compute a 64-char hex cache key.
41pub fn key(model: &str, messages: &[(&str, &str)], temperature: f32) -> String {
42    let mut input = String::new();
43    input.push_str("model=");
44    input.push_str(model);
45    input.push('\n');
46    input.push_str("temp=");
47    // Quantize to 2 decimals (1.0 == 1.0000001).
48    input.push_str(&format!("{:.2}", temperature));
49    input.push('\n');
50    for (role, content) in messages {
51        input.push_str("role=");
52        input.push_str(role);
53        input.push('\n');
54        input.push_str("body=");
55        input.push_str(&normalize_body(content));
56        input.push('\n');
57    }
58    sha256::hex(input.as_bytes())
59}
60
61fn normalize_body(s: &str) -> String {
62    // Normalize CRLF -> LF, strip trailing whitespace per line.
63    let s = s.replace("\r\n", "\n").replace('\r', "\n");
64    s.lines()
65        .map(|l| l.trim_end_matches(|c: char| c == ' ' || c == '\t'))
66        .collect::<Vec<_>>()
67        .join("\n")
68}