1pub fn infer_provider(model: &str) -> Option<&'static str> {
27 if model.is_empty() {
28 return None;
29 }
30
31 if model.starts_with("gpt-") || model.starts_with("chatgpt-") {
33 return Some("openai");
34 }
35 if model == "o3" || model.starts_with("o3-") {
36 return Some("openai");
37 }
38 if model.starts_with("o4-") || model.starts_with("o5-") {
39 return Some("openai");
40 }
41
42 if model.starts_with("claude-") {
44 return Some("anthropic");
45 }
46
47 if model.starts_with("gemini-") {
49 return Some("gemini");
50 }
51
52 if model.starts_with("mistral-")
54 || model.starts_with("mixtral-")
55 || model.starts_with("pixtral-")
56 || model.starts_with("codestral-")
57 || model.starts_with("ministral-")
58 {
59 return Some("mistral");
60 }
61
62 None
63}
64
65pub fn known_to_differ(a: &str, b: &str) -> bool {
70 match (infer_provider(a), infer_provider(b)) {
71 (Some(x), Some(y)) => x != y,
72 _ => false,
73 }
74}
75
76pub fn local_backend(model: &str) -> Option<&'static str> {
81 for id in ["ollama", "vllm", "lmstudio"] {
82 if let Some(rest) = model.strip_prefix(id).and_then(|r| r.strip_prefix('/')) {
83 if !rest.is_empty() {
84 return Some(id);
85 }
86 }
87 }
88 None
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn openai_prefixes() {
97 assert_eq!(infer_provider("gpt-4o"), Some("openai"));
98 assert_eq!(infer_provider("gpt-4o-mini"), Some("openai"));
99 assert_eq!(infer_provider("gpt-5.5"), Some("openai"));
100 assert_eq!(infer_provider("chatgpt-4o-latest"), Some("openai"));
101 assert_eq!(infer_provider("o3"), Some("openai"));
102 assert_eq!(infer_provider("o3-mini"), Some("openai"));
103 assert_eq!(infer_provider("o4-mini"), Some("openai"));
104 }
105
106 #[test]
107 fn anthropic_prefix() {
108 assert_eq!(infer_provider("claude-opus-4-7"), Some("anthropic"));
109 assert_eq!(infer_provider("claude-sonnet-4-6"), Some("anthropic"));
110 assert_eq!(infer_provider("claude-haiku-4-5"), Some("anthropic"));
111 }
112
113 #[test]
114 fn gemini_prefix() {
115 assert_eq!(infer_provider("gemini-2.5-pro"), Some("gemini"));
116 assert_eq!(infer_provider("gemini-1.5-flash"), Some("gemini"));
117 }
118
119 #[test]
120 fn mistral_family_prefixes() {
121 assert_eq!(infer_provider("mistral-large-2407"), Some("mistral"));
122 assert_eq!(infer_provider("mixtral-8x22b"), Some("mistral"));
123 assert_eq!(infer_provider("pixtral-12b"), Some("mistral"));
124 assert_eq!(infer_provider("codestral-22b"), Some("mistral"));
125 assert_eq!(infer_provider("ministral-8b"), Some("mistral"));
126 }
127
128 #[test]
129 fn unknown_returns_none() {
130 assert_eq!(infer_provider("llama-3.3-70b"), None);
133 assert_eq!(infer_provider("qwen2.5-72b"), None);
134 assert_eq!(infer_provider("deepseek-r1"), None);
135 assert_eq!(infer_provider("totally-custom-model"), None);
136 assert_eq!(infer_provider(""), None);
137 }
138
139 #[test]
140 fn known_to_differ_only_blocks_known_pairs() {
141 assert!(!known_to_differ("gpt-4o", "gpt-4o-mini"));
143 assert!(!known_to_differ("claude-sonnet-4-6", "claude-haiku-4-5"));
144 assert!(known_to_differ("gpt-4o", "claude-sonnet-4-6"));
146 assert!(known_to_differ("claude-haiku-4-5", "gemini-2.5-pro"));
147 assert!(!known_to_differ("gpt-4o", "llama-3.3-70b"));
149 assert!(!known_to_differ("custom-1", "custom-2"));
150 assert!(!known_to_differ("custom-1", "gpt-4o"));
151 }
152
153 #[test]
154 fn local_backend_recognizes_prefixes() {
155 assert_eq!(local_backend("ollama/llama3.1:8b"), Some("ollama"));
156 assert_eq!(local_backend("vllm/Qwen2.5-7B"), Some("vllm"));
157 assert_eq!(local_backend("lmstudio/phi-4"), Some("lmstudio"));
158 assert_eq!(local_backend("ollama"), None);
159 assert_eq!(local_backend("ollama/"), None);
160 assert_eq!(local_backend("gpt-4o"), None);
161 assert_eq!(local_backend(""), None);
162 }
163}