Skip to main content

claude_codex/providers/cursor/
model.rs

1/// Cursor model catalog -- resolves incoming model names to Cursor model IDs.
2///
3/// Resolution rules:
4/// - `cursor:`, `cursor-plan:`, `cursor-ask:` prefixes are stripped and mapped
5///   to the corresponding agent mode.
6/// - Legacy names like `cursor`, `cursor-agent`, `cursor-composer`,
7///   `cursor-composer-fast`, `cursor-plan`, `cursor-ask`, `composer-2.5`,
8///   `composer-2.5-fast` are recognized.
9/// - `cursor-agent:` is also supported for agent mode routing.
10
11pub const CURSOR_LEGACY_MODELS: &[&str] = &[
12    "cursor",
13    "cursor-agent",
14    "cursor-composer",
15    "cursor-composer-fast",
16    "cursor-plan",
17    "cursor-ask",
18    "composer-2.5",
19    "composer-2.5-fast",
20];
21
22/// Agent mode derived from model prefix or name.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum CursorAgentMode {
25    Agent,
26    Plan,
27    Ask,
28}
29
30impl CursorAgentMode {
31    pub fn as_str(&self) -> &'static str {
32        match self {
33            CursorAgentMode::Agent => "AGENT_MODE_AGENT",
34            CursorAgentMode::Plan => "AGENT_MODE_PLAN",
35            CursorAgentMode::Ask => "AGENT_MODE_ASK",
36        }
37    }
38}
39
40/// Resolve a model string into a (model_id, mode) pair.
41///
42/// Returns an error if the model is not recognized.
43pub fn resolve_cursor_model(model: &str) -> Result<CursorModelResolution, String> {
44    let model = model.trim();
45
46    // Strip known prefixes
47    if let Some(rest) = model.strip_prefix("cursor-agent:") {
48        return Ok(CursorModelResolution {
49            model_id: rest.to_string(),
50            mode: CursorAgentMode::Agent,
51        });
52    }
53    if let Some(rest) = model.strip_prefix("cursor-plan:") {
54        return Ok(CursorModelResolution {
55            model_id: rest.to_string(),
56            mode: CursorAgentMode::Plan,
57        });
58    }
59    if let Some(rest) = model.strip_prefix("cursor-ask:") {
60        return Ok(CursorModelResolution {
61            model_id: rest.to_string(),
62            mode: CursorAgentMode::Ask,
63        });
64    }
65    if let Some(rest) = model.strip_prefix("cursor:") {
66        return Ok(CursorModelResolution {
67            model_id: rest.to_string(),
68            mode: CursorAgentMode::Agent,
69        });
70    }
71
72    // Legacy exact names
73    match model {
74        "cursor" | "cursor-agent" | "cursor-composer" | "cursor-composer-fast" => {
75            Ok(CursorModelResolution {
76                model_id: model.to_string(),
77                mode: CursorAgentMode::Agent,
78            })
79        }
80        "cursor-plan" | "composer-2.5" => Ok(CursorModelResolution {
81            model_id: model.to_string(),
82            mode: CursorAgentMode::Plan,
83        }),
84        "cursor-ask" | "composer-2.5-fast" => Ok(CursorModelResolution {
85            model_id: model.to_string(),
86            mode: CursorAgentMode::Ask,
87        }),
88        _ => Err(format!(
89            "unknown cursor model: {model}. Supported: cursor:<id>, cursor-plan:<id>, cursor-ask:<id>, cursor-agent"
90        )),
91    }
92}
93
94#[derive(Debug, Clone)]
95pub struct CursorModelResolution {
96    pub model_id: String,
97    pub mode: CursorAgentMode,
98}
99
100/// Build the list of supported Cursor model names.
101pub fn cursor_supported_models() -> Vec<String> {
102    let mut out: Vec<String> = CURSOR_LEGACY_MODELS
103        .iter()
104        .map(|s| (*s).to_string())
105        .collect();
106    out.sort_unstable();
107    out
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn resolve_legacy_cursor() {
116        let r = resolve_cursor_model("cursor").unwrap();
117        assert_eq!(r.model_id, "cursor");
118        assert_eq!(r.mode, CursorAgentMode::Agent);
119    }
120
121    #[test]
122    fn resolve_legacy_cursor_agent() {
123        let r = resolve_cursor_model("cursor-agent").unwrap();
124        assert_eq!(r.mode, CursorAgentMode::Agent);
125    }
126
127    #[test]
128    fn resolve_legacy_cursor_plan() {
129        let r = resolve_cursor_model("cursor-plan").unwrap();
130        assert_eq!(r.mode, CursorAgentMode::Plan);
131    }
132
133    #[test]
134    fn resolve_legacy_cursor_ask() {
135        let r = resolve_cursor_model("cursor-ask").unwrap();
136        assert_eq!(r.mode, CursorAgentMode::Ask);
137    }
138
139    #[test]
140    fn resolve_prefixed_cursor() {
141        let r = resolve_cursor_model("cursor:gpt-5.5").unwrap();
142        assert_eq!(r.model_id, "gpt-5.5");
143        assert_eq!(r.mode, CursorAgentMode::Agent);
144    }
145
146    #[test]
147    fn resolve_prefixed_cursor_plan() {
148        let r = resolve_cursor_model("cursor-plan:gpt-5.5").unwrap();
149        assert_eq!(r.model_id, "gpt-5.5");
150        assert_eq!(r.mode, CursorAgentMode::Plan);
151    }
152
153    #[test]
154    fn resolve_prefixed_cursor_ask() {
155        let r = resolve_cursor_model("cursor-ask:gpt-5.5").unwrap();
156        assert_eq!(r.model_id, "gpt-5.5");
157        assert_eq!(r.mode, CursorAgentMode::Ask);
158    }
159
160    #[test]
161    fn resolve_prefixed_cursor_agent() {
162        let r = resolve_cursor_model("cursor-agent:gpt-5.5").unwrap();
163        assert_eq!(r.model_id, "gpt-5.5");
164        assert_eq!(r.mode, CursorAgentMode::Agent);
165    }
166
167    #[test]
168    fn resolve_unknown_model_errors() {
169        let r = resolve_cursor_model("unknown-model");
170        assert!(r.is_err());
171    }
172
173    #[test]
174    fn resolve_composer_models() {
175        let r = resolve_cursor_model("composer-2.5").unwrap();
176        assert_eq!(r.mode, CursorAgentMode::Plan);
177
178        let r = resolve_cursor_model("composer-2.5-fast").unwrap();
179        assert_eq!(r.mode, CursorAgentMode::Ask);
180    }
181
182    #[test]
183    fn supported_models_includes_all_legacy() {
184        let models = cursor_supported_models();
185        for m in CURSOR_LEGACY_MODELS {
186            assert!(models.contains(&m.to_string()), "missing {m}");
187        }
188    }
189}