1use crate::Provider;
4use okbot_core::AgentError;
5
6pub fn model_parts(provider: Provider, model: &str) -> (&str, Option<&str>) {
8 if matches!(provider, Provider::Claude | Provider::Codex) {
9 model
10 .rsplit_once('@')
11 .map_or((model, None), |(m, e)| (m, Some(e)))
12 } else {
13 (model, None)
14 }
15}
16
17pub fn validate_model(provider: Provider, model: &str) -> Result<(), AgentError> {
19 let invalid = |message: &str| AgentError::Failed {
20 message: message.into(),
21 retryable: false,
22 };
23 let (name, effort) = model_parts(provider, model);
24 if name.trim().is_empty() {
25 return Err(invalid("model must not be empty"));
26 }
27 if let Some(effort) = effort {
28 let valid: &[&str] = match provider {
29 Provider::Claude => &["low", "medium", "high", "xhigh", "max"],
30 Provider::Codex => &["low", "medium", "high", "xhigh", "max", "ultra"],
31 _ => &[],
32 };
33 if !valid.contains(&effort) {
34 return Err(invalid(&format!(
35 "unsupported effort `{effort}`; use {}",
36 valid.join(", ")
37 )));
38 }
39 }
40 if provider == Provider::OpenCodeV2 && !name.contains('/') {
41 return Err(invalid("OpenCode model must be provider/model[#variant]"));
42 }
43 Ok(())
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 #[test]
50 fn model_syntax_matches_pinned_adapter() {
51 for (provider, model, valid) in [
52 (Provider::Claude, "sonnet@max", true),
53 (Provider::Claude, "sonnet@ultra", false),
54 (Provider::Codex, "test-model@ultra", true),
55 (Provider::Codex, "@high", false),
56 (Provider::Codex, "test@wrong", false),
57 (Provider::OpenCodeV2, "vendor/model#high", true),
58 (Provider::OpenCodeV2, "model", false),
59 (Provider::Devin, "test", true),
60 ] {
61 assert_eq!(validate_model(provider, model).is_ok(), valid, "{model}");
62 }
63 }
64}