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()
31 .iter()
32 .position(|&e| e == self)
33 .expect("variant must be in all()")
34 }
35
36 pub fn cycle_within(current: Option<Self>, levels: &[Self]) -> Option<Self> {
39 if levels.is_empty() {
40 return None;
41 }
42 match current {
43 None => Some(levels[0]),
44 Some(effort) => levels
45 .iter()
46 .position(|&l| l == effort)
47 .and_then(|i| levels.get(i + 1))
48 .copied(),
49 }
50 }
51
52 pub fn clamp_to(self, levels: &[Self]) -> Self {
55 if levels.contains(&self) {
56 return self;
57 }
58 levels
59 .iter()
60 .rev()
61 .find(|&&l| l.ordinal() <= self.ordinal())
62 .copied()
63 .unwrap_or(*levels.first().expect("levels must not be empty"))
64 }
65
66 pub fn config_str(effort: Option<Self>) -> &'static str {
68 effort.map_or("none", Self::as_str)
69 }
70
71 pub fn parse(s: &str) -> Result<Option<Self>, String> {
74 match s {
75 "none" | "" => Ok(None),
76 other => other.parse().map(Some),
77 }
78 }
79}
80
81impl fmt::Display for ReasoningEffort {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 f.write_str(self.as_str())
84 }
85}
86
87impl FromStr for ReasoningEffort {
88 type Err = String;
89
90 fn from_str(s: &str) -> Result<Self, Self::Err> {
91 match s {
92 "low" => Ok(Self::Low),
93 "medium" => Ok(Self::Medium),
94 "high" => Ok(Self::High),
95 "xhigh" => Ok(Self::Xhigh),
96 _ => Err(format!("Unknown reasoning effort: '{s}'")),
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn display_roundtrip() {
107 for effort in ReasoningEffort::all() {
108 let s = effort.to_string();
109 let parsed: ReasoningEffort = s.parse().unwrap();
110 assert_eq!(*effort, parsed);
111 }
112 }
113
114 #[test]
115 fn as_str_matches_display() {
116 for effort in ReasoningEffort::all() {
117 assert_eq!(effort.as_str(), effort.to_string());
118 }
119 }
120
121 #[test]
122 fn from_str_rejects_unknown() {
123 assert!("max".parse::<ReasoningEffort>().is_err());
124 }
125
126 #[test]
127 fn all_returns_four_variants() {
128 assert_eq!(ReasoningEffort::all().len(), 4);
129 }
130
131 #[test]
132 fn parse_none_and_empty() {
133 assert_eq!(ReasoningEffort::parse("none").unwrap(), None);
134 assert_eq!(ReasoningEffort::parse("").unwrap(), None);
135 }
136
137 #[test]
138 fn parse_valid_levels() {
139 assert_eq!(
140 ReasoningEffort::parse("high").unwrap(),
141 Some(ReasoningEffort::High)
142 );
143 assert_eq!(
144 ReasoningEffort::parse("low").unwrap(),
145 Some(ReasoningEffort::Low)
146 );
147 }
148
149 #[test]
150 fn parse_rejects_unknown() {
151 assert!(ReasoningEffort::parse("max").is_err());
152 }
153
154 #[test]
155 fn config_str_values() {
156 assert_eq!(ReasoningEffort::config_str(None), "none");
157 assert_eq!(
158 ReasoningEffort::config_str(Some(ReasoningEffort::Low)),
159 "low"
160 );
161 assert_eq!(
162 ReasoningEffort::config_str(Some(ReasoningEffort::High)),
163 "high"
164 );
165 }
166
167 #[test]
168 fn serialize_produces_lowercase() {
169 for effort in ReasoningEffort::all() {
170 let json = serde_json::to_value(effort).unwrap();
171 assert_eq!(json.as_str().unwrap(), effort.as_str());
172 }
173 }
174
175 #[test]
176 fn ordinal_values() {
177 assert_eq!(ReasoningEffort::Low.ordinal(), 0);
178 assert_eq!(ReasoningEffort::Medium.ordinal(), 1);
179 assert_eq!(ReasoningEffort::High.ordinal(), 2);
180 assert_eq!(ReasoningEffort::Xhigh.ordinal(), 3);
181 }
182
183 #[test]
184 fn cycle_within_three_levels() {
185 use ReasoningEffort::*;
186 let levels = &[Low, Medium, High];
187 assert_eq!(ReasoningEffort::cycle_within(None, levels), Some(Low));
188 assert_eq!(
189 ReasoningEffort::cycle_within(Some(Low), levels),
190 Some(Medium)
191 );
192 assert_eq!(
193 ReasoningEffort::cycle_within(Some(Medium), levels),
194 Some(High)
195 );
196 assert_eq!(ReasoningEffort::cycle_within(Some(High), levels), None);
197 }
198
199 #[test]
200 fn cycle_within_four_levels() {
201 use ReasoningEffort::*;
202 let levels = &[Low, Medium, High, Xhigh];
203 assert_eq!(ReasoningEffort::cycle_within(None, levels), Some(Low));
204 assert_eq!(
205 ReasoningEffort::cycle_within(Some(High), levels),
206 Some(Xhigh)
207 );
208 assert_eq!(ReasoningEffort::cycle_within(Some(Xhigh), levels), None);
209 }
210
211 #[test]
212 fn cycle_within_empty_returns_none() {
213 assert_eq!(ReasoningEffort::cycle_within(None, &[]), None);
214 assert_eq!(
215 ReasoningEffort::cycle_within(Some(ReasoningEffort::Low), &[]),
216 None
217 );
218 }
219
220 #[test]
221 fn cycle_within_unknown_current_wraps_to_none() {
222 use ReasoningEffort::*;
223 assert_eq!(
225 ReasoningEffort::cycle_within(Some(Xhigh), &[Low, Medium, High]),
226 None
227 );
228 }
229
230 #[test]
231 fn clamp_to_self_in_levels() {
232 use ReasoningEffort::*;
233 assert_eq!(High.clamp_to(&[Low, Medium, High]), High);
234 assert_eq!(Xhigh.clamp_to(&[Low, Medium, High, Xhigh]), Xhigh);
235 }
236
237 #[test]
238 fn clamp_to_highest_le() {
239 use ReasoningEffort::*;
240 assert_eq!(Xhigh.clamp_to(&[Low, Medium, High]), High);
242 }
243
244 #[test]
245 fn clamp_to_fallback_first() {
246 use ReasoningEffort::*;
247 assert_eq!(Low.clamp_to(&[Medium, High]), Medium);
249 }
250
251 #[test]
252 fn parse_xhigh() {
253 assert_eq!(
254 ReasoningEffort::parse("xhigh").unwrap(),
255 Some(ReasoningEffort::Xhigh)
256 );
257 }
258}