gestura_core_foundation/
model_display.rs1pub fn format_openai_model_name(id: &str) -> String {
12 match id {
13 "gpt-4o" => "GPT-4o".to_string(),
14 "gpt-4o-mini" => "GPT-4o Mini".to_string(),
15 "gpt-4-turbo" => "GPT-4 Turbo".to_string(),
16 "gpt-4-turbo-preview" => "GPT-4 Turbo Preview".to_string(),
17 "gpt-4" => "GPT-4".to_string(),
18 "gpt-3.5-turbo" => "GPT-3.5 Turbo".to_string(),
19 "o1-preview" => "o1 Preview".to_string(),
20 "o1-mini" => "o1 Mini".to_string(),
21 "o3-mini" => "o3 Mini".to_string(),
22 _ => title_case_kebab(id),
23 }
24}
25
26pub fn format_anthropic_model_name(id: &str) -> String {
39 let parts: Vec<&str> = id.split('-').collect();
40
41 if parts.is_empty() || parts[0] != "claude" || parts.len() < 3 {
43 return title_case_kebab(id);
44 }
45
46 let first_is_numeric = parts[1].chars().all(|c| c.is_ascii_digit());
57
58 if first_is_numeric {
59 let mut version_parts: Vec<&str> = Vec::new();
62 let mut idx = 1;
63 while idx < parts.len() && parts[idx].chars().all(|c| c.is_ascii_digit()) {
64 version_parts.push(parts[idx]);
65 idx += 1;
66 }
67 let version = version_parts.join(".");
68
69 let variant = if idx < parts.len() {
71 capitalize_first(parts[idx])
72 } else {
73 String::new()
74 };
75 idx += 1;
76
77 let suffix = if idx < parts.len() {
79 parts[idx..].join("-")
80 } else {
81 String::new()
82 };
83
84 if suffix.is_empty() {
85 format!("Claude {} {}", version, variant).trim().to_string()
86 } else {
87 format!("Claude {} {} ({})", version, variant, suffix)
88 .trim()
89 .to_string()
90 }
91 } else {
92 let variant = capitalize_first(parts[1]);
94 let major = if parts.len() > 2 { parts[2] } else { "" };
95
96 let suffix = if parts.len() > 3 {
98 parts[3..].join("-")
99 } else {
100 String::new()
101 };
102
103 if suffix.is_empty() {
104 format!("Claude {} {}", variant, major).trim().to_string()
105 } else {
106 format!("Claude {} {} ({})", variant, major, suffix)
107 .trim()
108 .to_string()
109 }
110 }
111}
112
113pub fn format_grok_model_name(id: &str) -> String {
126 let parts: Vec<&str> = id.split('-').collect();
127 let mut name = String::new();
128
129 for (i, part) in parts.iter().enumerate() {
130 if i == 0 {
131 name.push_str("Grok");
132 } else if part.chars().all(|c| c.is_ascii_digit()) {
133 if i == 1 {
134 name.push_str(&format!(" {}", part));
136 } else {
137 name.push_str(&format!(" ({})", part));
139 }
140 } else {
141 let formatted = match *part {
142 "mini" => "Mini",
143 "fast" => "Fast",
144 "vision" => "Vision",
145 "code" => "Code",
146 "beta" => "Beta",
147 _ => part,
148 };
149 name.push_str(&format!(" {}", formatted));
150 }
151 }
152
153 name.trim().to_string()
154}
155
156pub fn format_gemini_model_name(id: &str) -> String {
163 let parts: Vec<&str> = id.split('-').collect();
164 let mut name = String::new();
165
166 for (i, part) in parts.iter().enumerate() {
167 if i == 0 && part.eq_ignore_ascii_case("gemini") {
168 name.push_str("Gemini");
169 } else {
170 let formatted = match *part {
171 "pro" => "Pro",
172 "flash" => "Flash",
173 "lite" => "Lite",
174 "ultra" => "Ultra",
175 "nano" => "Nano",
176 "exp" => "Experimental",
177 "latest" => "(Latest)",
178 other => {
179 name.push(' ');
181 name.push_str(other);
182 continue;
183 }
184 };
185 name.push(' ');
186 name.push_str(formatted);
187 }
188 }
189
190 name.trim().to_string()
191}
192
193pub fn format_model_name(provider: &str, model_id: &str) -> String {
199 match provider.to_lowercase().as_str() {
200 "openai" => format_openai_model_name(model_id),
201 "anthropic" => format_anthropic_model_name(model_id),
202 "grok" => format_grok_model_name(model_id),
203 "gemini" => format_gemini_model_name(model_id),
204 "ollama" => capitalize_first(model_id),
205 _ => model_id.to_string(),
206 }
207}
208
209pub fn is_local_provider(provider: &str) -> bool {
211 matches!(provider.to_lowercase().as_str(), "ollama" | "local")
212}
213
214fn title_case_kebab(s: &str) -> String {
216 s.split('-')
217 .map(capitalize_first)
218 .collect::<Vec<_>>()
219 .join(" ")
220}
221
222fn capitalize_first(s: &str) -> String {
224 let mut chars = s.chars();
225 match chars.next() {
226 None => String::new(),
227 Some(first) => first.to_uppercase().chain(chars).collect(),
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_openai_format() {
237 assert_eq!(format_openai_model_name("gpt-4o"), "GPT-4o");
238 assert_eq!(format_openai_model_name("gpt-4o-mini"), "GPT-4o Mini");
239 }
240
241 #[test]
242 fn test_anthropic_format_old_single_version() {
243 assert_eq!(
245 format_anthropic_model_name("claude-3-opus-20240229"),
246 "Claude 3 Opus (20240229)"
247 );
248 assert_eq!(
249 format_anthropic_model_name("claude-3-sonnet-20240229"),
250 "Claude 3 Sonnet (20240229)"
251 );
252 assert_eq!(
253 format_anthropic_model_name("claude-3-haiku-20240307"),
254 "Claude 3 Haiku (20240307)"
255 );
256 }
257
258 #[test]
259 fn test_anthropic_format_old_double_version() {
260 assert_eq!(
262 format_anthropic_model_name("claude-3-5-sonnet-20241022"),
263 "Claude 3.5 Sonnet (20241022)"
264 );
265 assert_eq!(
266 format_anthropic_model_name("claude-3-5-haiku-20241022"),
267 "Claude 3.5 Haiku (20241022)"
268 );
269 assert_eq!(
270 format_anthropic_model_name("claude-3-7-sonnet-20250219"),
271 "Claude 3.7 Sonnet (20250219)"
272 );
273 }
274
275 #[test]
276 fn test_anthropic_format_new_convention() {
277 assert_eq!(
279 format_anthropic_model_name("claude-sonnet-4-20250514"),
280 "Claude Sonnet 4 (20250514)"
281 );
282 assert_eq!(
283 format_anthropic_model_name("claude-opus-4-20250514"),
284 "Claude Opus 4 (20250514)"
285 );
286 }
287
288 #[test]
289 fn test_anthropic_format_latest_alias() {
290 assert_eq!(
291 format_anthropic_model_name("claude-3-5-sonnet-latest"),
292 "Claude 3.5 Sonnet (latest)"
293 );
294 }
295
296 #[test]
297 fn test_grok_format_with_version_suffix() {
298 assert_eq!(format_grok_model_name("grok-3"), "Grok 3");
299 assert_eq!(format_grok_model_name("grok-3-mini"), "Grok 3 Mini");
300 assert_eq!(
301 format_grok_model_name("grok-3-mini-fast"),
302 "Grok 3 Mini Fast"
303 );
304 assert_eq!(format_grok_model_name("grok-4-0709"), "Grok 4 (0709)");
305 assert_eq!(format_grok_model_name("grok-2-1212"), "Grok 2 (1212)");
306 assert_eq!(
307 format_grok_model_name("grok-2-vision-1212"),
308 "Grok 2 Vision (1212)"
309 );
310 assert_eq!(format_grok_model_name("grok-beta"), "Grok Beta");
311 }
312
313 #[test]
314 fn test_gemini_format() {
315 assert_eq!(
316 format_gemini_model_name("gemini-2.0-flash"),
317 "Gemini 2.0 Flash"
318 );
319 assert_eq!(
320 format_gemini_model_name("gemini-2.0-flash-lite"),
321 "Gemini 2.0 Flash Lite"
322 );
323 assert_eq!(format_gemini_model_name("gemini-1.5-pro"), "Gemini 1.5 Pro");
324 assert_eq!(
325 format_gemini_model_name("gemini-1.5-flash"),
326 "Gemini 1.5 Flash"
327 );
328 }
329
330 #[test]
331 fn test_format_model_name_gemini_dispatch() {
332 assert_eq!(
333 format_model_name("gemini", "gemini-2.0-flash"),
334 "Gemini 2.0 Flash"
335 );
336 }
337
338 #[test]
339 fn test_local_provider() {
340 assert!(is_local_provider("ollama"));
341 assert!(is_local_provider("Ollama"));
342 assert!(!is_local_provider("openai"));
343 }
344}