Skip to main content

tiy_core/transform/
tool_calls.rs

1//! ToolCall ID normalization utilities.
2
3/// Normalize a tool call ID to be compatible with different providers.
4///
5/// Different providers have different requirements:
6/// - OpenAI: accepts most IDs
7/// - Anthropic: requires `^[a-zA-Z0-9_-]+$`, max 64 chars
8/// - Google: similar to OpenAI
9pub fn normalize_tool_call_id(id: &str, target_provider: &crate::types::Provider) -> String {
10    match target_provider {
11        crate::types::Provider::Anthropic => {
12            // Handle pipe-separated IDs from OpenAI Responses API
13            // Format: {call_id}|{id} where {id} can be 400+ chars
14            let id = if id.contains('|') {
15                id.split('|').next().unwrap_or(id)
16            } else {
17                id
18            };
19
20            // Sanitize to allowed chars and truncate to 64 chars
21            let sanitized: String = id
22                .chars()
23                .map(|c| {
24                    if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
25                        c
26                    } else {
27                        '_'
28                    }
29                })
30                .take(64)
31                .collect();
32
33            sanitized
34        }
35        crate::types::Provider::OpenAI | crate::types::Provider::Groq => {
36            // OpenAI limits ID to 40 chars
37            if id.len() > 40 {
38                id[..40].to_string()
39            } else {
40                id.to_string()
41            }
42        }
43        _ => id.to_string(),
44    }
45}
46
47/// Create a mapping for tool call IDs between providers.
48pub struct ToolCallIdMapper {
49    /// Map from original ID to normalized ID
50    to_normalized: std::collections::HashMap<String, String>,
51    /// Map from normalized ID to original ID
52    from_normalized: std::collections::HashMap<String, String>,
53    /// Target provider
54    target_provider: crate::types::Provider,
55}
56
57impl ToolCallIdMapper {
58    /// Create a new mapper for a target provider.
59    pub fn new(target_provider: crate::types::Provider) -> Self {
60        Self {
61            to_normalized: std::collections::HashMap::new(),
62            from_normalized: std::collections::HashMap::new(),
63            target_provider,
64        }
65    }
66
67    /// Normalize an ID, caching the mapping.
68    pub fn normalize(&mut self, id: &str) -> String {
69        if let Some(normalized) = self.to_normalized.get(id) {
70            return normalized.clone();
71        }
72
73        let normalized = normalize_tool_call_id(id, &self.target_provider);
74
75        // Handle collisions
76        let mut final_normalized = normalized.clone();
77        let mut counter = 1;
78        while self.from_normalized.contains_key(&final_normalized) {
79            final_normalized = format!("{}_{}", normalized, counter);
80            counter += 1;
81        }
82
83        self.to_normalized
84            .insert(id.to_string(), final_normalized.clone());
85        self.from_normalized
86            .insert(final_normalized.clone(), id.to_string());
87
88        final_normalized
89    }
90
91    /// Get the original ID from a normalized one.
92    pub fn denormalize(&self, normalized: &str) -> Option<&String> {
93        self.from_normalized.get(normalized)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_normalize_for_anthropic() {
103        let id = "call_abc123+def/ghi=";
104        let normalized = normalize_tool_call_id(id, &crate::types::Provider::Anthropic);
105        assert!(normalized.len() <= 64);
106        assert!(normalized
107            .chars()
108            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'));
109    }
110
111    #[test]
112    fn test_normalize_pipe_separated() {
113        let id = "call_123|very_long_suffix_here";
114        let normalized = normalize_tool_call_id(id, &crate::types::Provider::Anthropic);
115        assert!(!normalized.contains('|'));
116        assert_eq!(normalized, "call_123");
117    }
118
119    #[test]
120    fn test_normalize_for_openai() {
121        let id = "a".repeat(50);
122        let normalized = normalize_tool_call_id(&id, &crate::types::Provider::OpenAI);
123        assert_eq!(normalized.len(), 40);
124    }
125}