Skip to main content

chant/operations/
model.rs

1//! Model selection and detection logic.
2//!
3//! Handles model name resolution with the following priority:
4//! 1. CHANT_MODEL env var (explicit override)
5//! 2. ANTHROPIC_MODEL env var (Claude CLI default)
6//! 3. defaults.model in config
7//! 4. Parse from `claude --version` output (last resort)
8//!
9//! All model names are normalized to shorthand (e.g., "claude-sonnet-4-20250514" -> "sonnet").
10
11use crate::config::Config;
12use crate::spec::normalize_model_name;
13
14/// Get the model name using the following priority:
15/// 1. CHANT_MODEL env var (explicit override)
16/// 2. ANTHROPIC_MODEL env var (Claude CLI default)
17/// 3. defaults.model in config
18/// 4. Parse from `claude --version` output (last resort)
19///
20/// Model names are normalized to shorthand (e.g., "claude-sonnet-4-20250514" -> "sonnet").
21pub fn get_model_name(config: Option<&Config>) -> Option<String> {
22    get_model_name_with_default(config.and_then(|c| c.defaults.model.as_deref()))
23}
24
25/// Get the model name with an optional default from config.
26/// Used by parallel execution where full Config isn't available.
27///
28/// Model names are normalized to shorthand (e.g., "claude-sonnet-4-20250514" -> "sonnet").
29pub fn get_model_name_with_default(config_model: Option<&str>) -> Option<String> {
30    // 1. CHANT_MODEL env var
31    if let Ok(model) = std::env::var("CHANT_MODEL") {
32        if !model.is_empty() {
33            return Some(normalize_model_name(&model));
34        }
35    }
36
37    // 2. ANTHROPIC_MODEL env var
38    if let Ok(model) = std::env::var("ANTHROPIC_MODEL") {
39        if !model.is_empty() {
40            return Some(normalize_model_name(&model));
41        }
42    }
43
44    // 3. defaults.model from config
45    if let Some(model) = config_model {
46        if !model.is_empty() {
47            return Some(normalize_model_name(model));
48        }
49    }
50
51    // 4. Parse from claude --version output
52    parse_model_from_claude_version().map(|m| normalize_model_name(&m))
53}
54
55/// Parse model name from `claude --version` output.
56/// Expected format: "X.Y.Z (model-name)" or similar patterns.
57fn parse_model_from_claude_version() -> Option<String> {
58    use std::process::Command;
59
60    let output = Command::new("claude").arg("--version").output().ok()?;
61
62    if !output.status.success() {
63        return None;
64    }
65
66    let version_str = String::from_utf8_lossy(&output.stdout);
67
68    // Try to extract model from parentheses, e.g., "1.0.0 (claude-sonnet-4)"
69    if let Some(start) = version_str.find('(') {
70        if let Some(end) = version_str.find(')') {
71            if start < end {
72                let model = version_str[start + 1..end].trim();
73                // Check if it looks like a model name (contains "claude" or common model patterns)
74                if model.contains("claude")
75                    || model.contains("sonnet")
76                    || model.contains("opus")
77                    || model.contains("haiku")
78                {
79                    return Some(model.to_string());
80                }
81            }
82        }
83    }
84
85    None
86}