use blake3::Hasher;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Pooling {
Cls,
Mean,
Max,
LastToken,
}
impl Pooling {
fn discriminant_byte(&self) -> u8 {
match self {
Self::Cls => 0,
Self::Mean => 1,
Self::Max => 2,
Self::LastToken => 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Normalization {
None,
L2,
}
impl Normalization {
fn discriminant_byte(&self) -> u8 {
match self {
Self::None => 0,
Self::L2 => 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CacheKey {
pub model_digest: [u8; 32],
pub tokenizer_digest: [u8; 32],
pub prompt_role_and_version: u32,
pub pooling: Pooling,
pub normalization: Normalization,
pub output_dimensions: u32,
pub content_hash: [u8; 32],
}
impl CacheKey {
pub fn fingerprint(&self) -> [u8; 32] {
let mut hasher = Hasher::new();
hasher.update(b"LEINDEX-CACHEKEY-V1");
hasher.update(&self.model_digest);
hasher.update(&self.tokenizer_digest);
hasher.update(&self.prompt_role_and_version.to_le_bytes());
hasher.update(&[self.pooling.discriminant_byte()]);
hasher.update(&[self.normalization.discriminant_byte()]);
hasher.update(&self.output_dimensions.to_le_bytes());
hasher.update(&self.content_hash);
hasher.finalize().into()
}
pub fn model_digest(model_bytes: &[u8]) -> [u8; 32] {
blake3::hash(model_bytes).into()
}
pub fn tokenizer_digest(tokenizer_bytes: &[u8]) -> [u8; 32] {
blake3::hash(tokenizer_bytes).into()
}
pub fn content_hash(text: &str) -> [u8; 32] {
blake3::hash(text.as_bytes()).into()
}
pub fn pack_prompt_role_and_version(role: u16, version: u16) -> u32 {
(u32::from(version) << 16) | u32::from(role)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_key(content: &str) -> CacheKey {
CacheKey {
model_digest: CacheKey::model_digest(b"model-bytes-v1"),
tokenizer_digest: CacheKey::tokenizer_digest(b"tokenizer-config"),
prompt_role_and_version: CacheKey::pack_prompt_role_and_version(1, 1),
pooling: Pooling::Mean,
normalization: Normalization::L2,
output_dimensions: 1024,
content_hash: CacheKey::content_hash(content),
}
}
#[test]
fn test_same_fields_produce_same_fingerprint() {
let key_a = sample_key("hello world");
let key_b = sample_key("hello world");
assert_eq!(key_a.fingerprint(), key_b.fingerprint());
}
#[test]
fn test_model_digest_differ() {
let key_a = sample_key("hello");
let mut key_b = key_a.clone();
key_b.model_digest = CacheKey::model_digest(b"different-model");
assert_ne!(key_a.fingerprint(), key_b.fingerprint());
}
#[test]
fn test_tokenizer_digest_differ() {
let key_a = sample_key("hello");
let mut key_b = key_a.clone();
key_b.tokenizer_digest = CacheKey::tokenizer_digest(b"different-tokenizer");
assert_ne!(key_a.fingerprint(), key_b.fingerprint());
}
#[test]
fn test_prompt_role_and_version_differ() {
let key_a = sample_key("hello");
let mut key_b = key_a.clone();
key_b.prompt_role_and_version = CacheKey::pack_prompt_role_and_version(2, 1);
assert_ne!(key_a.fingerprint(), key_b.fingerprint());
let mut key_c = sample_key("hello");
key_c.prompt_role_and_version = CacheKey::pack_prompt_role_and_version(1, 2);
assert_ne!(key_a.fingerprint(), key_c.fingerprint());
}
#[test]
fn test_pooling_differ() {
let key_a = sample_key("hello");
let mut key_b = key_a.clone();
key_b.pooling = Pooling::Cls;
assert_ne!(key_a.fingerprint(), key_b.fingerprint());
}
#[test]
fn test_normalization_differ() {
let key_a = sample_key("hello");
let mut key_b = key_a.clone();
key_b.normalization = Normalization::None;
assert_ne!(key_a.fingerprint(), key_b.fingerprint());
}
#[test]
fn test_output_dimensions_differ() {
let key_a = sample_key("hello");
let mut key_b = key_a.clone();
key_b.output_dimensions = 768;
assert_ne!(key_a.fingerprint(), key_b.fingerprint());
}
#[test]
fn test_content_hash_differ() {
let key_a = sample_key("hello world");
let key_b = sample_key("goodbye world");
assert_ne!(key_a.fingerprint(), key_b.fingerprint());
}
#[test]
fn test_content_hash_same_across_projects() {
let text = "fn main() { println!(\"hello\"); }";
let hash_a = CacheKey::content_hash(text);
let hash_b = CacheKey::content_hash(text);
assert_eq!(hash_a, hash_b);
let key_a = sample_key(text);
let key_b = sample_key(text);
assert_eq!(key_a.fingerprint(), key_b.fingerprint());
}
#[test]
fn test_model_upgrade_new_namespace() {
let key_v1 = CacheKey {
model_digest: CacheKey::model_digest(b"model-v1-bytes"),
tokenizer_digest: CacheKey::tokenizer_digest(b"tok"),
prompt_role_and_version: 0,
pooling: Pooling::Mean,
normalization: Normalization::L2,
output_dimensions: 1024,
content_hash: CacheKey::content_hash("same text"),
};
let key_v2 = CacheKey {
model_digest: CacheKey::model_digest(b"model-v2-bytes"),
..key_v1.clone()
};
assert_ne!(
key_v1.fingerprint(),
key_v2.fingerprint(),
"model upgrade must produce a new namespace"
);
}
#[test]
fn test_fingerprint_is_32_bytes() {
let key = sample_key("test");
let fp = key.fingerprint();
assert_eq!(fp.len(), 32);
}
#[test]
fn test_pack_prompt_role_and_version() {
let packed = CacheKey::pack_prompt_role_and_version(5, 3);
assert_eq!(packed & 0xFFFF, 5);
assert_eq!(packed >> 16, 3);
}
#[test]
fn test_all_pooling_variants_distinct_fingerprints() {
let base = sample_key("hello");
let poolings = [
Pooling::Cls,
Pooling::Mean,
Pooling::Max,
Pooling::LastToken,
];
let mut fingerprints = std::collections::HashSet::new();
for p in &poolings {
let mut key = base.clone();
key.pooling = *p;
fingerprints.insert(key.fingerprint());
}
assert_eq!(
fingerprints.len(),
poolings.len(),
"all pooling variants must produce distinct fingerprints"
);
}
#[test]
fn test_all_normalization_variants_distinct_fingerprints() {
let base = sample_key("hello");
let norms = [Normalization::None, Normalization::L2];
let mut fingerprints = std::collections::HashSet::new();
for n in &norms {
let mut key = base.clone();
key.normalization = *n;
fingerprints.insert(key.fingerprint());
}
assert_eq!(
fingerprints.len(),
norms.len(),
"all normalization variants must produce distinct fingerprints"
);
}
}