Skip to main content

mur_common/
llm.rs

1use crate::error::LlmError;
2
3/// Trait for LLM providers (Anthropic, OpenAI, Ollama).
4/// Shared between mur-core and mur-commander.
5///
6/// Edition 2024 supports async fn in traits natively.
7pub trait LlmClient: Send + Sync {
8    /// Text completion
9    fn complete(
10        &self,
11        prompt: &str,
12        system: Option<&str>,
13    ) -> impl Future<Output = Result<String, LlmError>> + Send;
14
15    /// Generate embedding vector
16    fn embed(&self, text: &str) -> impl Future<Output = Result<Vec<f32>, LlmError>> + Send;
17}
18
19use std::future::Future;
20
21/// Default Anthropic API base URL.
22pub const ANTHROPIC_DEFAULT_BASE_URL: &str = "https://api.anthropic.com";
23
24/// Resolve the Anthropic API base URL from `ANTHROPIC_BASE_URL` env, with a
25/// trailing slash stripped. Falls back to `ANTHROPIC_DEFAULT_BASE_URL`.
26///
27/// Honored at every upstream call site so that users can route Anthropic
28/// traffic through Bedrock, Vertex, a corporate egress proxy, an external
29/// auth bridge, or test fixtures without touching code.
30pub 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/// Check if a model name matches recommended reasoning models for session analysis.
37///
38/// Recommended: Anthropic Opus, OpenAI GPT-5/O3/O4, Gemini Pro 3+,
39/// or any model with "reasoning" or "think" in the name.
40#[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        // Anthropic opus models
78        assert!(is_reasoning_model("claude-opus-4-6"));
79        assert!(is_reasoning_model("claude-opus-4-20250514"));
80
81        // OpenAI reasoning models
82        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        // Gemini pro >= 3
88        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        // Generic reasoning/thinking
94        assert!(is_reasoning_model("deepseek-reasoning-v2"));
95        assert!(is_reasoning_model("qwen-thinking-32b"));
96
97        // Non-recommended
98        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}