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