1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum ReasoningEffort {
8 Low,
9 Medium,
10 High,
11 Xhigh,
12}
13
14impl ReasoningEffort {
15 pub fn as_str(self) -> &'static str {
16 match self {
17 Self::Low => "low",
18 Self::Medium => "medium",
19 Self::High => "high",
20 Self::Xhigh => "xhigh",
21 }
22 }
23
24 pub fn all() -> &'static [ReasoningEffort] {
25 &[Self::Low, Self::Medium, Self::High, Self::Xhigh]
26 }
27
28 pub fn ordinal(self) -> usize {
30 Self::all().iter().position(|&e| e == self).expect("variant must be in all()")
31 }
32
33 pub fn cycle_within(current: Option<Self>, levels: &[Self]) -> Option<Self> {
36 if levels.is_empty() {
37 return None;
38 }
39 match current {
40 None => Some(levels[0]),
41 Some(effort) => levels.iter().position(|&l| l == effort).and_then(|i| levels.get(i + 1)).copied(),
42 }
43 }
44
45 pub fn clamp_to(self, levels: &[Self]) -> Self {
48 if levels.contains(&self) {
49 return self;
50 }
51 levels
52 .iter()
53 .rev()
54 .find(|&&l| l.ordinal() <= self.ordinal())
55 .copied()
56 .unwrap_or(*levels.first().expect("levels must not be empty"))
57 }
58
59 pub fn config_str(effort: Option<Self>) -> &'static str {
61 effort.map_or("none", Self::as_str)
62 }
63
64 pub fn parse(s: &str) -> Result<Option<Self>, String> {
67 match s {
68 "none" | "" => Ok(None),
69 other => other.parse().map(Some),
70 }
71 }
72}
73
74impl fmt::Display for ReasoningEffort {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str(self.as_str())
77 }
78}
79
80impl FromStr for ReasoningEffort {
81 type Err = String;
82
83 fn from_str(s: &str) -> Result<Self, Self::Err> {
84 match s {
85 "low" => Ok(Self::Low),
86 "medium" => Ok(Self::Medium),
87 "high" => Ok(Self::High),
88 "xhigh" => Ok(Self::Xhigh),
89 _ => Err(format!("Unknown reasoning effort: '{s}'")),
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn display_roundtrip() {
100 for effort in ReasoningEffort::all() {
101 let s = effort.to_string();
102 let parsed: ReasoningEffort = s.parse().unwrap();
103 assert_eq!(*effort, parsed);
104 }
105 }
106
107 #[test]
108 fn as_str_matches_display() {
109 for effort in ReasoningEffort::all() {
110 assert_eq!(effort.as_str(), effort.to_string());
111 }
112 }
113
114 #[test]
115 fn from_str_rejects_unknown() {
116 assert!("max".parse::<ReasoningEffort>().is_err());
117 }
118
119 #[test]
120 fn all_returns_four_variants() {
121 assert_eq!(ReasoningEffort::all().len(), 4);
122 }
123
124 #[test]
125 fn parse_none_and_empty() {
126 assert_eq!(ReasoningEffort::parse("none").unwrap(), None);
127 assert_eq!(ReasoningEffort::parse("").unwrap(), None);
128 }
129
130 #[test]
131 fn parse_valid_levels() {
132 assert_eq!(ReasoningEffort::parse("high").unwrap(), Some(ReasoningEffort::High));
133 assert_eq!(ReasoningEffort::parse("low").unwrap(), Some(ReasoningEffort::Low));
134 }
135
136 #[test]
137 fn parse_rejects_unknown() {
138 assert!(ReasoningEffort::parse("max").is_err());
139 }
140
141 #[test]
142 fn config_str_values() {
143 assert_eq!(ReasoningEffort::config_str(None), "none");
144 assert_eq!(ReasoningEffort::config_str(Some(ReasoningEffort::Low)), "low");
145 assert_eq!(ReasoningEffort::config_str(Some(ReasoningEffort::High)), "high");
146 }
147
148 #[test]
149 fn serialize_produces_lowercase() {
150 for effort in ReasoningEffort::all() {
151 let json = serde_json::to_value(effort).unwrap();
152 assert_eq!(json.as_str().unwrap(), effort.as_str());
153 }
154 }
155
156 #[test]
157 fn ordinal_values() {
158 assert_eq!(ReasoningEffort::Low.ordinal(), 0);
159 assert_eq!(ReasoningEffort::Medium.ordinal(), 1);
160 assert_eq!(ReasoningEffort::High.ordinal(), 2);
161 assert_eq!(ReasoningEffort::Xhigh.ordinal(), 3);
162 }
163
164 #[test]
165 fn cycle_within_three_levels() {
166 use ReasoningEffort::*;
167 let levels = &[Low, Medium, High];
168 assert_eq!(ReasoningEffort::cycle_within(None, levels), Some(Low));
169 assert_eq!(ReasoningEffort::cycle_within(Some(Low), levels), Some(Medium));
170 assert_eq!(ReasoningEffort::cycle_within(Some(Medium), levels), Some(High));
171 assert_eq!(ReasoningEffort::cycle_within(Some(High), levels), None);
172 }
173
174 #[test]
175 fn cycle_within_four_levels() {
176 use ReasoningEffort::*;
177 let levels = &[Low, Medium, High, Xhigh];
178 assert_eq!(ReasoningEffort::cycle_within(None, levels), Some(Low));
179 assert_eq!(ReasoningEffort::cycle_within(Some(High), levels), Some(Xhigh));
180 assert_eq!(ReasoningEffort::cycle_within(Some(Xhigh), levels), None);
181 }
182
183 #[test]
184 fn cycle_within_empty_returns_none() {
185 assert_eq!(ReasoningEffort::cycle_within(None, &[]), None);
186 assert_eq!(ReasoningEffort::cycle_within(Some(ReasoningEffort::Low), &[]), None);
187 }
188
189 #[test]
190 fn cycle_within_unknown_current_wraps_to_none() {
191 use ReasoningEffort::*;
192 assert_eq!(ReasoningEffort::cycle_within(Some(Xhigh), &[Low, Medium, High]), None);
194 }
195
196 #[test]
197 fn clamp_to_self_in_levels() {
198 use ReasoningEffort::*;
199 assert_eq!(High.clamp_to(&[Low, Medium, High]), High);
200 assert_eq!(Xhigh.clamp_to(&[Low, Medium, High, Xhigh]), Xhigh);
201 }
202
203 #[test]
204 fn clamp_to_highest_le() {
205 use ReasoningEffort::*;
206 assert_eq!(Xhigh.clamp_to(&[Low, Medium, High]), High);
208 }
209
210 #[test]
211 fn clamp_to_fallback_first() {
212 use ReasoningEffort::*;
213 assert_eq!(Low.clamp_to(&[Medium, High]), Medium);
215 }
216
217 #[test]
218 fn parse_xhigh() {
219 assert_eq!(ReasoningEffort::parse("xhigh").unwrap(), Some(ReasoningEffort::Xhigh));
220 }
221}