Skip to main content

embacle_server/
provider_resolver.rs

1// ABOUTME: Parses model strings with optional provider prefix into (CliRunnerType, model) pairs
2// ABOUTME: Supports "provider:model", "provider", and bare "model" with server default fallback
3//
4// SPDX-License-Identifier: Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7use embacle::config::CliRunnerType;
8
9use crate::runner::parse_runner_type;
10
11/// Resolved provider and model from a model string
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ResolvedProvider {
14    /// The CLI runner type to use
15    pub runner_type: CliRunnerType,
16    /// Optional model name (None means use provider default)
17    pub model: Option<String>,
18}
19
20/// Parse a model string into a provider type and optional model name
21///
22/// Formats supported:
23/// - `"copilot:gpt-4o"` → (Copilot, Some("gpt-4o"))
24/// - `"claude:opus"` → (`ClaudeCode`, Some("opus"))
25/// - `"copilot"` → (Copilot, None) — use provider default model
26/// - `"gpt-4o"` → (`default_provider`, Some("gpt-4o")) — no prefix, use server default
27pub fn resolve_model(model_str: &str, default_provider: CliRunnerType) -> ResolvedProvider {
28    if let Some((prefix, model)) = model_str.split_once(':') {
29        if let Some(runner_type) = parse_runner_type(prefix) {
30            return ResolvedProvider {
31                runner_type,
32                model: if model.is_empty() {
33                    None
34                } else {
35                    Some(model.to_owned())
36                },
37            };
38        }
39        // Colon present but prefix not recognized — treat as bare model with default provider
40        ResolvedProvider {
41            runner_type: default_provider,
42            model: Some(model_str.to_owned()),
43        }
44    } else if let Some(runner_type) = parse_runner_type(model_str) {
45        // Exact provider name with no model suffix
46        ResolvedProvider {
47            runner_type,
48            model: None,
49        }
50    } else {
51        // Bare model name — use default provider
52        ResolvedProvider {
53            runner_type: default_provider,
54            model: Some(model_str.to_owned()),
55        }
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn resolve_provider_with_model() {
65        let result = resolve_model("copilot:gpt-4o", CliRunnerType::ClaudeCode);
66        assert_eq!(result.runner_type, CliRunnerType::Copilot);
67        assert_eq!(result.model.as_deref(), Some("gpt-4o"));
68    }
69
70    #[test]
71    fn resolve_claude_with_model() {
72        let result = resolve_model("claude:opus", CliRunnerType::Copilot);
73        assert_eq!(result.runner_type, CliRunnerType::ClaudeCode);
74        assert_eq!(result.model.as_deref(), Some("opus"));
75    }
76
77    #[test]
78    fn resolve_provider_only() {
79        let result = resolve_model("copilot", CliRunnerType::ClaudeCode);
80        assert_eq!(result.runner_type, CliRunnerType::Copilot);
81        assert!(result.model.is_none());
82    }
83
84    #[test]
85    fn resolve_bare_model_uses_default() {
86        let result = resolve_model("gpt-4o", CliRunnerType::Copilot);
87        assert_eq!(result.runner_type, CliRunnerType::Copilot);
88        assert_eq!(result.model.as_deref(), Some("gpt-4o"));
89    }
90
91    #[test]
92    fn resolve_provider_with_empty_model() {
93        let result = resolve_model("copilot:", CliRunnerType::ClaudeCode);
94        assert_eq!(result.runner_type, CliRunnerType::Copilot);
95        assert!(result.model.is_none());
96    }
97
98    #[test]
99    fn resolve_unknown_prefix_as_bare_model() {
100        let result = resolve_model("unknown:something", CliRunnerType::Copilot);
101        assert_eq!(result.runner_type, CliRunnerType::Copilot);
102        assert_eq!(result.model.as_deref(), Some("unknown:something"));
103    }
104
105    #[test]
106    fn resolve_case_insensitive_provider() {
107        let result = resolve_model("CLAUDE:opus", CliRunnerType::Copilot);
108        assert_eq!(result.runner_type, CliRunnerType::ClaudeCode);
109        assert_eq!(result.model.as_deref(), Some("opus"));
110    }
111
112    #[test]
113    fn resolve_cursor_agent_variants() {
114        for prefix in &["cursor_agent", "cursor-agent", "cursoragent"] {
115            let model_str = format!("{prefix}:model");
116            let result = resolve_model(&model_str, CliRunnerType::Copilot);
117            assert_eq!(result.runner_type, CliRunnerType::CursorAgent);
118            assert_eq!(result.model.as_deref(), Some("model"));
119        }
120    }
121
122    #[test]
123    fn resolve_opencode_variants() {
124        let result = resolve_model("opencode:latest", CliRunnerType::Copilot);
125        assert_eq!(result.runner_type, CliRunnerType::OpenCode);
126        assert_eq!(result.model.as_deref(), Some("latest"));
127
128        let result = resolve_model("open_code:latest", CliRunnerType::Copilot);
129        assert_eq!(result.runner_type, CliRunnerType::OpenCode);
130        assert_eq!(result.model.as_deref(), Some("latest"));
131    }
132}