Skip to main content

vct_core/models/
provider.rs

1use std::fmt;
2
3/// Supported AI coding assistant providers.
4///
5/// Used both to tag a parsed session with its source assistant and to route
6/// per-provider usage aggregation. [`Provider::Unknown`] is the fallback when a
7/// model name matches none of the known prefixes.
8///
9/// # Examples
10///
11/// ```
12/// use vct_core::models::Provider;
13///
14/// assert_eq!(Provider::from_model_name("claude-sonnet-4"), Provider::ClaudeCode);
15/// assert_eq!(Provider::ClaudeCode.display_name(), "Claude");
16/// ```
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum Provider {
19    /// Anthropic Claude Code.
20    ClaudeCode,
21    /// OpenAI Codex CLI (also matches raw `gpt-*` / `o1` / `o3` model names).
22    Codex,
23    /// GitHub Copilot CLI.
24    Copilot,
25    /// Google Gemini CLI.
26    Gemini,
27    /// OpenCode.
28    OpenCode,
29    /// Cursor CLI / IDE.
30    Cursor,
31    /// Hermes.
32    Hermes,
33    /// xAI Grok CLI.
34    Grok,
35    /// Model name matched no known provider prefix.
36    Unknown,
37}
38
39impl Provider {
40    /// Detects the AI provider from a model name using byte-level prefix matching.
41    ///
42    /// Recognizes the `claude`, `copilot`, `gemini`, and `grok` prefixes, and
43    /// treats `gpt*` / `o1*` / `o3*` model names as [`Provider::Codex`].
44    /// Matching is case-sensitive (lowercase) and operates directly on the
45    /// UTF-8 bytes, so it stays `const` and allocation-free. Returns
46    /// [`Provider::Unknown`] when no prefix matches.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use vct_core::models::Provider;
52    ///
53    /// assert_eq!(Provider::from_model_name("gpt-4-turbo"), Provider::Codex);
54    /// assert_eq!(Provider::from_model_name("o3-mini"), Provider::Codex);
55    /// assert_eq!(Provider::from_model_name("gemini-2.0-flash"), Provider::Gemini);
56    /// assert_eq!(Provider::from_model_name("mystery-model"), Provider::Unknown);
57    /// ```
58    pub const fn from_model_name(model: &str) -> Self {
59        // Use byte comparison for better performance
60        let bytes = model.as_bytes();
61
62        if bytes.len() >= 6 {
63            // Check for "claude" prefix
64            if bytes[0] == b'c'
65                && bytes[1] == b'l'
66                && bytes[2] == b'a'
67                && bytes[3] == b'u'
68                && bytes[4] == b'd'
69                && bytes[5] == b'e'
70            {
71                return Self::ClaudeCode;
72            }
73        }
74
75        // Check for "copilot" prefix
76        if bytes.len() >= 7
77            && bytes[0] == b'c'
78            && bytes[1] == b'o'
79            && bytes[2] == b'p'
80            && bytes[3] == b'i'
81            && bytes[4] == b'l'
82            && bytes[5] == b'o'
83            && bytes[6] == b't'
84        {
85            return Self::Copilot;
86        }
87
88        if bytes.len() >= 6
89            && bytes[0] == b'g'
90            && bytes[1] == b'e'
91            && bytes[2] == b'm'
92            && bytes[3] == b'i'
93            && bytes[4] == b'n'
94            && bytes[5] == b'i'
95        {
96            return Self::Gemini;
97        }
98
99        if bytes.len() >= 4
100            && bytes[0] == b'g'
101            && bytes[1] == b'r'
102            && bytes[2] == b'o'
103            && bytes[3] == b'k'
104        {
105            return Self::Grok;
106        }
107
108        // Check for OpenAI/Codex models
109        if bytes.len() >= 3 && bytes[0] == b'g' && bytes[1] == b'p' && bytes[2] == b't' {
110            return Self::Codex;
111        }
112
113        if bytes.len() >= 2 && bytes[0] == b'o' && (bytes[1] == b'1' || bytes[1] == b'3') {
114            return Self::Codex;
115        }
116
117        Self::Unknown
118    }
119
120    /// Returns the human-readable display name of the provider.
121    ///
122    /// This is the same string produced by the [`std::fmt::Display`] impl.
123    pub const fn display_name(&self) -> &'static str {
124        match self {
125            Self::ClaudeCode => "Claude",
126            Self::Codex => "Codex",
127            Self::Copilot => "Copilot",
128            Self::Gemini => "Gemini",
129            Self::OpenCode => "OpenCode",
130            Self::Cursor => "Cursor",
131            Self::Hermes => "Hermes",
132            Self::Grok => "Grok",
133            Self::Unknown => "Unknown",
134        }
135    }
136}
137
138impl fmt::Display for Provider {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        write!(f, "{}", self.display_name())
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_provider_detection() {
150        assert_eq!(
151            Provider::from_model_name("claude-sonnet-4"),
152            Provider::ClaudeCode
153        );
154        assert_eq!(
155            Provider::from_model_name("claude-3-opus"),
156            Provider::ClaudeCode
157        );
158        assert_eq!(Provider::from_model_name("gpt-4-turbo"), Provider::Codex);
159        assert_eq!(Provider::from_model_name("gpt-3.5"), Provider::Codex);
160        assert_eq!(Provider::from_model_name("o1-preview"), Provider::Codex);
161        assert_eq!(Provider::from_model_name("o3-mini"), Provider::Codex);
162        assert_eq!(Provider::from_model_name("copilot"), Provider::Copilot);
163        assert_eq!(
164            Provider::from_model_name("copilot-gpt-4"),
165            Provider::Copilot
166        );
167        assert_eq!(Provider::from_model_name("gemini-pro"), Provider::Gemini);
168        assert_eq!(
169            Provider::from_model_name("gemini-2.0-flash"),
170            Provider::Gemini
171        );
172        assert_eq!(Provider::from_model_name("grok-4.5"), Provider::Grok);
173        assert_eq!(
174            Provider::from_model_name("unknown-model"),
175            Provider::Unknown
176        );
177    }
178
179    #[test]
180    fn test_provider_display() {
181        assert_eq!(Provider::ClaudeCode.display_name(), "Claude");
182        assert_eq!(Provider::Codex.display_name(), "Codex");
183        assert_eq!(Provider::Copilot.display_name(), "Copilot");
184        assert_eq!(Provider::Gemini.display_name(), "Gemini");
185        assert_eq!(Provider::OpenCode.display_name(), "OpenCode");
186        assert_eq!(Provider::Cursor.display_name(), "Cursor");
187        assert_eq!(Provider::Hermes.display_name(), "Hermes");
188        assert_eq!(Provider::Grok.display_name(), "Grok");
189        assert_eq!(Provider::Unknown.display_name(), "Unknown");
190    }
191}