Skip to main content

construct/agent/operator/providers/
mod.rs

1//! Provider detection and tool-layer dispatch.
2//!
3//! Determines which LLM provider is running the operator from the model
4//! name string and returns the appropriate tool-calling prompt layer.
5
6pub mod claude;
7pub mod gemini;
8pub mod ollama;
9pub mod openai;
10
11/// Known LLM provider families.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Provider {
14    /// Anthropic Claude models (Opus, Sonnet, Haiku).
15    Claude,
16    /// OpenAI models (GPT, Codex, o-series).
17    OpenAi,
18    /// Local/open-source models via Ollama or similar (Llama, Mistral, Qwen).
19    Local,
20    /// Google Gemini models.
21    Gemini,
22}
23
24impl Provider {
25    /// Detect the provider from a model name string.
26    ///
27    /// Handles prefixed names like `"openrouter/claude-opus-4-6"` by stripping
28    /// everything before the last `/`.
29    ///
30    /// Defaults to [`Provider::OpenAi`] for unrecognised models — the OpenAI
31    /// JSON format is the most universal fallback.
32    pub fn detect(model_name: &str) -> Self {
33        let lower = model_name.to_lowercase();
34
35        // Strip provider/router prefix (e.g. "openrouter/", "together/")
36        let model = match lower.rfind('/') {
37            Some(pos) => &lower[pos + 1..],
38            None => &lower,
39        };
40
41        if model.starts_with("claude") {
42            Provider::Claude
43        } else if model.starts_with("gpt-")
44            || model.starts_with("gpt4")
45            || model.starts_with("o1")
46            || model.starts_with("o3")
47            || model.starts_with("o4")
48            || model.starts_with("codex")
49            || model.starts_with("chatgpt")
50        {
51            Provider::OpenAi
52        } else if model.starts_with("gemini") {
53            Provider::Gemini
54        } else if model.starts_with("llama")
55            || model.starts_with("mistral")
56            || model.starts_with("qwen")
57            || model.starts_with("phi")
58            || model.starts_with("deepseek")
59            || model.starts_with("command")
60            || model.contains(':')
61        // Ollama-style "model:tag"
62        {
63            Provider::Local
64        } else {
65            // Default: OpenAI JSON format is the safest universal fallback.
66            Provider::OpenAi
67        }
68    }
69
70    /// Return the provider-specific tool-layer prompt.
71    pub fn tool_layer(&self) -> &'static str {
72        match self {
73            Provider::Claude => claude::TOOL_LAYER,
74            Provider::OpenAi => openai::TOOL_LAYER,
75            Provider::Local => ollama::TOOL_LAYER,
76            Provider::Gemini => gemini::TOOL_LAYER,
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn detect_claude_models() {
87        assert_eq!(Provider::detect("claude-opus-4-6"), Provider::Claude);
88        assert_eq!(Provider::detect("claude-sonnet-4-6"), Provider::Claude);
89        assert_eq!(
90            Provider::detect("claude-haiku-4-5-20251001"),
91            Provider::Claude
92        );
93        assert_eq!(Provider::detect("Claude-Opus-4-6"), Provider::Claude);
94    }
95
96    #[test]
97    fn detect_openai_models() {
98        assert_eq!(Provider::detect("gpt-5.4"), Provider::OpenAi);
99        assert_eq!(Provider::detect("gpt-4-turbo"), Provider::OpenAi);
100        assert_eq!(Provider::detect("gpt4o"), Provider::OpenAi);
101        assert_eq!(Provider::detect("o1-preview"), Provider::OpenAi);
102        assert_eq!(Provider::detect("o3-mini"), Provider::OpenAi);
103        assert_eq!(Provider::detect("codex-mini"), Provider::OpenAi);
104    }
105
106    #[test]
107    fn detect_gemini_models() {
108        assert_eq!(Provider::detect("gemini-pro"), Provider::Gemini);
109        assert_eq!(Provider::detect("gemini-2.0-flash"), Provider::Gemini);
110    }
111
112    #[test]
113    fn detect_local_models() {
114        assert_eq!(Provider::detect("llama3:70b"), Provider::Local);
115        assert_eq!(Provider::detect("mistral-7b"), Provider::Local);
116        assert_eq!(Provider::detect("qwen2:14b"), Provider::Local);
117        assert_eq!(Provider::detect("deepseek-coder-v2"), Provider::Local);
118        assert_eq!(Provider::detect("phi-3-mini"), Provider::Local);
119    }
120
121    #[test]
122    fn detect_with_router_prefix() {
123        assert_eq!(
124            Provider::detect("openrouter/claude-opus-4-6"),
125            Provider::Claude
126        );
127        assert_eq!(Provider::detect("openrouter/gpt-5.4"), Provider::OpenAi);
128        assert_eq!(Provider::detect("together/llama3-70b"), Provider::Local);
129    }
130
131    #[test]
132    fn unknown_defaults_to_openai() {
133        assert_eq!(Provider::detect("some-unknown-model"), Provider::OpenAi);
134    }
135
136    #[test]
137    fn tool_layer_not_empty() {
138        for provider in [
139            Provider::Claude,
140            Provider::OpenAi,
141            Provider::Local,
142            Provider::Gemini,
143        ] {
144            let layer = provider.tool_layer();
145            assert!(!layer.is_empty(), "{provider:?} has empty tool layer");
146            assert!(
147                layer.contains("create_agent"),
148                "{provider:?} tool layer missing create_agent"
149            );
150        }
151    }
152}