agtrace_providers/claude/
models.rs

1use std::collections::HashMap;
2
3/// Model specification with named fields for type safety
4///
5/// NOTE: Why struct instead of tuple (&str, u64)?
6/// Tuples are position-dependent and lack semantic meaning:
7/// - ("claude-3-5", 200_000) vs (200_000, "claude-3-5") - compiler can't catch order mistakes
8/// - No field names make code less self-documenting
9/// - Hard to extend (adding output_limit would create complex tuples)
10///
11/// Structs provide:
12/// - Named fields prevent position errors (can't swap prefix and context_window)
13/// - Self-documenting code (field names explain purpose)
14/// - Easy to extend with new fields (e.g., output_limit, cache_support)
15/// - IDE auto-completion works
16/// - Compiler enforces all fields are provided
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct ModelSpec {
19    pub prefix: &'static str,
20    pub context_window: u64,
21    /// Compaction buffer percentage (0-100)
22    /// When input tokens exceed (100% - compaction_buffer_pct), compaction is triggered
23    pub compaction_buffer_pct: f64,
24}
25
26impl ModelSpec {
27    /// Create a new model specification
28    ///
29    /// NOTE: Why const fn?
30    /// Allows construction at compile time in const context (MODEL_SPECS array).
31    /// Zero runtime overhead - all values computed at compile time.
32    pub const fn new(
33        prefix: &'static str,
34        context_window: u64,
35        compaction_buffer_pct: f64,
36    ) -> Self {
37        Self {
38            prefix,
39            context_window,
40            compaction_buffer_pct,
41        }
42    }
43}
44
45/// Compaction buffer percentage for Claude models (Claude Code default)
46/// When input tokens exceed (100% - COMPACTION_BUFFER_PCT), compaction is triggered
47const COMPACTION_BUFFER_PCT: f64 = 22.5;
48
49/// Claude provider model specifications
50///
51/// NOTE: Why array of structs instead of HashMap::insert or tuples?
52/// - Type safety: Named fields prevent position errors
53/// - Immutability: Data defined at compile time, cannot be accidentally modified
54/// - Duplicate detection: Tests verify no duplicate prefixes
55/// - Maintainability: Clear structure makes it obvious what each value represents
56/// - Extensibility: Easy to add new fields without breaking existing code
57const MODEL_SPECS: &[ModelSpec] = &[
58    // Claude 4.5 series (as of 2025-12-17)
59    ModelSpec::new("claude-sonnet-4-5", 200_000, COMPACTION_BUFFER_PCT),
60    ModelSpec::new("claude-haiku-4-5", 200_000, COMPACTION_BUFFER_PCT),
61    ModelSpec::new("claude-opus-4-5", 200_000, COMPACTION_BUFFER_PCT),
62    // Claude 4 series
63    ModelSpec::new("claude-sonnet-4", 200_000, COMPACTION_BUFFER_PCT),
64    ModelSpec::new("claude-haiku-4", 200_000, COMPACTION_BUFFER_PCT),
65    ModelSpec::new("claude-opus-4", 200_000, COMPACTION_BUFFER_PCT),
66    // Claude 3.5 series
67    ModelSpec::new("claude-3-5", 200_000, COMPACTION_BUFFER_PCT),
68    // Claude 3 series (fallback)
69    ModelSpec::new("claude-3", 200_000, COMPACTION_BUFFER_PCT),
70];
71
72/// Returns model prefix -> (context window, compaction buffer %) mapping
73pub fn get_model_limits() -> HashMap<&'static str, (u64, f64)> {
74    MODEL_SPECS
75        .iter()
76        .map(|spec| {
77            (
78                spec.prefix,
79                (spec.context_window, spec.compaction_buffer_pct),
80            )
81        })
82        .collect()
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use std::collections::HashSet;
89
90    #[test]
91    fn test_no_duplicate_prefixes() {
92        let prefixes: Vec<&str> = MODEL_SPECS.iter().map(|spec| spec.prefix).collect();
93        let unique_prefixes: HashSet<&str> = prefixes.iter().copied().collect();
94
95        assert_eq!(
96            prefixes.len(),
97            unique_prefixes.len(),
98            "Duplicate prefixes found in MODEL_SPECS: {:?}",
99            prefixes
100                .iter()
101                .enumerate()
102                .filter(|(i, p)| prefixes.iter().skip(i + 1).any(|other| other == *p))
103                .map(|(_, p)| p)
104                .collect::<Vec<_>>()
105        );
106    }
107
108    #[test]
109    fn test_model_limits_coverage() {
110        let limits = get_model_limits();
111
112        // Verify key models are defined
113        assert_eq!(limits.get("claude-sonnet-4-5"), Some(&(200_000, 22.5)));
114        assert_eq!(limits.get("claude-haiku-4-5"), Some(&(200_000, 22.5)));
115        assert_eq!(limits.get("claude-opus-4-5"), Some(&(200_000, 22.5)));
116        assert_eq!(limits.get("claude-sonnet-4"), Some(&(200_000, 22.5)));
117        assert_eq!(limits.get("claude-3-5"), Some(&(200_000, 22.5)));
118        assert_eq!(limits.get("claude-3"), Some(&(200_000, 22.5)));
119    }
120
121    #[test]
122    fn test_all_specs_converted() {
123        let limits = get_model_limits();
124        assert_eq!(
125            limits.len(),
126            MODEL_SPECS.len(),
127            "HashMap size should match MODEL_SPECS length"
128        );
129    }
130}