1use crate::error::LlmError;
2
3pub trait LlmClient: Send + Sync {
8 fn complete(
10 &self,
11 prompt: &str,
12 system: Option<&str>,
13 ) -> impl Future<Output = Result<String, LlmError>> + Send;
14
15 fn embed(&self, text: &str) -> impl Future<Output = Result<Vec<f32>, LlmError>> + Send;
17}
18
19use std::future::Future;
20
21pub const ANTHROPIC_DEFAULT_BASE_URL: &str = "https://api.anthropic.com";
23
24pub fn anthropic_base_url() -> String {
31 let raw = std::env::var("ANTHROPIC_BASE_URL")
32 .unwrap_or_else(|_| ANTHROPIC_DEFAULT_BASE_URL.to_string());
33 raw.trim_end_matches('/').to_string()
34}
35
36#[allow(clippy::collapsible_if)]
41pub fn is_reasoning_model(model: &str) -> bool {
42 let m = model.to_lowercase();
43
44 if m.contains("opus") {
45 return true;
46 }
47 if m.contains("gpt-5") || m.contains("o3") || m.contains("o4") {
48 return true;
49 }
50 if m.contains("gemini") && m.contains("pro") {
51 if let Some(pos) = m.find("pro") {
52 let after = &m[pos + 3..];
53 let version_str: String = after
54 .chars()
55 .skip_while(|c| !c.is_ascii_digit())
56 .take_while(|c| c.is_ascii_digit())
57 .collect();
58 if let Ok(v) = version_str.parse::<u32>()
59 && v >= 3
60 {
61 return true;
62 }
63 }
64 }
65 if m.contains("reasoning") || m.contains("think") {
66 return true;
67 }
68 false
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn test_is_reasoning_model() {
77 assert!(is_reasoning_model("claude-opus-4-6"));
79 assert!(is_reasoning_model("claude-opus-4-20250514"));
80
81 assert!(is_reasoning_model("gpt-5"));
83 assert!(is_reasoning_model("chatgpt-5.4"));
84 assert!(is_reasoning_model("o3-mini"));
85 assert!(is_reasoning_model("o4-preview"));
86
87 assert!(is_reasoning_model("gemini-pro-3.5"));
89 assert!(is_reasoning_model("gemini-pro-3"));
90 assert!(!is_reasoning_model("gemini-pro-2"));
91 assert!(!is_reasoning_model("gemini-pro-1.5"));
92
93 assert!(is_reasoning_model("deepseek-reasoning-v2"));
95 assert!(is_reasoning_model("qwen-thinking-32b"));
96
97 assert!(!is_reasoning_model("claude-sonnet-4-20250514"));
99 assert!(!is_reasoning_model("gpt-4o"));
100 assert!(!is_reasoning_model("gemini-flash-2"));
101 assert!(!is_reasoning_model("llama3"));
102 }
103}