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