use std::borrow::Cow;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ToolCallIdFormat {
#[default]
Any,
MaxLength(usize),
UrlSafe,
Alphanumeric(usize),
}
impl ToolCallIdFormat {
#[must_use]
pub fn accepts(self, id: &str) -> bool {
match self {
Self::Any => true,
Self::MaxLength(max) => id.chars().count() <= max,
Self::UrlSafe => !id.is_empty() && id.chars().all(is_url_safe),
Self::Alphanumeric(len) => {
id.len() == len && id.chars().all(|c| c.is_ascii_alphanumeric())
}
}
}
#[must_use]
pub fn conform(self, id: &str) -> Cow<'_, str> {
if id.is_empty() || self.accepts(id) {
return Cow::Borrowed(id);
}
let replacement = match self {
Self::Any => return Cow::Borrowed(id),
Self::MaxLength(max) if max > HASH_HEX_LEN + 1 => {
let prefix: String = id.chars().take(max - HASH_HEX_LEN - 1).collect();
format!("{prefix}_{:016x}", hash(id, 0))
}
Self::MaxLength(max) => alphanumeric_digest(id, max),
Self::UrlSafe => {
let prefix: String = id
.chars()
.take(URL_SAFE_PREFIX_LEN)
.map(|c| if is_url_safe(c) { c } else { '_' })
.collect();
format!("{prefix}_{:016x}", hash(id, 0))
}
Self::Alphanumeric(len) => alphanumeric_digest(id, len),
};
Cow::Owned(replacement)
}
}
const HASH_HEX_LEN: usize = 16;
const URL_SAFE_PREFIX_LEN: usize = 40;
const BASE62: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
fn is_url_safe(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
fn hash(id: &str, round: u64) -> u64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
round
.to_le_bytes()
.iter()
.chain(id.as_bytes())
.fold(OFFSET, |h, byte| (h ^ u64::from(*byte)).wrapping_mul(PRIME))
}
fn alphanumeric_digest(id: &str, len: usize) -> String {
let mut out = String::with_capacity(len);
let mut round = 0;
while out.len() < len {
let mut h = hash(id, round);
for _ in 0..10 {
if out.len() == len {
break;
}
out.push(char::from(BASE62[(h % 62) as usize]));
h /= 62;
}
round += 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const FOREIGN: [&str; 6] = [
"gemini:resp:call:0",
"call_abc123",
"toolu_01ABCdef",
"D681PevKs",
"a/b c+d",
"ünïcödé-id",
];
#[test]
fn accepted_ids_pass_through() {
assert!(matches!(
ToolCallIdFormat::UrlSafe.conform("toolu_01-x"),
Cow::Borrowed("toolu_01-x")
));
assert!(matches!(
ToolCallIdFormat::Alphanumeric(9).conform("D681PevKs"),
Cow::Borrowed(_)
));
assert!(matches!(
ToolCallIdFormat::MaxLength(64).conform("gemini:resp:call:0"),
Cow::Borrowed(_)
));
assert_eq!(ToolCallIdFormat::Any.conform("a b:c"), "a b:c");
assert_eq!(ToolCallIdFormat::UrlSafe.conform(""), "");
}
#[test]
fn replacements_are_accepted_deterministic_and_distinct() {
for format in [
ToolCallIdFormat::UrlSafe,
ToolCallIdFormat::Alphanumeric(9),
ToolCallIdFormat::Alphanumeric(24),
ToolCallIdFormat::MaxLength(12),
ToolCallIdFormat::MaxLength(8),
] {
let mut seen = std::collections::HashSet::new();
for id in FOREIGN {
let out = format.conform(id);
assert!(format.accepts(&out), "{format:?} {id} -> {out}");
assert_eq!(out, format.conform(id), "{format:?} {id}");
assert!(
seen.insert(out.into_owned()),
"{format:?} collision on {id}"
);
}
}
}
#[test]
fn ids_differing_only_in_refused_characters_stay_distinct() {
let format = ToolCallIdFormat::UrlSafe;
let (a, b) = (format.conform("x:1"), format.conform("x/1"));
assert!(a.starts_with("x_1_") && b.starts_with("x_1_"));
assert_ne!(a, b);
}
#[test]
fn replacements_stay_within_openais_limit() {
let long = "x:".repeat(200);
assert_eq!(ToolCallIdFormat::UrlSafe.conform(&long).len(), 57);
let capped = ToolCallIdFormat::MaxLength(64).conform(&long);
assert_eq!(capped.chars().count(), 64);
assert!(capped.starts_with("x:x:"));
}
#[test]
fn replacements_are_stable() {
assert_eq!(
ToolCallIdFormat::UrlSafe.conform("gemini:r:call:0"),
"gemini_r_call_0_9b6d3111a480fdea"
);
assert_eq!(
ToolCallIdFormat::Alphanumeric(9).conform("gemini:r:call:0"),
"kPfSHykzk"
);
}
}